Merge branch 'release/0.10.0'
This commit is contained in:
commit
c20030783b
25
.travis.yml
25
.travis.yml
@ -1,28 +1,27 @@
|
||||
sudo: false
|
||||
os:
|
||||
- linux
|
||||
|
||||
- linux
|
||||
language: python
|
||||
python:
|
||||
- 3.6
|
||||
- nightly
|
||||
- 3.6
|
||||
- nightly
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: nightly
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libelf-dev
|
||||
- libdw-dev
|
||||
- binutils-dev
|
||||
|
||||
install:
|
||||
- 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
|
||||
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||
- pip install -r requirements.txt
|
||||
|
||||
- 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
|
||||
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||
- pip install -r requirements.txt
|
||||
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=
|
||||
|
@ -44,6 +44,9 @@ profit dips below -10% for a given trade. This parameter is optional.
|
||||
Possible values are `running` or `stopped`. (default=`running`)
|
||||
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,
|
||||
if not feel free to raise a github issue.
|
||||
|
||||
|
68
analyze.py
68
analyze.py
@ -3,7 +3,6 @@ from datetime import timedelta
|
||||
import logging
|
||||
import arrow
|
||||
import requests
|
||||
from pandas.io.json import json_normalize
|
||||
from pandas import DataFrame
|
||||
import talib.abstract as ta
|
||||
|
||||
@ -23,7 +22,7 @@ def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict:
|
||||
}
|
||||
params = {
|
||||
'marketName': pair.replace('_', '-'),
|
||||
'tickInterval': 'OneMin',
|
||||
'tickInterval': 'fiveMin',
|
||||
'_': minimum_date.timestamp * 1000
|
||||
}
|
||||
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
|
||||
"""
|
||||
dataframe['close_30_ema'] = ta.EMA(dataframe, timeperiod=30)
|
||||
dataframe['close_90_ema'] = ta.EMA(dataframe, timeperiod=90)
|
||||
|
||||
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']
|
||||
dataframe['ema'] = ta.EMA(dataframe, timeperiod=33)
|
||||
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.22)
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
return dataframe
|
||||
|
||||
@ -72,13 +61,29 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
:param dataframe: DataFrame
|
||||
: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['stochrsi'] < 20)
|
||||
& (dataframe['macd'] > dataframe['macds'])
|
||||
& (dataframe['close'] > dataframe['sar']),
|
||||
'buy'
|
||||
(dataframe['close'] > dataframe['sar']) &
|
||||
(prev_close > prev_sar) &
|
||||
(prev_close2 < prev_sar2),
|
||||
'swap'
|
||||
] = 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']
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
@ -127,27 +132,20 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
||||
matplotlib.use("Qt5Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Three subplots sharing x axe
|
||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||
# Two subplots sharing x axis
|
||||
fig, (ax1, ax2) = plt.subplots(2, sharex=True)
|
||||
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_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['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()
|
||||
|
||||
ax2.plot(dataframe.index.values, dataframe['macd'], label='MACD')
|
||||
ax2.plot(dataframe.index.values, dataframe['macds'], label='MACDS')
|
||||
ax2.plot(dataframe.index.values, dataframe['macdh'], label='MACD Histogram')
|
||||
ax2.plot(dataframe.index.values, [0] * len(dataframe.index.values))
|
||||
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
|
||||
ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
|
||||
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
|
||||
# all but bottom plot.
|
||||
fig.subplots_adjust(hspace=0)
|
||||
@ -158,8 +156,8 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
||||
if __name__ == '__main__':
|
||||
# Install PYQT5==5.9 manually if you want to test this helper function
|
||||
while True:
|
||||
pair = 'BTC_ANT'
|
||||
test_pair = 'BTC_ANT'
|
||||
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||
# get_buy_signal(pair)
|
||||
plot_dataframe(analyze_ticker(pair), pair)
|
||||
plot_dataframe(analyze_ticker(test_pair), test_pair)
|
||||
time.sleep(60)
|
||||
|
@ -9,11 +9,8 @@
|
||||
"0": 0.02
|
||||
},
|
||||
"stoploss": -0.10,
|
||||
"poloniex": {
|
||||
"enabled": false,
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"pair_whitelist": []
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"bittrex": {
|
||||
"enabled": true,
|
||||
|
45
exchange.py
45
exchange.py
@ -3,7 +3,6 @@ import logging
|
||||
from typing import List
|
||||
|
||||
from bittrex.bittrex import Bittrex
|
||||
from poloniex import Poloniex
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -14,7 +13,6 @@ _CONF = {}
|
||||
|
||||
|
||||
class Exchange(enum.Enum):
|
||||
POLONIEX = 0
|
||||
BITTREX = 1
|
||||
|
||||
|
||||
@ -33,13 +31,8 @@ def init(config: dict) -> None:
|
||||
if config['dry_run']:
|
||||
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)
|
||||
|
||||
if use_poloniex:
|
||||
EXCHANGE = Exchange.POLONIEX
|
||||
_API = Poloniex(key=config['poloniex']['key'], secret=config['poloniex']['secret'])
|
||||
elif use_bittrex:
|
||||
if use_bittrex:
|
||||
EXCHANGE = Exchange.BITTREX
|
||||
_API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
|
||||
else:
|
||||
@ -47,9 +40,10 @@ def init(config: dict) -> None:
|
||||
|
||||
# Check if all pairs are available
|
||||
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:
|
||||
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:
|
||||
@ -62,9 +56,6 @@ def buy(pair: str, rate: float, amount: float) -> str:
|
||||
"""
|
||||
if _CONF['dry_run']:
|
||||
return 'dry_run'
|
||||
elif EXCHANGE == Exchange.POLONIEX:
|
||||
_API.buy(pair, rate, amount)
|
||||
# TODO: return order id
|
||||
elif EXCHANGE == Exchange.BITTREX:
|
||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||
if not data['success']:
|
||||
@ -82,9 +73,6 @@ def sell(pair: str, rate: float, amount: float) -> str:
|
||||
"""
|
||||
if _CONF['dry_run']:
|
||||
return 'dry_run'
|
||||
elif EXCHANGE == Exchange.POLONIEX:
|
||||
_API.sell(pair, rate, amount)
|
||||
# TODO: return order id
|
||||
elif EXCHANGE == Exchange.BITTREX:
|
||||
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
||||
if not data['success']:
|
||||
@ -100,9 +88,6 @@ def get_balance(currency: str) -> float:
|
||||
"""
|
||||
if _CONF['dry_run']:
|
||||
return 999.9
|
||||
elif EXCHANGE == Exchange.POLONIEX:
|
||||
data = _API.returnBalances()
|
||||
return float(data[currency])
|
||||
elif EXCHANGE == Exchange.BITTREX:
|
||||
data = _API.get_balance(currency)
|
||||
if not data['success']:
|
||||
@ -116,14 +101,7 @@ def get_ticker(pair: str) -> dict:
|
||||
:param pair: Pair as str, format: BTC_ETC
|
||||
:return: dict
|
||||
"""
|
||||
if EXCHANGE == Exchange.POLONIEX:
|
||||
data = _API.returnTicker()
|
||||
return {
|
||||
'bid': float(data[pair]['highestBid']),
|
||||
'ask': float(data[pair]['lowestAsk']),
|
||||
'last': float(data[pair]['last'])
|
||||
}
|
||||
elif EXCHANGE == Exchange.BITTREX:
|
||||
if EXCHANGE == Exchange.BITTREX:
|
||||
data = _API.get_ticker(pair.replace('_', '-'))
|
||||
if not data['success']:
|
||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||
@ -142,8 +120,6 @@ def cancel_order(order_id: str) -> None:
|
||||
"""
|
||||
if _CONF['dry_run']:
|
||||
pass
|
||||
elif EXCHANGE == Exchange.POLONIEX:
|
||||
raise NotImplemented('Not implemented')
|
||||
elif EXCHANGE == Exchange.BITTREX:
|
||||
data = _API.cancel(order_id)
|
||||
if not data['success']:
|
||||
@ -158,8 +134,6 @@ def get_open_orders(pair: str) -> List[dict]:
|
||||
"""
|
||||
if _CONF['dry_run']:
|
||||
return []
|
||||
elif EXCHANGE == Exchange.POLONIEX:
|
||||
raise NotImplemented('Not implemented')
|
||||
elif EXCHANGE == Exchange.BITTREX:
|
||||
data = _API.get_open_orders(pair.replace('_', '-'))
|
||||
if not data['success']:
|
||||
@ -180,9 +154,7 @@ def get_pair_detail_url(pair: str) -> str:
|
||||
:param pair: pair as str, format: BTC_ANT
|
||||
:return: url as str
|
||||
"""
|
||||
if EXCHANGE == Exchange.POLONIEX:
|
||||
raise NotImplemented('Not implemented')
|
||||
elif EXCHANGE == Exchange.BITTREX:
|
||||
if EXCHANGE == Exchange.BITTREX:
|
||||
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
||||
|
||||
|
||||
@ -191,10 +163,7 @@ def get_markets() -> List[str]:
|
||||
Returns all available markets
|
||||
:return: list of all available pairs
|
||||
"""
|
||||
if EXCHANGE == Exchange.POLONIEX:
|
||||
# TODO: implement
|
||||
raise NotImplemented('Not implemented')
|
||||
elif EXCHANGE == Exchange. BITTREX:
|
||||
if EXCHANGE == Exchange. BITTREX:
|
||||
data = _API.get_markets()
|
||||
if not data['success']:
|
||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||
|
57
main.py
57
main.py
@ -4,7 +4,7 @@ import logging
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import Dict, Optional
|
||||
|
||||
from jsonschema import validate
|
||||
|
||||
@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
||||
__author__ = "gcarq"
|
||||
__copyright__ = "gcarq 2017"
|
||||
__license__ = "GPLv3"
|
||||
__version__ = "0.9.0"
|
||||
__version__ = "0.10.0"
|
||||
|
||||
_CONF = {}
|
||||
|
||||
@ -94,8 +94,10 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
|
||||
# Get available balance
|
||||
currency = trade.pair.split('_')[1]
|
||||
balance = exchange.get_balance(currency)
|
||||
whitelist = _CONF[trade.exchange.name.lower()]['pair_whitelist']
|
||||
|
||||
profit = trade.exec_sell_order(current_rate, balance)
|
||||
whitelist.append(trade.pair)
|
||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
||||
trade.exchange.name,
|
||||
trade.pair.replace('_', '/'),
|
||||
@ -107,6 +109,28 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
|
||||
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:
|
||||
"""
|
||||
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))
|
||||
|
||||
logger.debug('Handling open trade %s ...', trade)
|
||||
# Get current rate
|
||||
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
current_profit = 100.0 * ((current_rate - trade.open_rate) / trade.open_rate)
|
||||
|
||||
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']) * 100.0:
|
||||
logger.debug('Stop loss hit.')
|
||||
if should_sell(trade, current_rate, datetime.utcnow()):
|
||||
execute_sell(trade, current_rate)
|
||||
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:
|
||||
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]:
|
||||
"""
|
||||
@ -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)
|
||||
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:
|
||||
raise ValueError(
|
||||
'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:
|
||||
return None
|
||||
|
||||
open_rate = exchange.get_ticker(pair)['ask']
|
||||
open_rate = get_target_bid(exchange.get_ticker(pair))
|
||||
amount = stake_amount / open_rate
|
||||
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)
|
||||
telegram.send_msg(message)
|
||||
return Trade(pair=pair,
|
||||
btc_amount=stake_amount,
|
||||
stake_amount=stake_amount,
|
||||
open_rate=open_rate,
|
||||
open_date=datetime.utcnow(),
|
||||
amount=amount,
|
||||
|
15
misc.py
15
misc.py
@ -48,7 +48,18 @@ CONF_SCHEMA = {
|
||||
'minProperties': 1
|
||||
},
|
||||
'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'},
|
||||
'telegram': {
|
||||
'type': 'object',
|
||||
@ -78,7 +89,6 @@ CONF_SCHEMA = {
|
||||
}
|
||||
},
|
||||
'anyOf': [
|
||||
{'required': ['poloniex']},
|
||||
{'required': ['bittrex']}
|
||||
],
|
||||
'required': [
|
||||
@ -87,6 +97,7 @@ CONF_SCHEMA = {
|
||||
'stake_amount',
|
||||
'dry_run',
|
||||
'minimal_roi',
|
||||
'bid_strategy',
|
||||
'telegram'
|
||||
]
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ class Trade(Base):
|
||||
open_rate = Column(Float, nullable=False)
|
||||
close_rate = 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)
|
||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
close_date = Column(DateTime)
|
||||
|
@ -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
|
||||
SQLAlchemy==1.1.13
|
||||
python-telegram-bot==7.0.1
|
||||
|
@ -157,7 +157,7 @@ def _profit(bot: Bot, update: Update) -> None:
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
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)
|
||||
|
||||
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
||||
|
71
test/test_backtesting.py
Normal file
71
test/test_backtesting.py
Normal 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)
|
@ -4,7 +4,7 @@ from unittest.mock import patch, MagicMock
|
||||
from jsonschema import validate
|
||||
|
||||
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 persistence import Trade
|
||||
|
||||
@ -20,11 +20,8 @@ class TestMain(unittest.TestCase):
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
},
|
||||
"poloniex": {
|
||||
"enabled": False,
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"pair_whitelist": []
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"bittrex": {
|
||||
"enabled": True,
|
||||
@ -61,7 +58,7 @@ class TestMain(unittest.TestCase):
|
||||
self.assertEqual(trade.pair, 'BTC_ETH')
|
||||
self.assertEqual(trade.exchange, exchange.Exchange.BITTREX)
|
||||
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.assertIsNotNone(trade.open_date)
|
||||
buy_signal.assert_called_once_with('BTC_ETH')
|
||||
@ -96,6 +93,18 @@ class TestMain(unittest.TestCase):
|
||||
self.assertTrue(closed)
|
||||
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
|
||||
def setUpClass(cls):
|
||||
validate(cls.conf, CONF_SCHEMA)
|
||||
|
@ -10,7 +10,7 @@ class TestTrade(unittest.TestCase):
|
||||
with patch('main.exchange.sell', side_effect='mocked_order_id') as api_mock:
|
||||
trade = Trade(
|
||||
pair='BTC_ETH',
|
||||
btc_amount=1.00,
|
||||
stake_amount=1.00,
|
||||
open_rate=0.50,
|
||||
amount=10.00,
|
||||
exchange=Exchange.BITTREX,
|
||||
|
@ -28,11 +28,8 @@ class TestTelegram(unittest.TestCase):
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
},
|
||||
"poloniex": {
|
||||
"enabled": False,
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"pair_whitelist": []
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"bittrex": {
|
||||
"enabled": True,
|
||||
|
1
test/testdata/btc-edg.json
vendored
Normal file
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
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
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
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
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
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
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
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
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
1
test/testdata/btc-qtum.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user