Merge branch 'release/0.9.0'

This commit is contained in:
gcarq 2017-09-10 22:57:09 +02:00
commit 16b0a0aaab
17 changed files with 1110 additions and 641 deletions

28
.travis.yml Normal file
View File

@ -0,0 +1,28 @@
sudo: false
os:
- linux
language: python
python:
- 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
script:
- python -m unittest

View File

@ -6,7 +6,6 @@ RUN apt-get -y install build-essential
RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
RUN tar zxvf ta-lib-0.4.0-src.tar.gz
RUN cd ta-lib && ./configure && make && make install
RUN pip install TA-Lib
ENV LD_LIBRARY_PATH /usr/local/lib
RUN mkdir -p /freqtrade

View File

@ -1,4 +1,7 @@
# freqtrade
[![Build Status](https://travis-ci.org/gcarq/freqtrade.svg?branch=develop)](https://travis-ci.org/gcarq/freqtrade)
Simple High frequency trading bot for crypto currencies.
Currently supported exchanges: bittrex, poloniex (partly implemented)
@ -11,7 +14,7 @@ and enter the telegram `token` and your `chat_id` in `config.json`
Persistence is achieved through sqlite.
##### Telegram RPC commands:
#### Telegram RPC commands:
* /start: Starts the trader
* /stop: Stops the trader
* /status: Lists all open trades
@ -19,7 +22,7 @@ Persistence is achieved through sqlite.
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
* /performance: Show performance of each finished trade grouped by pair
##### Config
#### Config
`minimal_roi` is a JSON object where the key is a duration
in minutes and the value is the minimum ROI in percent.
See the example below:
@ -37,15 +40,19 @@ See the example below:
For example value `-0.10` will cause immediate sell if the
profit dips below -10% for a given trade. This parameter is optional.
`initial_state` is an optional field that defines the initial application state.
Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first.
The other values should be self-explanatory,
if not feel free to raise a github issue.
##### Prerequisites
#### Prerequisites
* python3.6
* sqlite
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
##### Install
#### Install
```
$ cd freqtrade/
# copy example config. Dont forget to insert your api keys
@ -56,6 +63,11 @@ $ pip install -r requirements.txt
$ ./main.py
```
#### Execute tests
```
$ python -m unittest
```
#### Docker
```

View File

@ -13,13 +13,10 @@ logging.basicConfig(level=logging.DEBUG,
logger = logging.getLogger(__name__)
def get_ticker_dataframe(pair: str) -> DataFrame:
def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict:
"""
Analyses the trend for the given pair
:param pair: pair as str in format BTC_ETH or BTC-ETH
:return: DataFrame
Request ticker data from Bittrex for a given currency pair
"""
minimum_date = arrow.now() - timedelta(hours=6)
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
@ -32,17 +29,26 @@ def get_ticker_dataframe(pair: str) -> DataFrame:
data = requests.get(url, params=params, headers=headers).json()
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data
data = [{
'close': t['C'],
'volume': t['V'],
'open': t['O'],
'high': t['H'],
'low': t['L'],
'date': t['T'],
} for t in sorted(data['result'], key=lambda k: k['T']) if arrow.get(t['T']) > minimum_date]
dataframe = DataFrame(json_normalize(data))
def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
"""
Analyses the trend for the given pair
:param pair: pair as str in format BTC_ETH or BTC-ETH
:return: DataFrame
"""
df = DataFrame(ticker) \
.drop('BV', 1) \
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
.sort_values('date')
return df[df['date'].map(arrow.get) > minimum_date]
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)
@ -60,37 +66,42 @@ def get_ticker_dataframe(pair: str) -> DataFrame:
return dataframe
def populate_trends(dataframe: DataFrame) -> DataFrame:
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
"""
Populates the trends for the given dataframe
Based on TA indicators, populates the buy trend for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with populated trends
"""
"""
dataframe.loc[
(dataframe['stochrsi'] < 20)
& (dataframe['close_30_ema'] > (1 + 0.0025) * dataframe['close_60_ema']),
'underpriced'
] = 1
:return: DataFrame with buy column
"""
dataframe.loc[
(dataframe['stochrsi'] < 20)
& (dataframe['macd'] > dataframe['macds'])
& (dataframe['close'] > dataframe['sar']),
'underpriced'
'buy'
] = 1
dataframe.loc[dataframe['underpriced'] == 1, 'buy'] = dataframe['close']
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe
def analyze_ticker(pair: str) -> DataFrame:
"""
Get ticker data for given currency pair, push it to a DataFrame and
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
"""
minimum_date = arrow.utcnow().shift(hours=-6)
data = get_ticker(pair, minimum_date)
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe)
return dataframe
def get_buy_signal(pair: str) -> bool:
"""
Calculates a buy signal based on StochRSI indicator
Calculates a buy signal based several technical analysis indicators
:param pair: pair in format BTC_ANT or BTC-ANT
:return: True if pair is underpriced, False otherwise
:return: True if pair is good for buying, False otherwise
"""
dataframe = get_ticker_dataframe(pair)
dataframe = populate_trends(dataframe)
dataframe = analyze_ticker(pair)
latest = dataframe.iloc[-1]
# Check if dataframe is out of date
@ -98,7 +109,7 @@ def get_buy_signal(pair: str) -> bool:
if signal_date < arrow.now() - timedelta(minutes=10):
return False
signal = latest['underpriced'] == 1
signal = latest['buy'] == 1
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
return signal
@ -123,7 +134,7 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
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'], 'bo', label='buy')
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
ax1.legend()
ax2.plot(dataframe.index.values, dataframe['macd'], label='MACD')
@ -145,11 +156,10 @@ 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'
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
# get_buy_signal(pair)
dataframe = get_ticker_dataframe(pair)
dataframe = populate_trends(dataframe)
plot_dataframe(dataframe, pair)
plot_dataframe(analyze_ticker(pair), pair)
time.sleep(60)

View File

@ -35,5 +35,6 @@
"enabled": true,
"token": "token",
"chat_id": "chat_id"
}
},
"initial_state": "running"
}

View File

@ -4,11 +4,13 @@ from typing import List
from bittrex.bittrex import Bittrex
from poloniex import Poloniex
from wrapt import synchronized
logger = logging.getLogger(__name__)
_exchange_api = None
# Current selected exchange
EXCHANGE = None
_API = None
_CONF = {}
class Exchange(enum.Enum):
@ -16,44 +18,41 @@ class Exchange(enum.Enum):
BITTREX = 1
class ApiWrapper(object):
def init(config: dict) -> None:
"""
Wrapper for exchanges.
Currently implemented:
* Bittrex
* Poloniex (partly)
"""
def __init__(self, config: dict):
"""
Initializes the ApiWrapper with the given config,
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: dict
:param config: config to use
:return: None
"""
self.dry_run = config['dry_run']
if self.dry_run:
global _API, EXCHANGE
_CONF.update(config)
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:
self.exchange = Exchange.POLONIEX
self.api = Poloniex(key=config['poloniex']['key'], secret=config['poloniex']['secret'])
EXCHANGE = Exchange.POLONIEX
_API = Poloniex(key=config['poloniex']['key'], secret=config['poloniex']['secret'])
elif use_bittrex:
self.exchange = Exchange.BITTREX
self.api = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
EXCHANGE = Exchange.BITTREX
_API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
else:
self.api = None
raise RuntimeError('No exchange specified. Aborting!')
# Check if all pairs are available
markets = self.get_markets()
for pair in config[self.exchange.name.lower()]['pair_whitelist']:
markets = get_markets()
for pair in config[EXCHANGE.name.lower()]['pair_whitelist']:
if pair not in markets:
raise RuntimeError('Pair {} is not available at Poloniex'.format(pair))
def buy(self, pair: str, rate: float, amount: float) -> str:
def buy(pair: str, rate: float, amount: float) -> str:
"""
Places a limit buy order.
:param pair: Pair as str, format: BTC_ETH
@ -61,18 +60,19 @@ class ApiWrapper(object):
:param amount: The amount to purchase
:return: order_id of the placed buy order
"""
if self.dry_run:
pass
elif self.exchange == Exchange.POLONIEX:
self.api.buy(pair, rate, amount)
if _CONF['dry_run']:
return 'dry_run'
elif EXCHANGE == Exchange.POLONIEX:
_API.buy(pair, rate, amount)
# TODO: return order id
elif self.exchange == Exchange.BITTREX:
data = self.api.buy_limit(pair.replace('_', '-'), amount, rate)
elif EXCHANGE == Exchange.BITTREX:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data['result']['uuid']
def sell(self, pair: str, rate: float, amount: float) -> str:
def sell(pair: str, rate: float, amount: float) -> str:
"""
Places a limit sell order.
:param pair: Pair as str, format: BTC_ETH
@ -80,49 +80,51 @@ class ApiWrapper(object):
:param amount: The amount to sell
:return: None
"""
if self.dry_run:
pass
elif self.exchange == Exchange.POLONIEX:
self.api.sell(pair, rate, amount)
if _CONF['dry_run']:
return 'dry_run'
elif EXCHANGE == Exchange.POLONIEX:
_API.sell(pair, rate, amount)
# TODO: return order id
elif self.exchange == Exchange.BITTREX:
data = self.api.sell_limit(pair.replace('_', '-'), amount, rate)
elif EXCHANGE == Exchange.BITTREX:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data['result']['uuid']
def get_balance(self, currency: str) -> float:
def get_balance(currency: str) -> float:
"""
Get account balance.
:param currency: currency as str, format: BTC
:return: float
"""
if self.dry_run:
if _CONF['dry_run']:
return 999.9
elif self.exchange == Exchange.POLONIEX:
data = self.api.returnBalances()
elif EXCHANGE == Exchange.POLONIEX:
data = _API.returnBalances()
return float(data[currency])
elif self.exchange == Exchange.BITTREX:
data = self.api.get_balance(currency)
elif EXCHANGE == Exchange.BITTREX:
data = _API.get_balance(currency)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return float(data['result']['Balance'] or 0.0)
def get_ticker(self, pair: str) -> dict:
def get_ticker(pair: str) -> dict:
"""
Get Ticker for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: dict
"""
if self.exchange == Exchange.POLONIEX:
data = self.api.returnTicker()
if EXCHANGE == Exchange.POLONIEX:
data = _API.returnTicker()
return {
'bid': float(data[pair]['highestBid']),
'ask': float(data[pair]['lowestAsk']),
'last': float(data[pair]['last'])
}
elif self.exchange == Exchange.BITTREX:
data = self.api.get_ticker(pair.replace('_', '-'))
elif EXCHANGE == Exchange.BITTREX:
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return {
@ -131,33 +133,35 @@ class ApiWrapper(object):
'last': float(data['result']['Last']),
}
def cancel_order(self, order_id: str) -> None:
def cancel_order(order_id: str) -> None:
"""
Cancel order for given order_id
:param order_id: id as str
:return: None
"""
if self.dry_run:
if _CONF['dry_run']:
pass
elif self.exchange == Exchange.POLONIEX:
elif EXCHANGE == Exchange.POLONIEX:
raise NotImplemented('Not implemented')
elif self.exchange == Exchange.BITTREX:
data = self.api.cancel(order_id)
elif EXCHANGE == Exchange.BITTREX:
data = _API.cancel(order_id)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
def get_open_orders(self, pair: str) -> List[dict]:
def get_open_orders(pair: str) -> List[dict]:
"""
Get all open orders for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: list of dicts
"""
if self.dry_run:
if _CONF['dry_run']:
return []
elif self.exchange == Exchange.POLONIEX:
elif EXCHANGE == Exchange.POLONIEX:
raise NotImplemented('Not implemented')
elif self.exchange == Exchange.BITTREX:
data = self.api.get_open_orders(pair.replace('_', '-'))
elif EXCHANGE == Exchange.BITTREX:
data = _API.get_open_orders(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return [{
@ -169,39 +173,29 @@ class ApiWrapper(object):
'remaining': entry['QuantityRemaining'],
} for entry in data['result']]
def get_pair_detail_url(self, pair: str) -> str:
def get_pair_detail_url(pair: str) -> str:
"""
Returns the market detail url for the given pair
:param pair: pair as str, format: BTC_ANT
:return: url as str
"""
if self.exchange == Exchange.POLONIEX:
if EXCHANGE == Exchange.POLONIEX:
raise NotImplemented('Not implemented')
elif self.exchange == Exchange.BITTREX:
elif EXCHANGE == Exchange.BITTREX:
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
def get_markets(self) -> List[str]:
def get_markets() -> List[str]:
"""
Returns all available markets
:return: list of all available pairs
"""
if self.exchange == Exchange.POLONIEX:
if EXCHANGE == Exchange.POLONIEX:
# TODO: implement
raise NotImplemented('Not implemented')
elif self.exchange == Exchange. BITTREX:
data = self.api.get_markets()
elif EXCHANGE == Exchange. BITTREX:
data = _API.get_markets()
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']]
@synchronized
def get_exchange_api(conf: dict) -> ApiWrapper:
"""
Returns the current exchange api or instantiates a new one
:return: exchange.ApiWrapper
"""
global _exchange_api
if not _exchange_api:
_exchange_api = ApiWrapper(conf)
return _exchange_api

232
main.py
View File

@ -1,19 +1,19 @@
#!/usr/bin/env python
import json
import logging
import threading
import time
import traceback
from datetime import datetime
from json import JSONDecodeError
from typing import Optional
from requests import ConnectionError
from wrapt import synchronized
from jsonschema import validate
import exchange
import persistence
from persistence import Trade
from analyze import get_buy_signal
from persistence import Trade, Session
from exchange import get_exchange_api, Exchange
from rpc.telegram import TelegramHandler
from utils import get_conf
from misc import CONF_SCHEMA, get_state, State, update_state
from rpc import telegram
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
@ -22,60 +22,26 @@ logger = logging.getLogger(__name__)
__author__ = "gcarq"
__copyright__ = "gcarq 2017"
__license__ = "GPLv3"
__version__ = "0.8.0"
__version__ = "0.9.0"
_CONF = {}
CONFIG = get_conf()
api_wrapper = get_exchange_api(CONFIG)
class TradeThread(threading.Thread):
def __init__(self):
super().__init__()
self._should_stop = False
def stop(self) -> None:
""" stops the trader thread """
self._should_stop = True
def run(self) -> None:
"""
Threaded main function
:return: None
"""
try:
TelegramHandler.send_msg('*Status:* `trader started`')
logger.info('Trader started')
while not self._should_stop:
try:
self._process()
except (ConnectionError, JSONDecodeError, ValueError) as error:
msg = 'Got {} during _process()'.format(error.__class__.__name__)
logger.exception(msg)
finally:
Session.flush()
time.sleep(25)
except (RuntimeError, JSONDecodeError):
TelegramHandler.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
logger.exception('RuntimeError. Stopping trader ...')
finally:
TelegramHandler.send_msg('*Status:* `Trader has stopped`')
@staticmethod
def _process() -> None:
def _process() -> None:
"""
Queries the persistence layer for open trades and handles them,
otherwise a new trade is created.
:return: None
"""
try:
# Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < CONFIG['max_open_trades']:
if len(trades) < _CONF['max_open_trades']:
try:
# Create entity and execute trade
trade = create_trade(float(CONFIG['stake_amount']), api_wrapper.exchange)
trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE)
if trade:
Session.add(trade)
Trade.session.add(trade)
else:
logging.info('Got no buy signal...')
except ValueError:
@ -83,45 +49,21 @@ class TradeThread(threading.Thread):
for trade in trades:
# Check if there is already an open order for this trade
orders = api_wrapper.get_open_orders(trade.pair)
orders = exchange.get_open_orders(trade.pair)
orders = [o for o in orders if o['id'] == trade.open_order_id]
if orders:
msg = 'There exists an open order for {}: Order(total={}, remaining={}, type={}, id={})' \
.format(
trade,
round(orders[0]['amount'], 8),
round(orders[0]['remaining'], 8),
orders[0]['type'],
orders[0]['id'])
logger.info(msg)
continue
logger.info('There is an open order for: %s', orders[0])
else:
# Update state
trade.open_order_id = None
# Check if this trade can be marked as closed
if close_trade_if_fulfilled(trade):
logger.info('No open orders found and trade is fulfilled. Marking %s as closed ...', trade)
continue
# Check if this trade can be closed
if not close_trade_if_fulfilled(trade):
# Check if we can sell our current pair
handle_trade(trade)
# Initial stopped TradeThread instance
_instance = TradeThread()
@synchronized
def get_instance(recreate: bool=False) -> TradeThread:
"""
Get the current instance of this thread. This is a singleton.
:param recreate: Must be True if you want to start the instance
:return: TradeThread instance
"""
global _instance
if recreate and not _instance.is_alive():
logger.debug('Creating thread instance...')
_instance = TradeThread()
return _instance
Trade.session.flush()
except (ConnectionError, json.JSONDecodeError) as error:
msg = 'Got {} in _process()'.format(error.__class__.__name__)
logger.exception(msg)
def close_trade_if_fulfilled(trade: Trade) -> bool:
@ -132,27 +74,37 @@ def close_trade_if_fulfilled(trade: Trade) -> bool:
"""
# If we don't have an open order and the close rate is already set,
# we can close this trade.
if trade.close_profit and trade.close_date and trade.close_rate and not trade.open_order_id:
if trade.close_profit is not None \
and trade.close_date is not None \
and trade.close_rate is not None \
and trade.open_order_id is None:
trade.is_open = False
Session.flush()
logger.info('No open orders found and trade is fulfilled. Marking %s as closed ...', trade)
return True
return False
def execute_sell(trade: Trade, current_rate: float) -> None:
"""
Executes a sell for the given trade and current rate
:param trade: Trade instance
:param current_rate: current rate
:return: None
"""
# Get available balance
currency = trade.pair.split('_')[1]
balance = api_wrapper.get_balance(currency)
balance = exchange.get_balance(currency)
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name,
trade.pair.replace('_', '/'),
api_wrapper.get_pair_detail_url(trade.pair),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
round(profit, 2)
)
logger.info(message)
TelegramHandler.send_msg(message)
telegram.send_msg(message)
def handle_trade(trade: Trade) -> None:
@ -166,39 +118,41 @@ def handle_trade(trade: Trade) -> None:
logger.debug('Handling open trade %s ...', trade)
# Get current rate
current_rate = api_wrapper.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 'stoploss' in CONFIG and current_profit < float(CONFIG['stoploss']) * 100.0:
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']) * 100.0:
logger.debug('Stop loss hit.')
execute_sell(trade, current_rate)
return
for duration, threshold in sorted(CONFIG['minimal_roi'].items()):
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
else:
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit)
except ValueError:
logger.exception('Unable to handle open order')
def create_trade(stake_amount: float, exchange: Exchange) -> Optional[Trade]:
def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend
:param exchange: exchange to use
:param _exchange: exchange to use
"""
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
whitelist = CONFIG[exchange.name.lower()]['pair_whitelist']
whitelist = _CONF[_exchange.name.lower()]['pair_whitelist']
# Check if btc_amount is fulfilled
if api_wrapper.get_balance(CONFIG['stake_currency']) < stake_amount:
raise ValueError('stake amount is not fulfilled (currency={}'.format(CONFIG['stake_currency']))
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError(
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency'])
)
# Remove currently opened and latest pairs from whitelist
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
@ -213,37 +167,91 @@ def create_trade(stake_amount: float, exchange: Exchange) -> Optional[Trade]:
raise ValueError('No pair in whitelist')
# Pick pair based on StochRSI buy signals
for p in whitelist:
if get_buy_signal(p):
pair = p
for _pair in whitelist:
if get_buy_signal(_pair):
pair = _pair
break
else:
return None
open_rate = api_wrapper.get_ticker(pair)['ask']
open_rate = exchange.get_ticker(pair)['ask']
amount = stake_amount / open_rate
exchange = exchange
order_id = api_wrapper.buy(pair, open_rate, amount)
order_id = exchange.buy(pair, open_rate, amount)
# Create trade entity and return
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
exchange.name,
_exchange.name,
pair.replace('_', '/'),
api_wrapper.get_pair_detail_url(pair),
exchange.get_pair_detail_url(pair),
open_rate
)
logger.info(message)
TelegramHandler.send_msg(message)
telegram.send_msg(message)
return Trade(pair=pair,
btc_amount=stake_amount,
open_rate=open_rate,
open_date=datetime.utcnow(),
amount=amount,
exchange=exchange,
open_order_id=order_id)
exchange=_exchange,
open_order_id=order_id,
is_open=True)
def init(config: dict, db_url: Optional[str] = None) -> None:
"""
Initializes all modules and updates the config
:param config: config as dict
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
# Initialize all modules
telegram.init(config)
persistence.init(config, db_url)
exchange.init(config)
# Set initial application state
initial_state = config.get('initial_state')
if initial_state:
update_state(State[initial_state.upper()])
else:
update_state(State.STOPPED)
def app(config: dict) -> None:
"""
Main function which handles the application state
:param config: config as dict
:return: None
"""
logger.info('Starting freqtrade %s', __version__)
init(config)
try:
old_state = get_state()
logger.info('Initial State: %s', old_state)
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logging.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
_process()
# We need to sleep here because otherwise we would run into bittrex rate limit
time.sleep(25)
old_state = new_state
except RuntimeError:
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
logger.exception('RuntimeError. Trader stopped!')
finally:
telegram.send_msg('*Status:* `Trader has stopped`')
if __name__ == '__main__':
logger.info('Starting freqtrade %s', __version__)
TelegramHandler.listen()
while True:
time.sleep(0.5)
with open('config.json') as file:
_CONF = json.load(file)
validate(_CONF, CONF_SCHEMA)
app(_CONF)

View File

@ -1,16 +1,39 @@
import json
import logging
import enum
from jsonschema import validate
from wrapt import synchronized
logger = logging.getLogger(__name__)
_cur_conf = None
class State(enum.Enum):
RUNNING = 0
STOPPED = 1
# Current application state
_STATE = State.STOPPED
@synchronized
def update_state(state: State) -> None:
"""
Updates the application state
:param state: new state
:return: None
"""
global _STATE
_STATE = state
@synchronized
def get_state() -> State:
"""
Gets the current application state
:return:
"""
return _STATE
# Required json-schema for user specified config
_conf_schema = {
CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 1},
@ -35,7 +58,8 @@ _conf_schema = {
'chat_id': {'type': 'string'},
},
'required': ['enabled', 'token', 'chat_id']
}
},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
},
'definitions': {
'exchange': {
@ -66,18 +90,3 @@ _conf_schema = {
'telegram'
]
}
@synchronized
def get_conf(filename: str='config.json') -> dict:
"""
Loads the config into memory validates it
and returns the singleton instance
:return: dict
"""
global _cur_conf
if not _cur_conf:
with open(filename) as file:
_cur_conf = json.load(file)
validate(_cur_conf, _conf_schema)
return _cur_conf

View File

@ -1,4 +1,5 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
@ -7,26 +8,42 @@ from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.types import Enum
from exchange import Exchange, get_exchange_api
from utils import get_conf
import exchange
conf = get_conf()
if conf.get('dry_run', False):
db_handle = 'sqlite:///tradesv2.dry_run.sqlite'
else:
db_handle = 'sqlite:///tradesv2.sqlite'
engine = create_engine(db_handle, echo=False)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Session = session()
_CONF = {}
Base = declarative_base()
def init(config: dict, db_url: Optional[str] = None) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
_CONF.update(config)
if not db_url:
if _CONF.get('dry_run', False):
db_url = 'sqlite:///tradesv2.dry_run.sqlite'
else:
db_url = 'sqlite:///tradesv2.sqlite'
engine = create_engine(db_url, echo=False)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session()
Trade.query = session.query_property()
Base.metadata.create_all(engine)
class Trade(Base):
__tablename__ = 'trades'
query = session.query_property()
id = Column(Integer, primary_key=True)
exchange = Column(Enum(Exchange), nullable=False)
exchange = Column(Enum(exchange.Exchange), nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
open_rate = Column(Float, nullable=False)
@ -39,12 +56,16 @@ class Trade(Base):
open_order_id = Column(String)
def __repr__(self):
if self.is_open:
open_since = 'closed'
else:
open_since = round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
self.id,
self.pair,
self.amount,
self.open_rate,
'closed' if not self.is_open else round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
open_since
)
def exec_sell_order(self, rate: float, amount: float) -> float:
@ -57,12 +78,12 @@ class Trade(Base):
profit = 100 * ((rate - self.open_rate) / self.open_rate)
# Execute sell and update trade record
order_id = get_exchange_api(conf).sell(self.pair, rate, amount)
order_id = exchange.sell(str(self.pair), rate, amount)
self.close_rate = rate
self.close_profit = profit
self.close_date = datetime.utcnow()
self.open_order_id = order_id
Session.flush()
return profit
Base.metadata.create_all(engine)
# Flush changes
Trade.session.flush()
return profit

View File

@ -8,8 +8,8 @@ urllib3==1.22
wrapt==1.10.11
pandas==0.20.3
matplotlib==2.0.2
PYQT5==5.9
scikit-learn==0.19.0
scipy==0.19.1
jsonschema==2.6.0
TA-Lib==0.4.10
#PYQT5==5.9

View File

@ -0,0 +1 @@
from . import telegram

View File

@ -3,15 +3,15 @@ from datetime import timedelta
from typing import Callable, Any
import arrow
from sqlalchemy import and_, func
from sqlalchemy import and_, func, text
from telegram.error import NetworkError
from telegram.ext import CommandHandler, Updater
from telegram import ParseMode, Bot, Update
from wrapt import synchronized
from persistence import Trade, Session
from exchange import get_exchange_api
from utils import get_conf
from misc import get_state, State, update_state
from persistence import Trade
import exchange
# Remove noisy log messages
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
@ -19,9 +19,43 @@ logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)
_updater = None
_CONF = {}
conf = get_conf()
api_wrapper = get_exchange_api(conf)
def init(config: dict) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:return: None
"""
global _updater
_updater = Updater(token=config['telegram']['token'], workers=0)
_CONF.update(config)
# Register command handler and start telegram message polling
handles = [
CommandHandler('status', _status),
CommandHandler('profit', _profit),
CommandHandler('start', _start),
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance),
]
for handle in handles:
_updater.dispatcher.add_handler(handle)
_updater.start_polling(
clean=True,
bootstrap_retries=3,
timeout=30,
read_latency=60,
)
logger.info(
'rpc.telegram is listening for following commands: %s',
[h.command for h in handles]
)
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
@ -31,11 +65,12 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
:return: decorated function
"""
def wrapper(*args, **kwargs):
bot, update = args[0], args[1]
bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1]
if not isinstance(bot, Bot) or not isinstance(update, Update):
raise ValueError('Received invalid Arguments: {}'.format(*args))
chat_id = int(conf['telegram']['chat_id'])
chat_id = int(_CONF['telegram']['chat_id'])
if int(update.message.chat_id) == chat_id:
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
return command_handler(*args, **kwargs)
@ -44,10 +79,8 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
return wrapper
class TelegramHandler(object):
@staticmethod
@authorized_only
def _status(bot: Bot, update: Update) -> None:
@authorized_only
def _status(bot: Bot, update: Update) -> None:
"""
Handler for /status.
Returns the current TradeThread status
@ -57,19 +90,22 @@ class TelegramHandler(object):
"""
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
from main import get_instance
if not get_instance().is_alive():
TelegramHandler.send_msg('*Status:* `trader is not running`', bot=bot)
if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades:
TelegramHandler.send_msg('*Status:* `no active order`', bot=bot)
send_msg('*Status:* `no active order`', bot=bot)
else:
for trade in trades:
# calculate profit and send message to user
current_rate = api_wrapper.get_ticker(trade.pair)['bid']
current_rate = exchange.get_ticker(trade.pair)['bid']
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
orders = api_wrapper.get_open_orders(trade.pair)
orders = exchange.get_open_orders(trade.pair)
orders = [o for o in orders if o['id'] == trade.open_order_id]
order = orders[0] if orders else None
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit, 2)
) if trade.close_profit else None
message = """
*Trade ID:* `{trade_id}`
*Current Pair:* [{pair}]({market_url})
@ -79,26 +115,26 @@ class TelegramHandler(object):
*Close Rate:* `{close_rate}`
*Current Rate:* `{current_rate}`
*Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit}%`
*Current Profit:* `{current_profit:.2f}%`
*Open Order:* `{open_order}`
""".format(
trade_id=trade.id,
pair=trade.pair,
market_url=api_wrapper.get_pair_detail_url(trade.pair),
market_url=exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit='{}%'.format(round(trade.close_profit, 2)) if trade.close_profit else None,
close_profit=fmt_close_profit,
current_profit=round(current_profit, 2),
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None,
)
TelegramHandler.send_msg(message, bot=bot)
send_msg(message, bot=bot)
@staticmethod
@authorized_only
def _profit(bot: Bot, update: Update) -> None:
@authorized_only
def _profit(bot: Bot, update: Update) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
@ -118,25 +154,30 @@ class TelegramHandler(object):
profit = trade.close_profit
else:
# Get current rate
current_rate = api_wrapper.get_ticker(trade.pair)['bid']
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)
profits.append(profit)
bp_pair, bp_rate = 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')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by('profit_sum DESC') \
.order_by(text('profit_sum DESC')) \
.first()
if not best_pair:
send_msg('*Status:* `no closed trade`', bot=bot)
return
bp_pair, bp_rate = best_pair
markdown_msg = """
*ROI:* `{profit_btc} ({profit}%)`
*ROI:* `{profit_btc:.2f} ({profit:.2f}%)`
*Trade Count:* `{trade_count}`
*First Trade opened:* `{first_trade_date}`
*Latest Trade opened:* `{latest_trade_date}`
*Avg. Duration:* `{avg_duration}`
*Best Performing:* `{best_pair}: {best_rate}%`
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
""".format(
profit_btc=round(sum(profit_amounts), 8),
profit=round(sum(profits), 2),
@ -147,11 +188,11 @@ class TelegramHandler(object):
best_pair=bp_pair,
best_rate=round(bp_rate, 2),
)
TelegramHandler.send_msg(markdown_msg, bot=bot)
send_msg(markdown_msg, bot=bot)
@staticmethod
@authorized_only
def _start(bot: Bot, update: Update) -> None:
@authorized_only
def _start(bot: Bot, update: Update) -> None:
"""
Handler for /start.
Starts TradeThread
@ -159,15 +200,14 @@ class TelegramHandler(object):
:param update: message update
:return: None
"""
from main import get_instance
if get_instance().is_alive():
TelegramHandler.send_msg('*Status:* `already running`', bot=bot)
if get_state() == State.RUNNING:
send_msg('*Status:* `already running`', bot=bot)
else:
get_instance(recreate=True).start()
update_state(State.RUNNING)
@staticmethod
@authorized_only
def _stop(bot: Bot, update: Update) -> None:
@authorized_only
def _stop(bot: Bot, update: Update) -> None:
"""
Handler for /stop.
Stops TradeThread
@ -175,16 +215,15 @@ class TelegramHandler(object):
:param update: message update
:return: None
"""
from main import get_instance
if get_instance().is_alive():
TelegramHandler.send_msg('`Stopping trader ...`', bot=bot)
get_instance().stop()
if get_state() == State.RUNNING:
send_msg('`Stopping trader ...`', bot=bot)
update_state(State.STOPPED)
else:
TelegramHandler.send_msg('*Status:* `already stopped`', bot=bot)
send_msg('*Status:* `already stopped`', bot=bot)
@staticmethod
@authorized_only
def _forcesell(bot: Bot, update: Update) -> None:
@authorized_only
def _forcesell(bot: Bot, update: Update) -> None:
"""
Handler for /forcesell <id>.
Sells the given trade at current price
@ -192,9 +231,8 @@ class TelegramHandler(object):
:param update: message update
:return: None
"""
from main import get_instance
if not get_instance().is_alive():
TelegramHandler.send_msg('`trader is not running`', bot=bot)
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
try:
@ -207,32 +245,32 @@ class TelegramHandler(object):
Trade.is_open.is_(True)
)).first()
if not trade:
TelegramHandler.send_msg('There is no open trade with ID: `{}`'.format(trade_id))
send_msg('There is no open trade with ID: `{}`'.format(trade_id))
return
# Get current rate
current_rate = api_wrapper.get_ticker(trade.pair)['bid']
current_rate = exchange.get_ticker(trade.pair)['bid']
# Get available balance
currency = trade.pair.split('_')[1]
balance = api_wrapper.get_balance(currency)
balance = exchange.get_balance(currency)
# Execute sell
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name,
trade.pair.replace('_', '/'),
api_wrapper.get_pair_detail_url(trade.pair),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
round(profit, 2)
)
logger.info(message)
TelegramHandler.send_msg(message)
send_msg(message)
except ValueError:
TelegramHandler.send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
logger.warning('/forcesell: Invalid argument received')
@staticmethod
@authorized_only
def _performance(bot: Bot, update: Update) -> None:
@authorized_only
def _performance(bot: Bot, update: Update) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
@ -240,63 +278,28 @@ class TelegramHandler(object):
:param update: message update
:return: None
"""
from main import get_instance
if not get_instance().is_alive():
TelegramHandler.send_msg('`trader is not running`', bot=bot)
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
pair_rates = Session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
pair_rates = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by('profit_sum DESC') \
.order_by(text('profit_sum DESC')) \
.all()
stats = '\n'.join('{}. <code>{}\t{}%</code>'.format(i + 1, pair, round(rate, 2)) for i, (pair, rate) in enumerate(pair_rates))
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format(
index=i + 1,
pair=pair,
profit=round(rate, 2)
) for i, (pair, rate) in enumerate(pair_rates))
message = '<b>Performance:</b>\n{}\n'.format(stats)
logger.debug(message)
TelegramHandler.send_msg(message, parse_mode=ParseMode.HTML)
send_msg(message, parse_mode=ParseMode.HTML)
@staticmethod
@synchronized
def get_updater(config: dict) -> Updater:
"""
Returns the current telegram updater or instantiates a new one
:param config: dict
:return: telegram.ext.Updater
"""
global _updater
if not _updater:
_updater = Updater(token=config['telegram']['token'], workers=0)
return _updater
@staticmethod
def listen() -> None:
"""
Registers all known command handlers and starts polling for message updates
:return: None
"""
# Register command handler and start telegram message polling
handles = [
CommandHandler('status', TelegramHandler._status),
CommandHandler('profit', TelegramHandler._profit),
CommandHandler('start', TelegramHandler._start),
CommandHandler('stop', TelegramHandler._stop),
CommandHandler('forcesell', TelegramHandler._forcesell),
CommandHandler('performance', TelegramHandler._performance),
]
for handle in handles:
TelegramHandler.get_updater(conf).dispatcher.add_handler(handle)
TelegramHandler.get_updater(conf).start_polling(
clean=True,
bootstrap_retries=3,
timeout=30,
read_latency=60,
)
logger.info('TelegramHandler is listening for following commands: {}'
.format([h.command for h in handles]))
@staticmethod
def send_msg(msg: str, bot: Bot=None, parse_mode: ParseMode=ParseMode.MARKDOWN) -> None:
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
:param msg: message
@ -304,15 +307,18 @@ class TelegramHandler(object):
:param parse_mode: telegram parse mode
:return: None
"""
if conf['telegram'].get('enabled', False):
if _CONF['telegram'].get('enabled', False):
try:
bot = bot or TelegramHandler.get_updater(conf).bot
bot = bot or _updater.bot
try:
bot.send_message(conf['telegram']['chat_id'], msg, parse_mode=parse_mode)
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
except NetworkError as error:
# Sometimes the telegram server resets the current connection,
# if this is the case we send the message again.
logger.warning('Got Telegram NetworkError: %s! Trying one more time.', error.message)
bot.send_message(conf['telegram']['chat_id'], msg, parse_mode=parse_mode)
logger.warning(
'Got Telegram NetworkError: %s! Trying one more time.',
error.message
)
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
except Exception:
logger.exception('Exception occurred within Telegram API')

0
test/__init__.py Normal file
View File

49
test/test_analyze.py Normal file
View File

@ -0,0 +1,49 @@
# pragma pylint: disable=missing-docstring
import unittest
from unittest.mock import patch
from pandas import DataFrame
import arrow
from analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, analyze_ticker, get_buy_signal
RESULT_BITTREX = {
'success': True,
'message': '',
'result': [
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 22.17210568, 'T': '2017-08-30T10:40:00', 'BV': 0.01448082},
{'O': 0.00066194, 'H': 0.00066195, 'L': 0.00066194, 'C': 0.00066195, 'V': 33.4727437, 'T': '2017-08-30T10:34:00', 'BV': 0.02215696},
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 53.85127609, 'T': '2017-08-30T10:37:00', 'BV': 0.0351708},
{'O': 0.00066194, 'H': 0.00066194, 'L': 0.00065311, 'C': 0.00065311, 'V': 46.29210665, 'T': '2017-08-30T10:42:00', 'BV': 0.03063118},
]
}
class TestAnalyze(unittest.TestCase):
def setUp(self):
self.result = parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
def test_1_dataframe_has_correct_columns(self):
self.assertEqual(self.result.columns.tolist(),
['close', 'high', 'low', 'open', 'date', 'volume'])
def test_2_orders_by_date(self):
self.assertEqual(self.result['date'].tolist(),
['2017-08-30T10:34:00',
'2017-08-30T10:37:00',
'2017-08-30T10:40:00',
'2017-08-30T10:42:00'])
def test_3_populates_buy_trend(self):
dataframe = populate_buy_trend(populate_indicators(self.result))
self.assertTrue('buy' in dataframe.columns)
self.assertTrue('buy_price' in dataframe.columns)
def test_4_returns_latest_buy_signal(self):
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
with patch('analyze.analyze_ticker', return_value=buydf):
self.assertEqual(get_buy_signal('BTC-ETH'), True)
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
with patch('analyze.analyze_ticker', return_value=buydf):
self.assertEqual(get_buy_signal('BTC-ETH'), False)
if __name__ == '__main__':
unittest.main()

105
test/test_main.py Normal file
View File

@ -0,0 +1,105 @@
import unittest
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 misc import CONF_SCHEMA
from persistence import Trade
class TestMain(unittest.TestCase):
conf = {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"poloniex": {
"enabled": False,
"key": "key",
"secret": "secret",
"pair_whitelist": []
},
"bittrex": {
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "chat_id"
}
}
def test_1_create_trade(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True) as buy_signal:
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
trade = create_trade(15.0, exchange.Exchange.BITTREX)
Trade.session.add(trade)
Trade.session.flush()
self.assertIsNotNone(trade)
self.assertEqual(trade.open_rate, 0.072661)
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.is_open, True)
self.assertIsNotNone(trade.open_date)
buy_signal.assert_called_once_with('BTC_ETH')
def test_2_handle_trade(self):
with patch.dict('main._CONF', self.conf):
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.17256061,
'ask': 0.172661,
'last': 0.17256061
}),
buy=MagicMock(return_value='mocked_order_id')):
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
self.assertTrue(trade)
handle_trade(trade)
self.assertEqual(trade.close_rate, 0.17256061)
self.assertEqual(trade.close_profit, 137.4872490056564)
self.assertIsNotNone(trade.close_date)
self.assertEqual(trade.open_order_id, 'dry_run')
def test_3_close_trade(self):
with patch.dict('main._CONF', self.conf):
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
self.assertTrue(trade)
# Simulate that there is no open order
trade.open_order_id = None
closed = close_trade_if_fulfilled(trade)
self.assertTrue(closed)
self.assertEqual(trade.is_open, False)
@classmethod
def setUpClass(cls):
validate(cls.conf, CONF_SCHEMA)
if __name__ == '__main__':
unittest.main()

28
test/test_persistence.py Normal file
View File

@ -0,0 +1,28 @@
import unittest
from unittest.mock import patch
from exchange import Exchange
from persistence import Trade
class TestTrade(unittest.TestCase):
def test_1_exec_sell_order(self):
with patch('main.exchange.sell', side_effect='mocked_order_id') as api_mock:
trade = Trade(
pair='BTC_ETH',
btc_amount=1.00,
open_rate=0.50,
amount=10.00,
exchange=Exchange.BITTREX,
open_order_id='mocked'
)
profit = trade.exec_sell_order(1.00, 10.00)
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
self.assertEqual(profit, 100.0)
self.assertEqual(trade.close_rate, 1.0)
self.assertEqual(trade.close_profit, profit)
self.assertIsNotNone(trade.close_date)
if __name__ == '__main__':
unittest.main()

198
test/test_telegram.py Normal file
View File

@ -0,0 +1,198 @@
import unittest
from unittest.mock import patch, MagicMock
from datetime import datetime
from jsonschema import validate
from telegram import Bot, Update, Message, Chat
import exchange
from main import init, create_trade
from misc import CONF_SCHEMA, update_state, State, get_state
from persistence import Trade
from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
class MagicBot(MagicMock, Bot):
pass
class TestTelegram(unittest.TestCase):
conf = {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"poloniex": {
"enabled": False,
"key": "key",
"secret": "secret",
"pair_whitelist": []
},
"bittrex": {
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "0"
},
"initial_state": "running"
}
def test_1_status_handle(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX)
self.assertTrue(trade)
Trade.session.add(trade)
Trade.session.flush()
_status(bot=MagicBot(), update=self.update)
self.assertEqual(msg_mock.call_count, 2)
self.assertIn('[BTC_ETH]', msg_mock.call_args_list[-1][0][0])
def test_2_profit_handle(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX)
self.assertTrue(trade)
trade.close_rate = 0.07256061
trade.close_profit = 100.00
trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_profit(bot=MagicBot(), update=self.update)
self.assertEqual(msg_mock.call_count, 2)
self.assertIn('(100.00%)', msg_mock.call_args_list[-1][0][0])
def test_3_forcesell_handle(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX)
self.assertTrue(trade)
Trade.session.add(trade)
Trade.session.flush()
self.update.message.text = '/forcesell 1'
_forcesell(bot=MagicBot(), update=self.update)
self.assertEqual(msg_mock.call_count, 2)
self.assertIn('Selling [BTC/ETH]', msg_mock.call_args_list[-1][0][0])
self.assertIn('0.072561', msg_mock.call_args_list[-1][0][0])
def test_4_performance_handle(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX)
self.assertTrue(trade)
trade.close_rate = 0.07256061
trade.close_profit = 100.00
trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_performance(bot=MagicBot(), update=self.update)
self.assertEqual(msg_mock.call_count, 2)
self.assertIn('Performance', msg_mock.call_args_list[-1][0][0])
self.assertIn('BTC_ETH 100.00%', msg_mock.call_args_list[-1][0][0])
def test_5_start_handle(self):
with patch.dict('main._CONF', self.conf):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
init(self.conf, 'sqlite://')
update_state(State.STOPPED)
self.assertEqual(get_state(), State.STOPPED)
_start(bot=MagicBot(), update=self.update)
self.assertEqual(get_state(), State.RUNNING)
self.assertEqual(msg_mock.call_count, 0)
def test_6_stop_handle(self):
with patch.dict('main._CONF', self.conf):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
init(self.conf, 'sqlite://')
update_state(State.RUNNING)
self.assertEqual(get_state(), State.RUNNING)
_stop(bot=MagicBot(), update=self.update)
self.assertEqual(get_state(), State.STOPPED)
self.assertEqual(msg_mock.call_count, 1)
self.assertIn('Stopping trader', msg_mock.call_args_list[0][0][0])
def setUp(self):
self.update = Update(0)
self.update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
@classmethod
def setUpClass(cls):
validate(cls.conf, CONF_SCHEMA)
if __name__ == '__main__':
unittest.main()