Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
f1730eee3a
@ -6,10 +6,10 @@
|
|||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
"20": 0.02
|
"20": 0.02,
|
||||||
"0": 0.04
|
"0": 0.04
|
||||||
},
|
},
|
||||||
"stoploss": -0.40,
|
"stoploss": -0.10,
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,16 @@
|
|||||||
""" FreqTrade bot """
|
""" FreqTrade bot """
|
||||||
__version__ = '0.14.3'
|
__version__ = '0.14.3'
|
||||||
|
|
||||||
from . import main
|
|
||||||
|
class DependencyException(BaseException):
|
||||||
|
"""
|
||||||
|
Indicates that a assumed dependency is not met.
|
||||||
|
This could happen when there is currently not enough money on the account.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class OperationalException(BaseException):
|
||||||
|
"""
|
||||||
|
Requires manual intervention.
|
||||||
|
This happens when an exchange returns an unexpected error during runtime.
|
||||||
|
"""
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||||
"""
|
"""
|
||||||
from enum import Enum
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
@ -61,6 +61,10 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
hilbert = ta.HT_SINE(dataframe)
|
hilbert = ta.HT_SINE(dataframe)
|
||||||
dataframe['htsine'] = hilbert['sine']
|
dataframe['htsine'] = hilbert['sine']
|
||||||
dataframe['htleadsine'] = hilbert['leadsine']
|
dataframe['htleadsine'] = hilbert['leadsine']
|
||||||
|
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||||
|
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||||
|
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||||
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
@ -71,14 +75,21 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(dataframe['tema'] <= dataframe['blower']) &
|
(
|
||||||
(dataframe['rsi'] < 37) &
|
(dataframe['rsi'] < 35) &
|
||||||
(dataframe['fastd'] < 48) &
|
(dataframe['fastd'] < 35) &
|
||||||
(dataframe['adx'] > 31),
|
(dataframe['adx'] > 30) &
|
||||||
|
(dataframe['plus_di'] > 0.5)
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 65) &
|
||||||
|
(dataframe['plus_di'] > 0.5)
|
||||||
|
),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the sell signal for the given dataframe
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
@ -86,9 +97,19 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
|||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(crossed_above(dataframe['rsi'], 70)),
|
(
|
||||||
|
(
|
||||||
|
(crossed_above(dataframe['rsi'], 70)) |
|
||||||
|
(crossed_above(dataframe['fastd'], 70))
|
||||||
|
) &
|
||||||
|
(dataframe['adx'] > 10) &
|
||||||
|
(dataframe['minus_di'] > 0)
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 70) &
|
||||||
|
(dataframe['minus_di'] > 0.5)
|
||||||
|
),
|
||||||
'sell'] = 1
|
'sell'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
@ -107,9 +128,6 @@ def analyze_ticker(pair: str) -> DataFrame:
|
|||||||
dataframe = populate_indicators(dataframe)
|
dataframe = populate_indicators(dataframe)
|
||||||
dataframe = populate_buy_trend(dataframe)
|
dataframe = populate_buy_trend(dataframe)
|
||||||
dataframe = populate_sell_trend(dataframe)
|
dataframe = populate_sell_trend(dataframe)
|
||||||
# TODO: buy_price and sell_price are only used by the plotter, should probably be moved there
|
|
||||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
|
||||||
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
@ -119,7 +137,12 @@ def get_signal(pair: str, signal: SignalType) -> bool:
|
|||||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||||
:return: True if pair is good for buying, False otherwise
|
:return: True if pair is good for buying, False otherwise
|
||||||
"""
|
"""
|
||||||
dataframe = analyze_ticker(pair)
|
try:
|
||||||
|
dataframe = analyze_ticker(pair)
|
||||||
|
except ValueError as ex:
|
||||||
|
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
|
||||||
|
return False
|
||||||
|
|
||||||
if dataframe.empty:
|
if dataframe.empty:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import arrow
|
|||||||
import requests
|
import requests
|
||||||
from cachetools import cached, TTLCache
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
from freqtrade.exchange.interface import Exchange
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ def init(config: dict) -> None:
|
|||||||
try:
|
try:
|
||||||
exchange_class = Exchanges[name.upper()].value
|
exchange_class = Exchanges[name.upper()].value
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise RuntimeError('Exchange {} is not supported'.format(name))
|
raise OperationalException('Exchange {} is not supported'.format(name))
|
||||||
|
|
||||||
_API = exchange_class(exchange_config)
|
_API = exchange_class(exchange_config)
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ def init(config: dict) -> None:
|
|||||||
def validate_pairs(pairs: List[str]) -> None:
|
def validate_pairs(pairs: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if all given pairs are tradable on the current exchange.
|
Checks if all given pairs are tradable on the current exchange.
|
||||||
Raises RuntimeError if one pair is not available.
|
Raises OperationalException if one pair is not available.
|
||||||
:param pairs: list of pairs
|
:param pairs: list of pairs
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
@ -75,11 +76,12 @@ def validate_pairs(pairs: List[str]) -> None:
|
|||||||
stake_cur = _CONF['stake_currency']
|
stake_cur = _CONF['stake_currency']
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
if not pair.startswith(stake_cur):
|
if not pair.startswith(stake_cur):
|
||||||
raise RuntimeError(
|
raise OperationalException(
|
||||||
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
|
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
|
||||||
)
|
)
|
||||||
if pair not in markets:
|
if pair not in markets:
|
||||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
raise OperationalException(
|
||||||
|
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
||||||
|
|
||||||
|
|
||||||
def buy(pair: str, rate: float, amount: float) -> str:
|
def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
|
@ -4,6 +4,7 @@ from typing import List, Dict
|
|||||||
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
|
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
|
||||||
from requests.exceptions import ContentDecodingError
|
from requests.exceptions import ContentDecodingError
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
from freqtrade.exchange.interface import Exchange
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -46,7 +47,7 @@ class Bittrex(Exchange):
|
|||||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
|
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair,
|
pair=pair,
|
||||||
rate=rate,
|
rate=rate,
|
||||||
@ -56,7 +57,7 @@ class Bittrex(Exchange):
|
|||||||
def sell(self, pair: str, rate: float, amount: float) -> str:
|
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||||
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
|
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair,
|
pair=pair,
|
||||||
rate=rate,
|
rate=rate,
|
||||||
@ -66,7 +67,7 @@ class Bittrex(Exchange):
|
|||||||
def get_balance(self, currency: str) -> float:
|
def get_balance(self, currency: str) -> float:
|
||||||
data = _API.get_balance(currency)
|
data = _API.get_balance(currency)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({currency})'.format(
|
raise OperationalException('{message} params=({currency})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
currency=currency))
|
currency=currency))
|
||||||
return float(data['result']['Balance'] or 0.0)
|
return float(data['result']['Balance'] or 0.0)
|
||||||
@ -74,13 +75,13 @@ class Bittrex(Exchange):
|
|||||||
def get_balances(self):
|
def get_balances(self):
|
||||||
data = _API.get_balances()
|
data = _API.get_balances()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message}'.format(message=data['message']))
|
raise OperationalException('{message}'.format(message=data['message']))
|
||||||
return data['result']
|
return data['result']
|
||||||
|
|
||||||
def get_ticker(self, pair: str) -> dict:
|
def get_ticker(self, pair: str) -> dict:
|
||||||
data = _API.get_ticker(pair.replace('_', '-'))
|
data = _API.get_ticker(pair.replace('_', '-'))
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({pair})'.format(
|
raise OperationalException('{message} params=({pair})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair))
|
pair=pair))
|
||||||
|
|
||||||
@ -121,7 +122,7 @@ class Bittrex(Exchange):
|
|||||||
pair=pair))
|
pair=pair))
|
||||||
|
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({pair})'.format(
|
raise OperationalException('{message} params=({pair})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair))
|
pair=pair))
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ class Bittrex(Exchange):
|
|||||||
def get_order(self, order_id: str) -> Dict:
|
def get_order(self, order_id: str) -> Dict:
|
||||||
data = _API.get_order(order_id)
|
data = _API.get_order(order_id)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({order_id})'.format(
|
raise OperationalException('{message} params=({order_id})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
order_id=order_id))
|
order_id=order_id))
|
||||||
data = data['result']
|
data = data['result']
|
||||||
@ -148,7 +149,7 @@ class Bittrex(Exchange):
|
|||||||
def cancel_order(self, order_id: str) -> None:
|
def cancel_order(self, order_id: str) -> None:
|
||||||
data = _API.cancel(order_id)
|
data = _API.cancel(order_id)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({order_id})'.format(
|
raise OperationalException('{message} params=({order_id})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
order_id=order_id))
|
order_id=order_id))
|
||||||
|
|
||||||
@ -158,19 +159,19 @@ class Bittrex(Exchange):
|
|||||||
def get_markets(self) -> List[str]:
|
def get_markets(self) -> List[str]:
|
||||||
data = _API.get_markets()
|
data = _API.get_markets()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message}'.format(message=data['message']))
|
raise OperationalException('{message}'.format(message=data['message']))
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||||
|
|
||||||
def get_market_summaries(self) -> List[Dict]:
|
def get_market_summaries(self) -> List[Dict]:
|
||||||
data = _API.get_market_summaries()
|
data = _API.get_market_summaries()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message}'.format(message=data['message']))
|
raise OperationalException('{message}'.format(message=data['message']))
|
||||||
return data['result']
|
return data['result']
|
||||||
|
|
||||||
def get_wallet_health(self) -> List[Dict]:
|
def get_wallet_health(self) -> List[Dict]:
|
||||||
data = _API_V2.get_wallet_health()
|
data = _API_V2.get_wallet_health()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message}'.format(message=data['message']))
|
raise OperationalException('{message}'.format(message=data['message']))
|
||||||
return [{
|
return [{
|
||||||
'Currency': entry['Health']['Currency'],
|
'Currency': entry['Health']['Currency'],
|
||||||
'IsActive': entry['Health']['IsActive'],
|
'IsActive': entry['Health']['IsActive'],
|
||||||
|
@ -6,16 +6,16 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
|
||||||
from typing import Dict, Optional, List
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from cachetools import cached, TTLCache
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
from freqtrade import __version__, exchange, persistence, rpc
|
from freqtrade import __version__, exchange, persistence, rpc, DependencyException, \
|
||||||
|
OperationalException
|
||||||
from freqtrade.analyze import get_signal, SignalType
|
from freqtrade.analyze import get_signal, SignalType
|
||||||
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
|
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
|
||||||
load_config, FreqtradeException
|
load_config
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
@ -67,16 +67,13 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
|||||||
if len(trades) < _CONF['max_open_trades']:
|
if len(trades) < _CONF['max_open_trades']:
|
||||||
try:
|
try:
|
||||||
# Create entity and execute trade
|
# Create entity and execute trade
|
||||||
trade = create_trade(float(_CONF['stake_amount']))
|
state_changed = create_trade(float(_CONF['stake_amount']))
|
||||||
if trade:
|
if not state_changed:
|
||||||
Trade.session.add(trade)
|
|
||||||
state_changed = True
|
|
||||||
else:
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Checked all whitelisted currencies. '
|
'Checked all whitelisted currencies. '
|
||||||
'Found no suitable entry positions for buying. Will keep looking ...'
|
'Found no suitable entry positions for buying. Will keep looking ...'
|
||||||
)
|
)
|
||||||
except FreqtradeException as e:
|
except DependencyException as e:
|
||||||
logger.warning('Unable to create trade: %s', e)
|
logger.warning('Unable to create trade: %s', e)
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -97,12 +94,12 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
|||||||
error
|
error
|
||||||
)
|
)
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
except RuntimeError:
|
except OperationalException:
|
||||||
rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
|
rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format(
|
||||||
traceback=traceback.format_exc(),
|
traceback=traceback.format_exc(),
|
||||||
hint='Issue `/start` if you think it is safe to restart.'
|
hint='Issue `/start` if you think it is safe to restart.'
|
||||||
))
|
))
|
||||||
logger.exception('Got RuntimeError. Stopping trader ...')
|
logger.exception('Got OperationalException. Stopping trader ...')
|
||||||
update_state(State.STOPPED)
|
update_state(State.STOPPED)
|
||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
@ -126,6 +123,7 @@ def execute_sell(trade: Trade, limit: float) -> None:
|
|||||||
limit,
|
limit,
|
||||||
fmt_exp_profit
|
fmt_exp_profit
|
||||||
))
|
))
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
|
||||||
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
||||||
@ -172,11 +170,12 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
|
|||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
|
||||||
def create_trade(stake_amount: float) -> Optional[Trade]:
|
def create_trade(stake_amount: float) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
:param stake_amount: amount of btc to spend
|
:param stake_amount: amount of btc to spend
|
||||||
|
:return: True if a trade object has been created and persisted, False otherwise
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.info(
|
||||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||||
@ -185,7 +184,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||||
raise FreqtradeException(
|
raise DependencyException(
|
||||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -195,7 +194,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
whitelist.remove(trade.pair)
|
whitelist.remove(trade.pair)
|
||||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||||
if not whitelist:
|
if not whitelist:
|
||||||
raise FreqtradeException('No pair in whitelist')
|
raise DependencyException('No pair in whitelist')
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
# Pick pair based on StochRSI buy signals
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
@ -203,12 +202,11 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
pair = _pair
|
pair = _pair
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return None
|
return False
|
||||||
|
|
||||||
# Calculate amount and subtract fee
|
# Calculate amount
|
||||||
fee = exchange.get_fee()
|
|
||||||
buy_limit = get_target_bid(exchange.get_ticker(pair))
|
buy_limit = get_target_bid(exchange.get_ticker(pair))
|
||||||
amount = (1 - fee) * stake_amount / buy_limit
|
amount = stake_amount / buy_limit
|
||||||
|
|
||||||
order_id = exchange.buy(pair, buy_limit, amount)
|
order_id = exchange.buy(pair, buy_limit, amount)
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
@ -219,14 +217,19 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
buy_limit
|
buy_limit
|
||||||
))
|
))
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
return Trade(pair=pair,
|
trade = Trade(
|
||||||
stake_amount=stake_amount,
|
pair=pair,
|
||||||
amount=amount,
|
stake_amount=stake_amount,
|
||||||
fee=fee * 2,
|
amount=amount,
|
||||||
open_rate=buy_limit,
|
fee=exchange.get_fee() * 2,
|
||||||
open_date=datetime.utcnow(),
|
open_rate=buy_limit,
|
||||||
exchange=exchange.get_name().upper(),
|
open_date=datetime.utcnow(),
|
||||||
open_order_id=order_id)
|
exchange=exchange.get_name().upper(),
|
||||||
|
open_order_id=order_id
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||||
@ -248,10 +251,6 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
else:
|
else:
|
||||||
update_state(State.STOPPED)
|
update_state(State.STOPPED)
|
||||||
|
|
||||||
# Register signal handlers
|
|
||||||
for sig in (SIGINT, SIGTERM, SIGABRT):
|
|
||||||
signal(sig, cleanup)
|
|
||||||
|
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
|
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
|
||||||
@ -270,7 +269,7 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum
|
|||||||
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
|
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
|
||||||
|
|
||||||
|
|
||||||
def cleanup(*args, **kwargs) -> None:
|
def cleanup() -> None:
|
||||||
"""
|
"""
|
||||||
Cleanup the application state und finish all pending tasks
|
Cleanup the application state und finish all pending tasks
|
||||||
:return: None
|
:return: None
|
||||||
@ -283,7 +282,7 @@ def cleanup(*args, **kwargs) -> None:
|
|||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
"""
|
"""
|
||||||
Loads and validates the config and handles the main loop
|
Loads and validates the config and handles the main loop
|
||||||
:return: None
|
:return: None
|
||||||
@ -311,24 +310,32 @@ def main():
|
|||||||
# Initialize all modules and start main loop
|
# Initialize all modules and start main loop
|
||||||
if args.dynamic_whitelist:
|
if args.dynamic_whitelist:
|
||||||
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
||||||
init(_CONF)
|
|
||||||
old_state = None
|
|
||||||
while True:
|
|
||||||
new_state = get_state()
|
|
||||||
# Log state transition
|
|
||||||
if new_state != old_state:
|
|
||||||
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
|
||||||
logger.info('Changing state to: %s', new_state.name)
|
|
||||||
|
|
||||||
if new_state == State.STOPPED:
|
try:
|
||||||
time.sleep(1)
|
init(_CONF)
|
||||||
elif new_state == State.RUNNING:
|
old_state = None
|
||||||
throttle(
|
while True:
|
||||||
_process,
|
new_state = get_state()
|
||||||
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
# Log state transition
|
||||||
dynamic_whitelist=args.dynamic_whitelist,
|
if new_state != old_state:
|
||||||
)
|
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||||
old_state = new_state
|
logger.info('Changing state to: %s', new_state.name)
|
||||||
|
|
||||||
|
if new_state == State.STOPPED:
|
||||||
|
time.sleep(1)
|
||||||
|
elif new_state == State.RUNNING:
|
||||||
|
throttle(
|
||||||
|
_process,
|
||||||
|
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
||||||
|
dynamic_whitelist=args.dynamic_whitelist,
|
||||||
|
)
|
||||||
|
old_state = new_state
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info('Got SIGINT, aborting ...')
|
||||||
|
except BaseException:
|
||||||
|
logger.exception('Got fatal exception!')
|
||||||
|
finally:
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -15,10 +15,6 @@ from freqtrade import __version__
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FreqtradeException(BaseException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class State(enum.Enum):
|
class State(enum.Enum):
|
||||||
RUNNING = 0
|
RUNNING = 0
|
||||||
STOPPED = 1
|
STOPPED = 1
|
||||||
@ -150,6 +146,12 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
|||||||
type=int,
|
type=int,
|
||||||
metavar='INT',
|
metavar='INT',
|
||||||
)
|
)
|
||||||
|
backtest.add_argument(
|
||||||
|
'--limit-max-trades',
|
||||||
|
help='uses max_open_trades from config to simulate real world limitations',
|
||||||
|
action='store_true',
|
||||||
|
dest='limit_max_trades',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_backtesting(args) -> None:
|
def start_backtesting(args) -> None:
|
||||||
@ -165,6 +167,7 @@ def start_backtesting(args) -> None:
|
|||||||
'BACKTEST_LIVE': 'true' if args.live else '',
|
'BACKTEST_LIVE': 'true' if args.live else '',
|
||||||
'BACKTEST_CONFIG': args.config,
|
'BACKTEST_CONFIG': args.config,
|
||||||
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
|
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
|
||||||
|
'BACKTEST_LIMIT_MAX_TRADES': 'true' if args.limit_max_trades else '',
|
||||||
})
|
})
|
||||||
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
|
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
|
||||||
pytest.main(['-s', path])
|
pytest.main(['-s', path])
|
||||||
|
@ -2,11 +2,11 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Callable, Any
|
from typing import Callable, Any
|
||||||
from pandas import DataFrame
|
|
||||||
from tabulate import tabulate
|
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
from pandas import DataFrame
|
||||||
from sqlalchemy import and_, func, text
|
from sqlalchemy import and_, func, text
|
||||||
|
from tabulate import tabulate
|
||||||
from telegram import ParseMode, Bot, Update
|
from telegram import ParseMode, Bot, Update
|
||||||
from telegram.error import NetworkError, TelegramError
|
from telegram.error import NetworkError, TelegramError
|
||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
|
@ -23,7 +23,7 @@ def default_conf():
|
|||||||
"20": 0.02,
|
"20": 0.02,
|
||||||
"0": 0.04
|
"0": 0.04
|
||||||
},
|
},
|
||||||
"stoploss": -0.05,
|
"stoploss": -0.10,
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
@ -54,6 +54,7 @@ def default_conf():
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def backtest_conf():
|
def backtest_conf():
|
||||||
return {
|
return {
|
||||||
|
"max_open_trades": 3,
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.01,
|
"stake_amount": 0.01,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
@ -62,7 +63,7 @@ def backtest_conf():
|
|||||||
"20": 0.02,
|
"20": 0.02,
|
||||||
"0": 0.04
|
"0": 0.04
|
||||||
},
|
},
|
||||||
"stoploss": -0.05
|
"stoploss": -0.10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0621
|
# pragma pylint: disable=missing-docstring,W0621
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
@ -83,24 +83,46 @@ def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currenc
|
|||||||
return tabulate(tabular_data, headers=headers)
|
return tabulate(tabular_data, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
def backtest(backtest_conf, processed, mocker):
|
def backtest(config: Dict, processed, mocker, max_open_trades=0):
|
||||||
|
"""
|
||||||
|
Implements backtesting functionality
|
||||||
|
:param config: config to use
|
||||||
|
:param processed: a processed dictionary with format {pair, data}
|
||||||
|
:param mocker: mocker instance
|
||||||
|
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||||
|
:return: DataFrame
|
||||||
|
"""
|
||||||
trades = []
|
trades = []
|
||||||
|
trade_count_lock = {}
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
mocker.patch.dict('freqtrade.main._CONF', config)
|
||||||
for pair, pair_data in processed.items():
|
for pair, pair_data in processed.items():
|
||||||
pair_data['buy'] = 0
|
pair_data['buy'], pair_data['sell'] = 0, 0
|
||||||
pair_data['sell'] = 0
|
|
||||||
ticker = populate_sell_trend(populate_buy_trend(pair_data))
|
ticker = populate_sell_trend(populate_buy_trend(pair_data))
|
||||||
# for each buy point
|
# for each buy point
|
||||||
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
||||||
|
if max_open_trades > 0:
|
||||||
|
# Check if max_open_trades has already been reached for the given date
|
||||||
|
if not trade_count_lock.get(row.date, 0) < max_open_trades:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if max_open_trades > 0:
|
||||||
|
# Increase lock
|
||||||
|
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
open_rate=row.close,
|
open_rate=row.close,
|
||||||
open_date=row.date,
|
open_date=row.date,
|
||||||
amount=backtest_conf['stake_amount'],
|
amount=config['stake_amount'],
|
||||||
fee=exchange.get_fee() * 2
|
fee=exchange.get_fee() * 2
|
||||||
)
|
)
|
||||||
|
|
||||||
# calculate win/lose forwards from buy point
|
# calculate win/lose forwards from buy point
|
||||||
for row2 in ticker[row.Index:].itertuples(index=True):
|
for row2 in ticker[row.Index + 1:].itertuples(index=True):
|
||||||
|
if max_open_trades > 0:
|
||||||
|
# Increase trade_count_lock for every iteration
|
||||||
|
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
||||||
|
|
||||||
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
|
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
|
||||||
current_profit = trade.calc_profit(row2.close)
|
current_profit = trade.calc_profit(row2.close)
|
||||||
|
|
||||||
@ -110,6 +132,13 @@ def backtest(backtest_conf, processed, mocker):
|
|||||||
return DataFrame.from_records(trades, columns=labels)
|
return DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_open_trades(config):
|
||||||
|
if not os.environ.get('BACKTEST_LIMIT_MAX_TRADES'):
|
||||||
|
return 0
|
||||||
|
print('Using max_open_trades: {} ...'.format(config['max_open_trades']))
|
||||||
|
return config['max_open_trades']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
|
||||||
def test_backtest(backtest_conf, mocker):
|
def test_backtest(backtest_conf, mocker):
|
||||||
print('')
|
print('')
|
||||||
@ -147,8 +176,6 @@ def test_backtest(backtest_conf, mocker):
|
|||||||
))
|
))
|
||||||
|
|
||||||
# Execute backtest and print results
|
# Execute backtest and print results
|
||||||
results = backtest(config, preprocess(data), mocker)
|
results = backtest(config, preprocess(data), mocker, get_max_open_trades(config))
|
||||||
print('====================== BACKTESTING REPORT ======================================\n\n'
|
print('====================== BACKTESTING REPORT ======================================\n\n')
|
||||||
'NOTE: This Report doesn\'t respect the limits of max_open_trades, \n'
|
|
||||||
' so the projected values should be taken with a grain of salt.\n')
|
|
||||||
print(generate_text_table(data, results, config['stake_currency']))
|
print(generate_text_table(data, results, config['stake_currency']))
|
||||||
|
@ -3,6 +3,7 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
from freqtrade.exchange import validate_pairs
|
from freqtrade.exchange import validate_pairs
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ def test_validate_pairs_not_available(default_conf, mocker):
|
|||||||
api_mock.get_markets = MagicMock(return_value=[])
|
api_mock.get_markets = MagicMock(return_value=[])
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
with pytest.raises(RuntimeError, match=r'not available'):
|
with pytest.raises(OperationalException, match=r'not available'):
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
|
||||||
@ -31,5 +32,5 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
|||||||
default_conf['stake_currency'] = 'ETH'
|
default_conf['stake_currency'] = 'ETH'
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
with pytest.raises(RuntimeError, match=r'not compatible'):
|
with pytest.raises(OperationalException, match=r'not compatible'):
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
@ -6,11 +6,12 @@ import pytest
|
|||||||
import requests
|
import requests
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade import DependencyException, OperationalException
|
||||||
from freqtrade.analyze import SignalType
|
from freqtrade.analyze import SignalType
|
||||||
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.main import create_trade, handle_trade, init, \
|
from freqtrade.main import create_trade, handle_trade, init, \
|
||||||
get_target_bid, _process
|
get_target_bid, _process
|
||||||
from freqtrade.misc import get_state, State, FreqtradeException
|
from freqtrade.misc import get_state, State
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
|
|||||||
assert trade.open_date is not None
|
assert trade.open_date is not None
|
||||||
assert trade.exchange == Exchanges.BITTREX.name
|
assert trade.exchange == Exchanges.BITTREX.name
|
||||||
assert trade.open_rate == 0.072661
|
assert trade.open_rate == 0.072661
|
||||||
assert trade.amount == 0.6864067381401302
|
assert trade.amount == 0.6881270557795791
|
||||||
|
|
||||||
|
|
||||||
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||||
@ -59,7 +60,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
|||||||
assert sleep_mock.has_calls()
|
assert sleep_mock.has_calls()
|
||||||
|
|
||||||
|
|
||||||
def test_process_runtime_error(default_conf, ticker, health, mocker):
|
def test_process_operational_exception(default_conf, ticker, health, mocker):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
|
||||||
@ -68,14 +69,14 @@ def test_process_runtime_error(default_conf, ticker, health, mocker):
|
|||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_wallet_health=health,
|
get_wallet_health=health,
|
||||||
buy=MagicMock(side_effect=RuntimeError))
|
buy=MagicMock(side_effect=OperationalException))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
assert get_state() == State.RUNNING
|
assert get_state() == State.RUNNING
|
||||||
|
|
||||||
result = _process()
|
result = _process()
|
||||||
assert result is False
|
assert result is False
|
||||||
assert get_state() == State.STOPPED
|
assert get_state() == State.STOPPED
|
||||||
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0]
|
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
|
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
|
||||||
@ -114,9 +115,9 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
|||||||
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
trade = create_trade(15.0)
|
create_trade(15.0)
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
trade = Trade.query.first()
|
||||||
assert trade is not None
|
assert trade is not None
|
||||||
assert trade.stake_amount == 15.0
|
assert trade.stake_amount == 15.0
|
||||||
assert trade.is_open
|
assert trade.is_open
|
||||||
@ -132,6 +133,21 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
|||||||
assert whitelist == default_conf['exchange']['pair_whitelist']
|
assert whitelist == default_conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
buy_mock = mocker.patch('freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy'))
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker)
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
min_stake_amount = 0.0005
|
||||||
|
create_trade(min_stake_amount)
|
||||||
|
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
||||||
|
assert rate * amount >= min_stake_amount
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
@ -141,7 +157,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
||||||
with pytest.raises(FreqtradeException, match=r'.*stake amount.*'):
|
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||||
create_trade(default_conf['stake_amount'])
|
create_trade(default_conf['stake_amount'])
|
||||||
|
|
||||||
|
|
||||||
@ -154,7 +170,7 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
|
|
||||||
with pytest.raises(FreqtradeException, match=r'.*No pair in whitelist.*'):
|
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
|
||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'] = []
|
conf['exchange']['pair_whitelist'] = []
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
@ -175,13 +191,14 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
|||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||||
sell=MagicMock(return_value='mocked_limit_sell'))
|
sell=MagicMock(return_value='mocked_limit_sell'))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
trade = create_trade(15.0)
|
create_trade(15.0)
|
||||||
trade.update(limit_buy_order)
|
|
||||||
Trade.session.add(trade)
|
trade = Trade.query.first()
|
||||||
Trade.session.flush()
|
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
assert trade.is_open is True
|
||||||
|
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
assert trade.open_order_id == 'mocked_limit_sell'
|
assert trade.open_order_id == 'mocked_limit_sell'
|
||||||
|
|
||||||
@ -204,15 +221,14 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
|
|||||||
|
|
||||||
# Create trade and sell it
|
# Create trade and sell it
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
trade = create_trade(15.0)
|
create_trade(15.0)
|
||||||
Trade.session.add(trade)
|
|
||||||
trade.update(limit_buy_order)
|
trade = Trade.query.first()
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
|
trade.update(limit_buy_order)
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(False)).first()
|
assert trade.is_open is False
|
||||||
assert trade
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
|
@ -109,6 +109,7 @@ def test_start_backtesting(mocker):
|
|||||||
live=True,
|
live=True,
|
||||||
loglevel=20,
|
loglevel=20,
|
||||||
ticker_interval=1,
|
ticker_interval=1,
|
||||||
|
limit_max_trades=True,
|
||||||
)
|
)
|
||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
assert env_mock == {
|
assert env_mock == {
|
||||||
@ -116,6 +117,7 @@ def test_start_backtesting(mocker):
|
|||||||
'BACKTEST_LIVE': 'true',
|
'BACKTEST_LIVE': 'true',
|
||||||
'BACKTEST_CONFIG': 'config.json',
|
'BACKTEST_CONFIG': 'config.json',
|
||||||
'BACKTEST_TICKER_INTERVAL': '1',
|
'BACKTEST_TICKER_INTERVAL': '1',
|
||||||
|
'BACKTEST_LIMIT_MAX_TRADES': 'true',
|
||||||
}
|
}
|
||||||
assert pytest_mock.call_count == 1
|
assert pytest_mock.call_count == 1
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.rpc import init, cleanup, send_msg
|
from freqtrade.rpc import init, cleanup, send_msg
|
||||||
|
|
||||||
|
@ -101,11 +101,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0)
|
create_trade(15.0)
|
||||||
assert trade
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
# Trigger status while we have a fulfilled order for the open trade
|
# Trigger status while we have a fulfilled order for the open trade
|
||||||
_status(bot=MagicMock(), update=update)
|
_status(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
@ -141,10 +137,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0)
|
create_trade(15.0)
|
||||||
assert trade
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
_status_table(bot=MagicMock(), update=update)
|
_status_table(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
@ -177,8 +170,8 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0)
|
create_trade(15.0)
|
||||||
assert trade
|
trade = Trade.query.first()
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
@ -193,8 +186,6 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
_profit(bot=MagicMock(), update=update)
|
_profit(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
@ -216,11 +207,10 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
|||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0)
|
create_trade(15.0)
|
||||||
assert trade
|
|
||||||
|
|
||||||
Trade.session.add(trade)
|
trade = Trade.query.first()
|
||||||
Trade.session.flush()
|
assert trade
|
||||||
|
|
||||||
update.message.text = '/forcesell 1'
|
update.message.text = '/forcesell 1'
|
||||||
_forcesell(bot=MagicMock(), update=update)
|
_forcesell(bot=MagicMock(), update=update)
|
||||||
@ -245,8 +235,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
|||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
Trade.session.add(create_trade(15.0))
|
create_trade(15.0)
|
||||||
Trade.session.flush()
|
|
||||||
rpc_mock.reset_mock()
|
rpc_mock.reset_mock()
|
||||||
|
|
||||||
update.message.text = '/forcesell all'
|
update.message.text = '/forcesell all'
|
||||||
@ -309,7 +298,8 @@ def test_performance_handle(
|
|||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0)
|
create_trade(15.0)
|
||||||
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
@ -320,8 +310,6 @@ def test_performance_handle(
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
_performance(bot=MagicMock(), update=update)
|
_performance(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
@ -351,9 +339,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
|||||||
update_state(State.RUNNING)
|
update_state(State.RUNNING)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
Trade.session.add(create_trade(15.0))
|
create_trade(15.0)
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
_count(bot=MagicMock(), update=update)
|
_count(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
|
8
freqtrade/vendor/qtpylib/indicators.py
vendored
8
freqtrade/vendor/qtpylib/indicators.py
vendored
@ -19,12 +19,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import warnings
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from pandas.core.base import PandasObject
|
from pandas.core.base import PandasObject
|
||||||
|
|
||||||
# =============================================
|
# =============================================
|
||||||
|
@ -18,6 +18,9 @@ def plot_analyzed_dataframe(pair: str) -> None:
|
|||||||
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
||||||
dataframe = analyze.analyze_ticker(pair)
|
dataframe = analyze.analyze_ticker(pair)
|
||||||
|
|
||||||
|
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||||
|
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
||||||
|
|
||||||
# Two subplots sharing x axis
|
# Two subplots sharing x axis
|
||||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
||||||
|
Loading…
Reference in New Issue
Block a user