Merge branch 'release/0.10.0'

This commit is contained in:
gcarq 2017-09-28 19:17:01 +02:00
commit c20030783b
24 changed files with 214 additions and 132 deletions

View File

@ -1,28 +1,27 @@
sudo: false sudo: false
os: os:
- linux - linux
language: python language: python
python: python:
- 3.6 - 3.6
- nightly - nightly
matrix: matrix:
allow_failures: allow_failures:
- python: nightly - python: nightly
addons: addons:
apt: apt:
packages: packages:
- libelf-dev - libelf-dev
- libdw-dev - libdw-dev
- binutils-dev - binutils-dev
install: install:
- wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz - wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
- tar zxvf ta-lib-0.4.0-src.tar.gz - tar zxvf ta-lib-0.4.0-src.tar.gz
- cd ta-lib && ./configure && sudo make && sudo make install && cd .. - cd ta-lib && ./configure && sudo make && sudo make install && cd ..
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install -r requirements.txt - pip install -r requirements.txt
script: script:
- python -m unittest - python -m unittest
notifications:
slack:
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=

View File

@ -44,6 +44,9 @@ profit dips below -10% for a given trade. This parameter is optional.
Possible values are `running` or `stopped`. (default=`running`) Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first. If the value is `stopped` the bot has to be started with `/start` first.
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
use the `last` price and values between those interpolate between ask and last price. Using `ask` price will guarantee quick success in bid, but bot will also end up paying more then would probably have been necessary.
The other values should be self-explanatory, 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.

View File

@ -3,7 +3,6 @@ from datetime import timedelta
import logging import logging
import arrow import arrow
import requests import requests
from pandas.io.json import json_normalize
from pandas import DataFrame from pandas import DataFrame
import talib.abstract as ta import talib.abstract as ta
@ -23,7 +22,7 @@ def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict:
} }
params = { params = {
'marketName': pair.replace('_', '-'), 'marketName': pair.replace('_', '-'),
'tickInterval': 'OneMin', 'tickInterval': 'fiveMin',
'_': minimum_date.timestamp * 1000 '_': minimum_date.timestamp * 1000
} }
data = requests.get(url, params=params, headers=headers).json() data = requests.get(url, params=params, headers=headers).json()
@ -49,19 +48,9 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
""" """
dataframe['close_30_ema'] = ta.EMA(dataframe, timeperiod=30) dataframe['ema'] = ta.EMA(dataframe, timeperiod=33)
dataframe['close_90_ema'] = ta.EMA(dataframe, timeperiod=90) dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.22)
dataframe['adx'] = ta.ADX(dataframe)
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.2)
# calculate StochRSI
stochrsi = ta.STOCHRSI(dataframe)
dataframe['stochrsi'] = stochrsi['fastd'] # values between 0-100, not 0-1
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macds'] = macd['macdsignal']
dataframe['macdh'] = macd['macdhist']
return dataframe return dataframe
@ -72,13 +61,29 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
:param dataframe: DataFrame :param dataframe: DataFrame
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
prev_sar = dataframe['sar'].shift(1)
prev_close = dataframe['close'].shift(1)
prev_sar2 = dataframe['sar'].shift(2)
prev_close2 = dataframe['close'].shift(2)
# wait for stable turn from bearish to bullish market
dataframe.loc[ dataframe.loc[
(dataframe['stochrsi'] < 20) (dataframe['close'] > dataframe['sar']) &
& (dataframe['macd'] > dataframe['macds']) (prev_close > prev_sar) &
& (dataframe['close'] > dataframe['sar']), (prev_close2 < prev_sar2),
'buy' 'swap'
] = 1 ] = 1
# consider prices above ema to be in upswing
dataframe.loc[dataframe['ema'] <= dataframe['close'], 'upswing'] = 1
dataframe.loc[
(dataframe['upswing'] == 1) &
(dataframe['swap'] == 1) &
(dataframe['adx'] > 25), # adx over 25 tells there's enough momentum
'buy'] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close'] dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe return dataframe
@ -127,27 +132,20 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
matplotlib.use("Qt5Agg") matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
# Three subplots sharing x axe # Two subplots sharing x axis
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) fig, (ax1, ax2) = plt.subplots(2, sharex=True)
fig.suptitle(pair, fontsize=14, fontweight='bold') fig.suptitle(pair, fontsize=14, fontweight='bold')
ax1.plot(dataframe.index.values, dataframe['sar'], 'g_', label='pSAR')
ax1.plot(dataframe.index.values, dataframe['close'], label='close') ax1.plot(dataframe.index.values, dataframe['close'], label='close')
ax1.plot(dataframe.index.values, dataframe['close_30_ema'], label='EMA(30)')
ax1.plot(dataframe.index.values, dataframe['close_90_ema'], label='EMA(90)')
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell') # ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy') ax1.plot(dataframe.index.values, dataframe['ema'], '--', label='EMA(20)')
ax1.plot(dataframe.index.values, dataframe['buy'], 'bo', label='buy')
ax1.legend() ax1.legend()
ax2.plot(dataframe.index.values, dataframe['macd'], label='MACD') ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
ax2.plot(dataframe.index.values, dataframe['macds'], label='MACDS') ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
ax2.plot(dataframe.index.values, dataframe['macdh'], label='MACD Histogram')
ax2.plot(dataframe.index.values, [0] * len(dataframe.index.values))
ax2.legend() ax2.legend()
ax3.plot(dataframe.index.values, dataframe['stochrsi'], label='StochRSI')
ax3.plot(dataframe.index.values, [80] * len(dataframe.index.values))
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
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
# all but bottom plot. # all but bottom plot.
fig.subplots_adjust(hspace=0) fig.subplots_adjust(hspace=0)
@ -158,8 +156,8 @@ 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 # Install PYQT5==5.9 manually if you want to test this helper function
while True: while True:
pair = 'BTC_ANT' test_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']:
# get_buy_signal(pair) # get_buy_signal(pair)
plot_dataframe(analyze_ticker(pair), pair) plot_dataframe(analyze_ticker(test_pair), test_pair)
time.sleep(60) time.sleep(60)

