Merge branch 'master' into parabolic-sar
This commit is contained in:
commit
44b9da5159
@ -29,7 +29,7 @@ See the example below:
|
|||||||
"1440": 0.01, # Sell after 24 hours if there is at least 1% profit
|
"1440": 0.01, # Sell after 24 hours if there is at least 1% profit
|
||||||
"720": 0.02, # Sell after 12 hours if there is at least 2% profit
|
"720": 0.02, # Sell after 12 hours if there is at least 2% profit
|
||||||
"360": 0.02, # Sell after 6 hours if there is at least 2% profit
|
"360": 0.02, # Sell after 6 hours if there is at least 2% profit
|
||||||
"0": 0.025 # Sell immediatly if there is at least 2.5% profit
|
"0": 0.025 # Sell immediately if there is at least 2.5% profit
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ The other values should be self-explanatory,
|
|||||||
if not feel free to raise a github issue.
|
if not feel free to raise a github issue.
|
||||||
|
|
||||||
##### Prerequisites
|
##### Prerequisites
|
||||||
* python3
|
* python3.6
|
||||||
* sqlite
|
* sqlite
|
||||||
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
||||||
|
|
||||||
|
42
analyze.py
42
analyze.py
@ -4,7 +4,7 @@ import logging
|
|||||||
import arrow
|
import arrow
|
||||||
import requests
|
import requests
|
||||||
from pandas.io.json import json_normalize
|
from pandas.io.json import json_normalize
|
||||||
from stockstats import StockDataFrame
|
from pandas import DataFrame
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ logging.basicConfig(level=logging.DEBUG,
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_ticker_dataframe(pair):
|
def get_ticker_dataframe(pair: str) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Analyses the trend for the given pair
|
Analyses the trend for the given pair
|
||||||
:param pair: pair as str in format BTC_ETH or BTC-ETH
|
:param pair: pair as str in format BTC_ETH or BTC-ETH
|
||||||
:return: StockDataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
minimum_date = arrow.now() - timedelta(hours=6)
|
minimum_date = arrow.now() - timedelta(hours=6)
|
||||||
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
|
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
|
||||||
@ -41,35 +41,37 @@ def get_ticker_dataframe(pair):
|
|||||||
'low': t['L'],
|
'low': t['L'],
|
||||||
'date': t['T'],
|
'date': t['T'],
|
||||||
} for t in sorted(data['result'], key=lambda k: k['T']) if arrow.get(t['T']) > minimum_date]
|
} for t in sorted(data['result'], key=lambda k: k['T']) if arrow.get(t['T']) > minimum_date]
|
||||||
dataframe = StockDataFrame(json_normalize(data))
|
dataframe = DataFrame(json_normalize(data))
|
||||||
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.2)
|
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.2)
|
||||||
|
|
||||||
# calculate StochRSI
|
# calculate StochRSI
|
||||||
window = 14
|
stochrsi = ta.STOCHRSI(dataframe)
|
||||||
rsi = dataframe['rsi_{}'.format(window)]
|
dataframe['stochrsi'] = stochrsi['fastd'] # values between 0-100, not 0-1
|
||||||
rolling = rsi.rolling(window=window, center=False)
|
|
||||||
low = rolling.min()
|
macd = ta.MACD(dataframe)
|
||||||
high = rolling.max()
|
dataframe['macd'] = macd['macd']
|
||||||
dataframe['stochrsi'] = (rsi - low) / (high - low)
|
dataframe['macds'] = macd['macdsignal']
|
||||||
|
dataframe['macdh'] = macd['macdhist']
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def populate_trends(dataframe):
|
def populate_trends(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Populates the trends for the given dataframe
|
Populates the trends for the given dataframe
|
||||||
:param dataframe: StockDataFrame
|
:param dataframe: DataFrame
|
||||||
:return: StockDataFrame with populated trends
|
:return: DataFrame with populated trends
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(dataframe['stochrsi'] < 0.20)
|
(dataframe['stochrsi'] < 20)
|
||||||
& (dataframe['close_30_ema'] > (1 + 0.0025) * dataframe['close_60_ema']),
|
& (dataframe['close_30_ema'] > (1 + 0.0025) * dataframe['close_60_ema']),
|
||||||
'underpriced'
|
'underpriced'
|
||||||
] = 1
|
] = 1
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(dataframe['stochrsi'] < 0.20)
|
(dataframe['stochrsi'] < 20)
|
||||||
& (dataframe['macd'] > dataframe['macds'])
|
& (dataframe['macd'] > dataframe['macds'])
|
||||||
& (dataframe['close'] > dataframe['sar']),
|
& (dataframe['close'] > dataframe['sar']),
|
||||||
'underpriced'
|
'underpriced'
|
||||||
@ -78,7 +80,7 @@ def populate_trends(dataframe):
|
|||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def get_buy_signal(pair):
|
def get_buy_signal(pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Calculates a buy signal based on StochRSI indicator
|
Calculates a buy signal based on StochRSI indicator
|
||||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||||
@ -98,10 +100,10 @@ def get_buy_signal(pair):
|
|||||||
return signal
|
return signal
|
||||||
|
|
||||||
|
|
||||||
def plot_dataframe(dataframe, pair):
|
def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
||||||
"""
|
"""
|
||||||
Plots the given dataframe
|
Plots the given dataframe
|
||||||
:param dataframe: StockDataFrame
|
:param dataframe: DataFrame
|
||||||
:param pair: pair as str
|
:param pair: pair as str
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
@ -129,8 +131,8 @@ def plot_dataframe(dataframe, pair):
|
|||||||
ax2.legend()
|
ax2.legend()
|
||||||
|
|
||||||
ax3.plot(dataframe.index.values, dataframe['stochrsi'], label='StochRSI')
|
ax3.plot(dataframe.index.values, dataframe['stochrsi'], label='StochRSI')
|
||||||
ax3.plot(dataframe.index.values, [0.80] * len(dataframe.index.values))
|
ax3.plot(dataframe.index.values, [80] * len(dataframe.index.values))
|
||||||
ax3.plot(dataframe.index.values, [0.20] * len(dataframe.index.values))
|
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
|
||||||
ax3.legend()
|
ax3.legend()
|
||||||
|
|
||||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
||||||
|
69
exchange.py
69
exchange.py
@ -1,5 +1,7 @@
|
|||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from bittrex.bittrex import Bittrex
|
from bittrex.bittrex import Bittrex
|
||||||
from poloniex import Poloniex
|
from poloniex import Poloniex
|
||||||
from wrapt import synchronized
|
from wrapt import synchronized
|
||||||
@ -9,18 +11,6 @@ logger = logging.getLogger(__name__)
|
|||||||
_exchange_api = None
|
_exchange_api = None
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
|
||||||
def get_exchange_api(conf):
|
|
||||||
"""
|
|
||||||
Returns the current exchange api or instantiates a new one
|
|
||||||
:return: exchange.ApiWrapper
|
|
||||||
"""
|
|
||||||
global _exchange_api
|
|
||||||
if not _exchange_api:
|
|
||||||
_exchange_api = ApiWrapper(conf)
|
|
||||||
return _exchange_api
|
|
||||||
|
|
||||||
|
|
||||||
class Exchange(enum.Enum):
|
class Exchange(enum.Enum):
|
||||||
POLONIEX = 0
|
POLONIEX = 0
|
||||||
BITTREX = 1
|
BITTREX = 1
|
||||||
@ -33,9 +23,11 @@ class ApiWrapper(object):
|
|||||||
* Bittrex
|
* Bittrex
|
||||||
* Poloniex (partly)
|
* Poloniex (partly)
|
||||||
"""
|
"""
|
||||||
def __init__(self, config):
|
def __init__(self, config: dict):
|
||||||
"""
|
"""
|
||||||
Initializes the ApiWrapper with the given config, it does not validate those values.
|
Initializes the ApiWrapper with the given config,
|
||||||
|
it does basic validation whether the specified
|
||||||
|
exchange and pairs are valid.
|
||||||
:param config: dict
|
:param config: dict
|
||||||
"""
|
"""
|
||||||
self.dry_run = config['dry_run']
|
self.dry_run = config['dry_run']
|
||||||
@ -53,14 +45,21 @@ class ApiWrapper(object):
|
|||||||
self.api = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
|
self.api = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
|
||||||
else:
|
else:
|
||||||
self.api = None
|
self.api = None
|
||||||
|
raise RuntimeError('No exchange specified. Aborting!')
|
||||||
|
|
||||||
def buy(self, pair, rate, amount):
|
# Check if all pairs are available
|
||||||
|
markets = self.get_markets()
|
||||||
|
for pair in config[self.exchange.name.lower()]['pair_whitelist']:
|
||||||
|
if pair not in markets:
|
||||||
|
raise RuntimeError('Pair {} is not available at Poloniex'.format(pair))
|
||||||
|
|
||||||
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
"""
|
"""
|
||||||
Places a limit buy order.
|
Places a limit buy order.
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
:param pair: Pair as str, format: BTC_ETH
|
||||||
:param rate: Rate limit for order
|
:param rate: Rate limit for order
|
||||||
:param amount: The amount to purchase
|
:param amount: The amount to purchase
|
||||||
:return: None
|
:return: order_id of the placed buy order
|
||||||
"""
|
"""
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
pass
|
pass
|
||||||
@ -73,7 +72,7 @@ class ApiWrapper(object):
|
|||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
return data['result']['uuid']
|
return data['result']['uuid']
|
||||||
|
|
||||||
def sell(self, pair, rate, amount):
|
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||||
"""
|
"""
|
||||||
Places a limit sell order.
|
Places a limit sell order.
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
:param pair: Pair as str, format: BTC_ETH
|
||||||
@ -92,7 +91,7 @@ class ApiWrapper(object):
|
|||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
return data['result']['uuid']
|
return data['result']['uuid']
|
||||||
|
|
||||||
def get_balance(self, currency):
|
def get_balance(self, currency: str) -> float:
|
||||||
"""
|
"""
|
||||||
Get account balance.
|
Get account balance.
|
||||||
:param currency: currency as str, format: BTC
|
:param currency: currency as str, format: BTC
|
||||||
@ -109,7 +108,7 @@ class ApiWrapper(object):
|
|||||||
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)
|
||||||
|
|
||||||
def get_ticker(self, pair):
|
def get_ticker(self, pair: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Get Ticker for given pair.
|
Get Ticker for given pair.
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
@ -132,7 +131,7 @@ class ApiWrapper(object):
|
|||||||
'last': float(data['result']['Last']),
|
'last': float(data['result']['Last']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def cancel_order(self, order_id):
|
def cancel_order(self, order_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
Cancel order for given order_id
|
Cancel order for given order_id
|
||||||
:param order_id: id as str
|
:param order_id: id as str
|
||||||
@ -147,7 +146,7 @@ class ApiWrapper(object):
|
|||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
|
||||||
def get_open_orders(self, pair):
|
def get_open_orders(self, pair: str) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Get all open orders for given pair.
|
Get all open orders for given pair.
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
@ -170,7 +169,7 @@ class ApiWrapper(object):
|
|||||||
'remaining': entry['QuantityRemaining'],
|
'remaining': entry['QuantityRemaining'],
|
||||||
} for entry in data['result']]
|
} for entry in data['result']]
|
||||||
|
|
||||||
def get_pair_detail_url(self, pair):
|
def get_pair_detail_url(self, pair: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the market detail url for the given pair
|
Returns the market detail url for the given pair
|
||||||
:param pair: pair as str, format: BTC_ANT
|
:param pair: pair as str, format: BTC_ANT
|
||||||
@ -180,3 +179,29 @@ class ApiWrapper(object):
|
|||||||
raise NotImplemented('Not implemented')
|
raise NotImplemented('Not implemented')
|
||||||
elif self.exchange == Exchange.BITTREX:
|
elif self.exchange == Exchange.BITTREX:
|
||||||
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
||||||
|
|
||||||
|
def get_markets(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns all available markets
|
||||||
|
:return: list of all available pairs
|
||||||
|
"""
|
||||||
|
if self.exchange == Exchange.POLONIEX:
|
||||||
|
# TODO: implement
|
||||||
|
raise NotImplemented('Not implemented')
|
||||||
|
elif self.exchange == Exchange. BITTREX:
|
||||||
|
data = self.api.get_markets()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||||
|
|
||||||
|
|
||||||
|
@synchronized
|
||||||
|
def get_exchange_api(conf: dict) -> ApiWrapper:
|
||||||
|
"""
|
||||||
|
Returns the current exchange api or instantiates a new one
|
||||||
|
:return: exchange.ApiWrapper
|
||||||
|
"""
|
||||||
|
global _exchange_api
|
||||||
|
if not _exchange_api:
|
||||||
|
_exchange_api = ApiWrapper(conf)
|
||||||
|
return _exchange_api
|
||||||
|
40
main.py
40
main.py
@ -5,11 +5,13 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from requests import ConnectionError
|
from requests import ConnectionError
|
||||||
from wrapt import synchronized
|
from wrapt import synchronized
|
||||||
from analyze import get_buy_signal
|
from analyze import get_buy_signal
|
||||||
from persistence import Trade, Session
|
from persistence import Trade, Session
|
||||||
from exchange import get_exchange_api
|
from exchange import get_exchange_api, Exchange
|
||||||
from rpc.telegram import TelegramHandler
|
from rpc.telegram import TelegramHandler
|
||||||
from utils import get_conf
|
from utils import get_conf
|
||||||
|
|
||||||
@ -32,11 +34,11 @@ class TradeThread(threading.Thread):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self._should_stop = False
|
self._should_stop = False
|
||||||
|
|
||||||
def stop(self):
|
def stop(self) -> None:
|
||||||
""" stops the trader thread """
|
""" stops the trader thread """
|
||||||
self._should_stop = True
|
self._should_stop = True
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
"""
|
"""
|
||||||
Threaded main function
|
Threaded main function
|
||||||
:return: None
|
:return: None
|
||||||
@ -53,14 +55,14 @@ class TradeThread(threading.Thread):
|
|||||||
finally:
|
finally:
|
||||||
Session.flush()
|
Session.flush()
|
||||||
time.sleep(25)
|
time.sleep(25)
|
||||||
except (RuntimeError, JSONDecodeError) as e:
|
except (RuntimeError, JSONDecodeError):
|
||||||
TelegramHandler.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
TelegramHandler.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
||||||
logger.exception('RuntimeError. Stopping trader ...')
|
logger.exception('RuntimeError. Stopping trader ...')
|
||||||
finally:
|
finally:
|
||||||
TelegramHandler.send_msg('*Status:* `Trader has stopped`')
|
TelegramHandler.send_msg('*Status:* `Trader has stopped`')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _process():
|
def _process() -> None:
|
||||||
"""
|
"""
|
||||||
Queries the persistence layer for open trades and handles them,
|
Queries the persistence layer for open trades and handles them,
|
||||||
otherwise a new trade is created.
|
otherwise a new trade is created.
|
||||||
@ -69,11 +71,16 @@ class TradeThread(threading.Thread):
|
|||||||
# 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) < CONFIG['max_open_trades']:
|
if len(trades) < CONFIG['max_open_trades']:
|
||||||
# Create entity and execute trade
|
|
||||||
try:
|
try:
|
||||||
Session.add(create_trade(float(CONFIG['stake_amount']), api_wrapper.exchange))
|
# Create entity and execute trade
|
||||||
|
trade = create_trade(float(CONFIG['stake_amount']), api_wrapper.exchange)
|
||||||
|
if trade:
|
||||||
|
Session.add(trade)
|
||||||
|
else:
|
||||||
|
logging.info('Got no buy signal...')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.exception('ValueError during trade creation')
|
logger.exception('Unable to create trade')
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# Check if there is already an open order for this trade
|
# Check if there is already an open order for this trade
|
||||||
orders = api_wrapper.get_open_orders(trade.pair)
|
orders = api_wrapper.get_open_orders(trade.pair)
|
||||||
@ -102,8 +109,9 @@ class TradeThread(threading.Thread):
|
|||||||
# Initial stopped TradeThread instance
|
# Initial stopped TradeThread instance
|
||||||
_instance = TradeThread()
|
_instance = TradeThread()
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
@synchronized
|
||||||
def get_instance(recreate=False):
|
def get_instance(recreate: bool=False) -> TradeThread:
|
||||||
"""
|
"""
|
||||||
Get the current instance of this thread. This is a singleton.
|
Get the current instance of this thread. This is a singleton.
|
||||||
:param recreate: Must be True if you want to start the instance
|
:param recreate: Must be True if you want to start the instance
|
||||||
@ -111,13 +119,12 @@ def get_instance(recreate=False):
|
|||||||
"""
|
"""
|
||||||
global _instance
|
global _instance
|
||||||
if recreate and not _instance.is_alive():
|
if recreate and not _instance.is_alive():
|
||||||
logger.debug('Creating TradeThread instance')
|
logger.debug('Creating thread instance...')
|
||||||
_should_stop = False
|
|
||||||
_instance = TradeThread()
|
_instance = TradeThread()
|
||||||
return _instance
|
return _instance
|
||||||
|
|
||||||
|
|
||||||
def close_trade_if_fulfilled(trade):
|
def close_trade_if_fulfilled(trade: Trade) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if the trade is closable, and if so it is being closed.
|
Checks if the trade is closable, and if so it is being closed.
|
||||||
:param trade: Trade
|
:param trade: Trade
|
||||||
@ -132,7 +139,7 @@ def close_trade_if_fulfilled(trade):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def handle_trade(trade):
|
def handle_trade(trade: Trade) -> None:
|
||||||
"""
|
"""
|
||||||
Sells the current pair if the threshold is reached and updates the trade record.
|
Sells the current pair if the threshold is reached and updates the trade record.
|
||||||
:return: None
|
:return: None
|
||||||
@ -173,9 +180,10 @@ def handle_trade(trade):
|
|||||||
logger.exception('Unable to handle open order')
|
logger.exception('Unable to handle open order')
|
||||||
|
|
||||||
|
|
||||||
def create_trade(stake_amount: float, exchange):
|
def create_trade(stake_amount: float, exchange: Exchange) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Creates a new trade record with a random pair
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
:param stake_amount: amount of btc to spend
|
:param stake_amount: amount of btc to spend
|
||||||
:param exchange: exchange to use
|
:param exchange: exchange to use
|
||||||
"""
|
"""
|
||||||
@ -203,7 +211,7 @@ def create_trade(stake_amount: float, exchange):
|
|||||||
pair = p
|
pair = p
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise ValueError('No buy signal from pairs: {}'.format(', '.join(whitelist)))
|
return None
|
||||||
|
|
||||||
open_rate = api_wrapper.get_ticker(pair)['ask']
|
open_rate = api_wrapper.get_ticker(pair)['ask']
|
||||||
amount = stake_amount / open_rate
|
amount = stake_amount / open_rate
|
||||||
|
@ -46,7 +46,7 @@ class Trade(Base):
|
|||||||
'closed' if not self.is_open else round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
|
'closed' if not self.is_open else round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
def exec_sell_order(self, rate, amount):
|
def exec_sell_order(self, rate: float, amount: float) -> float:
|
||||||
"""
|
"""
|
||||||
Executes a sell for the given trade and updated the entity.
|
Executes a sell for the given trade and updated the entity.
|
||||||
:param rate: rate to sell for
|
:param rate: rate to sell for
|
||||||
|
@ -11,5 +11,5 @@ matplotlib==2.0.2
|
|||||||
PYQT5==5.9
|
PYQT5==5.9
|
||||||
scikit-learn==0.19.0
|
scikit-learn==0.19.0
|
||||||
scipy==0.19.1
|
scipy==0.19.1
|
||||||
stockstats==0.2.0
|
jsonschema==2.6.0
|
||||||
TA-Lib==0.4.10
|
TA-Lib==0.4.10
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Callable, Any
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import and_, func
|
from sqlalchemy import and_, func
|
||||||
@ -23,7 +24,7 @@ conf = get_conf()
|
|||||||
api_wrapper = get_exchange_api(conf)
|
api_wrapper = get_exchange_api(conf)
|
||||||
|
|
||||||
|
|
||||||
def authorized_only(command_handler):
|
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
Decorator to check if the message comes from the correct chat_id
|
Decorator to check if the message comes from the correct chat_id
|
||||||
:param command_handler: Telegram CommandHandler
|
:param command_handler: Telegram CommandHandler
|
||||||
@ -46,7 +47,7 @@ def authorized_only(command_handler):
|
|||||||
class TelegramHandler(object):
|
class TelegramHandler(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _status(bot, update):
|
def _status(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /status.
|
Handler for /status.
|
||||||
Returns the current TradeThread status
|
Returns the current TradeThread status
|
||||||
@ -97,7 +98,7 @@ class TelegramHandler(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _profit(bot, update):
|
def _profit(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /profit.
|
Handler for /profit.
|
||||||
Returns a cumulative profit statistics.
|
Returns a cumulative profit statistics.
|
||||||
@ -150,7 +151,7 @@ class TelegramHandler(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _start(bot, update):
|
def _start(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /start.
|
Handler for /start.
|
||||||
Starts TradeThread
|
Starts TradeThread
|
||||||
@ -166,7 +167,7 @@ class TelegramHandler(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _stop(bot, update):
|
def _stop(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /stop.
|
Handler for /stop.
|
||||||
Stops TradeThread
|
Stops TradeThread
|
||||||
@ -183,7 +184,7 @@ class TelegramHandler(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _forcesell(bot, update):
|
def _forcesell(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /forcesell <id>.
|
Handler for /forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
@ -231,7 +232,7 @@ class TelegramHandler(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _performance(bot, update):
|
def _performance(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /performance.
|
Handler for /performance.
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
@ -258,19 +259,19 @@ class TelegramHandler(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@synchronized
|
@synchronized
|
||||||
def get_updater(conf):
|
def get_updater(config: dict) -> Updater:
|
||||||
"""
|
"""
|
||||||
Returns the current telegram updater instantiates a new one
|
Returns the current telegram updater or instantiates a new one
|
||||||
:param conf:
|
:param config: dict
|
||||||
:return: telegram.ext.Updater
|
:return: telegram.ext.Updater
|
||||||
"""
|
"""
|
||||||
global _updater
|
global _updater
|
||||||
if not _updater:
|
if not _updater:
|
||||||
_updater = Updater(token=conf['telegram']['token'], workers=0)
|
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||||
return _updater
|
return _updater
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def listen():
|
def listen() -> None:
|
||||||
"""
|
"""
|
||||||
Registers all known command handlers and starts polling for message updates
|
Registers all known command handlers and starts polling for message updates
|
||||||
:return: None
|
:return: None
|
||||||
@ -286,12 +287,17 @@ class TelegramHandler(object):
|
|||||||
]
|
]
|
||||||
for handle in handles:
|
for handle in handles:
|
||||||
TelegramHandler.get_updater(conf).dispatcher.add_handler(handle)
|
TelegramHandler.get_updater(conf).dispatcher.add_handler(handle)
|
||||||
TelegramHandler.get_updater(conf).start_polling(clean=True, bootstrap_retries=3)
|
TelegramHandler.get_updater(conf).start_polling(
|
||||||
|
clean=True,
|
||||||
|
bootstrap_retries=3,
|
||||||
|
timeout=30,
|
||||||
|
read_latency=60,
|
||||||
|
)
|
||||||
logger.info('TelegramHandler is listening for following commands: {}'
|
logger.info('TelegramHandler is listening for following commands: {}'
|
||||||
.format([h.command for h in handles]))
|
.format([h.command for h in handles]))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def send_msg(msg, bot=None, parse_mode=ParseMode.MARKDOWN):
|
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
|
||||||
|
145
utils.py
145
utils.py
@ -1,101 +1,82 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from jsonschema import validate
|
||||||
from wrapt import synchronized
|
from wrapt import synchronized
|
||||||
from bittrex.bittrex import Bittrex
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_cur_conf = None
|
_cur_conf = None
|
||||||
|
|
||||||
|
|
||||||
|
# Required json-schema for user specified config
|
||||||
|
_conf_schema = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'max_open_trades': {'type': 'integer'},
|
||||||
|
'stake_currency': {'type': 'string'},
|
||||||
|
'stake_amount': {'type': 'number'},
|
||||||
|
'dry_run': {'type': 'boolean'},
|
||||||
|
'minimal_roi': {
|
||||||
|
'type': 'object',
|
||||||
|
'patternProperties': {
|
||||||
|
'^[0-9.]+$': {'type': 'number'}
|
||||||
|
},
|
||||||
|
'minProperties': 1
|
||||||
|
},
|
||||||
|
'poloniex': {'$ref': '#/definitions/exchange'},
|
||||||
|
'bittrex': {'$ref': '#/definitions/exchange'},
|
||||||
|
'telegram': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'token': {'type': 'string'},
|
||||||
|
'chat_id': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['enabled', 'token', 'chat_id']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'definitions': {
|
||||||
|
'exchange': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'key': {'type': 'string'},
|
||||||
|
'secret': {'type': 'string'},
|
||||||
|
'pair_whitelist': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {'type': 'string'},
|
||||||
|
'uniqueItems': True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'required': ['enabled', 'key', 'secret', 'pair_whitelist']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'anyOf': [
|
||||||
|
{'required': ['poloniex']},
|
||||||
|
{'required': ['bittrex']}
|
||||||
|
],
|
||||||
|
'required': [
|
||||||
|
'max_open_trades',
|
||||||
|
'stake_currency',
|
||||||
|
'stake_amount',
|
||||||
|
'dry_run',
|
||||||
|
'minimal_roi',
|
||||||
|
'telegram'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
@synchronized
|
||||||
def get_conf(filename='config.json'):
|
def get_conf(filename: str='config.json') -> dict:
|
||||||
"""
|
"""
|
||||||
Loads the config into memory and returns the instance of it
|
Loads the config into memory validates it
|
||||||
|
and returns the singleton instance
|
||||||
:return: dict
|
:return: dict
|
||||||
"""
|
"""
|
||||||
global _cur_conf
|
global _cur_conf
|
||||||
if not _cur_conf:
|
if not _cur_conf:
|
||||||
with open(filename) as file:
|
with open(filename) as file:
|
||||||
_cur_conf = json.load(file)
|
_cur_conf = json.load(file)
|
||||||
validate_conf(_cur_conf)
|
validate(_cur_conf, _conf_schema)
|
||||||
return _cur_conf
|
return _cur_conf
|
||||||
|
|
||||||
|
|
||||||
def validate_conf(conf):
|
|
||||||
"""
|
|
||||||
Validates if the minimal possible config is provided
|
|
||||||
:param conf: config as dict
|
|
||||||
:return: None, raises ValueError if something is wrong
|
|
||||||
"""
|
|
||||||
if not isinstance(conf.get('max_open_trades'), int):
|
|
||||||
raise ValueError('max_open_trades must be a int')
|
|
||||||
if not isinstance(conf.get('stake_currency'), str):
|
|
||||||
raise ValueError('stake_currency must be a str')
|
|
||||||
if not isinstance(conf.get('stake_amount'), float):
|
|
||||||
raise ValueError('stake_amount must be a float')
|
|
||||||
if not isinstance(conf.get('dry_run'), bool):
|
|
||||||
raise ValueError('dry_run must be a boolean')
|
|
||||||
if not isinstance(conf.get('minimal_roi'), dict):
|
|
||||||
raise ValueError('minimal_roi must be a dict')
|
|
||||||
|
|
||||||
for index, (minutes, threshold) in enumerate(conf.get('minimal_roi').items()):
|
|
||||||
if not isinstance(minutes, str):
|
|
||||||
raise ValueError('minimal_roi[{}].key must be a string'.format(index))
|
|
||||||
if not isinstance(threshold, float):
|
|
||||||
raise ValueError('minimal_roi[{}].value must be a float'.format(index))
|
|
||||||
|
|
||||||
if conf.get('telegram'):
|
|
||||||
telegram = conf.get('telegram')
|
|
||||||
if not isinstance(telegram.get('token'), str):
|
|
||||||
raise ValueError('telegram.token must be a string')
|
|
||||||
if not isinstance(telegram.get('chat_id'), str):
|
|
||||||
raise ValueError('telegram.chat_id must be a string')
|
|
||||||
|
|
||||||
if conf.get('poloniex'):
|
|
||||||
poloniex = conf.get('poloniex')
|
|
||||||
if not isinstance(poloniex.get('key'), str):
|
|
||||||
raise ValueError('poloniex.key must be a string')
|
|
||||||
if not isinstance(poloniex.get('secret'), str):
|
|
||||||
raise ValueError('poloniex.secret must be a string')
|
|
||||||
if not isinstance(poloniex.get('pair_whitelist'), list):
|
|
||||||
raise ValueError('poloniex.pair_whitelist must be a list')
|
|
||||||
if poloniex.get('enabled', False):
|
|
||||||
raise ValueError('poloniex is currently not implemented')
|
|
||||||
#if not poloniex.get('pair_whitelist'):
|
|
||||||
# raise ValueError('poloniex.pair_whitelist must contain some pairs')
|
|
||||||
|
|
||||||
if conf.get('bittrex'):
|
|
||||||
bittrex = conf.get('bittrex')
|
|
||||||
if not isinstance(bittrex.get('key'), str):
|
|
||||||
raise ValueError('bittrex.key must be a string')
|
|
||||||
if not isinstance(bittrex.get('secret'), str):
|
|
||||||
raise ValueError('bittrex.secret must be a string')
|
|
||||||
if not isinstance(bittrex.get('pair_whitelist'), list):
|
|
||||||
raise ValueError('bittrex.pair_whitelist must be a list')
|
|
||||||
if bittrex.get('enabled', False):
|
|
||||||
if not bittrex.get('pair_whitelist'):
|
|
||||||
raise ValueError('bittrex.pair_whitelist must contain some pairs')
|
|
||||||
validate_bittrex_pairs(bittrex.get('pair_whitelist'))
|
|
||||||
|
|
||||||
if conf.get('poloniex', {}).get('enabled', False) \
|
|
||||||
and conf.get('bittrex', {}).get('enabled', False):
|
|
||||||
raise ValueError('Cannot use poloniex and bittrex at the same time')
|
|
||||||
|
|
||||||
logger.info('Config is valid ...')
|
|
||||||
|
|
||||||
|
|
||||||
def validate_bittrex_pairs(pairs):
|
|
||||||
"""
|
|
||||||
Validates if all given pairs exist on bittrex
|
|
||||||
:param pairs: list of str
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
data = Bittrex(None, None).get_markets()
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
available_markets = [market['MarketName'].replace('-', '_')for market in data['result']]
|
|
||||||
for pair in pairs:
|
|
||||||
if pair not in available_markets:
|
|
||||||
raise ValueError('Invalid pair: {}'.format(pair))
|
|
||||||
|
Loading…
Reference in New Issue
Block a user