Compare commits

...

48 Commits

Author SHA1 Message Date
gcarq
b115963a70 Merge branch 'release/0.14.2' 2017-11-16 00:40:44 +01:00
gcarq
2e953a937d version bump 2017-11-16 00:40:36 +01:00
gcarq
4e05691cab check if balance list is empty (fixes #105) 2017-11-16 00:01:47 +01:00
gcarq
b5f58724a0 get_ticker_history: check if result is set (fixes #103) 2017-11-15 23:16:54 +01:00
gcarq
b83309b55d reduce calls_per_second to 1 2017-11-15 23:16:39 +01:00
gcarq
e8101a6da5 default BaseVolume to 0.0 if null 2017-11-14 17:48:19 +01:00
gcarq
dd9cb008fb refresh whitelist based on wallet health (fixes #60)
Refreshs the whitelist in each iteration based on the wallet health,
disabled wallets will be removed from the whitelist automatically.
2017-11-13 21:34:47 +01:00
gcarq
81f7172c4a sanitize get_ticker_history (fixes #100) 2017-11-13 19:54:09 +01:00
Michael Egger
bab59fbacd Merge pull request #99 from gcarq/more_triggers2
Expanding hyperopt
2017-11-13 12:11:15 +01:00
Janne Sinivirta
0f0b10b6cc adjust search spaces 2017-11-13 07:28:56 +02:00
Janne Sinivirta
8e68c5358e clean up prints during hyperopt 2017-11-12 09:44:31 +02:00
Janne Sinivirta
660f01b514 add hilbert transform leadsine trigger 2017-11-12 09:13:54 +02:00
Janne Sinivirta
13537e3ce4 add short ema guard to hyperopt 2017-11-12 08:45:32 +02:00
Janne Sinivirta
2963a90008 add stochastics trigger 2017-11-12 08:38:52 +02:00
Janne Sinivirta
15b20b83fa optimize hyperopt objective function 2017-11-12 08:30:58 +02:00
gcarq
1c3c316e45 reduce calls_per_second 2017-11-11 21:29:35 +01:00
gcarq
517879382b Add argument for dynamic-whitelist handling
If --dynamic-whitelist is passed the whitelist in the config file
is ignored. It gets automatically refreshed every 30 minutes and
currently selects the 20 topmost BaseVolume markets
2017-11-11 19:20:53 +01:00
gcarq
bcd3340a80 implement get_market_summaries 2017-11-11 19:20:16 +01:00
gcarq
12ae1e111e use get_candles from python-bittrex 2017-11-11 17:14:55 +01:00
gcarq
d3b3370f23 Add configurable throttle mechanism 2017-11-11 16:47:19 +01:00
gcarq
8f817a3634 use TTLCache for get_ticker_history 2017-11-11 15:29:31 +01:00
Janne Sinivirta
cf79b15651 use discrete values for filters 2017-11-11 11:50:10 +02:00
Janne Sinivirta
a4284351e3 fix green_candle 2017-11-11 11:22:12 +02:00
Janne Sinivirta
906caf329b remove two unused or poorly performing indicators 2017-11-11 11:22:12 +02:00
Janne Sinivirta
3db13fae13 add green_candle guard 2017-11-11 11:22:12 +02:00
Janne Sinivirta
274972f7af make faststoch trigger use crossed_above helper 2017-11-11 11:22:11 +02:00
Janne Sinivirta
83fd27e031 add sar reversal as trigger 2017-11-11 11:22:11 +02:00
gcarq
3126dcfcea drop sleep_time and use python-bittrex request delay 2017-11-10 23:39:49 +01:00
Michael Egger
72aec6c320 Merge pull request #96 from gcarq/feature/add-argparse
add argparse and implement basic arguments
2017-11-10 18:04:03 +01:00
gcarq
b709ccbf53 enhance logging messages 2017-11-10 17:56:03 +01:00
gcarq
7e99b13742 add missing commands to README 2017-11-10 17:27:19 +01:00
gcarq
8b464033ff add missing commands to README 2017-11-10 17:26:52 +01:00
gcarq
93c525a8fa Merge branch 'master' into develop 2017-11-10 17:18:21 +01:00
gcarq
54b15c1556 update README 2017-11-10 17:17:51 +01:00
gcarq
029f32af63 Merge tag '0.14.1' into develop
0.14.1
2017-11-09 23:53:14 +01:00
gcarq
de13df6ede Merge branch 'release/0.14.1' 2017-11-09 23:53:10 +01:00
gcarq
0de211674d version bump 2017-11-09 23:52:34 +01:00
gcarq
f7a27c156c add /version command handler 2017-11-09 23:51:32 +01:00
gcarq
98f11fc7bb fix sqlite threading issue 2017-11-09 23:45:22 +01:00
gcarq
013e13e546 use tabulate for /count 2017-11-09 23:45:03 +01:00
gcarq
6ff26c561a move plot_dataframe to scripts/ folder 2017-11-09 22:29:23 +01:00
gcarq
c81358c291 remove MagicBot 2017-11-09 22:11:02 +01:00
gcarq
ed34d9f22f add tests for /forcesell all 2017-11-09 22:08:28 +01:00
gcarq
ee05561ef3 refactor forcesellall to /forcesell all 2017-11-09 22:07:51 +01:00
Eoin
69ae99406a add telegram handler for forcesellall 2017-11-09 21:52:08 +01:00
gcarq
0cfbb56b6c enhance and test pair validation 2017-11-09 21:47:47 +01:00
gcarq
8960373f1c Merge tag '0.14.0' into develop
0.14.0
2017-11-09 20:56:12 +01:00
gcarq
e01c85bb3a add argparse and implement basic arguments 2017-11-08 22:43:47 +01:00
21 changed files with 621 additions and 237 deletions

View File

@@ -1,4 +1,5 @@
[run]
omit =
scripts/*
freqtrade/tests/*
freqtrade/vendor/*

View File

@@ -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:

View File

@@ -34,5 +34,8 @@
"token": "token",
"chat_id": "chat_id"
},
"initial_state": "running"
"initial_state": "running",
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -1,3 +1,3 @@
__version__ = '0.14.0'
__version__ = '0.14.2'
from . import main

View File

@@ -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)

View File

@@ -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()

View File

@@ -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']]

View File

@@ -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
},
...
"""

View File

@@ -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

View File

@@ -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': {

View File

@@ -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()

View File

@@ -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

View File

@@ -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 {

View 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'])

View File

@@ -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'))

View File

@@ -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)

View 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

View File

@@ -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',

View File

@@ -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
View 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')

View File

@@ -34,6 +34,7 @@ setup(name='freqtrade',
'jsonschema',
'TA-Lib',
'tabulate',
'cachetools',
],
dependency_links=[
"git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex"