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
|
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=
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
68
analyze.py
68
analyze.py
@ -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)
|
||||||
|
@ -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,
|
||||||
|
45
exchange.py
45
exchange.py
@ -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
57
main.py
@ -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
15
misc.py
@ -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'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
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
|
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)
|
||||||
|
@ -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,
|
||||||
|
@ -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
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