Merge branch 'release/0.9.0'
This commit is contained in:
commit
16b0a0aaab
28
.travis.yml
Normal file
28
.travis.yml
Normal 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
|
@ -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 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 tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
RUN cd ta-lib && ./configure && make && make install
|
RUN cd ta-lib && ./configure && make && make install
|
||||||
RUN pip install TA-Lib
|
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||||
|
|
||||||
RUN mkdir -p /freqtrade
|
RUN mkdir -p /freqtrade
|
||||||
|
20
README.md
20
README.md
@ -1,4 +1,7 @@
|
|||||||
# freqtrade
|
# 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.
|
Simple High frequency trading bot for crypto currencies.
|
||||||
Currently supported exchanges: bittrex, poloniex (partly implemented)
|
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.
|
Persistence is achieved through sqlite.
|
||||||
|
|
||||||
##### Telegram RPC commands:
|
#### Telegram RPC commands:
|
||||||
* /start: Starts the trader
|
* /start: Starts the trader
|
||||||
* /stop: Stops the trader
|
* /stop: Stops the trader
|
||||||
* /status: Lists all open trades
|
* /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`).
|
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
* /performance: Show performance of each finished trade grouped by pair
|
* /performance: Show performance of each finished trade grouped by pair
|
||||||
|
|
||||||
##### Config
|
#### Config
|
||||||
`minimal_roi` is a JSON object where the key is a duration
|
`minimal_roi` is a JSON object where the key is a duration
|
||||||
in minutes and the value is the minimum ROI in percent.
|
in minutes and the value is the minimum ROI in percent.
|
||||||
See the example below:
|
See the example below:
|
||||||
@ -37,15 +40,19 @@ See the example below:
|
|||||||
For example value `-0.10` will cause immediate sell if the
|
For example value `-0.10` will cause immediate sell if the
|
||||||
profit dips below -10% for a given trade. This parameter is optional.
|
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,
|
The other values should be self-explanatory,
|
||||||
if not feel free to raise a github issue.
|
if not feel free to raise a github issue.
|
||||||
|
|
||||||
##### Prerequisites
|
#### Prerequisites
|
||||||
* python3.6
|
* python3.6
|
||||||
* sqlite
|
* sqlite
|
||||||
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
||||||
|
|
||||||
##### Install
|
#### Install
|
||||||
```
|
```
|
||||||
$ cd freqtrade/
|
$ cd freqtrade/
|
||||||
# copy example config. Dont forget to insert your api keys
|
# copy example config. Dont forget to insert your api keys
|
||||||
@ -56,6 +63,11 @@ $ pip install -r requirements.txt
|
|||||||
$ ./main.py
|
$ ./main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Execute tests
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python -m unittest
|
||||||
|
```
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
```
|
```
|
||||||
|
80
analyze.py
80
analyze.py
@ -13,13 +13,10 @@ logging.basicConfig(level=logging.DEBUG,
|
|||||||
logger = logging.getLogger(__name__)
|
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
|
Request ticker data from Bittrex for a given currency pair
|
||||||
:param pair: pair as str in format BTC_ETH or BTC-ETH
|
|
||||||
:return: DataFrame
|
|
||||||
"""
|
"""
|
||||||
minimum_date = arrow.now() - timedelta(hours=6)
|
|
||||||
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
|
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
|
||||||
headers = {
|
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',
|
'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()
|
data = requests.get(url, params=params, headers=headers).json()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
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_30_ema'] = ta.EMA(dataframe, timeperiod=30)
|
||||||
dataframe['close_90_ema'] = ta.EMA(dataframe, timeperiod=90)
|
dataframe['close_90_ema'] = ta.EMA(dataframe, timeperiod=90)
|
||||||
|
|
||||||
@ -60,37 +66,42 @@ def get_ticker_dataframe(pair: str) -> DataFrame:
|
|||||||
return 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
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with populated trends
|
:return: DataFrame with buy column
|
||||||
"""
|
|
||||||
"""
|
|
||||||
dataframe.loc[
|
|
||||||
(dataframe['stochrsi'] < 20)
|
|
||||||
& (dataframe['close_30_ema'] > (1 + 0.0025) * dataframe['close_60_ema']),
|
|
||||||
'underpriced'
|
|
||||||
] = 1
|
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(dataframe['stochrsi'] < 20)
|
(dataframe['stochrsi'] < 20)
|
||||||
& (dataframe['macd'] > dataframe['macds'])
|
& (dataframe['macd'] > dataframe['macds'])
|
||||||
& (dataframe['close'] > dataframe['sar']),
|
& (dataframe['close'] > dataframe['sar']),
|
||||||
'underpriced'
|
'buy'
|
||||||
] = 1
|
] = 1
|
||||||
dataframe.loc[dataframe['underpriced'] == 1, 'buy'] = dataframe['close']
|
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||||
return dataframe
|
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:
|
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
|
: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 = analyze_ticker(pair)
|
||||||
dataframe = populate_trends(dataframe)
|
|
||||||
latest = dataframe.iloc[-1]
|
latest = dataframe.iloc[-1]
|
||||||
|
|
||||||
# Check if dataframe is out of date
|
# 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):
|
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
signal = latest['underpriced'] == 1
|
signal = latest['buy'] == 1
|
||||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
||||||
return 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_30_ema'], label='EMA(30)')
|
||||||
ax1.plot(dataframe.index.values, dataframe['close_90_ema'], label='EMA(90)')
|
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'], 'bo', label='buy')
|
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
|
||||||
ax1.legend()
|
ax1.legend()
|
||||||
|
|
||||||
ax2.plot(dataframe.index.values, dataframe['macd'], label='MACD')
|
ax2.plot(dataframe.index.values, dataframe['macd'], label='MACD')
|
||||||
@ -145,11 +156,10 @@ 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
|
||||||
while True:
|
while True:
|
||||||
pair = 'BTC_ANT'
|
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)
|
||||||
dataframe = get_ticker_dataframe(pair)
|
plot_dataframe(analyze_ticker(pair), pair)
|
||||||
dataframe = populate_trends(dataframe)
|
|
||||||
plot_dataframe(dataframe, pair)
|
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
|
@ -35,5 +35,6 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "token",
|
"token": "token",
|
||||||
"chat_id": "chat_id"
|
"chat_id": "chat_id"
|
||||||
}
|
},
|
||||||
|
"initial_state": "running"
|
||||||
}
|
}
|
354
exchange.py
354
exchange.py
@ -4,11 +4,13 @@ from typing import List
|
|||||||
|
|
||||||
from bittrex.bittrex import Bittrex
|
from bittrex.bittrex import Bittrex
|
||||||
from poloniex import Poloniex
|
from poloniex import Poloniex
|
||||||
from wrapt import synchronized
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_exchange_api = None
|
# Current selected exchange
|
||||||
|
EXCHANGE = None
|
||||||
|
_API = None
|
||||||
|
_CONF = {}
|
||||||
|
|
||||||
|
|
||||||
class Exchange(enum.Enum):
|
class Exchange(enum.Enum):
|
||||||
@ -16,192 +18,184 @@ class Exchange(enum.Enum):
|
|||||||
BITTREX = 1
|
BITTREX = 1
|
||||||
|
|
||||||
|
|
||||||
class ApiWrapper(object):
|
def init(config: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Wrapper for exchanges.
|
Initializes this module with the given config,
|
||||||
Currently implemented:
|
it does basic validation whether the specified
|
||||||
* Bittrex
|
exchange and pairs are valid.
|
||||||
* Poloniex (partly)
|
:param config: config to use
|
||||||
|
:return: None
|
||||||
"""
|
"""
|
||||||
def __init__(self, config: dict):
|
global _API, EXCHANGE
|
||||||
"""
|
|
||||||
Initializes the ApiWrapper with the given config,
|
|
||||||
it does basic validation whether the specified
|
|
||||||
exchange and pairs are valid.
|
|
||||||
:param config: dict
|
|
||||||
"""
|
|
||||||
self.dry_run = config['dry_run']
|
|
||||||
if self.dry_run:
|
|
||||||
logger.info('Instance is running with dry_run enabled')
|
|
||||||
|
|
||||||
use_poloniex = config.get('poloniex', {}).get('enabled', False)
|
_CONF.update(config)
|
||||||
use_bittrex = config.get('bittrex', {}).get('enabled', False)
|
|
||||||
|
|
||||||
if use_poloniex:
|
if config['dry_run']:
|
||||||
self.exchange = Exchange.POLONIEX
|
logger.info('Instance is running with dry_run enabled')
|
||||||
self.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'])
|
|
||||||
else:
|
|
||||||
self.api = None
|
|
||||||
raise RuntimeError('No exchange specified. Aborting!')
|
|
||||||
|
|
||||||
# Check if all pairs are available
|
use_poloniex = config.get('poloniex', {}).get('enabled', False)
|
||||||
markets = self.get_markets()
|
use_bittrex = config.get('bittrex', {}).get('enabled', False)
|
||||||
for pair in config[self.exchange.name.lower()]['pair_whitelist']:
|
|
||||||
if pair not in markets:
|
|
||||||
raise RuntimeError('Pair {} is not available at Poloniex'.format(pair))
|
|
||||||
|
|
||||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
if use_poloniex:
|
||||||
"""
|
EXCHANGE = Exchange.POLONIEX
|
||||||
Places a limit buy order.
|
_API = Poloniex(key=config['poloniex']['key'], secret=config['poloniex']['secret'])
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
elif use_bittrex:
|
||||||
:param rate: Rate limit for order
|
EXCHANGE = Exchange.BITTREX
|
||||||
:param amount: The amount to purchase
|
_API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
|
||||||
:return: order_id of the placed buy order
|
else:
|
||||||
"""
|
raise RuntimeError('No exchange specified. Aborting!')
|
||||||
if self.dry_run:
|
|
||||||
pass
|
|
||||||
elif self.exchange == Exchange.POLONIEX:
|
|
||||||
self.api.buy(pair, rate, amount)
|
|
||||||
# TODO: return order id
|
|
||||||
elif self.exchange == Exchange.BITTREX:
|
|
||||||
data = self.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:
|
# Check if all pairs are available
|
||||||
"""
|
markets = get_markets()
|
||||||
Places a limit sell order.
|
for pair in config[EXCHANGE.name.lower()]['pair_whitelist']:
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
if pair not in markets:
|
||||||
:param rate: Rate limit for order
|
raise RuntimeError('Pair {} is not available at Poloniex'.format(pair))
|
||||||
:param amount: The amount to sell
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if self.dry_run:
|
|
||||||
pass
|
|
||||||
elif self.exchange == Exchange.POLONIEX:
|
|
||||||
self.api.sell(pair, rate, amount)
|
|
||||||
# TODO: return order id
|
|
||||||
elif self.exchange == Exchange.BITTREX:
|
|
||||||
data = self.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:
|
|
||||||
"""
|
|
||||||
Get account balance.
|
|
||||||
:param currency: currency as str, format: BTC
|
|
||||||
:return: float
|
|
||||||
"""
|
|
||||||
if self.dry_run:
|
|
||||||
return 999.9
|
|
||||||
elif self.exchange == Exchange.POLONIEX:
|
|
||||||
data = self.api.returnBalances()
|
|
||||||
return float(data[currency])
|
|
||||||
elif self.exchange == Exchange.BITTREX:
|
|
||||||
data = self.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:
|
|
||||||
"""
|
|
||||||
Get Ticker for given pair.
|
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
|
||||||
:return: dict
|
|
||||||
"""
|
|
||||||
if self.exchange == Exchange.POLONIEX:
|
|
||||||
data = self.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('_', '-'))
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return {
|
|
||||||
'bid': float(data['result']['Bid']),
|
|
||||||
'ask': float(data['result']['Ask']),
|
|
||||||
'last': float(data['result']['Last']),
|
|
||||||
}
|
|
||||||
|
|
||||||
def cancel_order(self, order_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Cancel order for given order_id
|
|
||||||
:param order_id: id as str
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if self.dry_run:
|
|
||||||
pass
|
|
||||||
elif self.exchange == Exchange.POLONIEX:
|
|
||||||
raise NotImplemented('Not implemented')
|
|
||||||
elif self.exchange == Exchange.BITTREX:
|
|
||||||
data = self.api.cancel(order_id)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
|
|
||||||
def get_open_orders(self, 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:
|
|
||||||
return []
|
|
||||||
elif self.exchange == Exchange.POLONIEX:
|
|
||||||
raise NotImplemented('Not implemented')
|
|
||||||
elif self.exchange == Exchange.BITTREX:
|
|
||||||
data = self.api.get_open_orders(pair.replace('_', '-'))
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return [{
|
|
||||||
'id': entry['OrderUuid'],
|
|
||||||
'type': entry['OrderType'],
|
|
||||||
'opened': entry['Opened'],
|
|
||||||
'rate': entry['PricePerUnit'],
|
|
||||||
'amount': entry['Quantity'],
|
|
||||||
'remaining': entry['QuantityRemaining'],
|
|
||||||
} for entry in data['result']]
|
|
||||||
|
|
||||||
def get_pair_detail_url(self, 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:
|
|
||||||
raise NotImplemented('Not implemented')
|
|
||||||
elif self.exchange == Exchange.BITTREX:
|
|
||||||
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
|
||||||
|
|
||||||
def get_markets(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Returns all available markets
|
|
||||||
:return: list of all available pairs
|
|
||||||
"""
|
|
||||||
if self.exchange == Exchange.POLONIEX:
|
|
||||||
# TODO: implement
|
|
||||||
raise NotImplemented('Not implemented')
|
|
||||||
elif self.exchange == Exchange. BITTREX:
|
|
||||||
data = self.api.get_markets()
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
def get_exchange_api(conf: dict) -> ApiWrapper:
|
|
||||||
"""
|
"""
|
||||||
Returns the current exchange api or instantiates a new one
|
Places a limit buy order.
|
||||||
:return: exchange.ApiWrapper
|
:param pair: Pair as str, format: BTC_ETH
|
||||||
|
:param rate: Rate limit for order
|
||||||
|
:param amount: The amount to purchase
|
||||||
|
:return: order_id of the placed buy order
|
||||||
"""
|
"""
|
||||||
global _exchange_api
|
if _CONF['dry_run']:
|
||||||
if not _exchange_api:
|
return 'dry_run'
|
||||||
_exchange_api = ApiWrapper(conf)
|
elif EXCHANGE == Exchange.POLONIEX:
|
||||||
return _exchange_api
|
_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']:
|
||||||
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
return data['result']['uuid']
|
||||||
|
|
||||||
|
|
||||||
|
def sell(pair: str, rate: float, amount: float) -> str:
|
||||||
|
"""
|
||||||
|
Places a limit sell order.
|
||||||
|
:param pair: Pair as str, format: BTC_ETH
|
||||||
|
:param rate: Rate limit for order
|
||||||
|
:param amount: The amount to sell
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
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']:
|
||||||
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
return data['result']['uuid']
|
||||||
|
|
||||||
|
|
||||||
|
def get_balance(currency: str) -> float:
|
||||||
|
"""
|
||||||
|
Get account balance.
|
||||||
|
:param currency: currency as str, format: BTC
|
||||||
|
:return: 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']:
|
||||||
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
return float(data['result']['Balance'] or 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ticker(pair: str) -> dict:
|
||||||
|
"""
|
||||||
|
Get Ticker for given pair.
|
||||||
|
: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:
|
||||||
|
data = _API.get_ticker(pair.replace('_', '-'))
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
return {
|
||||||
|
'bid': float(data['result']['Bid']),
|
||||||
|
'ask': float(data['result']['Ask']),
|
||||||
|
'last': float(data['result']['Last']),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_order(order_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Cancel order for given order_id
|
||||||
|
:param order_id: id as str
|
||||||
|
:return: 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']:
|
||||||
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
|
||||||
|
|
||||||
|
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 _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']:
|
||||||
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
return [{
|
||||||
|
'id': entry['OrderUuid'],
|
||||||
|
'type': entry['OrderType'],
|
||||||
|
'opened': entry['Opened'],
|
||||||
|
'rate': entry['PricePerUnit'],
|
||||||
|
'amount': entry['Quantity'],
|
||||||
|
'remaining': entry['QuantityRemaining'],
|
||||||
|
} for entry in data['result']]
|
||||||
|
|
||||||
|
|
||||||
|
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 EXCHANGE == Exchange.POLONIEX:
|
||||||
|
raise NotImplemented('Not implemented')
|
||||||
|
elif EXCHANGE == Exchange.BITTREX:
|
||||||
|
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
data = _API.get_markets()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||||
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||||
|
252
main.py
252
main.py
@ -1,19 +1,19 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import JSONDecodeError
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from requests import ConnectionError
|
from jsonschema import validate
|
||||||
from wrapt import synchronized
|
|
||||||
|
import exchange
|
||||||
|
import persistence
|
||||||
|
from persistence import Trade
|
||||||
from analyze import get_buy_signal
|
from analyze import get_buy_signal
|
||||||
from persistence import Trade, Session
|
from misc import CONF_SCHEMA, get_state, State, update_state
|
||||||
from exchange import get_exchange_api, Exchange
|
from rpc import telegram
|
||||||
from rpc.telegram import TelegramHandler
|
|
||||||
from utils import get_conf
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
@ -22,60 +22,26 @@ logger = logging.getLogger(__name__)
|
|||||||
__author__ = "gcarq"
|
__author__ = "gcarq"
|
||||||
__copyright__ = "gcarq 2017"
|
__copyright__ = "gcarq 2017"
|
||||||
__license__ = "GPLv3"
|
__license__ = "GPLv3"
|
||||||
__version__ = "0.8.0"
|
__version__ = "0.9.0"
|
||||||
|
|
||||||
|
_CONF = {}
|
||||||
|
|
||||||
|
|
||||||
CONFIG = get_conf()
|
def _process() -> None:
|
||||||
api_wrapper = get_exchange_api(CONFIG)
|
"""
|
||||||
|
Queries the persistence layer for open trades and handles them,
|
||||||
|
otherwise a new trade is created.
|
||||||
class TradeThread(threading.Thread):
|
:return: None
|
||||||
def __init__(self):
|
"""
|
||||||
super().__init__()
|
try:
|
||||||
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:
|
|
||||||
"""
|
|
||||||
Queries the persistence layer for open trades and handles them,
|
|
||||||
otherwise a new trade is created.
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# Query trades from persistence layer
|
# Query trades from persistence layer
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if len(trades) < CONFIG['max_open_trades']:
|
if len(trades) < _CONF['max_open_trades']:
|
||||||
try:
|
try:
|
||||||
# Create entity and execute trade
|
# 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:
|
if trade:
|
||||||
Session.add(trade)
|
Trade.session.add(trade)
|
||||||
else:
|
else:
|
||||||
logging.info('Got no buy signal...')
|
logging.info('Got no buy signal...')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -83,45 +49,21 @@ class TradeThread(threading.Thread):
|
|||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# Check if there is already an open order for this trade
|
# Check if there is already an open order for this trade
|
||||||
orders = api_wrapper.get_open_orders(trade.pair)
|
orders = exchange.get_open_orders(trade.pair)
|
||||||
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
||||||
if orders:
|
if orders:
|
||||||
msg = 'There exists an open order for {}: Order(total={}, remaining={}, type={}, id={})' \
|
logger.info('There is an open order for: %s', orders[0])
|
||||||
.format(
|
else:
|
||||||
trade,
|
# Update state
|
||||||
round(orders[0]['amount'], 8),
|
trade.open_order_id = None
|
||||||
round(orders[0]['remaining'], 8),
|
# Check if this trade can be closed
|
||||||
orders[0]['type'],
|
if not close_trade_if_fulfilled(trade):
|
||||||
orders[0]['id'])
|
# Check if we can sell our current pair
|
||||||
logger.info(msg)
|
handle_trade(trade)
|
||||||
continue
|
Trade.session.flush()
|
||||||
|
except (ConnectionError, json.JSONDecodeError) as error:
|
||||||
# Update state
|
msg = 'Got {} in _process()'.format(error.__class__.__name__)
|
||||||
trade.open_order_id = None
|
logger.exception(msg)
|
||||||
# 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 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
|
|
||||||
|
|
||||||
|
|
||||||
def close_trade_if_fulfilled(trade: Trade) -> bool:
|
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,
|
# If we don't have an open order and the close rate is already set,
|
||||||
# we can close this trade.
|
# 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
|
trade.is_open = False
|
||||||
Session.flush()
|
logger.info('No open orders found and trade is fulfilled. Marking %s as closed ...', trade)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def execute_sell(trade: Trade, current_rate: float) -> None:
|
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
|
# Get available balance
|
||||||
currency = trade.pair.split('_')[1]
|
currency = trade.pair.split('_')[1]
|
||||||
balance = api_wrapper.get_balance(currency)
|
balance = exchange.get_balance(currency)
|
||||||
|
|
||||||
profit = trade.exec_sell_order(current_rate, balance)
|
profit = trade.exec_sell_order(current_rate, balance)
|
||||||
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('_', '/'),
|
||||||
api_wrapper.get_pair_detail_url(trade.pair),
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
trade.close_rate,
|
trade.close_rate,
|
||||||
round(profit, 2)
|
round(profit, 2)
|
||||||
)
|
)
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
TelegramHandler.send_msg(message)
|
telegram.send_msg(message)
|
||||||
|
|
||||||
|
|
||||||
def handle_trade(trade: Trade) -> None:
|
def handle_trade(trade: Trade) -> None:
|
||||||
@ -166,39 +118,41 @@ def handle_trade(trade: Trade) -> None:
|
|||||||
|
|
||||||
logger.debug('Handling open trade %s ...', trade)
|
logger.debug('Handling open trade %s ...', trade)
|
||||||
# Get current rate
|
# 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)
|
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.')
|
logger.debug('Stop loss hit.')
|
||||||
execute_sell(trade, current_rate)
|
execute_sell(trade, current_rate)
|
||||||
return
|
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)
|
duration, threshold = float(duration), float(threshold)
|
||||||
# Check if time matches and current rate is above threshold
|
# Check if time matches and current rate is above threshold
|
||||||
time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60
|
time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60
|
||||||
if time_diff > duration and current_rate > (1 + threshold) * trade.open_rate:
|
if time_diff > duration and current_rate > (1 + threshold) * trade.open_rate:
|
||||||
execute_sell(trade, current_rate)
|
execute_sell(trade, current_rate)
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit)
|
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 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,
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
:param stake_amount: amount of btc to spend
|
:param stake_amount: amount of btc to spend
|
||||||
:param exchange: exchange to use
|
:param _exchange: exchange to use
|
||||||
"""
|
"""
|
||||||
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
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
|
# Check if btc_amount is fulfilled
|
||||||
if api_wrapper.get_balance(CONFIG['stake_currency']) < stake_amount:
|
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||||
raise ValueError('stake amount is not fulfilled (currency={}'.format(CONFIG['stake_currency']))
|
raise ValueError(
|
||||||
|
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency'])
|
||||||
|
)
|
||||||
|
|
||||||
# Remove currently opened and latest pairs from whitelist
|
# Remove currently opened and latest pairs from whitelist
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
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')
|
raise ValueError('No pair in whitelist')
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
# Pick pair based on StochRSI buy signals
|
||||||
for p in whitelist:
|
for _pair in whitelist:
|
||||||
if get_buy_signal(p):
|
if get_buy_signal(_pair):
|
||||||
pair = p
|
pair = _pair
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
open_rate = api_wrapper.get_ticker(pair)['ask']
|
open_rate = exchange.get_ticker(pair)['ask']
|
||||||
amount = stake_amount / open_rate
|
amount = stake_amount / open_rate
|
||||||
exchange = exchange
|
order_id = exchange.buy(pair, open_rate, amount)
|
||||||
order_id = api_wrapper.buy(pair, open_rate, amount)
|
|
||||||
|
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
||||||
exchange.name,
|
_exchange.name,
|
||||||
pair.replace('_', '/'),
|
pair.replace('_', '/'),
|
||||||
api_wrapper.get_pair_detail_url(pair),
|
exchange.get_pair_detail_url(pair),
|
||||||
open_rate
|
open_rate
|
||||||
)
|
)
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
TelegramHandler.send_msg(message)
|
telegram.send_msg(message)
|
||||||
return Trade(pair=pair,
|
return Trade(pair=pair,
|
||||||
btc_amount=stake_amount,
|
btc_amount=stake_amount,
|
||||||
open_rate=open_rate,
|
open_rate=open_rate,
|
||||||
|
open_date=datetime.utcnow(),
|
||||||
amount=amount,
|
amount=amount,
|
||||||
exchange=exchange,
|
exchange=_exchange,
|
||||||
open_order_id=order_id)
|
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__':
|
if __name__ == '__main__':
|
||||||
logger.info('Starting freqtrade %s', __version__)
|
with open('config.json') as file:
|
||||||
TelegramHandler.listen()
|
_CONF = json.load(file)
|
||||||
while True:
|
validate(_CONF, CONF_SCHEMA)
|
||||||
time.sleep(0.5)
|
app(_CONF)
|
||||||
|
@ -1,16 +1,39 @@
|
|||||||
import json
|
import enum
|
||||||
import logging
|
|
||||||
|
|
||||||
from jsonschema import validate
|
|
||||||
from wrapt import synchronized
|
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
|
# Required json-schema for user specified config
|
||||||
_conf_schema = {
|
CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
||||||
@ -35,7 +58,8 @@ _conf_schema = {
|
|||||||
'chat_id': {'type': 'string'},
|
'chat_id': {'type': 'string'},
|
||||||
},
|
},
|
||||||
'required': ['enabled', 'token', 'chat_id']
|
'required': ['enabled', 'token', 'chat_id']
|
||||||
}
|
},
|
||||||
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
},
|
},
|
||||||
'definitions': {
|
'definitions': {
|
||||||
'exchange': {
|
'exchange': {
|
||||||
@ -66,18 +90,3 @@ _conf_schema = {
|
|||||||
'telegram'
|
'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
|
|
@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
|
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
@ -7,26 +8,42 @@ from sqlalchemy.orm.session import sessionmaker
|
|||||||
|
|
||||||
from sqlalchemy.types import Enum
|
from sqlalchemy.types import Enum
|
||||||
|
|
||||||
from exchange import Exchange, get_exchange_api
|
import exchange
|
||||||
from utils import get_conf
|
|
||||||
|
|
||||||
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)
|
_CONF = {}
|
||||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
|
||||||
Session = session()
|
|
||||||
Base = declarative_base()
|
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):
|
class Trade(Base):
|
||||||
__tablename__ = 'trades'
|
__tablename__ = 'trades'
|
||||||
query = session.query_property()
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
exchange = Column(Enum(Exchange), nullable=False)
|
exchange = Column(Enum(exchange.Exchange), nullable=False)
|
||||||
pair = Column(String, nullable=False)
|
pair = Column(String, nullable=False)
|
||||||
is_open = Column(Boolean, nullable=False, default=True)
|
is_open = Column(Boolean, nullable=False, default=True)
|
||||||
open_rate = Column(Float, nullable=False)
|
open_rate = Column(Float, nullable=False)
|
||||||
@ -39,12 +56,16 @@ class Trade(Base):
|
|||||||
open_order_id = Column(String)
|
open_order_id = Column(String)
|
||||||
|
|
||||||
def __repr__(self):
|
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(
|
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
|
||||||
self.id,
|
self.id,
|
||||||
self.pair,
|
self.pair,
|
||||||
self.amount,
|
self.amount,
|
||||||
self.open_rate,
|
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:
|
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)
|
profit = 100 * ((rate - self.open_rate) / self.open_rate)
|
||||||
|
|
||||||
# Execute sell and update trade record
|
# 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_rate = rate
|
||||||
self.close_profit = profit
|
self.close_profit = profit
|
||||||
self.close_date = datetime.utcnow()
|
self.close_date = datetime.utcnow()
|
||||||
self.open_order_id = order_id
|
self.open_order_id = order_id
|
||||||
Session.flush()
|
|
||||||
return profit
|
|
||||||
|
|
||||||
Base.metadata.create_all(engine)
|
# Flush changes
|
||||||
|
Trade.session.flush()
|
||||||
|
return profit
|
||||||
|
@ -8,8 +8,8 @@ urllib3==1.22
|
|||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.20.3
|
pandas==0.20.3
|
||||||
matplotlib==2.0.2
|
matplotlib==2.0.2
|
||||||
PYQT5==5.9
|
|
||||||
scikit-learn==0.19.0
|
scikit-learn==0.19.0
|
||||||
scipy==0.19.1
|
scipy==0.19.1
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
TA-Lib==0.4.10
|
TA-Lib==0.4.10
|
||||||
|
#PYQT5==5.9
|
@ -0,0 +1 @@
|
|||||||
|
from . import telegram
|
518
rpc/telegram.py
518
rpc/telegram.py
@ -3,15 +3,15 @@ from datetime import timedelta
|
|||||||
from typing import Callable, Any
|
from typing import Callable, Any
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import and_, func
|
from sqlalchemy import and_, func, text
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
from telegram import ParseMode, Bot, Update
|
from telegram import ParseMode, Bot, Update
|
||||||
from wrapt import synchronized
|
|
||||||
|
|
||||||
from persistence import Trade, Session
|
from misc import get_state, State, update_state
|
||||||
from exchange import get_exchange_api
|
from persistence import Trade
|
||||||
from utils import get_conf
|
|
||||||
|
import exchange
|
||||||
|
|
||||||
# Remove noisy log messages
|
# Remove noisy log messages
|
||||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||||
@ -19,9 +19,43 @@ logging.getLogger('telegram').setLevel(logging.INFO)
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_updater = None
|
_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]:
|
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
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
def wrapper(*args, **kwargs):
|
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):
|
if not isinstance(bot, Bot) or not isinstance(update, Update):
|
||||||
raise ValueError('Received invalid Arguments: {}'.format(*args))
|
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:
|
if int(update.message.chat_id) == chat_id:
|
||||||
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
|
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
|
||||||
return command_handler(*args, **kwargs)
|
return command_handler(*args, **kwargs)
|
||||||
@ -44,33 +79,34 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class TelegramHandler(object):
|
@authorized_only
|
||||||
@staticmethod
|
def _status(bot: Bot, update: Update) -> None:
|
||||||
@authorized_only
|
"""
|
||||||
def _status(bot: Bot, update: Update) -> None:
|
Handler for /status.
|
||||||
"""
|
Returns the current TradeThread status
|
||||||
Handler for /status.
|
:param bot: telegram bot
|
||||||
Returns the current TradeThread status
|
:param update: message update
|
||||||
:param bot: telegram bot
|
:return: None
|
||||||
:param update: message update
|
"""
|
||||||
:return: None
|
# Fetch open trade
|
||||||
"""
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
# Fetch open trade
|
if get_state() != State.RUNNING:
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
send_msg('*Status:* `trader is not running`', bot=bot)
|
||||||
from main import get_instance
|
elif not trades:
|
||||||
if not get_instance().is_alive():
|
send_msg('*Status:* `no active order`', bot=bot)
|
||||||
TelegramHandler.send_msg('*Status:* `trader is not running`', bot=bot)
|
else:
|
||||||
elif not trades:
|
for trade in trades:
|
||||||
TelegramHandler.send_msg('*Status:* `no active order`', bot=bot)
|
# calculate profit and send message to user
|
||||||
else:
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
for trade in trades:
|
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
||||||
# calculate profit and send message to user
|
orders = exchange.get_open_orders(trade.pair)
|
||||||
current_rate = api_wrapper.get_ticker(trade.pair)['bid']
|
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
||||||
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
order = orders[0] if orders else None
|
||||||
orders = api_wrapper.get_open_orders(trade.pair)
|
|
||||||
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
fmt_close_profit = '{:.2f}%'.format(
|
||||||
order = orders[0] if orders else None
|
round(trade.close_profit, 2)
|
||||||
message = """
|
) if trade.close_profit else None
|
||||||
|
message = """
|
||||||
*Trade ID:* `{trade_id}`
|
*Trade ID:* `{trade_id}`
|
||||||
*Current Pair:* [{pair}]({market_url})
|
*Current Pair:* [{pair}]({market_url})
|
||||||
*Open Since:* `{date}`
|
*Open Since:* `{date}`
|
||||||
@ -79,240 +115,210 @@ class TelegramHandler(object):
|
|||||||
*Close Rate:* `{close_rate}`
|
*Close Rate:* `{close_rate}`
|
||||||
*Current Rate:* `{current_rate}`
|
*Current Rate:* `{current_rate}`
|
||||||
*Close Profit:* `{close_profit}`
|
*Close Profit:* `{close_profit}`
|
||||||
*Current Profit:* `{current_profit}%`
|
*Current Profit:* `{current_profit:.2f}%`
|
||||||
*Open Order:* `{open_order}`
|
*Open Order:* `{open_order}`
|
||||||
""".format(
|
""".format(
|
||||||
trade_id=trade.id,
|
trade_id=trade.id,
|
||||||
pair=trade.pair,
|
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(),
|
date=arrow.get(trade.open_date).humanize(),
|
||||||
open_rate=trade.open_rate,
|
open_rate=trade.open_rate,
|
||||||
close_rate=trade.close_rate,
|
close_rate=trade.close_rate,
|
||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
amount=round(trade.amount, 8),
|
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),
|
current_profit=round(current_profit, 2),
|
||||||
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None,
|
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:
|
|
||||||
"""
|
|
||||||
Handler for /profit.
|
|
||||||
Returns a cumulative profit statistics.
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
trades = Trade.query.order_by(Trade.id).all()
|
|
||||||
|
|
||||||
profit_amounts = []
|
@authorized_only
|
||||||
profits = []
|
def _profit(bot: Bot, update: Update) -> None:
|
||||||
durations = []
|
"""
|
||||||
for trade in trades:
|
Handler for /profit.
|
||||||
if trade.close_date:
|
Returns a cumulative profit statistics.
|
||||||
durations.append((trade.close_date - trade.open_date).total_seconds())
|
:param bot: telegram bot
|
||||||
if trade.close_profit:
|
:param update: message update
|
||||||
profit = trade.close_profit
|
:return: None
|
||||||
else:
|
"""
|
||||||
# Get current rate
|
trades = Trade.query.order_by(Trade.id).all()
|
||||||
current_rate = api_wrapper.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 = []
|
||||||
profits.append(profit)
|
profits = []
|
||||||
|
durations = []
|
||||||
|
for trade in trades:
|
||||||
|
if trade.close_date:
|
||||||
|
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||||
|
if trade.close_profit:
|
||||||
|
profit = trade.close_profit
|
||||||
|
else:
|
||||||
|
# Get current rate
|
||||||
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
|
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
||||||
|
|
||||||
bp_pair, bp_rate = Session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
profit_amounts.append((profit / 100) * trade.btc_amount)
|
||||||
.filter(Trade.is_open.is_(False)) \
|
profits.append(profit)
|
||||||
.group_by(Trade.pair) \
|
|
||||||
.order_by('profit_sum DESC') \
|
|
||||||
.first()
|
|
||||||
|
|
||||||
markdown_msg = """
|
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
||||||
*ROI:* `{profit_btc} ({profit}%)`
|
.filter(Trade.is_open.is_(False)) \
|
||||||
|
.group_by(Trade.pair) \
|
||||||
|
.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:.2f} ({profit:.2f}%)`
|
||||||
*Trade Count:* `{trade_count}`
|
*Trade Count:* `{trade_count}`
|
||||||
*First Trade opened:* `{first_trade_date}`
|
*First Trade opened:* `{first_trade_date}`
|
||||||
*Latest Trade opened:* `{latest_trade_date}`
|
*Latest Trade opened:* `{latest_trade_date}`
|
||||||
*Avg. Duration:* `{avg_duration}`
|
*Avg. Duration:* `{avg_duration}`
|
||||||
*Best Performing:* `{best_pair}: {best_rate}%`
|
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
|
||||||
""".format(
|
""".format(
|
||||||
profit_btc=round(sum(profit_amounts), 8),
|
profit_btc=round(sum(profit_amounts), 8),
|
||||||
profit=round(sum(profits), 2),
|
profit=round(sum(profits), 2),
|
||||||
trade_count=len(trades),
|
trade_count=len(trades),
|
||||||
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
||||||
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
||||||
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
|
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
|
||||||
best_pair=bp_pair,
|
best_pair=bp_pair,
|
||||||
best_rate=round(bp_rate, 2),
|
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:
|
|
||||||
"""
|
|
||||||
Handler for /start.
|
|
||||||
Starts TradeThread
|
|
||||||
:param bot: telegram bot
|
|
||||||
:param update: message update
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
from main import get_instance
|
|
||||||
if get_instance().is_alive():
|
|
||||||
TelegramHandler.send_msg('*Status:* `already running`', bot=bot)
|
|
||||||
else:
|
|
||||||
get_instance(recreate=True).start()
|
|
||||||
|
|
||||||
@staticmethod
|
@authorized_only
|
||||||
@authorized_only
|
def _start(bot: Bot, update: Update) -> None:
|
||||||
def _stop(bot: Bot, update: Update) -> None:
|
"""
|
||||||
"""
|
Handler for /start.
|
||||||
Handler for /stop.
|
Starts TradeThread
|
||||||
Stops TradeThread
|
:param bot: telegram bot
|
||||||
:param bot: telegram bot
|
:param update: message update
|
||||||
:param update: message update
|
:return: None
|
||||||
:return: None
|
"""
|
||||||
"""
|
if get_state() == State.RUNNING:
|
||||||
from main import get_instance
|
send_msg('*Status:* `already running`', bot=bot)
|
||||||
if get_instance().is_alive():
|
else:
|
||||||
TelegramHandler.send_msg('`Stopping trader ...`', bot=bot)
|
update_state(State.RUNNING)
|
||||||
get_instance().stop()
|
|
||||||
else:
|
|
||||||
TelegramHandler.send_msg('*Status:* `already stopped`', bot=bot)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _forcesell(bot: Bot, update: Update) -> None:
|
def _stop(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /forcesell <id>.
|
Handler for /stop.
|
||||||
Sells the given trade at current price
|
Stops TradeThread
|
||||||
:param bot: telegram bot
|
:param bot: telegram bot
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
from main import get_instance
|
if get_state() == State.RUNNING:
|
||||||
if not get_instance().is_alive():
|
send_msg('`Stopping trader ...`', bot=bot)
|
||||||
TelegramHandler.send_msg('`trader is not running`', bot=bot)
|
update_state(State.STOPPED)
|
||||||
|
else:
|
||||||
|
send_msg('*Status:* `already stopped`', bot=bot)
|
||||||
|
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _forcesell(bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /forcesell <id>.
|
||||||
|
Sells the given trade at current price
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if get_state() != State.RUNNING:
|
||||||
|
send_msg('`trader is not running`', bot=bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
trade_id = int(update.message.text
|
||||||
|
.replace('/forcesell', '')
|
||||||
|
.strip())
|
||||||
|
# Query for trade
|
||||||
|
trade = Trade.query.filter(and_(
|
||||||
|
Trade.id == trade_id,
|
||||||
|
Trade.is_open.is_(True)
|
||||||
|
)).first()
|
||||||
|
if not trade:
|
||||||
|
send_msg('There is no open trade with ID: `{}`'.format(trade_id))
|
||||||
return
|
return
|
||||||
|
# Get current rate
|
||||||
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
|
# Get available balance
|
||||||
|
currency = trade.pair.split('_')[1]
|
||||||
|
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('_', '/'),
|
||||||
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
|
trade.close_rate,
|
||||||
|
round(profit, 2)
|
||||||
|
)
|
||||||
|
logger.info(message)
|
||||||
|
send_msg(message)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
|
||||||
|
logger.warning('/forcesell: Invalid argument received')
|
||||||
|
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _performance(bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /performance.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if get_state() != State.RUNNING:
|
||||||
|
send_msg('`trader is not running`', bot=bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
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(text('profit_sum DESC')) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
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)
|
||||||
|
send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
|
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||||
|
"""
|
||||||
|
Send given markdown message
|
||||||
|
:param msg: message
|
||||||
|
:param bot: alternative bot
|
||||||
|
:param parse_mode: telegram parse mode
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if _CONF['telegram'].get('enabled', False):
|
||||||
try:
|
try:
|
||||||
trade_id = int(update.message.text
|
bot = bot or _updater.bot
|
||||||
.replace('/forcesell', '')
|
|
||||||
.strip())
|
|
||||||
# Query for trade
|
|
||||||
trade = Trade.query.filter(and_(
|
|
||||||
Trade.id == trade_id,
|
|
||||||
Trade.is_open.is_(True)
|
|
||||||
)).first()
|
|
||||||
if not trade:
|
|
||||||
TelegramHandler.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']
|
|
||||||
# Get available balance
|
|
||||||
currency = trade.pair.split('_')[1]
|
|
||||||
balance = api_wrapper.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),
|
|
||||||
trade.close_rate,
|
|
||||||
round(profit, 2)
|
|
||||||
)
|
|
||||||
logger.info(message)
|
|
||||||
TelegramHandler.send_msg(message)
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
TelegramHandler.send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
|
|
||||||
logger.warning('/forcesell: Invalid argument received')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@authorized_only
|
|
||||||
def _performance(bot: Bot, update: Update) -> None:
|
|
||||||
"""
|
|
||||||
Handler for /performance.
|
|
||||||
Shows a performance statistic from finished trades
|
|
||||||
:param bot: telegram bot
|
|
||||||
: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)
|
|
||||||
return
|
|
||||||
pair_rates = 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') \
|
|
||||||
.all()
|
|
||||||
|
|
||||||
stats = '\n'.join('{}. <code>{}\t{}%</code>'.format(i + 1, pair, 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)
|
|
||||||
|
|
||||||
@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:
|
|
||||||
"""
|
|
||||||
Send given markdown message
|
|
||||||
:param msg: message
|
|
||||||
:param bot: alternative bot
|
|
||||||
:param parse_mode: telegram parse mode
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if conf['telegram'].get('enabled', False):
|
|
||||||
try:
|
try:
|
||||||
bot = bot or TelegramHandler.get_updater(conf).bot
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
try:
|
except NetworkError as error:
|
||||||
bot.send_message(conf['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
# Sometimes the telegram server resets the current connection,
|
||||||
except NetworkError as error:
|
# if this is the case we send the message again.
|
||||||
# Sometimes the telegram server resets the current connection,
|
logger.warning(
|
||||||
# if this is the case we send the message again.
|
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||||
logger.warning('Got Telegram NetworkError: %s! Trying one more time.', error.message)
|
error.message
|
||||||
bot.send_message(conf['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
)
|
||||||
except Exception:
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
logger.exception('Exception occurred within Telegram API')
|
except Exception:
|
||||||
|
logger.exception('Exception occurred within Telegram API')
|
||||||
|
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
49
test/test_analyze.py
Normal file
49
test/test_analyze.py
Normal 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
105
test/test_main.py
Normal 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
28
test/test_persistence.py
Normal 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
198
test/test_telegram.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user