View File

@ -9,11 +9,8 @@
"0": 0.02 "0": 0.02
}, },
"stoploss": -0.10, "stoploss": -0.10,
"poloniex": { "bid_strategy": {
"enabled": false, "ask_last_balance": 0.0
"key": "key",
"secret": "secret",
"pair_whitelist": []
}, },
"bittrex": { "bittrex": {
"enabled": true, "enabled": true,

View File

@ -3,7 +3,6 @@ import logging
from typing import List from typing import List
from bittrex.bittrex import Bittrex from bittrex.bittrex import Bittrex
from poloniex import Poloniex
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -14,7 +13,6 @@ _CONF = {}
class Exchange(enum.Enum): class Exchange(enum.Enum):
POLONIEX = 0
BITTREX = 1 BITTREX = 1
@ -33,13 +31,8 @@ def init(config: dict) -> None:
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')
use_poloniex = config.get('poloniex', {}).get('enabled', False)
use_bittrex = config.get('bittrex', {}).get('enabled', False) use_bittrex = config.get('bittrex', {}).get('enabled', False)
if use_bittrex:
if use_poloniex:
EXCHANGE = Exchange.POLONIEX
_API = Poloniex(key=config['poloniex']['key'], secret=config['poloniex']['secret'])
elif use_bittrex:
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:
@ -47,9 +40,10 @@ def init(config: dict) -> None:
# Check if all pairs are available # Check if all pairs are available
markets = get_markets() markets = get_markets()
for pair in config[EXCHANGE.name.lower()]['pair_whitelist']: exchange_name = EXCHANGE.name.lower()
for pair in config[exchange_name]['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 {}'.format(pair, exchange_name))
def buy(pair: str, rate: float, amount: float) -> str: def buy(pair: str, rate: float, amount: float) -> str:
@ -62,9 +56,6 @@ def buy(pair: str, rate: float, amount: float) -> str:
""" """
if _CONF['dry_run']: if _CONF['dry_run']:
return 'dry_run' return 'dry_run'
elif EXCHANGE == Exchange.POLONIEX:
_API.buy(pair, rate, amount)
# TODO: return order id
elif 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']:
@ -82,9 +73,6 @@ def sell(pair: str, rate: float, amount: float) -> str:
""" """
if _CONF['dry_run']: if _CONF['dry_run']:
return 'dry_run' return 'dry_run'
elif EXCHANGE == Exchange.POLONIEX:
_API.sell(pair, rate, amount)
# TODO: return order id
elif 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']:
@ -100,9 +88,6 @@ def get_balance(currency: str) -> float:
""" """
if _CONF['dry_run']: if _CONF['dry_run']:
return 999.9 return 999.9
elif EXCHANGE == Exchange.POLONIEX:
data = _API.returnBalances()
return float(data[currency])
elif 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']:
@ -116,14 +101,7 @@ 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 EXCHANGE == Exchange.POLONIEX: if EXCHANGE == Exchange.BITTREX:
data = _API.returnTicker()
return {
'bid': float(data[pair]['highestBid']),
'ask': float(data[pair]['lowestAsk']),
'last': float(data[pair]['last'])
}
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']))
@ -142,8 +120,6 @@ def cancel_order(order_id: str) -> None:
""" """
if _CONF['dry_run']: if _CONF['dry_run']:
pass pass
elif EXCHANGE == Exchange.POLONIEX:
raise NotImplemented('Not implemented')
elif 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']:
@ -158,8 +134,6 @@ def get_open_orders(pair: str) -> List[dict]:
""" """
if _CONF['dry_run']: if _CONF['dry_run']:
return [] return []
elif EXCHANGE == Exchange.POLONIEX:
raise NotImplemented('Not implemented')
elif 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']:
@ -180,9 +154,7 @@ 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 EXCHANGE == Exchange.POLONIEX: if EXCHANGE == Exchange.BITTREX:
raise NotImplemented('Not implemented')
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,10 +163,7 @@ 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 EXCHANGE == Exchange.POLONIEX: if EXCHANGE == Exchange. BITTREX:
# TODO: implement
raise NotImplemented('Not implemented')
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']))

57
main.py
View File

@ -4,7 +4,7 @@ import logging
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Dict, Optional
from jsonschema import validate from jsonschema import validate
@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
__author__ = "gcarq" __author__ = "gcarq"
__copyright__ = "gcarq 2017" __copyright__ = "gcarq 2017"
__license__ = "GPLv3" __license__ = "GPLv3"
__version__ = "0.9.0" __version__ = "0.10.0"
_CONF = {} _CONF = {}
@ -94,8 +94,10 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
# Get available balance # Get available balance
currency = trade.pair.split('_')[1] currency = trade.pair.split('_')[1]
balance = exchange.get_balance(currency) balance = exchange.get_balance(currency)
whitelist = _CONF[trade.exchange.name.lower()]['pair_whitelist']
profit = trade.exec_sell_order(current_rate, balance) profit = trade.exec_sell_order(current_rate, balance)
whitelist.append(trade.pair)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name, trade.exchange.name,
trade.pair.replace('_', '/'), trade.pair.replace('_', '/'),
@ -107,6 +109,28 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
telegram.send_msg(message) telegram.send_msg(message)
def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and configuration, decides whether bot should sell
:return True if bot should sell at current rate
"""
current_profit = (current_rate - trade.open_rate) / trade.open_rate
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
logger.debug('Stop loss hit.')
return True
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
duration, threshold = float(duration), float(threshold)
# Check if time matches and current rate is above threshold
time_diff = (current_time - trade.open_date).total_seconds() / 60
if time_diff > duration and current_profit > threshold:
return True
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
return False
def handle_trade(trade: Trade) -> None: 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.
@ -117,27 +141,22 @@ def handle_trade(trade: Trade) -> None:
raise ValueError('attempt to handle closed trade: {}'.format(trade)) raise ValueError('attempt to handle closed trade: {}'.format(trade))
logger.debug('Handling open trade %s ...', trade) logger.debug('Handling open trade %s ...', trade)
# Get current rate
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) if should_sell(trade, current_rate, datetime.utcnow()):
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']) * 100.0:
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()):
duration, threshold = float(duration), float(threshold)
# Check if time matches and current rate is above threshold
time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60
if time_diff > duration and current_rate > (1 + threshold) * trade.open_rate:
execute_sell(trade, current_rate)
return
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')
def get_target_bid(ticker: Dict[str, float]) -> float:
""" Calculates bid target between current ask price and last price """
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = _CONF['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]: def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]:
""" """
@ -148,7 +167,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
""" """
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 stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount: if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError( raise ValueError(
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency']) 'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency'])
@ -174,7 +193,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
else: else:
return None return None
open_rate = exchange.get_ticker(pair)['ask'] open_rate = get_target_bid(exchange.get_ticker(pair))
amount = stake_amount / open_rate amount = stake_amount / open_rate
order_id = exchange.buy(pair, open_rate, amount) order_id = exchange.buy(pair, open_rate, amount)
@ -188,7 +207,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
logger.info(message) logger.info(message)
telegram.send_msg(message) telegram.send_msg(message)
return Trade(pair=pair, return Trade(pair=pair,
btc_amount=stake_amount, stake_amount=stake_amount,
open_rate=open_rate, open_rate=open_rate,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
amount=amount, amount=amount,

15
misc.py
View File

@ -48,7 +48,18 @@ CONF_SCHEMA = {
'minProperties': 1 'minProperties': 1
}, },
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'poloniex': {'$ref': '#/definitions/exchange'}, 'bid_strategy': {
'type': 'object',
'properties': {
'ask_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False
},
},
'required': ['ask_last_balance']
},
'bittrex': {'$ref': '#/definitions/exchange'}, 'bittrex': {'$ref': '#/definitions/exchange'},
'telegram': { 'telegram': {
'type': 'object', 'type': 'object',
@ -78,7 +89,6 @@ CONF_SCHEMA = {
} }
}, },
'anyOf': [ 'anyOf': [
{'required': ['poloniex']},
{'required': ['bittrex']} {'required': ['bittrex']}
], ],
'required': [ 'required': [
@ -87,6 +97,7 @@ CONF_SCHEMA = {
'stake_amount', 'stake_amount',
'dry_run', 'dry_run',
'minimal_roi', 'minimal_roi',
'bid_strategy',
'telegram' 'telegram'
] ]
} }

View File

@ -49,7 +49,7 @@ class Trade(Base):
open_rate = Column(Float, nullable=False) open_rate = Column(Float, nullable=False)
close_rate = Column(Float) close_rate = Column(Float)
close_profit = Column(Float) close_profit = Column(Float)
btc_amount = Column(Float, nullable=False) stake_amount = Column(Float, name='btc_amount', nullable=False)
amount = Column(Float, nullable=False) amount = Column(Float, nullable=False)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime) close_date = Column(DateTime)

View File

@ -1,4 +1,3 @@
-e git+https://github.com/s4w3d0ff/python-poloniex.git#egg=Poloniex
-e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex -e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex
SQLAlchemy==1.1.13 SQLAlchemy==1.1.13
python-telegram-bot==7.0.1 python-telegram-bot==7.0.1

View File

@ -157,7 +157,7 @@ def _profit(bot: Bot, update: Update) -> None:
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
profit_amounts.append((profit / 100) * trade.btc_amount) profit_amounts.append((profit / 100) * trade.stake_amount)
profits.append(profit) profits.append(profit)
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \ best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \

71
test/test_backtesting.py Normal file
View File

@ -0,0 +1,71 @@
# pragma pylint: disable=missing-docstring
import unittest
from unittest.mock import patch
import os
import json
import logging
import arrow
from pandas import DataFrame
from analyze import analyze_ticker
from persistence import Trade
from main import should_sell
def print_results(results):
print('Made {} buys. Average profit {:.1f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
len(results.index),
results.profit.mean() * 100.0,
results.profit.sum(),
results.duration.mean()*5
))
class TestMain(unittest.TestCase):
pairs = ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay', 'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
conf = {
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"stoploss": -0.10
}
@classmethod
def setUpClass(cls):
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
@unittest.skipIf(not os.environ.get('BACKTEST', False), "slow, should be run manually")
def test_backtest(self):
trades = []
with patch.dict('main._CONF', self.conf):
for pair in self.pairs:
with open('test/testdata/'+pair+'.json') as data_file:
data = json.load(data_file)
with patch('analyze.get_ticker', return_value=data):
with patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')):
ticker = analyze_ticker(pair)
# for each buy point
for index, row in ticker[ticker.buy == 1].iterrows():
trade = Trade(
open_rate=row['close'],
open_date=arrow.get(row['date']).datetime,
amount=1,
)
# calculate win/lose forwards from buy point
for index2, row2 in ticker[index:].iterrows():
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
trades.append((pair, current_profit, index2 - index))
break
labels = ['currency', 'profit', 'duration']
results = DataFrame.from_records(trades, columns=labels)
print('====================== BACKTESTING REPORT ================================')
for pair in self.pairs:
print('For currency {}:'.format(pair))
print_results(results[results.currency == pair])
print('TOTAL OVER ALL TRADES:')
print_results(results)

View File

@ -4,7 +4,7 @@ from unittest.mock import patch, MagicMock
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, get_target_bid
from misc import CONF_SCHEMA from misc import CONF_SCHEMA
from persistence import Trade from persistence import Trade
@ -20,11 +20,8 @@ class TestMain(unittest.TestCase):
"720": 0.01, "720": 0.01,
"0": 0.02 "0": 0.02
}, },
"poloniex": { "bid_strategy": {
"enabled": False, "ask_last_balance": 0.0
"key": "key",
"secret": "secret",
"pair_whitelist": []
}, },
"bittrex": { "bittrex": {
"enabled": True, "enabled": True,
@ -61,7 +58,7 @@ class TestMain(unittest.TestCase):
self.assertEqual(trade.pair, 'BTC_ETH') self.assertEqual(trade.pair, 'BTC_ETH')
self.assertEqual(trade.exchange, exchange.Exchange.BITTREX) self.assertEqual(trade.exchange, exchange.Exchange.BITTREX)
self.assertEqual(trade.amount, 206.43811673387373) self.assertEqual(trade.amount, 206.43811673387373)
self.assertEqual(trade.btc_amount, 15.0) self.assertEqual(trade.stake_amount, 15.0)
self.assertEqual(trade.is_open, True) self.assertEqual(trade.is_open, True)
self.assertIsNotNone(trade.open_date) self.assertIsNotNone(trade.open_date)
buy_signal.assert_called_once_with('BTC_ETH') buy_signal.assert_called_once_with('BTC_ETH')
@ -96,6 +93,18 @@ class TestMain(unittest.TestCase):
self.assertTrue(closed) self.assertTrue(closed)
self.assertEqual(trade.is_open, False) self.assertEqual(trade.is_open, False)
def test_balance_fully_ask_side(self):
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}):
self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 20)
def test_balance_fully_last_side(self):
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}):
self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 10)
def test_balance_when_last_bigger_than_ask(self):
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}):
self.assertEqual(get_target_bid({'ask': 5, 'last': 10}), 5)
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
validate(cls.conf, CONF_SCHEMA) validate(cls.conf, CONF_SCHEMA)

View File

@ -10,7 +10,7 @@ class TestTrade(unittest.TestCase):
with patch('main.exchange.sell', side_effect='mocked_order_id') as api_mock: with patch('main.exchange.sell', side_effect='mocked_order_id') as api_mock:
trade = Trade( trade = Trade(
pair='BTC_ETH', pair='BTC_ETH',
btc_amount=1.00, stake_amount=1.00,
open_rate=0.50, open_rate=0.50,
amount=10.00, amount=10.00,
exchange=Exchange.BITTREX, exchange=Exchange.BITTREX,

View File

@ -28,11 +28,8 @@ class TestTelegram(unittest.TestCase):
"720": 0.01, "720": 0.01,
"0": 0.02 "0": 0.02
}, },
"poloniex": { "bid_strategy": {
"enabled": False, "ask_last_balance": 0.0
"key": "key",
"secret": "secret",
"pair_whitelist": []
}, },
"bittrex": { "bittrex": {
"enabled": True, "enabled": True,

1
test/testdata/btc-edg.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-etc.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-eth.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-ltc.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-mtl.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-neo.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-omg.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-pay.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-pivx.json vendored Normal file

File diff suppressed because one or more lines are too long

1
test/testdata/btc-qtum.json vendored Normal file

File diff suppressed because one or more lines are too long