Compare commits
96 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e9dbdc9247 | ||
|
86b6c6f334 | ||
|
cd5afd6ff4 | ||
|
d88cc084e6 | ||
|
5deaebf0c2 | ||
|
19734ad863 | ||
|
b16ccb9919 | ||
|
d41837817c | ||
|
3ab14dfe39 | ||
|
4a91ecd91a | ||
|
a3da2911e8 | ||
|
6f5b418f0b | ||
|
57691c82b1 | ||
|
37a74b38ba | ||
|
9ab81a987d | ||
|
4b08e3d571 | ||
|
187fea0c28 | ||
|
4e54b27398 | ||
|
aced5cc3ba | ||
|
669ec30413 | ||
|
0082b7abdd | ||
|
7903f3a546 | ||
|
ec75586bdd | ||
|
df9902d6a4 | ||
|
315919cdd6 | ||
|
63c95a3546 | ||
|
59d04d1d0c | ||
|
d1cc9e868b | ||
|
14de46576b | ||
|
bdff29a472 | ||
|
8655c6c264 | ||
|
3f4e4a23a0 | ||
|
b682262486 | ||
|
5be7be6189 | ||
|
3475a07522 | ||
|
fb7ea169d4 | ||
|
5469293e5f | ||
|
9b644b0305 | ||
|
0df1404d6a | ||
|
bb4a9ed20f | ||
|
77887d6fbc | ||
|
d89db50465 | ||
|
632d00e01d | ||
|
2a56031cdc | ||
|
16d412323c | ||
|
27a6b29c80 | ||
|
5d1f874041 | ||
|
174122a09b | ||
|
1b6a60ecb2 | ||
|
1ccb266032 | ||
|
a963f1820c | ||
|
b9983149ef | ||
|
c1ef3f526c | ||
|
6b7afb80b2 | ||
|
0b8afa12e9 | ||
|
1db0a7d4ce | ||
|
c12a9ebd92 | ||
|
d86dcc4752 | ||
|
0bc96241d5 | ||
|
a0bb7a61e6 | ||
|
b115963a70 | ||
|
2e953a937d | ||
|
4e05691cab | ||
|
b5f58724a0 | ||
|
b83309b55d | ||
|
e8101a6da5 | ||
|
dd9cb008fb | ||
|
81f7172c4a | ||
|
bab59fbacd | ||
|
0f0b10b6cc | ||
|
8e68c5358e | ||
|
660f01b514 | ||
|
13537e3ce4 | ||
|
2963a90008 | ||
|
15b20b83fa | ||
|
1c3c316e45 | ||
|
517879382b | ||
|
bcd3340a80 | ||
|
12ae1e111e | ||
|
d3b3370f23 | ||
|
8f817a3634 | ||
|
cf79b15651 | ||
|
a4284351e3 | ||
|
906caf329b | ||
|
3db13fae13 | ||
|
274972f7af | ||
|
83fd27e031 | ||
|
3126dcfcea | ||
|
72aec6c320 | ||
|
b709ccbf53 | ||
|
7e99b13742 | ||
|
8b464033ff | ||
|
93c525a8fa | ||
|
54b15c1556 | ||
|
029f32af63 | ||
|
e01c85bb3a |
@@ -1,3 +1,10 @@
|
|||||||
|
[MASTER]
|
||||||
|
extension-pkg-whitelist=numpy,talib
|
||||||
|
|
||||||
[BASIC]
|
[BASIC]
|
||||||
good-names=logger
|
good-names=logger
|
||||||
ignore=vendor
|
ignore=vendor
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
ignored-modules=numpy,talib
|
||||||
|
|
||||||
|
48
README.md
48
README.md
@@ -22,8 +22,11 @@ Persistence is achieved through sqlite.
|
|||||||
* /status [table]: Lists all open trades
|
* /status [table]: Lists all open trades
|
||||||
* /count: Displays number of open trades
|
* /count: Displays number of open trades
|
||||||
* /profit: Lists cumulative profit from all finished trades
|
* /profit: Lists cumulative profit from all finished trades
|
||||||
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
|
* /forcesell <trade_id>|all: 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
|
||||||
|
* /balance: Show account balance per currency
|
||||||
|
* /help: Show help message
|
||||||
|
* /version: Show version
|
||||||
|
|
||||||
### 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
|
||||||
@@ -112,14 +115,14 @@ filesystem):
|
|||||||
|
|
||||||
```
|
```
|
||||||
$ cd ~/.freq
|
$ cd ~/.freq
|
||||||
$ touch tradesv2.sqlite
|
$ touch tradesv3.sqlite
|
||||||
$ docker run -d \
|
$ docker run -d \
|
||||||
--name freqtrade \
|
--name freqtrade \
|
||||||
-v ~/.freq/config.json:/freqtrade/config.json \
|
-v ~/.freq/config.json:/freqtrade/config.json \
|
||||||
-v ~/.freq/tradesv2.sqlite:/freqtrade/tradesv2.sqlite \
|
-v ~/.freq/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
freqtrade
|
freqtrade
|
||||||
```
|
```
|
||||||
If you are using `dry_run=True` you need to bind `tradesv2.dry_run.sqlite` instead of `tradesv2.sqlite`.
|
If you are using `dry_run=True` it's not necessary to mount `tradesv3.sqlite`.
|
||||||
|
|
||||||
You can then use the following commands to monitor and manage your container:
|
You can then use the following commands to monitor and manage your container:
|
||||||
|
|
||||||
@@ -134,6 +137,43 @@ $ docker start freqtrade
|
|||||||
You do not need to rebuild the image for configuration
|
You do not need to rebuild the image for configuration
|
||||||
changes, it will suffice to edit `config.json` and restart the container.
|
changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
```
|
||||||
|
usage: freqtrade [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist]
|
||||||
|
{backtesting} ...
|
||||||
|
|
||||||
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{backtesting}
|
||||||
|
backtesting backtesting module
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
specify configuration file (default: config.json)
|
||||||
|
-v, --verbose be verbose
|
||||||
|
--version show program's version number and exit
|
||||||
|
--dynamic-whitelist dynamically generate and update whitelist based on 24h
|
||||||
|
BaseVolume
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backtesting
|
||||||
|
|
||||||
|
Backtesting also uses the config specified via `-c/--config`.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade backtesting [-h] [-l] [-i INT]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-l, --live using live data
|
||||||
|
-i INT, --ticker-interval INT
|
||||||
|
specify ticker interval in minutes (default: 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Execute tests
|
### Execute tests
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@@ -34,5 +34,8 @@
|
|||||||
"token": "token",
|
"token": "token",
|
||||||
"chat_id": "chat_id"
|
"chat_id": "chat_id"
|
||||||
},
|
},
|
||||||
"initial_state": "running"
|
"initial_state": "running",
|
||||||
|
"internals": {
|
||||||
|
"process_throttle_secs": 5
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,3 +1,4 @@
|
|||||||
__version__ = '0.14.1'
|
""" FreqTrade bot """
|
||||||
|
__version__ = '0.14.3'
|
||||||
|
|
||||||
from . import main
|
from . import main
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||||
|
"""
|
||||||
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
@@ -6,12 +10,15 @@ import talib.abstract as ta
|
|||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_ticker_history
|
||||||
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
|
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, crossed_above
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SignalType(Enum):
|
||||||
|
""" Enum to distinguish between buy and sell signals """
|
||||||
|
BUY = "buy"
|
||||||
|
SELL = "sell"
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@@ -41,9 +48,7 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
dataframe['cci'] = ta.CCI(dataframe)
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
dataframe['mom'] = ta.MOM(dataframe)
|
|
||||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||||
@@ -53,23 +58,36 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
dataframe['macd'] = macd['macd']
|
dataframe['macd'] = macd['macd']
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
dataframe['macdhist'] = macd['macdhist']
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
|
hilbert = ta.HT_SINE(dataframe)
|
||||||
|
dataframe['htsine'] = hilbert['sine']
|
||||||
|
dataframe['htleadsine'] = hilbert['leadsine']
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy trend for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.ix[
|
dataframe.loc[
|
||||||
(dataframe['close'] < dataframe['sma']) &
|
|
||||||
(dataframe['tema'] <= dataframe['blower']) &
|
(dataframe['tema'] <= dataframe['blower']) &
|
||||||
(dataframe['mfi'] < 25) &
|
(dataframe['rsi'] < 37) &
|
||||||
(dataframe['fastd'] < 25) &
|
(dataframe['fastd'] < 48) &
|
||||||
(dataframe['adx'] > 30),
|
(dataframe['adx'] > 31),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(crossed_above(dataframe['rsi'], 70)),
|
||||||
|
'sell'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@@ -80,26 +98,28 @@ def analyze_ticker(pair: str) -> DataFrame:
|
|||||||
add several TA indicators and buy signal to it
|
add several TA indicators and buy signal to it
|
||||||
:return DataFrame with ticker data and indicator data
|
:return DataFrame with ticker data and indicator data
|
||||||
"""
|
"""
|
||||||
data = get_ticker_history(pair)
|
ticker_hist = get_ticker_history(pair)
|
||||||
dataframe = parse_ticker_dataframe(data)
|
if not ticker_hist:
|
||||||
|
logger.warning('Empty ticker history for pair %s', pair)
|
||||||
if dataframe.empty:
|
return DataFrame()
|
||||||
logger.warning('Empty dataframe for pair %s', pair)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
|
dataframe = parse_ticker_dataframe(ticker_hist)
|
||||||
dataframe = populate_indicators(dataframe)
|
dataframe = populate_indicators(dataframe)
|
||||||
dataframe = populate_buy_trend(dataframe)
|
dataframe = populate_buy_trend(dataframe)
|
||||||
|
dataframe = populate_sell_trend(dataframe)
|
||||||
|
# TODO: buy_price and sell_price are only used by the plotter, should probably be moved there
|
||||||
|
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||||
|
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def get_buy_signal(pair: str) -> bool:
|
def get_signal(pair: str, signal: SignalType) -> bool:
|
||||||
"""
|
"""
|
||||||
Calculates a buy signal based several technical analysis indicators
|
Calculates current 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 good for buying, False otherwise
|
:return: True if pair is good for buying, False otherwise
|
||||||
"""
|
"""
|
||||||
dataframe = analyze_ticker(pair)
|
dataframe = analyze_ticker(pair)
|
||||||
|
|
||||||
if dataframe.empty:
|
if dataframe.empty:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -110,6 +130,6 @@ 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['buy'] == 1
|
result = latest[signal.value] == 1
|
||||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
|
||||||
return signal
|
return result
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
|
# pragma pylint: disable=W0603
|
||||||
|
""" Cryptocurrency Exchanges support """
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
import requests
|
||||||
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
from freqtrade.exchange.interface import Exchange
|
from freqtrade.exchange.interface import Exchange
|
||||||
@@ -62,7 +66,12 @@ def validate_pairs(pairs: List[str]) -> None:
|
|||||||
:param pairs: list of pairs
|
:param pairs: list of pairs
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
markets = _API.get_markets()
|
try:
|
||||||
|
markets = _API.get_markets()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
|
||||||
|
return
|
||||||
|
|
||||||
stake_cur = _CONF['stake_currency']
|
stake_cur = _CONF['stake_currency']
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
if not pair.startswith(stake_cur):
|
if not pair.startswith(stake_cur):
|
||||||
@@ -76,7 +85,7 @@ def validate_pairs(pairs: List[str]) -> None:
|
|||||||
def buy(pair: str, rate: float, amount: float) -> str:
|
def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_buy_{}'.format(randint(0, 1e6))
|
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'rate': rate,
|
'rate': rate,
|
||||||
@@ -94,7 +103,7 @@ def buy(pair: str, rate: float, amount: float) -> str:
|
|||||||
def sell(pair: str, rate: float, amount: float) -> str:
|
def sell(pair: str, rate: float, amount: float) -> str:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_sell_{}'.format(randint(0, 1e6))
|
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'rate': rate,
|
'rate': rate,
|
||||||
@@ -127,7 +136,8 @@ def get_ticker(pair: str) -> dict:
|
|||||||
return _API.get_ticker(pair)
|
return _API.get_ticker(pair)
|
||||||
|
|
||||||
|
|
||||||
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List:
|
@cached(TTLCache(maxsize=100, ttl=30))
|
||||||
|
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]:
|
||||||
return _API.get_ticker_history(pair, tick_interval)
|
return _API.get_ticker_history(pair, tick_interval)
|
||||||
|
|
||||||
|
|
||||||
@@ -157,13 +167,17 @@ def get_markets() -> List[str]:
|
|||||||
return _API.get_markets()
|
return _API.get_markets()
|
||||||
|
|
||||||
|
|
||||||
|
def get_market_summaries() -> List[Dict]:
|
||||||
|
return _API.get_market_summaries()
|
||||||
|
|
||||||
|
|
||||||
def get_name() -> str:
|
def get_name() -> str:
|
||||||
return _API.name
|
return _API.name
|
||||||
|
|
||||||
|
|
||||||
def get_sleep_time() -> float:
|
|
||||||
return _API.sleep_time
|
|
||||||
|
|
||||||
|
|
||||||
def get_fee() -> float:
|
def get_fee() -> float:
|
||||||
return _API.fee
|
return _API.fee
|
||||||
|
|
||||||
|
|
||||||
|
def get_wallet_health() -> List[Dict]:
|
||||||
|
return _API.get_wallet_health()
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
import requests
|
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
|
||||||
from bittrex.bittrex import Bittrex as _Bittrex
|
from requests.exceptions import ContentDecodingError
|
||||||
|
|
||||||
from freqtrade.exchange.interface import Exchange
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_API: _Bittrex = None
|
_API: _Bittrex = None
|
||||||
|
_API_V2: _Bittrex = None
|
||||||
_EXCHANGE_CONF: dict = {}
|
_EXCHANGE_CONF: dict = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -18,22 +19,23 @@ class Bittrex(Exchange):
|
|||||||
"""
|
"""
|
||||||
# Base URL and API endpoints
|
# Base URL and API endpoints
|
||||||
BASE_URL: str = 'https://www.bittrex.com'
|
BASE_URL: str = 'https://www.bittrex.com'
|
||||||
TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks'
|
|
||||||
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
|
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
|
||||||
|
|
||||||
@property
|
|
||||||
def sleep_time(self) -> float:
|
|
||||||
""" Sleep time to avoid rate limits, used in the main loop """
|
|
||||||
return 25
|
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
global _API, _EXCHANGE_CONF
|
global _API, _API_V2, _EXCHANGE_CONF
|
||||||
|
|
||||||
_EXCHANGE_CONF.update(config)
|
_EXCHANGE_CONF.update(config)
|
||||||
_API = _Bittrex(
|
_API = _Bittrex(
|
||||||
api_key=_EXCHANGE_CONF['key'],
|
api_key=_EXCHANGE_CONF['key'],
|
||||||
api_secret=_EXCHANGE_CONF['secret'],
|
api_secret=_EXCHANGE_CONF['secret'],
|
||||||
calls_per_second=5,
|
calls_per_second=1,
|
||||||
|
api_version=API_V1_1,
|
||||||
|
)
|
||||||
|
_API_V2 = _Bittrex(
|
||||||
|
api_key=_EXCHANGE_CONF['key'],
|
||||||
|
api_secret=_EXCHANGE_CONF['secret'],
|
||||||
|
calls_per_second=1,
|
||||||
|
api_version=API_V2_0,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -81,13 +83,21 @@ class Bittrex(Exchange):
|
|||||||
raise RuntimeError('{message} params=({pair})'.format(
|
raise RuntimeError('{message} params=({pair})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair))
|
pair=pair))
|
||||||
|
|
||||||
|
if not data.get('result') \
|
||||||
|
or not data['result'].get('Bid') \
|
||||||
|
or not data['result'].get('Ask') \
|
||||||
|
or not data['result'].get('Last'):
|
||||||
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
|
message='Got invalid response from bittrex',
|
||||||
|
pair=pair))
|
||||||
return {
|
return {
|
||||||
'bid': float(data['result']['Bid']),
|
'bid': float(data['result']['Bid']),
|
||||||
'ask': float(data['result']['Ask']),
|
'ask': float(data['result']['Ask']),
|
||||||
'last': float(data['result']['Last']),
|
'last': float(data['result']['Last']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_ticker_history(self, pair: str, tick_interval: int):
|
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
|
||||||
if tick_interval == 1:
|
if tick_interval == 1:
|
||||||
interval = 'oneMin'
|
interval = 'oneMin'
|
||||||
elif tick_interval == 5:
|
elif tick_interval == 5:
|
||||||
@@ -95,10 +105,21 @@ class Bittrex(Exchange):
|
|||||||
else:
|
else:
|
||||||
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
|
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
|
||||||
|
|
||||||
data = requests.get(self.TICKER_METHOD, params={
|
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
|
||||||
'marketName': pair.replace('_', '-'),
|
|
||||||
'tickInterval': interval,
|
# These sanity check are necessary because bittrex cannot keep their API stable.
|
||||||
}).json()
|
if not data.get('result'):
|
||||||
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
|
message='Got invalid response from bittrex',
|
||||||
|
pair=pair))
|
||||||
|
|
||||||
|
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
||||||
|
for tick in data['result']:
|
||||||
|
if prop not in tick.keys():
|
||||||
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
|
message='Required property {} not present in response'.format(prop),
|
||||||
|
pair=pair))
|
||||||
|
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({pair})'.format(
|
raise RuntimeError('{message} params=({pair})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
@@ -139,3 +160,20 @@ class Bittrex(Exchange):
|
|||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message}'.format(message=data['message']))
|
raise RuntimeError('{message}'.format(message=data['message']))
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||||
|
|
||||||
|
def get_market_summaries(self) -> List[Dict]:
|
||||||
|
data = _API.get_market_summaries()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{message}'.format(message=data['message']))
|
||||||
|
return data['result']
|
||||||
|
|
||||||
|
def get_wallet_health(self) -> List[Dict]:
|
||||||
|
data = _API_V2.get_wallet_health()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{message}'.format(message=data['message']))
|
||||||
|
return [{
|
||||||
|
'Currency': entry['Health']['Currency'],
|
||||||
|
'IsActive': entry['Health']['IsActive'],
|
||||||
|
'LastChecked': entry['Health']['LastChecked'],
|
||||||
|
'Notice': entry['Currency'].get('Notice'),
|
||||||
|
} for entry in data['result']]
|
||||||
|
@@ -18,14 +18,6 @@ class Exchange(ABC):
|
|||||||
:return: percentage in float
|
:return: percentage in float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def sleep_time(self) -> float:
|
|
||||||
"""
|
|
||||||
Sleep time in seconds for the main loop to avoid API rate limits.
|
|
||||||
:return: float
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -82,7 +74,7 @@ class Exchange(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_ticker_history(self, pair: str, tick_interval: int) -> List:
|
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Gets ticker history for given pair.
|
Gets ticker history for given pair.
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
@@ -139,3 +131,41 @@ class Exchange(ABC):
|
|||||||
Returns all available markets.
|
Returns all available markets.
|
||||||
:return: List of all available pairs
|
:return: List of all available pairs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_market_summaries(self) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Returns a 24h market summary for all available markets
|
||||||
|
:return: list, format: [
|
||||||
|
{
|
||||||
|
'MarketName': str,
|
||||||
|
'High': float,
|
||||||
|
'Low': float,
|
||||||
|
'Volume': float,
|
||||||
|
'Last': float,
|
||||||
|
'TimeStamp': datetime,
|
||||||
|
'BaseVolume': float,
|
||||||
|
'Bid': float,
|
||||||
|
'Ask': float,
|
||||||
|
'OpenBuyOrders': int,
|
||||||
|
'OpenSellOrders': int,
|
||||||
|
'PrevDay': float,
|
||||||
|
'Created': datetime
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_wallet_health(self) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Returns a list of all wallet health information
|
||||||
|
:return: list, format: [
|
||||||
|
{
|
||||||
|
'Currency': str,
|
||||||
|
'IsActive': bool,
|
||||||
|
'LastChecked': str,
|
||||||
|
'Notice': str
|
||||||
|
},
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
@@ -2,36 +2,66 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional
|
|
||||||
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
||||||
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from jsonschema import validate
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
from freqtrade import __version__, exchange, persistence
|
from freqtrade import __version__, exchange, persistence, rpc
|
||||||
from freqtrade.analyze import get_buy_signal
|
from freqtrade.analyze import get_signal, SignalType
|
||||||
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state
|
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
|
||||||
|
load_config, FreqtradeException
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import telegram
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logger = logging.getLogger('freqtrade')
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
|
|
||||||
|
|
||||||
def _process() -> bool:
|
def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Check wallet health and remove pair from whitelist if necessary
|
||||||
|
:param whitelist: a new whitelist (optional)
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
whitelist = whitelist or _CONF['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
sanitized_whitelist = []
|
||||||
|
health = exchange.get_wallet_health()
|
||||||
|
for status in health:
|
||||||
|
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
|
||||||
|
if pair not in whitelist:
|
||||||
|
continue
|
||||||
|
if status['IsActive']:
|
||||||
|
sanitized_whitelist.append(pair)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
'Ignoring %s from whitelist (reason: %s).',
|
||||||
|
pair, status.get('Notice') or 'wallet is not active'
|
||||||
|
)
|
||||||
|
if _CONF['exchange']['pair_whitelist'] != sanitized_whitelist:
|
||||||
|
logger.debug('Using refreshed pair whitelist: %s ...', sanitized_whitelist)
|
||||||
|
_CONF['exchange']['pair_whitelist'] = sanitized_whitelist
|
||||||
|
|
||||||
|
|
||||||
|
def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
||||||
"""
|
"""
|
||||||
Queries the persistence layer for open trades and handles them,
|
Queries the persistence layer for open trades and handles them,
|
||||||
otherwise a new trade is created.
|
otherwise a new trade is created.
|
||||||
|
:param: dynamic_whitelist: True is a dynamic whitelist should be generated (optional)
|
||||||
:return: True if a trade has been created or closed, False otherwise
|
:return: True if a trade has been created or closed, False otherwise
|
||||||
"""
|
"""
|
||||||
state_changed = False
|
state_changed = False
|
||||||
try:
|
try:
|
||||||
|
# Refresh whitelist based on wallet maintenance
|
||||||
|
refresh_whitelist(
|
||||||
|
gen_pair_whitelist(_CONF['stake_currency']) if dynamic_whitelist else 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) < _CONF['max_open_trades']:
|
if len(trades) < _CONF['max_open_trades']:
|
||||||
@@ -42,9 +72,12 @@ def _process() -> bool:
|
|||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
state_changed = True
|
state_changed = True
|
||||||
else:
|
else:
|
||||||
logging.info('Got no buy signal...')
|
logger.info(
|
||||||
except ValueError:
|
'Checked all whitelisted currencies. '
|
||||||
logger.exception('Unable to create trade')
|
'Found no suitable entry positions for buying. Will keep looking ...'
|
||||||
|
)
|
||||||
|
except FreqtradeException as e:
|
||||||
|
logger.warning('Unable to create trade: %s', e)
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# Get order details for actual price per unit
|
# Get order details for actual price per unit
|
||||||
@@ -53,17 +86,19 @@ def _process() -> bool:
|
|||||||
logger.info('Got open order for %s', trade)
|
logger.info('Got open order for %s', trade)
|
||||||
trade.update(exchange.get_order(trade.open_order_id))
|
trade.update(exchange.get_order(trade.open_order_id))
|
||||||
|
|
||||||
if not close_trade_if_fulfilled(trade):
|
if trade.is_open and trade.open_order_id is None:
|
||||||
# Check if we can sell our current pair
|
# Check if we can sell our current pair
|
||||||
state_changed = handle_trade(trade) or state_changed
|
state_changed = handle_trade(trade) or state_changed
|
||||||
|
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
||||||
msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
|
logger.warning(
|
||||||
logger.exception(msg)
|
'Got %s in _process(), retrying in 30 seconds...',
|
||||||
|
error
|
||||||
|
)
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
telegram.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
|
rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
|
||||||
traceback=traceback.format_exc(),
|
traceback=traceback.format_exc(),
|
||||||
hint='Issue `/start` if you think it is safe to restart.'
|
hint='Issue `/start` if you think it is safe to restart.'
|
||||||
))
|
))
|
||||||
@@ -72,24 +107,6 @@ def _process() -> bool:
|
|||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
|
|
||||||
def close_trade_if_fulfilled(trade: Trade) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if the trade is closable, and if so it is being closed.
|
|
||||||
:param trade: Trade
|
|
||||||
:return: True if trade has been closed else False
|
|
||||||
"""
|
|
||||||
# If we don't have an open order and the close rate is already set,
|
|
||||||
# we can close this trade.
|
|
||||||
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
|
|
||||||
logger.info('No open orders found and trade is fulfilled. Marking %s as closed ...', trade)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def execute_sell(trade: Trade, limit: float) -> None:
|
def execute_sell(trade: Trade, limit: float) -> None:
|
||||||
"""
|
"""
|
||||||
Executes a limit sell for the given trade and limit
|
Executes a limit sell for the given trade and limit
|
||||||
@@ -102,20 +119,18 @@ def execute_sell(trade: Trade, limit: float) -> None:
|
|||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
|
|
||||||
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
||||||
message = '*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
|
rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
|
||||||
trade.exchange,
|
trade.exchange,
|
||||||
trade.pair.replace('_', '/'),
|
trade.pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
limit,
|
limit,
|
||||||
fmt_exp_profit
|
fmt_exp_profit
|
||||||
)
|
))
|
||||||
logger.info(message)
|
|
||||||
telegram.send_msg(message)
|
|
||||||
|
|
||||||
|
|
||||||
def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
Based an earlier trade and current price and configuration, decides whether bot should sell
|
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
|
||||||
:return True if bot should sell at current rate
|
:return True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
current_profit = trade.calc_profit(current_rate)
|
current_profit = trade.calc_profit(current_rate)
|
||||||
@@ -123,9 +138,9 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
|
|||||||
logger.debug('Stop loss hit.')
|
logger.debug('Stop loss hit.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Check if time matches and current rate is above threshold
|
||||||
|
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
||||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||||
# Check if time matches and current rate is above threshold
|
|
||||||
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
|
||||||
if time_diff > float(duration) and current_profit > threshold:
|
if time_diff > float(duration) and current_profit > threshold:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -143,7 +158,7 @@ def handle_trade(trade: Trade) -> bool:
|
|||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
if should_sell(trade, current_rate, datetime.utcnow()):
|
if min_roi_reached(trade, current_rate, datetime.utcnow()) or get_signal(trade.pair, SignalType.SELL):
|
||||||
execute_sell(trade, current_rate)
|
execute_sell(trade, current_rate)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -163,11 +178,14 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
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
|
||||||
"""
|
"""
|
||||||
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
logger.info(
|
||||||
|
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||||
|
stake_amount
|
||||||
|
)
|
||||||
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||||
raise ValueError(
|
raise FreqtradeException(
|
||||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -177,11 +195,11 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
whitelist.remove(trade.pair)
|
whitelist.remove(trade.pair)
|
||||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||||
if not whitelist:
|
if not whitelist:
|
||||||
raise ValueError('No pair in whitelist')
|
raise FreqtradeException('No pair in whitelist')
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
# Pick pair based on StochRSI buy signals
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
if get_buy_signal(_pair):
|
if get_signal(_pair, SignalType.BUY):
|
||||||
pair = _pair
|
pair = _pair
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -194,14 +212,12 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
|
|
||||||
order_id = exchange.buy(pair, buy_limit, amount)
|
order_id = exchange.buy(pair, buy_limit, amount)
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
message = '*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
|
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
|
||||||
exchange.get_name().upper(),
|
exchange.get_name().upper(),
|
||||||
pair.replace('_', '/'),
|
pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(pair),
|
exchange.get_pair_detail_url(pair),
|
||||||
buy_limit
|
buy_limit
|
||||||
)
|
))
|
||||||
logger.info(message)
|
|
||||||
telegram.send_msg(message)
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
return Trade(pair=pair,
|
return Trade(pair=pair,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
@@ -221,7 +237,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Initialize all modules
|
# Initialize all modules
|
||||||
telegram.init(config)
|
rpc.init(config)
|
||||||
persistence.init(config, db_url)
|
persistence.init(config, db_url)
|
||||||
exchange.init(config)
|
exchange.init(config)
|
||||||
|
|
||||||
@@ -237,16 +253,33 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
signal(sig, cleanup)
|
signal(sig, cleanup)
|
||||||
|
|
||||||
|
|
||||||
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
|
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
|
||||||
|
"""
|
||||||
|
Updates the whitelist with with a dynamically generated list
|
||||||
|
:param base_currency: base currency as str
|
||||||
|
:param topn: maximum number of returned results
|
||||||
|
:param key: sort key (defaults to 'BaseVolume')
|
||||||
|
:return: List of pairs
|
||||||
|
"""
|
||||||
|
summaries = sorted(
|
||||||
|
(s for s in exchange.get_market_summaries() if s['MarketName'].startswith(base_currency)),
|
||||||
|
key=lambda s: s.get(key) or 0.0,
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
|
||||||
|
|
||||||
|
|
||||||
def cleanup(*args, **kwargs) -> None:
|
def cleanup(*args, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Cleanup the application state und finish all pending tasks
|
Cleanup the application state und finish all pending tasks
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
telegram.send_msg('*Status:* `Stopping trader...`')
|
rpc.send_msg('*Status:* `Stopping trader...`')
|
||||||
logger.info('Stopping trader and cleaning up modules...')
|
logger.info('Stopping trader and cleaning up modules...')
|
||||||
update_state(State.STOPPED)
|
update_state(State.STOPPED)
|
||||||
persistence.cleanup()
|
persistence.cleanup()
|
||||||
telegram.cleanup()
|
rpc.cleanup()
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
@@ -255,32 +288,46 @@ def main():
|
|||||||
Loads and validates the config and handles the main loop
|
Loads and validates the config and handles the main loop
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
logger.info('Starting freqtrade %s', __version__)
|
|
||||||
|
|
||||||
global _CONF
|
global _CONF
|
||||||
with open('config.json') as file:
|
args = parse_args(sys.argv[1:])
|
||||||
_CONF = json.load(file)
|
if not args:
|
||||||
|
exit(0)
|
||||||
|
|
||||||
logger.info('Validating configuration ...')
|
# Initialize logger
|
||||||
validate(_CONF, CONF_SCHEMA)
|
logging.basicConfig(
|
||||||
|
level=args.loglevel,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Starting freqtrade %s (loglevel=%s)',
|
||||||
|
__version__,
|
||||||
|
logging.getLevelName(args.loglevel)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load and validate configuration
|
||||||
|
_CONF = load_config(args.config)
|
||||||
|
|
||||||
|
# Initialize all modules and start main loop
|
||||||
|
if args.dynamic_whitelist:
|
||||||
|
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
||||||
init(_CONF)
|
init(_CONF)
|
||||||
old_state = get_state()
|
old_state = None
|
||||||
logger.info('Initial State: %s', old_state)
|
|
||||||
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
|
|
||||||
while True:
|
while True:
|
||||||
new_state = get_state()
|
new_state = get_state()
|
||||||
# Log state transition
|
# Log state transition
|
||||||
if new_state != old_state:
|
if new_state != old_state:
|
||||||
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||||
logging.info('Changing state to: %s', new_state.name)
|
logger.info('Changing state to: %s', new_state.name)
|
||||||
|
|
||||||
if new_state == State.STOPPED:
|
if new_state == State.STOPPED:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
elif new_state == State.RUNNING:
|
elif new_state == State.RUNNING:
|
||||||
_process()
|
throttle(
|
||||||
# We need to sleep here because otherwise we would run into bittrex rate limit
|
_process,
|
||||||
time.sleep(exchange.get_sleep_time())
|
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
||||||
|
dynamic_whitelist=args.dynamic_whitelist,
|
||||||
|
)
|
||||||
old_state = new_state
|
old_state = new_state
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,7 +1,23 @@
|
|||||||
|
import argparse
|
||||||
import enum
|
import enum
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from typing import Any, Callable, List, Dict
|
||||||
|
|
||||||
|
from jsonschema import validate, Draft4Validator
|
||||||
|
from jsonschema.exceptions import best_match, ValidationError
|
||||||
from wrapt import synchronized
|
from wrapt import synchronized
|
||||||
|
|
||||||
|
from freqtrade import __version__
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FreqtradeException(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class State(enum.Enum):
|
class State(enum.Enum):
|
||||||
RUNNING = 0
|
RUNNING = 0
|
||||||
@@ -32,6 +48,128 @@ def get_state() -> State:
|
|||||||
return _STATE
|
return _STATE
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(path: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Loads a config file from the given path
|
||||||
|
:param path: path as str
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
with open(path) as file:
|
||||||
|
conf = json.load(file)
|
||||||
|
if 'internals' not in conf:
|
||||||
|
conf['internals'] = {}
|
||||||
|
logger.info('Validating configuration ...')
|
||||||
|
try:
|
||||||
|
validate(conf, CONF_SCHEMA)
|
||||||
|
return conf
|
||||||
|
except ValidationError:
|
||||||
|
logger.fatal('Configuration is not valid! See config.json.example')
|
||||||
|
raise ValidationError(
|
||||||
|
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
|
"""
|
||||||
|
Throttles the given callable that it
|
||||||
|
takes at least `min_secs` to finish execution.
|
||||||
|
:param func: Any callable
|
||||||
|
:param min_secs: minimum execution time in seconds
|
||||||
|
:return: Any
|
||||||
|
"""
|
||||||
|
start = time.time()
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
end = time.time()
|
||||||
|
duration = max(min_secs - (end - start), 0.0)
|
||||||
|
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
|
||||||
|
time.sleep(duration)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(args: List[str]):
|
||||||
|
"""
|
||||||
|
Parses given arguments and returns an argparse Namespace instance.
|
||||||
|
Returns None if a sub command has been selected and executed.
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Simple High Frequency Trading Bot for crypto currencies'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-c', '--config',
|
||||||
|
help='specify configuration file (default: config.json)',
|
||||||
|
dest='config',
|
||||||
|
default='config.json',
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-v', '--verbose',
|
||||||
|
help='be verbose',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=logging.DEBUG,
|
||||||
|
default=logging.INFO,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version='%(prog)s {}'.format(__version__),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dynamic-whitelist',
|
||||||
|
help='dynamically generate and update whitelist based on 24h BaseVolume',
|
||||||
|
action='store_true',
|
||||||
|
)
|
||||||
|
build_subcommands(parser)
|
||||||
|
parsed_args = parser.parse_args(args)
|
||||||
|
|
||||||
|
# No subcommand as been selected
|
||||||
|
if not hasattr(parsed_args, 'func'):
|
||||||
|
return parsed_args
|
||||||
|
|
||||||
|
parsed_args.func(parsed_args)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
||||||
|
""" Builds and attaches all subcommands """
|
||||||
|
subparsers = parser.add_subparsers(dest='subparser')
|
||||||
|
backtest = subparsers.add_parser('backtesting', help='backtesting module')
|
||||||
|
backtest.set_defaults(func=start_backtesting)
|
||||||
|
backtest.add_argument(
|
||||||
|
'-l', '--live',
|
||||||
|
action='store_true',
|
||||||
|
dest='live',
|
||||||
|
help='using live data',
|
||||||
|
)
|
||||||
|
backtest.add_argument(
|
||||||
|
'-i', '--ticker-interval',
|
||||||
|
help='specify ticker interval in minutes (default: 5)',
|
||||||
|
dest='ticker_interval',
|
||||||
|
default=5,
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def start_backtesting(args) -> None:
|
||||||
|
"""
|
||||||
|
Exports all args as environment variables and starts backtesting via pytest.
|
||||||
|
:param args: arguments namespace
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
os.environ.update({
|
||||||
|
'BACKTEST': 'true',
|
||||||
|
'BACKTEST_LIVE': 'true' if args.live else '',
|
||||||
|
'BACKTEST_CONFIG': args.config,
|
||||||
|
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
|
||||||
|
})
|
||||||
|
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
|
||||||
|
pytest.main(['-s', path])
|
||||||
|
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
CONF_SCHEMA = {
|
CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@@ -71,6 +209,12 @@ CONF_SCHEMA = {
|
|||||||
'required': ['enabled', 'token', 'chat_id']
|
'required': ['enabled', 'token', 'chat_id']
|
||||||
},
|
},
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
|
'internals': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'process_throttle_secs': {'type': 'number'}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'definitions': {
|
'definitions': {
|
||||||
'exchange': {
|
'exchange': {
|
||||||
@@ -81,7 +225,10 @@ CONF_SCHEMA = {
|
|||||||
'secret': {'type': 'string'},
|
'secret': {'type': 'string'},
|
||||||
'pair_whitelist': {
|
'pair_whitelist': {
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': {'type': 'string'},
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -11,8 +11,6 @@ from sqlalchemy.orm.scoping import scoped_session
|
|||||||
from sqlalchemy.orm.session import sessionmaker
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
@@ -87,20 +85,27 @@ class Trade(_DECL_BASE):
|
|||||||
if not order['closed']:
|
if not order['closed']:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug('Updating trade (id=%d) ...', self.id)
|
logger.info('Updating trade (id=%d) ...', self.id)
|
||||||
if order['type'] == 'LIMIT_BUY':
|
if order['type'] == 'LIMIT_BUY':
|
||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
self.open_rate = order['rate']
|
self.open_rate = order['rate']
|
||||||
self.amount = order['amount']
|
self.amount = order['amount']
|
||||||
|
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
||||||
elif order['type'] == 'LIMIT_SELL':
|
elif order['type'] == 'LIMIT_SELL':
|
||||||
# Set close rate and set actual profit
|
# Set close rate and set actual profit
|
||||||
self.close_rate = order['rate']
|
self.close_rate = order['rate']
|
||||||
self.close_profit = self.calc_profit()
|
self.close_profit = self.calc_profit()
|
||||||
self.close_date = datetime.utcnow()
|
self.close_date = datetime.utcnow()
|
||||||
|
self.is_open = False
|
||||||
|
logger.info(
|
||||||
|
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
||||||
|
self
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown order type: {}'.format(order['type']))
|
raise ValueError('Unknown order type: {}'.format(order['type']))
|
||||||
|
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
def calc_profit(self, rate: Optional[float] = None) -> float:
|
def calc_profit(self, rate: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
|
@@ -1 +1,42 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from . import telegram
|
from . import telegram
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
REGISTERED_MODULES = []
|
||||||
|
|
||||||
|
|
||||||
|
def init(config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all enabled rpc modules
|
||||||
|
:param config: config to use
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if config['telegram'].get('enabled', False):
|
||||||
|
logger.info('Enabling rpc.telegram ...')
|
||||||
|
REGISTERED_MODULES.append('telegram')
|
||||||
|
telegram.init(config)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup() -> None:
|
||||||
|
"""
|
||||||
|
Stops all enabled rpc modules
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if 'telegram' in REGISTERED_MODULES:
|
||||||
|
logger.debug('Cleaning up rpc.telegram ...')
|
||||||
|
telegram.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def send_msg(msg: str) -> None:
|
||||||
|
"""
|
||||||
|
Send given markdown message to all registered rpc modules
|
||||||
|
:param msg: message
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info(msg)
|
||||||
|
if 'telegram' in REGISTERED_MODULES:
|
||||||
|
telegram.send_msg(msg)
|
||||||
|
@@ -8,7 +8,7 @@ from tabulate import tabulate
|
|||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import and_, func, text
|
from sqlalchemy import and_, func, text
|
||||||
from telegram import ParseMode, Bot, Update
|
from telegram import ParseMode, Bot, Update
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError, TelegramError
|
||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
|
|
||||||
from freqtrade import exchange, __version__
|
from freqtrade import exchange, __version__
|
||||||
@@ -57,7 +57,7 @@ def init(config: dict) -> None:
|
|||||||
_UPDATER.dispatcher.add_handler(handle)
|
_UPDATER.dispatcher.add_handler(handle)
|
||||||
_UPDATER.start_polling(
|
_UPDATER.start_polling(
|
||||||
clean=True,
|
clean=True,
|
||||||
bootstrap_retries=3,
|
bootstrap_retries=-1,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
read_latency=60,
|
read_latency=60,
|
||||||
)
|
)
|
||||||
@@ -273,18 +273,21 @@ def _balance(bot: Bot, update: Update) -> None:
|
|||||||
Handler for /balance
|
Handler for /balance
|
||||||
Returns current account balance per crypto
|
Returns current account balance per crypto
|
||||||
"""
|
"""
|
||||||
output = ""
|
output = ''
|
||||||
balances = exchange.get_balances()
|
balances = [
|
||||||
|
c for c in exchange.get_balances()
|
||||||
|
if c['Balance'] or c['Available'] or c['Pending']
|
||||||
|
]
|
||||||
|
if not balances:
|
||||||
|
output = '`All balances are zero.`'
|
||||||
|
|
||||||
for currency in balances:
|
for currency in balances:
|
||||||
if not currency['Balance'] and not currency['Available'] and not currency['Pending']:
|
|
||||||
continue
|
|
||||||
output += """*Currency*: {Currency}
|
output += """*Currency*: {Currency}
|
||||||
*Available*: {Available}
|
*Available*: {Available}
|
||||||
*Balance*: {Balance}
|
*Balance*: {Balance}
|
||||||
*Pending*: {Pending}
|
*Pending*: {Pending}
|
||||||
|
|
||||||
""".format(**currency)
|
""".format(**currency)
|
||||||
|
|
||||||
send_msg(output)
|
send_msg(output)
|
||||||
|
|
||||||
|
|
||||||
@@ -472,13 +475,17 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
|
|||||||
return
|
return
|
||||||
|
|
||||||
bot = bot or _UPDATER.bot
|
bot = bot or _UPDATER.bot
|
||||||
|
|
||||||
try:
|
try:
|
||||||
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 network_err:
|
||||||
# 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(
|
||||||
error.message
|
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||||
)
|
network_err.message
|
||||||
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 TelegramError as telegram_err:
|
||||||
|
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def load_backtesting_data(ticker_interval: int = 5):
|
||||||
|
path = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
result = {}
|
||||||
|
pairs = [
|
||||||
|
'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC',
|
||||||
|
'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK',
|
||||||
|
]
|
||||||
|
for pair in pairs:
|
||||||
|
with open('{abspath}/testdata/{pair}-{ticker_interval}.json'.format(
|
||||||
|
abspath=path,
|
||||||
|
pair=pair,
|
||||||
|
ticker_interval=ticker_interval,
|
||||||
|
)) as tickerdata:
|
||||||
|
result[pair] = json.load(tickerdata)
|
||||||
|
return result
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
import json
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@@ -37,7 +36,8 @@ def default_conf():
|
|||||||
"BTC_ETH",
|
"BTC_ETH",
|
||||||
"BTC_TKN",
|
"BTC_TKN",
|
||||||
"BTC_TRST",
|
"BTC_TRST",
|
||||||
"BTC_SWT"
|
"BTC_SWT",
|
||||||
|
"BTC_BCC"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
@@ -54,6 +54,8 @@ def default_conf():
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def backtest_conf():
|
def backtest_conf():
|
||||||
return {
|
return {
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"stake_amount": 0.01,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
@@ -64,16 +66,6 @@ def backtest_conf():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def backdata():
|
|
||||||
result = {}
|
|
||||||
for pair in ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
|
||||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']:
|
|
||||||
with open('freqtrade/tests/testdata/' + pair + '.json') as data_file:
|
|
||||||
result[pair] = json.load(data_file)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def update():
|
def update():
|
||||||
_update = Update(0)
|
_update = Update(0)
|
||||||
@@ -90,6 +82,36 @@ def ticker():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def health():
|
||||||
|
return MagicMock(return_value=[{
|
||||||
|
'Currency': 'BTC',
|
||||||
|
'IsActive': True,
|
||||||
|
'LastChecked': '2017-11-13T20:15:00.00',
|
||||||
|
'Notice': None
|
||||||
|
}, {
|
||||||
|
'Currency': 'ETH',
|
||||||
|
'IsActive': True,
|
||||||
|
'LastChecked': '2017-11-13T20:15:00.00',
|
||||||
|
'Notice': None
|
||||||
|
}, {
|
||||||
|
'Currency': 'TRST',
|
||||||
|
'IsActive': True,
|
||||||
|
'LastChecked': '2017-11-13T20:15:00.00',
|
||||||
|
'Notice': None
|
||||||
|
}, {
|
||||||
|
'Currency': 'SWT',
|
||||||
|
'IsActive': True,
|
||||||
|
'LastChecked': '2017-11-13T20:15:00.00',
|
||||||
|
'Notice': None
|
||||||
|
}, {
|
||||||
|
'Currency': 'BCC',
|
||||||
|
'IsActive': False,
|
||||||
|
'LastChecked': '2017-11-13T20:15:00.00',
|
||||||
|
'Notice': None
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def limit_buy_order():
|
def limit_buy_order():
|
||||||
return {
|
return {
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0621
|
||||||
from datetime import datetime
|
|
||||||
import json
|
import json
|
||||||
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||||
get_buy_signal
|
get_signal, SignalType, populate_sell_trend
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file))
|
return parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
@@ -20,20 +20,34 @@ def test_dataframe_correct_columns(result):
|
|||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
assert len(result.index) == 5751
|
assert len(result.index) == 14382
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
dataframe = populate_buy_trend(populate_indicators(result))
|
dataframe = populate_buy_trend(populate_indicators(result))
|
||||||
assert 'buy' in dataframe.columns
|
assert 'buy' in dataframe.columns
|
||||||
assert 'buy_price' in dataframe.columns
|
|
||||||
|
|
||||||
|
def test_populates_sell_trend(result):
|
||||||
|
dataframe = populate_sell_trend(populate_indicators(result))
|
||||||
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker):
|
def test_returns_latest_buy_signal(mocker):
|
||||||
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
|
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
assert get_buy_signal('BTC-ETH')
|
assert get_signal('BTC-ETH', SignalType.BUY)
|
||||||
|
|
||||||
buydf = DataFrame([{'buy': 0, 'date': datetime.today()}])
|
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
assert not get_buy_signal('BTC-ETH')
|
assert not get_signal('BTC-ETH', SignalType.BUY)
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_latest_sell_signal(mocker):
|
||||||
|
selldf = DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
|
||||||
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf)
|
||||||
|
assert get_signal('BTC-ETH', SignalType.SELL)
|
||||||
|
|
||||||
|
selldf = DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
|
||||||
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf)
|
||||||
|
assert not get_signal('BTC-ETH', SignalType.SELL)
|
||||||
|
@@ -1,65 +1,154 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import Tuple, Dict
|
||||||
|
|
||||||
import pytest
|
|
||||||
import arrow
|
import arrow
|
||||||
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.analyze import analyze_ticker
|
from freqtrade.analyze import parse_ticker_dataframe, populate_indicators, \
|
||||||
|
populate_buy_trend, populate_sell_trend
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
from freqtrade.main import should_sell
|
from freqtrade.main import min_roi_reached
|
||||||
|
from freqtrade.misc import load_config
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.tests import load_backtesting_data
|
||||||
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def format_results(results):
|
def format_results(results: DataFrame):
|
||||||
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
return ('Made {:6d} buys. Average profit {: 5.2f}%. '
|
||||||
len(results.index), results.profit.mean() * 100.0, results.profit.sum(), results.duration.mean() * 5)
|
'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format(
|
||||||
|
len(results.index),
|
||||||
|
results.profit.mean() * 100.0,
|
||||||
|
results.profit.sum(),
|
||||||
|
results.duration.mean() * 5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_pair_results(pair, results):
|
def preprocess(backdata) -> Dict[str, DataFrame]:
|
||||||
print('For currency {}:'.format(pair))
|
processed = {}
|
||||||
print(format_results(results[results.currency == pair]))
|
for pair, pair_data in backdata.items():
|
||||||
|
processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data))
|
||||||
|
return processed
|
||||||
|
|
||||||
|
|
||||||
def backtest(backtest_conf, backdata, mocker):
|
def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
|
"""
|
||||||
|
Get the maximum timeframe for the given backtest data
|
||||||
|
:param data: dictionary with backtesting data
|
||||||
|
:return: tuple containing min_date, max_date
|
||||||
|
"""
|
||||||
|
min_date, max_date = None, None
|
||||||
|
for values in data.values():
|
||||||
|
sorted_values = sorted(values, key=lambda d: arrow.get(d['T']))
|
||||||
|
if not min_date or sorted_values[0]['T'] < min_date:
|
||||||
|
min_date = sorted_values[0]['T']
|
||||||
|
if not max_date or sorted_values[-1]['T'] > max_date:
|
||||||
|
max_date = sorted_values[-1]['T']
|
||||||
|
return arrow.get(min_date), arrow.get(max_date)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currency) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:return: pretty printed table with tabulate as str
|
||||||
|
"""
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['pair', 'buy count', 'avg profit', 'total profit', 'avg duration']
|
||||||
|
for pair in data:
|
||||||
|
result = results[results.currency == pair]
|
||||||
|
tabular_data.append([
|
||||||
|
pair,
|
||||||
|
len(result.index),
|
||||||
|
'{:.2f}%'.format(result.profit.mean() * 100.0),
|
||||||
|
'{:.08f} {}'.format(result.profit.sum(), stake_currency),
|
||||||
|
'{:.2f}'.format(result.duration.mean() * 5),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Append Total
|
||||||
|
tabular_data.append([
|
||||||
|
'TOTAL',
|
||||||
|
len(results.index),
|
||||||
|
'{:.2f}%'.format(results.profit.mean() * 100.0),
|
||||||
|
'{:.08f} {}'.format(results.profit.sum(), stake_currency),
|
||||||
|
'{:.2f}'.format(results.duration.mean() * 5),
|
||||||
|
])
|
||||||
|
return tabulate(tabular_data, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
def backtest(backtest_conf, processed, mocker):
|
||||||
trades = []
|
trades = []
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
||||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
for pair, pair_data in processed.items():
|
||||||
for pair, pair_data in backdata.items():
|
pair_data['buy'] = 0
|
||||||
mocked_history.return_value = pair_data
|
pair_data['sell'] = 0
|
||||||
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
|
ticker = populate_sell_trend(populate_buy_trend(pair_data))
|
||||||
# for each buy point
|
# for each buy point
|
||||||
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
open_rate=row.close,
|
open_rate=row.close,
|
||||||
open_date=row.date,
|
open_date=row.date,
|
||||||
amount=1,
|
amount=backtest_conf['stake_amount'],
|
||||||
fee=exchange.get_fee() * 2
|
fee=exchange.get_fee() * 2
|
||||||
)
|
)
|
||||||
# calculate win/lose forwards from buy point
|
# calculate win/lose forwards from buy point
|
||||||
for row2 in ticker[row.Index:].itertuples(index=True):
|
for row2 in ticker[row.Index:].itertuples(index=True):
|
||||||
if should_sell(trade, row2.close, row2.date):
|
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
|
||||||
current_profit = trade.calc_profit(row2.close)
|
current_profit = trade.calc_profit(row2.close)
|
||||||
|
|
||||||
trades.append((pair, current_profit, row2.Index - row.Index))
|
trades.append((pair, current_profit, row2.Index - row.Index))
|
||||||
break
|
break
|
||||||
labels = ['currency', 'profit', 'duration']
|
labels = ['currency', 'profit', 'duration']
|
||||||
results = DataFrame.from_records(trades, columns=labels)
|
return DataFrame.from_records(trades, columns=labels)
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
|
||||||
def test_backtest(backtest_conf, backdata, mocker, report=True):
|
def test_backtest(backtest_conf, mocker):
|
||||||
results = backtest(backtest_conf, backdata, mocker)
|
print('')
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
print('====================== BACKTESTING REPORT ================================')
|
# Load configuration file based on env variable
|
||||||
for pair in backdata:
|
conf_path = os.environ.get('BACKTEST_CONFIG')
|
||||||
print_pair_results(pair, results)
|
if conf_path:
|
||||||
print('TOTAL OVER ALL TRADES:')
|
print('Using config: {} ...'.format(conf_path))
|
||||||
print(format_results(results))
|
config = load_config(conf_path)
|
||||||
|
else:
|
||||||
|
config = backtest_conf
|
||||||
|
|
||||||
|
# Parse ticker interval
|
||||||
|
ticker_interval = int(os.environ.get('BACKTEST_TICKER_INTERVAL') or 5)
|
||||||
|
print('Using ticker_interval: {} ...'.format(ticker_interval))
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
if os.environ.get('BACKTEST_LIVE'):
|
||||||
|
print('Downloading data for all pairs in whitelist ...')
|
||||||
|
for pair in config['exchange']['pair_whitelist']:
|
||||||
|
data[pair] = exchange.get_ticker_history(pair, ticker_interval)
|
||||||
|
else:
|
||||||
|
print('Using local backtesting data (ignoring whitelist in given config)...')
|
||||||
|
data = load_backtesting_data(ticker_interval)
|
||||||
|
|
||||||
|
print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format(
|
||||||
|
config['stake_currency'], config['stake_amount']
|
||||||
|
))
|
||||||
|
|
||||||
|
# Print timeframe
|
||||||
|
min_date, max_date = get_timeframe(data)
|
||||||
|
print('Measuring data from {} up to {} ...'.format(
|
||||||
|
min_date.isoformat(), max_date.isoformat()
|
||||||
|
))
|
||||||
|
|
||||||
|
# Execute backtest and print results
|
||||||
|
results = backtest(config, preprocess(data), mocker)
|
||||||
|
print('====================== BACKTESTING REPORT ======================================\n\n'
|
||||||
|
'NOTE: This Report doesn\'t respect the limits of max_open_trades, \n'
|
||||||
|
' so the projected values should be taken with a grain of salt.\n')
|
||||||
|
print(generate_text_table(data, results, config['stake_currency']))
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -8,7 +8,9 @@ from freqtrade.exchange import validate_pairs
|
|||||||
|
|
||||||
def test_validate_pairs(default_conf, mocker):
|
def test_validate_pairs(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_markets = MagicMock(return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT'])
|
api_mock.get_markets = MagicMock(return_value=[
|
||||||
|
'BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT', 'BTC_BCC',
|
||||||
|
])
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
@@ -31,4 +33,3 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
|||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
with pytest.raises(RuntimeError, match=r'not compatible'):
|
with pytest.raises(RuntimeError, match=r'not compatible'):
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
@@ -9,76 +9,92 @@ import pytest
|
|||||||
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import exchange
|
||||||
|
from freqtrade.exchange import Bittrex
|
||||||
|
from freqtrade.tests import load_backtesting_data
|
||||||
from freqtrade.tests.test_backtesting import backtest, format_results
|
from freqtrade.tests.test_backtesting import backtest, format_results
|
||||||
|
from freqtrade.tests.test_backtesting import preprocess
|
||||||
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
||||||
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||||
|
|
||||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
||||||
TARGET_TRADES = 1200
|
TARGET_TRADES = 1100
|
||||||
|
TOTAL_TRIES = 4
|
||||||
|
# pylint: disable=C0103
|
||||||
|
current_tries = 0
|
||||||
|
|
||||||
|
|
||||||
def buy_strategy_generator(params):
|
def buy_strategy_generator(params):
|
||||||
print(params)
|
|
||||||
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if params['uptrend_long_ema']['enabled']:
|
if params['uptrend_long_ema']['enabled']:
|
||||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||||
|
if params['uptrend_short_ema']['enabled']:
|
||||||
|
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
||||||
if params['mfi']['enabled']:
|
if params['mfi']['enabled']:
|
||||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||||
if params['fastd']['enabled']:
|
if params['fastd']['enabled']:
|
||||||
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
||||||
if params['adx']['enabled']:
|
if params['adx']['enabled']:
|
||||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||||
if params['cci']['enabled']:
|
|
||||||
conditions.append(dataframe['cci'] < params['cci']['value'])
|
|
||||||
if params['rsi']['enabled']:
|
if params['rsi']['enabled']:
|
||||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||||
if params['over_sar']['enabled']:
|
if params['over_sar']['enabled']:
|
||||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||||
|
if params['green_candle']['enabled']:
|
||||||
|
conditions.append(dataframe['close'] > dataframe['open'])
|
||||||
if params['uptrend_sma']['enabled']:
|
if params['uptrend_sma']['enabled']:
|
||||||
prevsma = dataframe['sma'].shift(1)
|
prevsma = dataframe['sma'].shift(1)
|
||||||
conditions.append(dataframe['sma'] > prevsma)
|
conditions.append(dataframe['sma'] > prevsma)
|
||||||
|
|
||||||
prev_fastd = dataframe['fastd'].shift(1)
|
|
||||||
# TRIGGERS
|
# TRIGGERS
|
||||||
triggers = {
|
triggers = {
|
||||||
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
||||||
'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10),
|
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
|
||||||
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
||||||
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
||||||
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
|
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
|
||||||
|
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
|
||||||
|
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
|
||||||
|
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
|
||||||
}
|
}
|
||||||
conditions.append(triggers.get(params['trigger']['type']))
|
conditions.append(triggers.get(params['trigger']['type']))
|
||||||
|
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
reduce(lambda x, y: x & y, conditions),
|
reduce(lambda x, y: x & y, conditions),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
return populate_buy_trend
|
return populate_buy_trend
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
def test_hyperopt(backtest_conf, backdata, mocker):
|
def test_hyperopt(backtest_conf, mocker):
|
||||||
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
|
mocked_buy_trend = mocker.patch('freqtrade.tests.test_backtesting.populate_buy_trend')
|
||||||
|
|
||||||
|
backdata = load_backtesting_data()
|
||||||
|
processed = preprocess(backdata)
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
def optimizer(params):
|
def optimizer(params):
|
||||||
mocked_buy_trend.side_effect = buy_strategy_generator(params)
|
mocked_buy_trend.side_effect = buy_strategy_generator(params)
|
||||||
|
|
||||||
results = backtest(backtest_conf, backdata, mocker)
|
results = backtest(backtest_conf, processed, mocker)
|
||||||
|
|
||||||
result = format_results(results)
|
result = format_results(results)
|
||||||
print(result)
|
|
||||||
|
|
||||||
total_profit = results.profit.sum() * 1000
|
total_profit = results.profit.sum() * 1000
|
||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
|
|
||||||
trade_loss = 1 - 0.8 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5)
|
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
||||||
profit_loss = exp(-total_profit**3 / 10**11)
|
profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000
|
||||||
|
|
||||||
|
# pylint: disable=W0603
|
||||||
|
global current_tries
|
||||||
|
current_tries += 1
|
||||||
|
print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'loss': trade_loss + profit_loss,
|
'loss': trade_loss + profit_loss,
|
||||||
@@ -89,32 +105,36 @@ def test_hyperopt(backtest_conf, backdata, mocker):
|
|||||||
space = {
|
space = {
|
||||||
'mfi': hp.choice('mfi', [
|
'mfi': hp.choice('mfi', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('mfi-value', 5, 15)}
|
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
|
||||||
]),
|
]),
|
||||||
'fastd': hp.choice('fastd', [
|
'fastd': hp.choice('fastd', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('fastd-value', 5, 40)}
|
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
|
||||||
]),
|
]),
|
||||||
'adx': hp.choice('adx', [
|
'adx': hp.choice('adx', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('adx-value', 10, 30)}
|
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
|
||||||
]),
|
|
||||||
'cci': hp.choice('cci', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True, 'value': hp.uniform('cci-value', -150, -100)}
|
|
||||||
]),
|
]),
|
||||||
'rsi': hp.choice('rsi', [
|
'rsi': hp.choice('rsi', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('rsi-value', 20, 30)}
|
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||||
]),
|
]),
|
||||||
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True}
|
{'enabled': True}
|
||||||
]),
|
]),
|
||||||
|
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
'over_sar': hp.choice('over_sar', [
|
'over_sar': hp.choice('over_sar', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True}
|
{'enabled': True}
|
||||||
]),
|
]),
|
||||||
|
'green_candle': hp.choice('green_candle', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
'uptrend_sma': hp.choice('uptrend_sma', [
|
'uptrend_sma': hp.choice('uptrend_sma', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True}
|
{'enabled': True}
|
||||||
@@ -125,11 +145,19 @@ def test_hyperopt(backtest_conf, backdata, mocker):
|
|||||||
{'type': 'ao_cross_zero'},
|
{'type': 'ao_cross_zero'},
|
||||||
{'type': 'ema5_cross_ema10'},
|
{'type': 'ema5_cross_ema10'},
|
||||||
{'type': 'macd_cross_signal'},
|
{'type': 'macd_cross_signal'},
|
||||||
|
{'type': 'sar_reversal'},
|
||||||
|
{'type': 'stochf_cross'},
|
||||||
|
{'type': 'ht_sine'},
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
trials = Trials()
|
trials = Trials()
|
||||||
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=4, trials=trials)
|
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials)
|
||||||
print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================')
|
print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================')
|
||||||
print('Best parameters {}'.format(best))
|
print('Best parameters {}'.format(best))
|
||||||
newlist = sorted(trials.results, key=itemgetter('loss'))
|
newlist = sorted(trials.results, key=itemgetter('loss'))
|
||||||
print('Result: {}'.format(newlist[0]['result']))
|
print('Result: {}'.format(newlist[0]['result']))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for profiling with cProfile and line_profiler
|
||||||
|
pytest.main([__file__, '-s'])
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
import copy
|
import copy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@@ -7,24 +7,26 @@ import requests
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
from freqtrade.analyze import SignalType
|
||||||
|
from freqtrade.main import create_trade, handle_trade, init, \
|
||||||
get_target_bid, _process
|
get_target_bid, _process
|
||||||
from freqtrade.misc import get_state, State
|
from freqtrade.misc import get_state, State, FreqtradeException
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_creation(default_conf, ticker, mocker):
|
def test_process_trade_creation(default_conf, ticker, health, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
get_wallet_health=health,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
assert len(trades) == 0
|
assert not trades
|
||||||
|
|
||||||
result = _process()
|
result = _process()
|
||||||
assert result is True
|
assert result is True
|
||||||
@@ -41,14 +43,15 @@ def test_process_trade_creation(default_conf, ticker, mocker):
|
|||||||
assert trade.amount == 0.6864067381401302
|
assert trade.amount == 0.6864067381401302
|
||||||
|
|
||||||
|
|
||||||
def test_process_exchange_failures(default_conf, ticker, mocker):
|
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
get_wallet_health=health,
|
||||||
buy=MagicMock(side_effect=requests.exceptions.RequestException))
|
buy=MagicMock(side_effect=requests.exceptions.RequestException))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
result = _process()
|
result = _process()
|
||||||
@@ -56,14 +59,15 @@ def test_process_exchange_failures(default_conf, ticker, mocker):
|
|||||||
assert sleep_mock.has_calls()
|
assert sleep_mock.has_calls()
|
||||||
|
|
||||||
|
|
||||||
def test_process_runtime_error(default_conf, ticker, mocker):
|
def test_process_runtime_error(default_conf, ticker, health, mocker):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
get_wallet_health=health,
|
||||||
buy=MagicMock(side_effect=RuntimeError))
|
buy=MagicMock(side_effect=RuntimeError))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
assert get_state() == State.RUNNING
|
assert get_state() == State.RUNNING
|
||||||
@@ -74,19 +78,21 @@ def test_process_runtime_error(default_conf, ticker, mocker):
|
|||||||
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0]
|
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_handling(default_conf, ticker, limit_buy_order, mocker):
|
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal',
|
||||||
|
side_effect=lambda *args: False if args[1] == SignalType.SELL else True)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
get_wallet_health=health,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||||
get_order=MagicMock(return_value=limit_buy_order))
|
get_order=MagicMock(return_value=limit_buy_order))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
assert len(trades) == 0
|
assert not trades
|
||||||
result = _process()
|
result = _process()
|
||||||
assert result is True
|
assert result is True
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
@@ -98,8 +104,8 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, mocker):
|
|||||||
|
|
||||||
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
@@ -128,27 +134,27 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
|||||||
|
|
||||||
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
||||||
with pytest.raises(ValueError, match=r'.*stake amount.*'):
|
with pytest.raises(FreqtradeException, match=r'.*stake amount.*'):
|
||||||
create_trade(default_conf['stake_amount'])
|
create_trade(default_conf['stake_amount'])
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'.*No pair in whitelist.*'):
|
with pytest.raises(FreqtradeException, match=r'.*No pair in whitelist.*'):
|
||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'] = []
|
conf['exchange']['pair_whitelist'] = []
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
@@ -157,8 +163,8 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
|||||||
|
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
@@ -178,7 +184,6 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
|||||||
|
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
assert trade.open_order_id == 'mocked_limit_sell'
|
assert trade.open_order_id == 'mocked_limit_sell'
|
||||||
assert close_trade_if_fulfilled(trade) is False
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
@@ -190,8 +195,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
|||||||
|
|
||||||
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
@@ -200,20 +205,15 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
|
|||||||
# Create trade and sell it
|
# Create trade and sell it
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
trade = create_trade(15.0)
|
trade = create_trade(15.0)
|
||||||
trade.update(limit_buy_order)
|
|
||||||
trade.update(limit_sell_order)
|
|
||||||
|
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
trade.update(limit_buy_order)
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
# Simulate that there is no open order
|
trade.update(limit_sell_order)
|
||||||
trade.open_order_id = None
|
trade = Trade.query.filter(Trade.is_open.is_(False)).first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
closed = close_trade_if_fulfilled(trade)
|
|
||||||
assert closed
|
|
||||||
assert not trade.is_open
|
|
||||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
|
|
||||||
|
149
freqtrade/tests/test_misc.py
Normal file
149
freqtrade/tests/test_misc.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from argparse import Namespace
|
||||||
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
|
from freqtrade.misc import throttle, parse_args, start_backtesting, load_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_throttle():
|
||||||
|
|
||||||
|
def func():
|
||||||
|
return 42
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
result = throttle(func, 0.1)
|
||||||
|
end = time.time()
|
||||||
|
|
||||||
|
assert result == 42
|
||||||
|
assert end - start > 0.1
|
||||||
|
|
||||||
|
result = throttle(func, -1)
|
||||||
|
assert result == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_defaults():
|
||||||
|
args = parse_args([])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == 'config.json'
|
||||||
|
assert args.dynamic_whitelist is False
|
||||||
|
assert args.loglevel == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_invalid():
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['-c'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_config():
|
||||||
|
args = parse_args(['-c', '/dev/null'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
args = parse_args(['--config', '/dev/null'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_verbose():
|
||||||
|
args = parse_args(['-v'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.loglevel == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist():
|
||||||
|
args = parse_args(['--dynamic-whitelist'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.dynamic_whitelist is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting(mocker):
|
||||||
|
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock())
|
||||||
|
args = parse_args(['backtesting'])
|
||||||
|
assert args is None
|
||||||
|
assert backtesting_mock.call_count == 1
|
||||||
|
|
||||||
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
|
assert call_args.config == 'config.json'
|
||||||
|
assert call_args.live is False
|
||||||
|
assert call_args.loglevel == 20
|
||||||
|
assert call_args.subparser == 'backtesting'
|
||||||
|
assert call_args.func is not None
|
||||||
|
assert call_args.ticker_interval == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_invalid():
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['--ticker-interval'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['--ticker-interval', 'abc'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_custom(mocker):
|
||||||
|
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock())
|
||||||
|
args = parse_args(['-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1'])
|
||||||
|
assert args is None
|
||||||
|
assert backtesting_mock.call_count == 1
|
||||||
|
|
||||||
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
|
assert call_args.config == 'test_conf.json'
|
||||||
|
assert call_args.live is True
|
||||||
|
assert call_args.loglevel == 20
|
||||||
|
assert call_args.subparser == 'backtesting'
|
||||||
|
assert call_args.func is not None
|
||||||
|
assert call_args.ticker_interval == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_backtesting(mocker):
|
||||||
|
pytest_mock = mocker.patch('pytest.main', MagicMock())
|
||||||
|
env_mock = mocker.patch('os.environ', {})
|
||||||
|
args = Namespace(
|
||||||
|
config='config.json',
|
||||||
|
live=True,
|
||||||
|
loglevel=20,
|
||||||
|
ticker_interval=1,
|
||||||
|
)
|
||||||
|
start_backtesting(args)
|
||||||
|
assert env_mock == {
|
||||||
|
'BACKTEST': 'true',
|
||||||
|
'BACKTEST_LIVE': 'true',
|
||||||
|
'BACKTEST_CONFIG': 'config.json',
|
||||||
|
'BACKTEST_TICKER_INTERVAL': '1',
|
||||||
|
}
|
||||||
|
assert pytest_mock.call_count == 1
|
||||||
|
|
||||||
|
main_call_args = pytest_mock.call_args[0][0]
|
||||||
|
assert main_call_args[0] == '-s'
|
||||||
|
assert main_call_args[1].endswith(os.path.join('freqtrade', 'tests', 'test_backtesting.py'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config(default_conf, mocker):
|
||||||
|
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
validated_conf = load_config('somefile')
|
||||||
|
assert file_mock.call_count == 1
|
||||||
|
assert validated_conf.items() >= default_conf.items()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_invalid_pair(default_conf, mocker):
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
||||||
|
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
|
||||||
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
|
load_config('somefile')
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_missing_attributes(default_conf, mocker):
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.pop('exchange')
|
||||||
|
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
|
||||||
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
|
load_config('somefile')
|
58
freqtrade/tests/test_rpc.py
Normal file
58
freqtrade/tests/test_rpc.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from freqtrade.rpc import init, cleanup, send_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_enabled(default_conf, mocker):
|
||||||
|
module_list = []
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
||||||
|
|
||||||
|
init(default_conf)
|
||||||
|
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
assert 'telegram' in module_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_disabled(default_conf, mocker):
|
||||||
|
module_list = []
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
init(conf)
|
||||||
|
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
assert 'telegram' not in module_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_enabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
|
||||||
|
cleanup()
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_disabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
|
||||||
|
cleanup()
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_enabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
|
||||||
|
send_msg('test')
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_disabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
|
||||||
|
send_msg('test')
|
||||||
|
assert telegram_mock.call_count == 0
|
@@ -1,10 +1,9 @@
|
|||||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from telegram import Update, Message, Chat
|
from telegram import Update, Message, Chat
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
@@ -14,10 +13,8 @@ from freqtrade.main import init, create_trade
|
|||||||
from freqtrade.misc import update_state, State, get_state
|
from freqtrade.misc import update_state, State, get_state
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import telegram
|
from freqtrade.rpc import telegram
|
||||||
from freqtrade.rpc.telegram import (
|
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
|
||||||
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance,
|
_profit, _forcesell, _performance, _count, _start, _stop, _balance, _version, _help
|
||||||
authorized_only, _help, is_enabled, send_msg,
|
|
||||||
_version)
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_enabled(default_conf, mocker):
|
def test_is_enabled(default_conf, mocker):
|
||||||
@@ -79,9 +76,10 @@ def test_authorized_only_exception(default_conf, mocker):
|
|||||||
|
|
||||||
def test_status_handle(default_conf, update, ticker, mocker):
|
def test_status_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -111,16 +109,17 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
|||||||
# Trigger status while we have a fulfilled order for the open trade
|
# Trigger status while we have a fulfilled order for the open trade
|
||||||
_status(bot=MagicMock(), update=update)
|
_status(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_status_table_handle(default_conf, update, ticker, mocker):
|
def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.main.telegram',
|
'freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -155,14 +154,15 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
|||||||
|
|
||||||
assert int(fields[0]) == 1
|
assert int(fields[0]) == 1
|
||||||
assert fields[1] == 'BTC_ETH'
|
assert fields[1] == 'BTC_ETH'
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -184,7 +184,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
_profit(bot=MagicMock(), update=update)
|
_profit(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
@@ -204,12 +204,12 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
|
|
||||||
def test_forcesell_handle(default_conf, update, ticker, mocker):
|
def test_forcesell_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
@@ -225,19 +225,19 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
|||||||
update.message.text = '/forcesell 1'
|
update.message.text = '/forcesell 1'
|
||||||
_forcesell(bot=MagicMock(), update=update)
|
_forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0]
|
assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
@@ -247,22 +247,21 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
|||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
Trade.session.add(create_trade(15.0))
|
Trade.session.add(create_trade(15.0))
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
rpc_mock.reset_mock()
|
||||||
msg_mock.reset_mock()
|
|
||||||
|
|
||||||
update.message.text = '/forcesell all'
|
update.message.text = '/forcesell all'
|
||||||
_forcesell(bot=MagicMock(), update=update)
|
_forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert rpc_mock.call_count == 4
|
||||||
for args in msg_mock.call_args_list:
|
for args in rpc_mock.call_args_list:
|
||||||
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
|
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -297,9 +296,10 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
|
|||||||
def test_performance_handle(
|
def test_performance_handle(
|
||||||
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -324,17 +324,17 @@ def test_performance_handle(
|
|||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
_performance(bot=MagicMock(), update=update)
|
_performance(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
|
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, mocker):
|
def test_count_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.main.telegram',
|
'freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -365,9 +365,9 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
|||||||
|
|
||||||
def test_performance_handle_invalid(default_conf, update, mocker):
|
def test_performance_handle_invalid(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -385,7 +385,7 @@ def test_performance_handle_invalid(default_conf, update, mocker):
|
|||||||
def test_start_handle(default_conf, update, mocker):
|
def test_start_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -403,7 +403,7 @@ def test_start_handle(default_conf, update, mocker):
|
|||||||
def test_start_handle_already_running(default_conf, update, mocker):
|
def test_start_handle_already_running(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -422,7 +422,7 @@ def test_start_handle_already_running(default_conf, update, mocker):
|
|||||||
def test_stop_handle(default_conf, update, mocker):
|
def test_stop_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -441,7 +441,7 @@ def test_stop_handle(default_conf, update, mocker):
|
|||||||
def test_stop_handle_already_stopped(default_conf, update, mocker):
|
def test_stop_handle_already_stopped(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -473,7 +473,7 @@ def test_balance_handle(default_conf, update, mocker):
|
|||||||
}]
|
}]
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -489,7 +489,7 @@ def test_balance_handle(default_conf, update, mocker):
|
|||||||
def test_help_handle(default_conf, update, mocker):
|
def test_help_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -502,7 +502,7 @@ def test_help_handle(default_conf, update, mocker):
|
|||||||
def test_version_handle(default_conf, update, mocker):
|
def test_version_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@@ -514,12 +514,12 @@ def test_version_handle(default_conf, update, mocker):
|
|||||||
|
|
||||||
def test_send_msg(default_conf, mocker):
|
def test_send_msg(default_conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock())
|
init=MagicMock())
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
send_msg('test', bot)
|
send_msg('test', bot)
|
||||||
assert len(bot.method_calls) == 0
|
assert not bot.method_calls
|
||||||
bot.reset_mock()
|
bot.reset_mock()
|
||||||
|
|
||||||
default_conf['telegram']['enabled'] = True
|
default_conf['telegram']['enabled'] = True
|
||||||
@@ -529,14 +529,13 @@ def test_send_msg(default_conf, mocker):
|
|||||||
|
|
||||||
def test_send_msg_network_error(default_conf, mocker):
|
def test_send_msg_network_error(default_conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock())
|
init=MagicMock())
|
||||||
default_conf['telegram']['enabled'] = True
|
default_conf['telegram']['enabled'] = True
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||||
with pytest.raises(NetworkError, match=r'Oh snap'):
|
send_msg('test', bot)
|
||||||
send_msg('test', bot)
|
|
||||||
|
|
||||||
# Bot should've tried to send it twice
|
# Bot should've tried to send it twice
|
||||||
assert len(bot.method_calls) == 2
|
assert len(bot.method_calls) == 2
|
1
freqtrade/tests/testdata/BTC_BCC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_BCC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_BCC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_BCC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LSK-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LSK-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LSK-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LSK-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_OK-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_OK-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_OK-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_OK-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_VTC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_VTC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_VTC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_VTC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_WAVES-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_WAVES-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_WAVES-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_WAVES-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-edg.json
vendored
1
freqtrade/tests/testdata/btc-edg.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-etc.json
vendored
1
freqtrade/tests/testdata/btc-etc.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-eth.json
vendored
1
freqtrade/tests/testdata/btc-eth.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-ltc.json
vendored
1
freqtrade/tests/testdata/btc-ltc.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-mtl.json
vendored
1
freqtrade/tests/testdata/btc-mtl.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-neo.json
vendored
1
freqtrade/tests/testdata/btc-neo.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-omg.json
vendored
1
freqtrade/tests/testdata/btc-omg.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pay.json
vendored
1
freqtrade/tests/testdata/btc-pay.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pivx.json
vendored
1
freqtrade/tests/testdata/btc-pivx.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-qtum.json
vendored
1
freqtrade/tests/testdata/btc-qtum.json
vendored
File diff suppressed because one or more lines are too long
@@ -7,8 +7,13 @@ from os import path
|
|||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
|
|
||||||
PAIRS = ['BTC-OK', 'BTC-NEO', 'BTC-DASH', 'BTC-ETC', 'BTC-ETH', 'BTC-SNT']
|
PAIRS = [
|
||||||
TICKER_INTERVAL = 1 # ticker interval in minutes (currently implemented: 1 and 5)
|
'BTC_BCC', 'BTC_ETH', 'BTC_MER', 'BTC_POWR', 'BTC_ETC',
|
||||||
|
'BTC_OK', 'BTC_NEO', 'BTC_EMC2', 'BTC_DASH', 'BTC_LSK',
|
||||||
|
'BTC_LTC', 'BTC_XZC', 'BTC_OMG', 'BTC_STRAT', 'BTC_XRP',
|
||||||
|
'BTC_QTUM', 'BTC_WAVES', 'BTC_VTC', 'BTC_XLM', 'BTC_MCO'
|
||||||
|
]
|
||||||
|
TICKER_INTERVAL = 5 # ticker interval in minutes (currently implemented: 1 and 5)
|
||||||
OUTPUT_DIR = path.dirname(path.realpath(__file__))
|
OUTPUT_DIR = path.dirname(path.realpath(__file__))
|
||||||
|
|
||||||
# Init Bittrex exchange
|
# Init Bittrex exchange
|
||||||
@@ -16,8 +21,8 @@ exchange._API = Bittrex({'key': '', 'secret': ''})
|
|||||||
|
|
||||||
for pair in PAIRS:
|
for pair in PAIRS:
|
||||||
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
|
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
|
||||||
filename = path.join(OUTPUT_DIR, '{}-{}m.json'.format(
|
filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
|
||||||
pair.lower(),
|
pair,
|
||||||
TICKER_INTERVAL,
|
TICKER_INTERVAL,
|
||||||
))
|
))
|
||||||
with open(filename, 'w') as fp:
|
with open(filename, 'w') as fp:
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
SQLAlchemy==1.1.14
|
SQLAlchemy==1.1.14
|
||||||
python-telegram-bot==8.1.1
|
python-telegram-bot==8.1.1
|
||||||
arrow==0.10.0
|
arrow==0.10.0
|
||||||
|
cachetools==2.0.1
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
|
1
setup.py
1
setup.py
@@ -34,6 +34,7 @@ setup(name='freqtrade',
|
|||||||
'jsonschema',
|
'jsonschema',
|
||||||
'TA-Lib',
|
'TA-Lib',
|
||||||
'tabulate',
|
'tabulate',
|
||||||
|
'cachetools',
|
||||||
],
|
],
|
||||||
dependency_links=[
|
dependency_links=[
|
||||||
"git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex"
|
"git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex"
|
||||||
|
Reference in New Issue
Block a user