pylint fixes
This commit is contained in:
parent
689cd11a6c
commit
996beae770
@ -145,6 +145,7 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# Install PYQT5==5.9 manually if you want to test this helper function
|
||||||
while True:
|
while True:
|
||||||
pair = 'BTC_ANT'
|
pair = 'BTC_ANT'
|
||||||
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||||
|
86
exchange.py
86
exchange.py
@ -7,10 +7,10 @@ from poloniex import Poloniex
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Current selected exchange
|
||||||
cur_exchange = None
|
EXCHANGE = None
|
||||||
_api = None
|
_API = None
|
||||||
_conf = {}
|
_CONF = {}
|
||||||
|
|
||||||
|
|
||||||
class Exchange(enum.Enum):
|
class Exchange(enum.Enum):
|
||||||
@ -26,9 +26,9 @@ def init(config: dict) -> None:
|
|||||||
:param config: config to use
|
:param config: config to use
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
global _api, cur_exchange
|
global _API, EXCHANGE
|
||||||
|
|
||||||
_conf.update(config)
|
_CONF.update(config)
|
||||||
|
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
@ -37,17 +37,17 @@ def init(config: dict) -> None:
|
|||||||
use_bittrex = config.get('bittrex', {}).get('enabled', False)
|
use_bittrex = config.get('bittrex', {}).get('enabled', False)
|
||||||
|
|
||||||
if use_poloniex:
|
if use_poloniex:
|
||||||
cur_exchange = Exchange.POLONIEX
|
EXCHANGE = Exchange.POLONIEX
|
||||||
_api = Poloniex(key=config['poloniex']['key'], secret=config['poloniex']['secret'])
|
_API = Poloniex(key=config['poloniex']['key'], secret=config['poloniex']['secret'])
|
||||||
elif use_bittrex:
|
elif use_bittrex:
|
||||||
cur_exchange = Exchange.BITTREX
|
EXCHANGE = Exchange.BITTREX
|
||||||
_api = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
|
_API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
|
||||||
else:
|
else:
|
||||||
raise RuntimeError('No exchange specified. Aborting!')
|
raise RuntimeError('No exchange specified. Aborting!')
|
||||||
|
|
||||||
# Check if all pairs are available
|
# Check if all pairs are available
|
||||||
markets = get_markets()
|
markets = get_markets()
|
||||||
for pair in config[cur_exchange.name.lower()]['pair_whitelist']:
|
for pair in config[EXCHANGE.name.lower()]['pair_whitelist']:
|
||||||
if pair not in markets:
|
if pair not in markets:
|
||||||
raise RuntimeError('Pair {} is not available at Poloniex'.format(pair))
|
raise RuntimeError('Pair {} is not available at Poloniex'.format(pair))
|
||||||
|
|
||||||
@ -60,13 +60,13 @@ def buy(pair: str, rate: float, amount: float) -> str:
|
|||||||
:param amount: The amount to purchase
|
:param amount: The amount to purchase
|
||||||
:return: order_id of the placed buy order
|
:return: order_id of the placed buy order
|
||||||
"""
|
"""
|
||||||
if _conf['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return 'dry_run'
|
return 'dry_run'
|
||||||
elif cur_exchange == Exchange.POLONIEX:
|
elif EXCHANGE == Exchange.POLONIEX:
|
||||||
_api.buy(pair, rate, amount)
|
_API.buy(pair, rate, amount)
|
||||||
# TODO: return order id
|
# TODO: return order id
|
||||||
elif cur_exchange == Exchange.BITTREX:
|
elif EXCHANGE == Exchange.BITTREX:
|
||||||
data = _api.buy_limit(pair.replace('_', '-'), amount, rate)
|
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
return data['result']['uuid']
|
return data['result']['uuid']
|
||||||
@ -80,13 +80,13 @@ def sell(pair: str, rate: float, amount: float) -> str:
|
|||||||
:param amount: The amount to sell
|
:param amount: The amount to sell
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if _conf['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return 'dry_run'
|
return 'dry_run'
|
||||||
elif cur_exchange == Exchange.POLONIEX:
|
elif EXCHANGE == Exchange.POLONIEX:
|
||||||
_api.sell(pair, rate, amount)
|
_API.sell(pair, rate, amount)
|
||||||
# TODO: return order id
|
# TODO: return order id
|
||||||
elif cur_exchange == Exchange.BITTREX:
|
elif EXCHANGE == Exchange.BITTREX:
|
||||||
data = _api.sell_limit(pair.replace('_', '-'), amount, rate)
|
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
return data['result']['uuid']
|
return data['result']['uuid']
|
||||||
@ -98,13 +98,13 @@ def get_balance(currency: str) -> float:
|
|||||||
:param currency: currency as str, format: BTC
|
:param currency: currency as str, format: BTC
|
||||||
:return: float
|
:return: float
|
||||||
"""
|
"""
|
||||||
if _conf['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return 999.9
|
return 999.9
|
||||||
elif cur_exchange == Exchange.POLONIEX:
|
elif EXCHANGE == Exchange.POLONIEX:
|
||||||
data = _api.returnBalances()
|
data = _API.returnBalances()
|
||||||
return float(data[currency])
|
return float(data[currency])
|
||||||
elif cur_exchange == Exchange.BITTREX:
|
elif EXCHANGE == Exchange.BITTREX:
|
||||||
data = _api.get_balance(currency)
|
data = _API.get_balance(currency)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
return float(data['result']['Balance'] or 0.0)
|
return float(data['result']['Balance'] or 0.0)
|
||||||
@ -116,15 +116,15 @@ def get_ticker(pair: str) -> dict:
|
|||||||
:param pair: Pair as str, format: BTC_ETC
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
:return: dict
|
:return: dict
|
||||||
"""
|
"""
|
||||||
if cur_exchange == Exchange.POLONIEX:
|
if EXCHANGE == Exchange.POLONIEX:
|
||||||
data = _api.returnTicker()
|
data = _API.returnTicker()
|
||||||
return {
|
return {
|
||||||
'bid': float(data[pair]['highestBid']),
|
'bid': float(data[pair]['highestBid']),
|
||||||
'ask': float(data[pair]['lowestAsk']),
|
'ask': float(data[pair]['lowestAsk']),
|
||||||
'last': float(data[pair]['last'])
|
'last': float(data[pair]['last'])
|
||||||
}
|
}
|
||||||
elif cur_exchange == Exchange.BITTREX:
|
elif EXCHANGE == Exchange.BITTREX:
|
||||||
data = _api.get_ticker(pair.replace('_', '-'))
|
data = _API.get_ticker(pair.replace('_', '-'))
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
return {
|
return {
|
||||||
@ -140,12 +140,12 @@ def cancel_order(order_id: str) -> None:
|
|||||||
:param order_id: id as str
|
:param order_id: id as str
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if _conf['dry_run']:
|
if _CONF['dry_run']:
|
||||||
pass
|
pass
|
||||||
elif cur_exchange == Exchange.POLONIEX:
|
elif EXCHANGE == Exchange.POLONIEX:
|
||||||
raise NotImplemented('Not implemented')
|
raise NotImplemented('Not implemented')
|
||||||
elif cur_exchange == Exchange.BITTREX:
|
elif EXCHANGE == Exchange.BITTREX:
|
||||||
data = _api.cancel(order_id)
|
data = _API.cancel(order_id)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
|
||||||
@ -156,12 +156,12 @@ def get_open_orders(pair: str) -> List[dict]:
|
|||||||
:param pair: Pair as str, format: BTC_ETC
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
:return: list of dicts
|
:return: list of dicts
|
||||||
"""
|
"""
|
||||||
if _conf['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return []
|
return []
|
||||||
elif cur_exchange == Exchange.POLONIEX:
|
elif EXCHANGE == Exchange.POLONIEX:
|
||||||
raise NotImplemented('Not implemented')
|
raise NotImplemented('Not implemented')
|
||||||
elif cur_exchange == Exchange.BITTREX:
|
elif EXCHANGE == Exchange.BITTREX:
|
||||||
data = _api.get_open_orders(pair.replace('_', '-'))
|
data = _API.get_open_orders(pair.replace('_', '-'))
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
return [{
|
return [{
|
||||||
@ -180,9 +180,9 @@ def get_pair_detail_url(pair: str) -> str:
|
|||||||
:param pair: pair as str, format: BTC_ANT
|
:param pair: pair as str, format: BTC_ANT
|
||||||
:return: url as str
|
:return: url as str
|
||||||
"""
|
"""
|
||||||
if cur_exchange == Exchange.POLONIEX:
|
if EXCHANGE == Exchange.POLONIEX:
|
||||||
raise NotImplemented('Not implemented')
|
raise NotImplemented('Not implemented')
|
||||||
elif cur_exchange == Exchange.BITTREX:
|
elif EXCHANGE == Exchange.BITTREX:
|
||||||
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
||||||
|
|
||||||
|
|
||||||
@ -191,11 +191,11 @@ def get_markets() -> List[str]:
|
|||||||
Returns all available markets
|
Returns all available markets
|
||||||
:return: list of all available pairs
|
:return: list of all available pairs
|
||||||
"""
|
"""
|
||||||
if cur_exchange == Exchange.POLONIEX:
|
if EXCHANGE == Exchange.POLONIEX:
|
||||||
# TODO: implement
|
# TODO: implement
|
||||||
raise NotImplemented('Not implemented')
|
raise NotImplemented('Not implemented')
|
||||||
elif cur_exchange == Exchange. BITTREX:
|
elif EXCHANGE == Exchange. BITTREX:
|
||||||
data = _api.get_markets()
|
data = _API.get_markets()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||||
|
67
main.py
67
main.py
@ -5,19 +5,17 @@ import logging
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import JSONDecodeError
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from requests import ConnectionError
|
|
||||||
from wrapt import synchronized
|
from wrapt import synchronized
|
||||||
|
|
||||||
import exchange
|
import exchange
|
||||||
import persistence
|
import persistence
|
||||||
from rpc import telegram
|
|
||||||
from analyze import get_buy_signal
|
|
||||||
from persistence import Trade
|
from persistence import Trade
|
||||||
from misc import conf_schema
|
from analyze import get_buy_signal
|
||||||
|
from misc import CONF_SCHEMA
|
||||||
|
from rpc import telegram
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
@ -35,8 +33,10 @@ class State(enum.Enum):
|
|||||||
TERMINATE = 2
|
TERMINATE = 2
|
||||||
|
|
||||||
|
|
||||||
_conf = {}
|
_CONF = {}
|
||||||
_cur_state = State.RUNNING
|
|
||||||
|
# Current application state
|
||||||
|
_STATE = State.RUNNING
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
@synchronized
|
||||||
@ -46,8 +46,8 @@ def update_state(state: State) -> None:
|
|||||||
:param state: new state
|
:param state: new state
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
global _cur_state
|
global _STATE
|
||||||
_cur_state = state
|
_STATE = state
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
@synchronized
|
||||||
@ -56,7 +56,7 @@ def get_state() -> State:
|
|||||||
Gets the current application state
|
Gets the current application state
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return _cur_state
|
return _STATE
|
||||||
|
|
||||||
|
|
||||||
def _process() -> None:
|
def _process() -> None:
|
||||||
@ -67,10 +67,10 @@ def _process() -> None:
|
|||||||
"""
|
"""
|
||||||
# Query trades from persistence layer
|
# Query trades from persistence layer
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if len(trades) < _conf['max_open_trades']:
|
if len(trades) < _CONF['max_open_trades']:
|
||||||
try:
|
try:
|
||||||
# Create entity and execute trade
|
# Create entity and execute trade
|
||||||
trade = create_trade(float(_conf['stake_amount']), exchange.cur_exchange)
|
trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE)
|
||||||
if trade:
|
if trade:
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
else:
|
else:
|
||||||
@ -80,14 +80,17 @@ def _process() -> None:
|
|||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
if close_trade_if_fulfilled(trade):
|
if close_trade_if_fulfilled(trade):
|
||||||
logger.info('No open orders found and trade is fulfilled. Marking %s as closed ...', trade)
|
logger.info(
|
||||||
|
'No open orders found and trade is fulfilled. Marking %s as closed ...',
|
||||||
|
trade
|
||||||
|
)
|
||||||
|
|
||||||
for trade in filter(lambda t: t.is_open, trades):
|
for trade in filter(lambda t: t.is_open, trades):
|
||||||
# Check if there is already an open order for this trade
|
# Check if there is already an open order for this trade
|
||||||
orders = exchange.get_open_orders(trade.pair)
|
orders = exchange.get_open_orders(trade.pair)
|
||||||
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
||||||
if orders:
|
if orders:
|
||||||
msg = 'There exists an open order for {}: Order(total={}, remaining={}, type={}, id={})' \
|
msg = 'There is an open order for {}: Order(total={}, remaining={}, type={}, id={})' \
|
||||||
.format(
|
.format(
|
||||||
trade,
|
trade,
|
||||||
round(orders[0]['amount'], 8),
|
round(orders[0]['amount'], 8),
|
||||||
@ -156,19 +159,19 @@ def handle_trade(trade: Trade) -> None:
|
|||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
current_profit = 100.0 * ((current_rate - trade.open_rate) / trade.open_rate)
|
current_profit = 100.0 * ((current_rate - trade.open_rate) / trade.open_rate)
|
||||||
|
|
||||||
if 'stoploss' in _conf and current_profit < float(_conf['stoploss']) * 100.0:
|
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']) * 100.0:
|
||||||
logger.debug('Stop loss hit.')
|
logger.debug('Stop loss hit.')
|
||||||
execute_sell(trade, current_rate)
|
execute_sell(trade, current_rate)
|
||||||
return
|
return
|
||||||
|
|
||||||
for duration, threshold in sorted(_conf['minimal_roi'].items()):
|
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||||
duration, threshold = float(duration), float(threshold)
|
duration, threshold = float(duration), float(threshold)
|
||||||
# Check if time matches and current rate is above threshold
|
# Check if time matches and current rate is above threshold
|
||||||
time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60
|
time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60
|
||||||
if time_diff > duration and current_rate > (1 + threshold) * trade.open_rate:
|
if time_diff > duration and current_rate > (1 + threshold) * trade.open_rate:
|
||||||
execute_sell(trade, current_rate)
|
execute_sell(trade, current_rate)
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit)
|
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.exception('Unable to handle open order')
|
logger.exception('Unable to handle open order')
|
||||||
@ -182,10 +185,12 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
|||||||
:param _exchange: exchange to use
|
:param _exchange: exchange to use
|
||||||
"""
|
"""
|
||||||
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
||||||
whitelist = _conf[_exchange.name.lower()]['pair_whitelist']
|
whitelist = _CONF[_exchange.name.lower()]['pair_whitelist']
|
||||||
# Check if btc_amount is fulfilled
|
# Check if btc_amount is fulfilled
|
||||||
if exchange.get_balance(_conf['stake_currency']) < stake_amount:
|
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||||
raise ValueError('stake amount is not fulfilled (currency={}'.format(_conf['stake_currency']))
|
raise ValueError(
|
||||||
|
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency'])
|
||||||
|
)
|
||||||
|
|
||||||
# Remove currently opened and latest pairs from whitelist
|
# Remove currently opened and latest pairs from whitelist
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
@ -200,9 +205,9 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
|||||||
raise ValueError('No pair in whitelist')
|
raise ValueError('No pair in whitelist')
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
# Pick pair based on StochRSI buy signals
|
||||||
for p in whitelist:
|
for _pair in whitelist:
|
||||||
if get_buy_signal(p):
|
if get_buy_signal(_pair):
|
||||||
pair = p
|
pair = _pair
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@ -230,20 +235,17 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
|||||||
is_open=True)
|
is_open=True)
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict, db_url: Optional[str]=None) -> None:
|
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes all modules and updates the config
|
Initializes all modules and updates the config
|
||||||
:param config: config as dict
|
:param config: config as dict
|
||||||
:param db_url: database connector string for sqlalchemy (Optional)
|
:param db_url: database connector string for sqlalchemy (Optional)
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
global _conf
|
|
||||||
|
|
||||||
# Initialize all modules
|
# Initialize all modules
|
||||||
telegram.init(config)
|
telegram.init(config)
|
||||||
persistence.init(config, db_url)
|
persistence.init(config, db_url)
|
||||||
exchange.init(config)
|
exchange.init(config)
|
||||||
_conf.update(config)
|
|
||||||
|
|
||||||
|
|
||||||
def app(config: dict) -> None:
|
def app(config: dict) -> None:
|
||||||
@ -264,12 +266,12 @@ def app(config: dict) -> None:
|
|||||||
try:
|
try:
|
||||||
_process()
|
_process()
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
except (ConnectionError, JSONDecodeError, ValueError) as error:
|
except (ConnectionError, json.JSONDecodeError, ValueError) as error:
|
||||||
msg = 'Got {} during _process()'.format(error.__class__.__name__)
|
msg = 'Got {} during _process()'.format(error.__class__.__name__)
|
||||||
logger.exception(msg)
|
logger.exception(msg)
|
||||||
finally:
|
finally:
|
||||||
time.sleep(25)
|
time.sleep(25)
|
||||||
except (RuntimeError, JSONDecodeError):
|
except (RuntimeError, json.JSONDecodeError):
|
||||||
telegram.send_msg(
|
telegram.send_msg(
|
||||||
'*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())
|
'*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())
|
||||||
)
|
)
|
||||||
@ -280,7 +282,6 @@ def app(config: dict) -> None:
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
with open('config.json') as file:
|
with open('config.json') as file:
|
||||||
conf = json.load(file)
|
_CONF = json.load(file)
|
||||||
validate(conf, conf_schema)
|
validate(_CONF, CONF_SCHEMA)
|
||||||
app(conf)
|
app(_CONF)
|
||||||
|
|
||||||
|
2
misc.py
2
misc.py
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
conf_schema = {
|
CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
||||||
|
@ -11,12 +11,12 @@ from sqlalchemy.types import Enum
|
|||||||
import exchange
|
import exchange
|
||||||
|
|
||||||
|
|
||||||
_conf = {}
|
_CONF = {}
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict, db_url: Optional[str]=None) -> None:
|
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
registers all known command handlers
|
registers all known command handlers
|
||||||
@ -25,9 +25,9 @@ def init(config: dict, db_url: Optional[str]=None) -> None:
|
|||||||
:param db_url: database connector string for sqlalchemy (Optional)
|
:param db_url: database connector string for sqlalchemy (Optional)
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
_conf.update(config)
|
_CONF.update(config)
|
||||||
if not db_url:
|
if not db_url:
|
||||||
if _conf.get('dry_run', False):
|
if _CONF.get('dry_run', False):
|
||||||
db_url = 'sqlite:///tradesv2.dry_run.sqlite'
|
db_url = 'sqlite:///tradesv2.dry_run.sqlite'
|
||||||
else:
|
else:
|
||||||
db_url = 'sqlite:///tradesv2.sqlite'
|
db_url = 'sqlite:///tradesv2.sqlite'
|
||||||
@ -56,12 +56,16 @@ class Trade(Base):
|
|||||||
open_order_id = Column(String)
|
open_order_id = Column(String)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
if self.is_open:
|
||||||
|
open_since = 'closed'
|
||||||
|
else:
|
||||||
|
open_since = round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
|
||||||
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
|
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
|
||||||
self.id,
|
self.id,
|
||||||
self.pair,
|
self.pair,
|
||||||
self.amount,
|
self.amount,
|
||||||
self.open_rate,
|
self.open_rate,
|
||||||
'closed' if not self.is_open else round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
|
open_since
|
||||||
)
|
)
|
||||||
|
|
||||||
def exec_sell_order(self, rate: float, amount: float) -> float:
|
def exec_sell_order(self, rate: float, amount: float) -> float:
|
||||||
@ -83,4 +87,3 @@ class Trade(Base):
|
|||||||
# Flush changes
|
# Flush changes
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
return profit
|
return profit
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ urllib3==1.22
|
|||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.20.3
|
pandas==0.20.3
|
||||||
matplotlib==2.0.2
|
matplotlib==2.0.2
|
||||||
PYQT5==5.9
|
|
||||||
scikit-learn==0.19.0
|
scikit-learn==0.19.0
|
||||||
scipy==0.19.1
|
scipy==0.19.1
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
TA-Lib==0.4.10
|
TA-Lib==0.4.10
|
||||||
|
#PYQT5==5.9
|
@ -18,7 +18,7 @@ logging.getLogger('telegram').setLevel(logging.INFO)
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_updater = None
|
_updater = None
|
||||||
_conf = {}
|
_CONF = {}
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict) -> None:
|
def init(config: dict) -> None:
|
||||||
@ -32,7 +32,7 @@ def init(config: dict) -> None:
|
|||||||
global _updater
|
global _updater
|
||||||
_updater = Updater(token=config['telegram']['token'], workers=0)
|
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||||
|
|
||||||
_conf.update(config)
|
_CONF.update(config)
|
||||||
|
|
||||||
# Register command handler and start telegram message polling
|
# Register command handler and start telegram message polling
|
||||||
handles = [
|
handles = [
|
||||||
@ -51,8 +51,10 @@ def init(config: dict) -> None:
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
read_latency=60,
|
read_latency=60,
|
||||||
)
|
)
|
||||||
logger.info('rpc.telegram is listening for following commands: {}'
|
logger.info(
|
||||||
.format([h.command for h in handles]))
|
'rpc.telegram is listening for following commands: %s',
|
||||||
|
[h.command for h in handles]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
||||||
@ -62,12 +64,12 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
|
|||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
bot, update = (args[0], args[1]) if args else (kwargs['bot'], kwargs['update'])
|
bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1]
|
||||||
|
|
||||||
if not isinstance(bot, Bot) or not isinstance(update, Update):
|
if not isinstance(bot, Bot) or not isinstance(update, Update):
|
||||||
raise ValueError('Received invalid Arguments: {}'.format(*args))
|
raise ValueError('Received invalid Arguments: {}'.format(*args))
|
||||||
|
|
||||||
chat_id = int(_conf['telegram']['chat_id'])
|
chat_id = int(_CONF['telegram']['chat_id'])
|
||||||
if int(update.message.chat_id) == chat_id:
|
if int(update.message.chat_id) == chat_id:
|
||||||
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
|
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
|
||||||
return command_handler(*args, **kwargs)
|
return command_handler(*args, **kwargs)
|
||||||
@ -88,7 +90,7 @@ def _status(bot: Bot, update: Update) -> None:
|
|||||||
# Fetch open trade
|
# Fetch open trade
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
from main import get_state, State
|
from main import get_state, State
|
||||||
if not get_state() == State.RUNNING:
|
if get_state() != State.RUNNING:
|
||||||
send_msg('*Status:* `trader is not running`', bot=bot)
|
send_msg('*Status:* `trader is not running`', bot=bot)
|
||||||
elif not trades:
|
elif not trades:
|
||||||
send_msg('*Status:* `no active order`', bot=bot)
|
send_msg('*Status:* `no active order`', bot=bot)
|
||||||
@ -100,6 +102,10 @@ def _status(bot: Bot, update: Update) -> None:
|
|||||||
orders = exchange.get_open_orders(trade.pair)
|
orders = exchange.get_open_orders(trade.pair)
|
||||||
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
||||||
order = orders[0] if orders else None
|
order = orders[0] if orders else None
|
||||||
|
|
||||||
|
fmt_close_profit = '{:.2f}%'.format(
|
||||||
|
round(trade.close_profit, 2)
|
||||||
|
) if trade.close_profit else None
|
||||||
message = """
|
message = """
|
||||||
*Trade ID:* `{trade_id}`
|
*Trade ID:* `{trade_id}`
|
||||||
*Current Pair:* [{pair}]({market_url})
|
*Current Pair:* [{pair}]({market_url})
|
||||||
@ -120,7 +126,7 @@ def _status(bot: Bot, update: Update) -> None:
|
|||||||
close_rate=trade.close_rate,
|
close_rate=trade.close_rate,
|
||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
amount=round(trade.amount, 8),
|
amount=round(trade.amount, 8),
|
||||||
close_profit='{}%'.format(round(trade.close_profit, 2)) if trade.close_profit else None,
|
close_profit=fmt_close_profit,
|
||||||
current_profit=round(current_profit, 2),
|
current_profit=round(current_profit, 2),
|
||||||
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None,
|
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None,
|
||||||
)
|
)
|
||||||
@ -297,7 +303,7 @@ def _performance(bot: Bot, update: Update) -> None:
|
|||||||
send_msg(message, parse_mode=ParseMode.HTML)
|
send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
def send_msg(msg: str, bot: Bot=None, parse_mode: ParseMode=ParseMode.MARKDOWN) -> None:
|
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||||
"""
|
"""
|
||||||
Send given markdown message
|
Send given markdown message
|
||||||
:param msg: message
|
:param msg: message
|
||||||
@ -305,15 +311,18 @@ def send_msg(msg: str, bot: Bot=None, parse_mode: ParseMode=ParseMode.MARKDOWN)
|
|||||||
:param parse_mode: telegram parse mode
|
:param parse_mode: telegram parse mode
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if _conf['telegram'].get('enabled', False):
|
if _CONF['telegram'].get('enabled', False):
|
||||||
try:
|
try:
|
||||||
bot = bot or _updater.bot
|
bot = bot or _updater.bot
|
||||||
try:
|
try:
|
||||||
bot.send_message(_conf['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
except NetworkError as error:
|
except NetworkError as error:
|
||||||
# Sometimes the telegram server resets the current connection,
|
# Sometimes the telegram server resets the current connection,
|
||||||
# if this is the case we send the message again.
|
# if this is the case we send the message again.
|
||||||
logger.warning('Got Telegram NetworkError: %s! Trying one more time.', error.message)
|
logger.warning(
|
||||||
bot.send_message(_conf['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||||
|
error.message
|
||||||
|
)
|
||||||
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('Exception occurred within Telegram API')
|
logger.exception('Exception occurred within Telegram API')
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
import os
|
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
|
|
||||||
import exchange
|
import exchange
|
||||||
from main import create_trade, handle_trade, close_trade_if_fulfilled, init
|
from main import create_trade, handle_trade, close_trade_if_fulfilled, init
|
||||||
from misc import conf_schema
|
from misc import CONF_SCHEMA
|
||||||
from persistence import Trade
|
from persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ class TestMain(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def test_1_create_trade(self):
|
def test_1_create_trade(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True) as buy_signal:
|
with patch('main.get_buy_signal', side_effect=lambda _: True) as buy_signal:
|
||||||
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
|
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
|
||||||
with patch.multiple('main.exchange',
|
with patch.multiple('main.exchange',
|
||||||
@ -68,7 +67,7 @@ class TestMain(unittest.TestCase):
|
|||||||
buy_signal.assert_called_once_with('BTC_ETH')
|
buy_signal.assert_called_once_with('BTC_ETH')
|
||||||
|
|
||||||
def test_2_handle_trade(self):
|
def test_2_handle_trade(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
|
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
|
||||||
with patch.multiple('main.exchange',
|
with patch.multiple('main.exchange',
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
@ -86,7 +85,7 @@ class TestMain(unittest.TestCase):
|
|||||||
self.assertEqual(trade.open_order_id, 'dry_run')
|
self.assertEqual(trade.open_order_id, 'dry_run')
|
||||||
|
|
||||||
def test_3_close_trade(self):
|
def test_3_close_trade(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||||
self.assertTrue(trade)
|
self.assertTrue(trade)
|
||||||
|
|
||||||
@ -99,7 +98,7 @@ class TestMain(unittest.TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
validate(cls.conf, conf_schema)
|
validate(cls.conf, CONF_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import os
|
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from telegram import Bot, Update, Message, Chat
|
from telegram import Bot, Update, Message, Chat
|
||||||
|
|
||||||
import exchange
|
import exchange
|
||||||
from main import init, create_trade, update_state, State, get_state
|
from main import init, create_trade, update_state, State, get_state
|
||||||
from misc import conf_schema
|
from misc import CONF_SCHEMA
|
||||||
from persistence import Trade
|
from persistence import Trade
|
||||||
from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
|
from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
|
||||||
|
|
||||||
@ -51,10 +50,10 @@ class TestTelegram(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def test_1_status_handle(self):
|
def test_1_status_handle(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
with patch.multiple('main.telegram', _conf=self.conf, init=MagicMock(), send_msg=msg_mock):
|
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||||
with patch.multiple('main.exchange',
|
with patch.multiple('main.exchange',
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.07256061,
|
'bid': 0.07256061,
|
||||||
@ -75,10 +74,10 @@ class TestTelegram(unittest.TestCase):
|
|||||||
self.assertIn('[BTC_ETH]', msg_mock.call_args_list[-1][0][0])
|
self.assertIn('[BTC_ETH]', msg_mock.call_args_list[-1][0][0])
|
||||||
|
|
||||||
def test_2_profit_handle(self):
|
def test_2_profit_handle(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
with patch.multiple('main.telegram', _conf=self.conf, init=MagicMock(), send_msg=msg_mock):
|
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||||
with patch.multiple('main.exchange',
|
with patch.multiple('main.exchange',
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.07256061,
|
'bid': 0.07256061,
|
||||||
@ -104,10 +103,10 @@ class TestTelegram(unittest.TestCase):
|
|||||||
self.assertIn('(100.00%)', msg_mock.call_args_list[-1][0][0])
|
self.assertIn('(100.00%)', msg_mock.call_args_list[-1][0][0])
|
||||||
|
|
||||||
def test_3_forcesell_handle(self):
|
def test_3_forcesell_handle(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
with patch.multiple('main.telegram', _conf=self.conf, init=MagicMock(), send_msg=msg_mock):
|
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||||
with patch.multiple('main.exchange',
|
with patch.multiple('main.exchange',
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.07256061,
|
'bid': 0.07256061,
|
||||||
@ -131,10 +130,10 @@ class TestTelegram(unittest.TestCase):
|
|||||||
self.assertIn('0.072561', msg_mock.call_args_list[-1][0][0])
|
self.assertIn('0.072561', msg_mock.call_args_list[-1][0][0])
|
||||||
|
|
||||||
def test_4_performance_handle(self):
|
def test_4_performance_handle(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
with patch.multiple('main.telegram', _conf=self.conf, init=MagicMock(), send_msg=msg_mock):
|
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||||
with patch.multiple('main.exchange',
|
with patch.multiple('main.exchange',
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.07256061,
|
'bid': 0.07256061,
|
||||||
@ -161,9 +160,9 @@ class TestTelegram(unittest.TestCase):
|
|||||||
self.assertIn('BTC_ETH 100.00%', msg_mock.call_args_list[-1][0][0])
|
self.assertIn('BTC_ETH 100.00%', msg_mock.call_args_list[-1][0][0])
|
||||||
|
|
||||||
def test_5_start_handle(self):
|
def test_5_start_handle(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
with patch.multiple('main.telegram', _conf=self.conf, init=MagicMock(), send_msg=msg_mock):
|
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||||
init(self.conf, 'sqlite://')
|
init(self.conf, 'sqlite://')
|
||||||
|
|
||||||
update_state(State.PAUSED)
|
update_state(State.PAUSED)
|
||||||
@ -173,9 +172,9 @@ class TestTelegram(unittest.TestCase):
|
|||||||
self.assertEqual(msg_mock.call_count, 0)
|
self.assertEqual(msg_mock.call_count, 0)
|
||||||
|
|
||||||
def test_6_stop_handle(self):
|
def test_6_stop_handle(self):
|
||||||
with patch.dict('main._conf', self.conf):
|
with patch.dict('main._CONF', self.conf):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
with patch.multiple('main.telegram', _conf=self.conf, init=MagicMock(), send_msg=msg_mock):
|
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||||
init(self.conf, 'sqlite://')
|
init(self.conf, 'sqlite://')
|
||||||
|
|
||||||
update_state(State.RUNNING)
|
update_state(State.RUNNING)
|
||||||
@ -191,7 +190,7 @@ class TestTelegram(unittest.TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
validate(cls.conf, conf_schema)
|
validate(cls.conf, CONF_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
Loading…
Reference in New Issue
Block a user