Compare commits
48 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
de13df6ede | ||
|
0de211674d | ||
|
f7a27c156c | ||
|
98f11fc7bb | ||
|
013e13e546 | ||
|
6ff26c561a | ||
|
c81358c291 | ||
|
ed34d9f22f | ||
|
ee05561ef3 | ||
|
69ae99406a | ||
|
0cfbb56b6c | ||
|
8960373f1c | ||
|
e01c85bb3a |
@@ -1,4 +1,5 @@
|
||||
[run]
|
||||
omit =
|
||||
scripts/*
|
||||
freqtrade/tests/*
|
||||
freqtrade/vendor/*
|
11
README.md
11
README.md
@@ -22,8 +22,11 @@ Persistence is achieved through sqlite.
|
||||
* /status [table]: Lists all open trades
|
||||
* /count: Displays number of open 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
|
||||
* /balance: Show account balance per currency
|
||||
* /help: Show help message
|
||||
* /version: Show version
|
||||
|
||||
### Config
|
||||
`minimal_roi` is a JSON object where the key is a duration
|
||||
@@ -112,14 +115,14 @@ filesystem):
|
||||
|
||||
```
|
||||
$ cd ~/.freq
|
||||
$ touch tradesv2.sqlite
|
||||
$ touch tradesv3.sqlite
|
||||
$ docker run -d \
|
||||
--name freqtrade \
|
||||
-v ~/.freq/config.json:/freqtrade/config.json \
|
||||
-v ~/.freq/tradesv2.sqlite:/freqtrade/tradesv2.sqlite \
|
||||
-v ~/.freq/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||
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:
|
||||
|
||||
|
@@ -34,5 +34,8 @@
|
||||
"token": "token",
|
||||
"chat_id": "chat_id"
|
||||
},
|
||||
"initial_state": "running"
|
||||
"initial_state": "running",
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
}
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
__version__ = '0.14.0'
|
||||
__version__ = '0.14.2'
|
||||
|
||||
from . import main
|
||||
|
@@ -1,17 +1,13 @@
|
||||
import logging
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
import arrow
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.exchange import Bittrex, get_ticker_history
|
||||
from freqtrade.exchange import get_ticker_history
|
||||
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -43,9 +39,7 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
dataframe['mom'] = ta.MOM(dataframe)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
@@ -55,6 +49,9 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
return dataframe
|
||||
|
||||
|
||||
@@ -82,13 +79,12 @@ def analyze_ticker(pair: str) -> DataFrame:
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
data = get_ticker_history(pair)
|
||||
dataframe = parse_ticker_dataframe(data)
|
||||
|
||||
if dataframe.empty:
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return dataframe
|
||||
ticker_hist = get_ticker_history(pair)
|
||||
if not ticker_hist:
|
||||
logger.warning('Empty ticker history for pair %s', pair)
|
||||
return DataFrame()
|
||||
|
||||
dataframe = parse_ticker_dataframe(ticker_hist)
|
||||
dataframe = populate_indicators(dataframe)
|
||||
dataframe = populate_buy_trend(dataframe)
|
||||
return dataframe
|
||||
@@ -101,7 +97,6 @@ def get_buy_signal(pair: str) -> bool:
|
||||
:return: True if pair is good for buying, False otherwise
|
||||
"""
|
||||
dataframe = analyze_ticker(pair)
|
||||
|
||||
if dataframe.empty:
|
||||
return False
|
||||
|
||||
@@ -115,53 +110,3 @@ def get_buy_signal(pair: str) -> bool:
|
||||
signal = latest['buy'] == 1
|
||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
||||
return signal
|
||||
|
||||
|
||||
def plot_analyzed_dataframe(pair: str) -> None:
|
||||
"""
|
||||
Calls analyze() and plots the returned dataframe
|
||||
:param pair: pair as str
|
||||
:return: None
|
||||
"""
|
||||
import matplotlib
|
||||
matplotlib.use("Qt5Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Init Bittrex to use public API
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
dataframe = analyze_ticker(pair)
|
||||
|
||||
# Two subplots sharing x axis
|
||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
||||
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
|
||||
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
|
||||
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
|
||||
ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA')
|
||||
ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low')
|
||||
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
|
||||
ax1.legend()
|
||||
|
||||
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
|
||||
ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI')
|
||||
# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
|
||||
ax2.legend()
|
||||
|
||||
ax3.plot(dataframe.index.values, dataframe['fastk'], label='k')
|
||||
ax3.plot(dataframe.index.values, dataframe['fastd'], label='d')
|
||||
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
|
||||
ax3.legend()
|
||||
|
||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
||||
# all but bottom plot.
|
||||
fig.subplots_adjust(hspace=0)
|
||||
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Install PYQT5==5.9 manually if you want to test this helper function
|
||||
while True:
|
||||
for p in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||
plot_analyzed_dataframe(p)
|
||||
time.sleep(60)
|
||||
|
@@ -4,6 +4,7 @@ from random import randint
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
import arrow
|
||||
from cachetools import cached, TTLCache
|
||||
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
@@ -63,7 +64,12 @@ def validate_pairs(pairs: List[str]) -> None:
|
||||
:return: None
|
||||
"""
|
||||
markets = _API.get_markets()
|
||||
stake_cur = _CONF['stake_currency']
|
||||
for pair in pairs:
|
||||
if not pair.startswith(stake_cur):
|
||||
raise RuntimeError(
|
||||
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
|
||||
)
|
||||
if pair not in markets:
|
||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
||||
|
||||
@@ -122,7 +128,8 @@ def get_ticker(pair: str) -> dict:
|
||||
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)
|
||||
|
||||
|
||||
@@ -152,13 +159,17 @@ def get_markets() -> List[str]:
|
||||
return _API.get_markets()
|
||||
|
||||
|
||||
def get_market_summaries() -> List[Dict]:
|
||||
return _API.get_market_summaries()
|
||||
|
||||
|
||||
def get_name() -> str:
|
||||
return _API.name
|
||||
|
||||
|
||||
def get_sleep_time() -> float:
|
||||
return _API.sleep_time
|
||||
|
||||
|
||||
def get_fee() -> float:
|
||||
return _API.fee
|
||||
|
||||
|
||||
def get_wallet_health() -> List[Dict]:
|
||||
return _API.get_wallet_health()
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
|
||||
import requests
|
||||
from bittrex.bittrex import Bittrex as _Bittrex
|
||||
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
|
||||
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_API: _Bittrex = None
|
||||
_API_V2: _Bittrex = None
|
||||
_EXCHANGE_CONF: dict = {}
|
||||
|
||||
|
||||
@@ -18,22 +18,23 @@ class Bittrex(Exchange):
|
||||
"""
|
||||
# Base URL and API endpoints
|
||||
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'
|
||||
|
||||
@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:
|
||||
global _API, _EXCHANGE_CONF
|
||||
global _API, _API_V2, _EXCHANGE_CONF
|
||||
|
||||
_EXCHANGE_CONF.update(config)
|
||||
_API = _Bittrex(
|
||||
api_key=_EXCHANGE_CONF['key'],
|
||||
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
|
||||
@@ -81,13 +82,17 @@ class Bittrex(Exchange):
|
||||
raise RuntimeError('{message} params=({pair})'.format(
|
||||
message=data['message'],
|
||||
pair=pair))
|
||||
if not data['result']['Bid'] or not data['result']['Ask'] or not data['result']['Last']:
|
||||
raise RuntimeError('{message} params=({pair})'.format(
|
||||
message=data['message'],
|
||||
pair=pair))
|
||||
return {
|
||||
'bid': float(data['result']['Bid']),
|
||||
'ask': float(data['result']['Ask']),
|
||||
'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:
|
||||
interval = 'oneMin'
|
||||
elif tick_interval == 5:
|
||||
@@ -95,10 +100,18 @@ class Bittrex(Exchange):
|
||||
else:
|
||||
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
|
||||
|
||||
data = requests.get(self.TICKER_METHOD, params={
|
||||
'marketName': pair.replace('_', '-'),
|
||||
'tickInterval': interval,
|
||||
}).json()
|
||||
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
|
||||
|
||||
# These sanity check are necessary because bittrex cannot keep their API stable.
|
||||
if not data.get('result'):
|
||||
return []
|
||||
|
||||
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
||||
for tick in data['result']:
|
||||
if prop not in tick.keys():
|
||||
logger.warning('Required property %s not present in response', prop)
|
||||
return []
|
||||
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair})'.format(
|
||||
message=data['message'],
|
||||
@@ -139,3 +152,20 @@ class Bittrex(Exchange):
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message}'.format(message=data['message']))
|
||||
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
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def sleep_time(self) -> float:
|
||||
"""
|
||||
Sleep time in seconds for the main loop to avoid API rate limits.
|
||||
:return: float
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||
"""
|
||||
@@ -82,7 +74,7 @@ class Exchange(ABC):
|
||||
"""
|
||||
|
||||
@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.
|
||||
:param pair: Pair as str, format: BTC_ETC
|
||||
@@ -139,3 +131,41 @@ class Exchange(ABC):
|
||||
Returns all available markets.
|
||||
: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
|
||||
},
|
||||
...
|
||||
"""
|
||||
|
@@ -5,33 +5,63 @@ import logging
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
import requests
|
||||
from cachetools import cached, TTLCache
|
||||
from jsonschema import validate
|
||||
|
||||
from freqtrade import __version__, exchange, persistence
|
||||
from freqtrade.analyze import get_buy_signal
|
||||
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state
|
||||
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state, build_arg_parser, throttle
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import telegram
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger('freqtrade')
|
||||
|
||||
_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,
|
||||
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
|
||||
"""
|
||||
state_changed = False
|
||||
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
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
if len(trades) < _CONF['max_open_trades']:
|
||||
@@ -42,7 +72,10 @@ def _process() -> bool:
|
||||
Trade.session.add(trade)
|
||||
state_changed = True
|
||||
else:
|
||||
logging.info('Got no buy signal...')
|
||||
logger.info(
|
||||
'Checked all whitelisted currencies. '
|
||||
'Found no suitable entry positions for buying. Will keep looking ...'
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception('Unable to create trade')
|
||||
|
||||
@@ -85,7 +118,10 @@ def close_trade_if_fulfilled(trade: Trade) -> bool:
|
||||
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)
|
||||
logger.info(
|
||||
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
||||
trade
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -163,7 +199,10 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
if one pair triggers the buy_signal a new trade record gets created
|
||||
: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'])
|
||||
# Check if stake_amount is fulfilled
|
||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||
@@ -237,6 +276,23 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
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:
|
||||
"""
|
||||
Cleanup the application state und finish all pending tasks
|
||||
@@ -255,32 +311,49 @@ def main():
|
||||
Loads and validates the config and handles the main loop
|
||||
:return: None
|
||||
"""
|
||||
logger.info('Starting freqtrade %s', __version__)
|
||||
|
||||
global _CONF
|
||||
with open('config.json') as file:
|
||||
_CONF = json.load(file)
|
||||
args = build_arg_parser().parse_args()
|
||||
|
||||
# Initialize logger
|
||||
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
|
||||
with open(args.config) as file:
|
||||
_CONF = json.load(file)
|
||||
if 'internals' not in _CONF:
|
||||
_CONF['internals'] = {}
|
||||
logger.info('Validating configuration ...')
|
||||
validate(_CONF, CONF_SCHEMA)
|
||||
|
||||
# Initialize all modules and start main loop
|
||||
if args.dynamic_whitelist:
|
||||
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
||||
init(_CONF)
|
||||
old_state = get_state()
|
||||
logger.info('Initial State: %s', old_state)
|
||||
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
|
||||
old_state = None
|
||||
while True:
|
||||
new_state = get_state()
|
||||
# Log state transition
|
||||
if new_state != old_state:
|
||||
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||
logging.info('Changing state to: %s', new_state.name)
|
||||
logger.info('Changing state to: %s', new_state.name)
|
||||
|
||||
if new_state == State.STOPPED:
|
||||
time.sleep(1)
|
||||
elif new_state == State.RUNNING:
|
||||
_process()
|
||||
# We need to sleep here because otherwise we would run into bittrex rate limit
|
||||
time.sleep(exchange.get_sleep_time())
|
||||
throttle(
|
||||
_process,
|
||||
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
||||
dynamic_whitelist=args.dynamic_whitelist,
|
||||
)
|
||||
old_state = new_state
|
||||
|
||||
|
||||
|
@@ -1,7 +1,15 @@
|
||||
import argparse
|
||||
import enum
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
|
||||
import time
|
||||
from wrapt import synchronized
|
||||
|
||||
from freqtrade import __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class State(enum.Enum):
|
||||
RUNNING = 0
|
||||
@@ -32,6 +40,57 @@ def get_state() -> State:
|
||||
return _STATE
|
||||
|
||||
|
||||
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 build_arg_parser() -> argparse.ArgumentParser:
|
||||
""" Builds and returns an ArgumentParser instance """
|
||||
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',
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
# Required json-schema for user specified config
|
||||
CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
@@ -71,6 +130,12 @@ CONF_SCHEMA = {
|
||||
'required': ['enabled', 'token', 'chat_id']
|
||||
},
|
||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||
'internals': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'process_throttle_secs': {'type': 'number'}
|
||||
}
|
||||
}
|
||||
},
|
||||
'definitions': {
|
||||
'exchange': {
|
||||
|
@@ -5,35 +5,37 @@ from typing import Optional, Dict
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
from sqlalchemy.orm.session import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CONF = {}
|
||||
_DECL_BASE = declarative_base()
|
||||
|
||||
|
||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
def init(config: dict, engine: Optional[Engine] = None) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
registers all known command handlers
|
||||
and starts polling for message updates
|
||||
:param config: config to use
|
||||
:param db_url: database connector string for sqlalchemy (Optional)
|
||||
:param engine: database engine for sqlalchemy (Optional)
|
||||
:return: None
|
||||
"""
|
||||
_CONF.update(config)
|
||||
if not db_url:
|
||||
if not engine:
|
||||
if _CONF.get('dry_run', False):
|
||||
db_url = 'sqlite://'
|
||||
engine = create_engine('sqlite://',
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=StaticPool,
|
||||
echo=False)
|
||||
else:
|
||||
db_url = 'sqlite:///tradesv3.sqlite'
|
||||
engine = create_engine('sqlite:///tradesv3.sqlite')
|
||||
|
||||
engine = create_engine(db_url, echo=False)
|
||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||
Trade.session = session()
|
||||
Trade.query = session.query_property()
|
||||
|
@@ -11,7 +11,7 @@ from telegram import ParseMode, Bot, Update
|
||||
from telegram.error import NetworkError
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade import exchange, __version__
|
||||
from freqtrade.misc import get_state, State, update_state
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
@@ -51,6 +51,7 @@ def init(config: dict) -> None:
|
||||
CommandHandler('performance', _performance),
|
||||
CommandHandler('count', _count),
|
||||
CommandHandler('help', _help),
|
||||
CommandHandler('version', _version),
|
||||
]
|
||||
for handle in handles:
|
||||
_UPDATER.dispatcher.add_handler(handle)
|
||||
@@ -272,18 +273,21 @@ def _balance(bot: Bot, update: Update) -> None:
|
||||
Handler for /balance
|
||||
Returns current account balance per crypto
|
||||
"""
|
||||
output = ""
|
||||
balances = exchange.get_balances()
|
||||
output = ''
|
||||
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:
|
||||
if not currency['Balance'] and not currency['Available'] and not currency['Pending']:
|
||||
continue
|
||||
output += """*Currency*: {Currency}
|
||||
*Available*: {Available}
|
||||
*Balance*: {Balance}
|
||||
*Pending*: {Pending}
|
||||
|
||||
""".format(**currency)
|
||||
|
||||
send_msg(output)
|
||||
|
||||
|
||||
@@ -331,26 +335,29 @@ def _forcesell(bot: Bot, update: Update) -> None:
|
||||
send_msg('`trader is not running`', bot=bot)
|
||||
return
|
||||
|
||||
try:
|
||||
trade_id = int(update.message.text
|
||||
.replace('/forcesell', '')
|
||||
.strip())
|
||||
# Query for trade
|
||||
trade = Trade.query.filter(and_(
|
||||
Trade.id == trade_id,
|
||||
Trade.is_open.is_(True)
|
||||
)).first()
|
||||
if not trade:
|
||||
send_msg('There is no open trade with ID: `{}`'.format(trade_id))
|
||||
return
|
||||
# Get current rate
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
from freqtrade.main import execute_sell
|
||||
execute_sell(trade, current_rate)
|
||||
trade_id = update.message.text.replace('/forcesell', '').strip()
|
||||
if trade_id == 'all':
|
||||
# Execute sell for all open orders
|
||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||
# Get current rate
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
from freqtrade.main import execute_sell
|
||||
execute_sell(trade, current_rate)
|
||||
return
|
||||
|
||||
except ValueError:
|
||||
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
|
||||
# Query for trade
|
||||
trade = Trade.query.filter(and_(
|
||||
Trade.id == trade_id,
|
||||
Trade.is_open.is_(True)
|
||||
)).first()
|
||||
if not trade:
|
||||
send_msg('Invalid argument. See `/help` to view usage')
|
||||
logger.warning('/forcesell: Invalid argument received')
|
||||
return
|
||||
# Get current rate
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
from freqtrade.main import execute_sell
|
||||
execute_sell(trade, current_rate)
|
||||
|
||||
|
||||
@authorized_only
|
||||
@@ -397,8 +404,12 @@ def _count(bot: Bot, update: Update) -> None:
|
||||
return
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
message = '<b>Count:</b>\ncurrent/max\n{}/{}\n'.format(len(trades), _CONF['max_open_trades'])
|
||||
|
||||
message = tabulate({
|
||||
'current': [len(trades)],
|
||||
'max': [_CONF['max_open_trades']]
|
||||
}, headers=['current', 'max'], tablefmt='simple')
|
||||
message = "<pre>{}</pre>".format(message)
|
||||
logger.debug(message)
|
||||
send_msg(message, parse_mode=ParseMode.HTML)
|
||||
|
||||
@@ -418,15 +429,28 @@ def _help(bot: Bot, update: Update) -> None:
|
||||
*/status [table]:* `Lists all open trades`
|
||||
*table :* `will display trades in a table`
|
||||
*/profit:* `Lists cumulative profit from all finished trades`
|
||||
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
|
||||
*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
|
||||
*/performance:* `Show performance of each finished trade grouped by pair`
|
||||
*/count:* `Show number of trades running compared to allowed number of trades`
|
||||
*/balance:* `Show account balance per currency`
|
||||
*/help:* `This help message`
|
||||
*/version:* `Show version`
|
||||
"""
|
||||
send_msg(message, bot=bot)
|
||||
|
||||
|
||||
@authorized_only
|
||||
def _version(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /version.
|
||||
Show version information
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
||||
|
||||
|
||||
def shorten_date(date):
|
||||
"""
|
||||
Trim the date so it fits on small screens
|
||||
|
@@ -37,7 +37,8 @@ def default_conf():
|
||||
"BTC_ETH",
|
||||
"BTC_TKN",
|
||||
"BTC_TRST",
|
||||
"BTC_SWT"
|
||||
"BTC_SWT",
|
||||
"BTC_BCC"
|
||||
]
|
||||
},
|
||||
"telegram": {
|
||||
@@ -90,6 +91,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
|
||||
def limit_buy_order():
|
||||
return {
|
||||
|
36
freqtrade/tests/test_exchange.py
Normal file
36
freqtrade/tests/test_exchange.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.exchange import validate_pairs
|
||||
|
||||
|
||||
def test_validate_pairs(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
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.dict('freqtrade.exchange._CONF', default_conf)
|
||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||
|
||||
|
||||
def test_validate_pairs_not_available(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
api_mock.get_markets = MagicMock(return_value=[])
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
with pytest.raises(RuntimeError, match=r'not available'):
|
||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||
|
||||
|
||||
def test_validate_pairs_not_compatible(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
api_mock.get_markets = MagicMock(return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT'])
|
||||
default_conf['stake_currency'] = 'ETH'
|
||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
with pytest.raises(RuntimeError, match=r'not compatible'):
|
||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||
|
@@ -15,41 +15,44 @@ from freqtrade.vendor.qtpylib.indicators import crossed_above
|
||||
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
|
||||
TARGET_TRADES = 1200
|
||||
|
||||
TARGET_TRADES = 1300
|
||||
TOTAL_TRIES = 4
|
||||
current_tries = 0
|
||||
|
||||
def buy_strategy_generator(params):
|
||||
print(params)
|
||||
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if params['uptrend_long_ema']['enabled']:
|
||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||
if params['uptrend_short_ema']['enabled']:
|
||||
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
||||
if params['mfi']['enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||
if params['fastd']['enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
||||
if params['adx']['enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||
if params['cci']['enabled']:
|
||||
conditions.append(dataframe['cci'] < params['cci']['value'])
|
||||
if params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
if params['over_sar']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||
if params['green_candle']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['open'])
|
||||
if params['uptrend_sma']['enabled']:
|
||||
prevsma = dataframe['sma'].shift(1)
|
||||
conditions.append(dataframe['sma'] > prevsma)
|
||||
|
||||
prev_fastd = dataframe['fastd'].shift(1)
|
||||
# TRIGGERS
|
||||
triggers = {
|
||||
'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)),
|
||||
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
||||
'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']))
|
||||
|
||||
@@ -72,13 +75,16 @@ def test_hyperopt(backtest_conf, backdata, mocker):
|
||||
results = backtest(backtest_conf, backdata, mocker)
|
||||
|
||||
result = format_results(results)
|
||||
print(result)
|
||||
|
||||
total_profit = results.profit.sum() * 1000
|
||||
trade_count = len(results.index)
|
||||
|
||||
trade_loss = 1 - 0.8 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5)
|
||||
profit_loss = exp(-total_profit**3 / 10**11)
|
||||
trade_loss = 1 - 0.4 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
||||
profit_loss = max(0, 1 - total_profit / 15000) # max profit 15000
|
||||
|
||||
global current_tries
|
||||
current_tries += 1
|
||||
print('{}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
|
||||
|
||||
return {
|
||||
'loss': trade_loss + profit_loss,
|
||||
@@ -89,32 +95,36 @@ def test_hyperopt(backtest_conf, backdata, mocker):
|
||||
space = {
|
||||
'mfi': hp.choice('mfi', [
|
||||
{'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', [
|
||||
{'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', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.uniform('adx-value', 10, 30)}
|
||||
]),
|
||||
'cci': hp.choice('cci', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.uniform('cci-value', -150, -100)}
|
||||
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
|
||||
]),
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'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', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'over_sar': hp.choice('over_sar', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'green_candle': hp.choice('green_candle', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_sma': hp.choice('uptrend_sma', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
@@ -125,10 +135,13 @@ def test_hyperopt(backtest_conf, backdata, mocker):
|
||||
{'type': 'ao_cross_zero'},
|
||||
{'type': 'ema5_cross_ema10'},
|
||||
{'type': 'macd_cross_signal'},
|
||||
{'type': 'sar_reversal'},
|
||||
{'type': 'stochf_cross'},
|
||||
{'type': 'ht_sine'},
|
||||
]),
|
||||
}
|
||||
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('Best parameters {}'.format(best))
|
||||
newlist = sorted(trials.results, key=itemgetter('loss'))
|
||||
|
@@ -4,6 +4,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
||||
@@ -12,15 +13,16 @@ from freqtrade.misc import get_state, State
|
||||
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.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_wallet_health=health,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert len(trades) == 0
|
||||
@@ -40,7 +42,7 @@ def test_process_trade_creation(default_conf, ticker, mocker):
|
||||
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.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
@@ -48,14 +50,15 @@ def test_process_exchange_failures(default_conf, ticker, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_wallet_health=health,
|
||||
buy=MagicMock(side_effect=requests.exceptions.RequestException))
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
result = _process()
|
||||
assert result is False
|
||||
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()
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock)
|
||||
@@ -63,8 +66,9 @@ def test_process_runtime_error(default_conf, ticker, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_wallet_health=health,
|
||||
buy=MagicMock(side_effect=RuntimeError))
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
assert get_state() == State.RUNNING
|
||||
|
||||
result = _process()
|
||||
@@ -73,16 +77,17 @@ def test_process_runtime_error(default_conf, ticker, mocker):
|
||||
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.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_wallet_health=health,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
get_order=MagicMock(return_value=limit_buy_order))
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert len(trades) == 0
|
||||
@@ -106,7 +111,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||
# Save state of current whitelist
|
||||
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
trade = create_trade(15.0)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
@@ -167,7 +172,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
sell=MagicMock(return_value='mocked_limit_sell'))
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
trade = create_trade(15.0)
|
||||
trade.update(limit_buy_order)
|
||||
Trade.session.add(trade)
|
||||
@@ -197,7 +202,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
# Create trade and sell it
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
trade = create_trade(15.0)
|
||||
trade.update(limit_buy_order)
|
||||
trade.update(limit_sell_order)
|
||||
|
20
freqtrade/tests/test_misc.py
Normal file
20
freqtrade/tests/test_misc.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import time
|
||||
|
||||
from freqtrade.misc import throttle
|
||||
|
||||
|
||||
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
|
@@ -5,21 +5,19 @@ from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from telegram import Bot, Update, Message, Chat
|
||||
from sqlalchemy import create_engine
|
||||
from telegram import Update, Message, Chat
|
||||
from telegram.error import NetworkError
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.main import init, create_trade
|
||||
from freqtrade.misc import update_state, State, get_state
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import telegram
|
||||
from freqtrade.rpc.telegram import (
|
||||
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance,
|
||||
authorized_only, _help, is_enabled, send_msg
|
||||
)
|
||||
|
||||
|
||||
class MagicBot(MagicMock, Bot):
|
||||
pass
|
||||
authorized_only, _help, is_enabled, send_msg,
|
||||
_version)
|
||||
|
||||
|
||||
def test_is_enabled(default_conf, mocker):
|
||||
@@ -90,16 +88,16 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
update_state(State.STOPPED)
|
||||
_status(bot=MagicBot(), update=update)
|
||||
_status(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
update_state(State.RUNNING)
|
||||
_status(bot=MagicBot(), update=update)
|
||||
_status(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -111,7 +109,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
||||
Trade.session.flush()
|
||||
|
||||
# Trigger status while we have a fulfilled order for the open trade
|
||||
_status(bot=MagicBot(), update=update)
|
||||
_status(bot=MagicMock(), update=update)
|
||||
|
||||
assert msg_mock.call_count == 2
|
||||
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||
@@ -130,15 +128,15 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
update_state(State.STOPPED)
|
||||
_status_table(bot=MagicBot(), update=update)
|
||||
_status_table(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
update_state(State.RUNNING)
|
||||
_status_table(bot=MagicBot(), update=update)
|
||||
_status_table(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no active order' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -149,7 +147,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_status_table(bot=MagicBot(), update=update)
|
||||
_status_table(bot=MagicMock(), update=update)
|
||||
|
||||
text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0])
|
||||
line = text.split("\n")
|
||||
@@ -171,9 +169,9 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
_profit(bot=MagicBot(), update=update)
|
||||
_profit(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -185,7 +183,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
_profit(bot=MagicBot(), update=update)
|
||||
_profit(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||
msg_mock.reset_mock()
|
||||
@@ -198,7 +196,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_profit(bot=MagicBot(), update=update)
|
||||
_profit(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*ROI:* `1.50701325 (10.05%)`' in msg_mock.call_args_list[-1][0][0]
|
||||
assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0]
|
||||
@@ -215,7 +213,7 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
@@ -225,13 +223,41 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
||||
Trade.session.flush()
|
||||
|
||||
update.message.text = '/forcesell 1'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
_forcesell(bot=MagicMock(), update=update)
|
||||
|
||||
assert msg_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||
assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
for _ in range(4):
|
||||
Trade.session.add(create_trade(15.0))
|
||||
Trade.session.flush()
|
||||
|
||||
msg_mock.reset_mock()
|
||||
|
||||
update.message.text = '/forcesell all'
|
||||
_forcesell(bot=MagicMock(), update=update)
|
||||
|
||||
assert msg_mock.call_count == 4
|
||||
for args in msg_mock.call_args_list:
|
||||
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
|
||||
|
||||
|
||||
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
@@ -242,12 +268,12 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Trader is not running
|
||||
update_state(State.STOPPED)
|
||||
update.message.text = '/forcesell 1'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
_forcesell(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
@@ -255,7 +281,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||
msg_mock.reset_mock()
|
||||
update_state(State.RUNNING)
|
||||
update.message.text = '/forcesell'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
_forcesell(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
@@ -263,12 +289,13 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||
msg_mock.reset_mock()
|
||||
update_state(State.RUNNING)
|
||||
update.message.text = '/forcesell 123456'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
_forcesell(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no open trade' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_performance_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
def test_performance_handle(
|
||||
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
@@ -279,7 +306,7 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order, limit
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
@@ -296,7 +323,7 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order, limit
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_performance(bot=MagicBot(), update=update)
|
||||
_performance(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
||||
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
|
||||
@@ -315,26 +342,25 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
update_state(State.STOPPED)
|
||||
_count(bot=MagicBot(), update=update)
|
||||
_count(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
update_state(State.RUNNING)
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
trade2 = create_trade(15.0)
|
||||
assert trade
|
||||
assert trade2
|
||||
Trade.session.add(trade)
|
||||
Trade.session.add(trade2)
|
||||
Trade.session.add(create_trade(15.0))
|
||||
Trade.session.flush()
|
||||
|
||||
_count(bot=MagicBot(), update=update)
|
||||
line = msg_mock.call_args_list[-1][0][0].split("\n")
|
||||
assert line[2] == '{}/{}'.format(2, default_conf['max_open_trades'])
|
||||
msg_mock.reset_mock()
|
||||
_count(bot=MagicMock(), update=update)
|
||||
|
||||
msg = '<pre> current max\n--------- -----\n 1 {}</pre>'.format(
|
||||
default_conf['max_open_trades']
|
||||
)
|
||||
assert msg in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_performance_handle_invalid(default_conf, update, mocker):
|
||||
@@ -347,11 +373,11 @@ def test_performance_handle_invalid(default_conf, update, mocker):
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Trader is not running
|
||||
update_state(State.STOPPED)
|
||||
_performance(bot=MagicBot(), update=update)
|
||||
_performance(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
@@ -366,10 +392,10 @@ def test_start_handle(default_conf, update, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
update_state(State.STOPPED)
|
||||
assert get_state() == State.STOPPED
|
||||
_start(bot=MagicBot(), update=update)
|
||||
_start(bot=MagicMock(), update=update)
|
||||
assert get_state() == State.RUNNING
|
||||
assert msg_mock.call_count == 0
|
||||
|
||||
@@ -384,10 +410,10 @@ def test_start_handle_already_running(default_conf, update, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
update_state(State.RUNNING)
|
||||
assert get_state() == State.RUNNING
|
||||
_start(bot=MagicBot(), update=update)
|
||||
_start(bot=MagicMock(), update=update)
|
||||
assert get_state() == State.RUNNING
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'already running' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -403,10 +429,10 @@ def test_stop_handle(default_conf, update, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
update_state(State.RUNNING)
|
||||
assert get_state() == State.RUNNING
|
||||
_stop(bot=MagicBot(), update=update)
|
||||
_stop(bot=MagicMock(), update=update)
|
||||
assert get_state() == State.STOPPED
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -422,10 +448,10 @@ def test_stop_handle_already_stopped(default_conf, update, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
update_state(State.STOPPED)
|
||||
assert get_state() == State.STOPPED
|
||||
_stop(bot=MagicBot(), update=update)
|
||||
_stop(bot=MagicMock(), update=update)
|
||||
assert get_state() == State.STOPPED
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -454,7 +480,7 @@ def test_balance_handle(default_conf, update, mocker):
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
get_balances=MagicMock(return_value=mock_balance))
|
||||
|
||||
_balance(bot=MagicBot(), update=update)
|
||||
_balance(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'Balance' in msg_mock.call_args_list[0][0][0]
|
||||
@@ -468,11 +494,24 @@ def test_help_handle(default_conf, update, mocker):
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
|
||||
_help(bot=MagicBot(), update=update)
|
||||
_help(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_version_handle(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
|
||||
_version(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_send_msg(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
|
@@ -2,6 +2,7 @@
|
||||
SQLAlchemy==1.1.14
|
||||
python-telegram-bot==8.1.1
|
||||
arrow==0.10.0
|
||||
cachetools==2.0.1
|
||||
requests==2.18.4
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
|
51
scripts/plot_dataframe.py
Executable file
51
scripts/plot_dataframe.py
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import matplotlib # Install PYQT5 manually if you want to test this helper function
|
||||
matplotlib.use("Qt5Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from freqtrade import exchange, analyze
|
||||
|
||||
|
||||
def plot_analyzed_dataframe(pair: str) -> None:
|
||||
"""
|
||||
Calls analyze() and plots the returned dataframe
|
||||
:param pair: pair as str
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Init Bittrex to use public API
|
||||
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
||||
dataframe = analyze.analyze_ticker(pair)
|
||||
|
||||
# Two subplots sharing x axis
|
||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
||||
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
|
||||
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
|
||||
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
|
||||
ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA')
|
||||
ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low')
|
||||
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
|
||||
ax1.legend()
|
||||
|
||||
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
|
||||
ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI')
|
||||
# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
|
||||
ax2.legend()
|
||||
|
||||
ax3.plot(dataframe.index.values, dataframe['fastk'], label='k')
|
||||
ax3.plot(dataframe.index.values, dataframe['fastd'], label='d')
|
||||
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
|
||||
ax3.legend()
|
||||
|
||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
||||
# all but bottom plot.
|
||||
fig.subplots_adjust(hspace=0)
|
||||
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
plot_analyzed_dataframe('BTC_ETH')
|
||||
|
Reference in New Issue
Block a user