Merge pull request #1365 from mishaker/edge_position

Fix edge position sizing.
This commit is contained in:
Matthias
2018-12-06 20:02:31 +01:00
committed by GitHub
13 changed files with 197 additions and 45 deletions

View File

@@ -192,6 +192,7 @@ CONF_SCHEMA = {
"process_throttle_secs": {'type': 'integer', 'minimum': 600},
"calculate_since_number_of_days": {'type': 'integer'},
"allowed_risk": {'type': 'number'},
"capital_available_percentage": {'type': 'number'},
"stoploss_range_min": {'type': 'number'},
"stoploss_range_max": {'type': 'number'},
"stoploss_range_step": {'type': 'number'},
@@ -200,7 +201,8 @@ CONF_SCHEMA = {
"min_trade_number": {'type': 'number'},
"max_trade_duration_minute": {'type': 'integer'},
"remove_pumps": {'type': 'boolean'}
}
},
'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage']
}
},
'anyOf': [

View File

@@ -9,6 +9,7 @@ import utils_find_1st as utf1st
from pandas import DataFrame
import freqtrade.optimize as optimize
from freqtrade import constants, OperationalException
from freqtrade.arguments import Arguments
from freqtrade.arguments import TimeRange
from freqtrade.strategy.interface import SellType
@@ -52,8 +53,17 @@ class Edge():
self.edge_config = self.config.get('edge', {})
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
self._final_pairs: list = []
self._total_capital: float = self.config['stake_amount']
# checking max_open_trades. it should be -1 as with Edge
# the number of trades is determined by position size
if self.config['max_open_trades'] != -1:
logger.critical('max_open_trades should be -1 in config !')
if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT:
raise OperationalException('Edge works only with unlimited stake amount')
self._capital_percentage: float = self.edge_config.get('capital_available_percentage')
self._allowed_risk: float = self.edge_config.get('allowed_risk')
self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14)
self._last_updated: int = 0 # Timestamp of pairs last updated time
@@ -150,11 +160,25 @@ class Edge():
return True
def stake_amount(self, pair: str) -> float:
stoploss = self._cached_pairs[pair].stoploss
allowed_capital_at_risk = round(self._total_capital * self._allowed_risk, 5)
position_size = abs(round((allowed_capital_at_risk / stoploss), 5))
return position_size
def stake_amount(self, pair: str, free_capital: float,
total_capital: float, capital_in_trade: float) -> float:
stoploss = self.stoploss(pair)
available_capital = (total_capital + capital_in_trade) * self._capital_percentage
allowed_capital_at_risk = available_capital * self._allowed_risk
max_position_size = abs(allowed_capital_at_risk / stoploss)
position_size = min(max_position_size, free_capital)
if pair in self._cached_pairs:
logger.info(
'winrate: %s, expectancy: %s, position size: %s, pair: %s,'
' capital in trade: %s, free capital: %s, total capital: %s,'
' stoploss: %s, available capital: %s.',
self._cached_pairs[pair].winrate,
self._cached_pairs[pair].expectancy,
position_size, pair,
capital_in_trade, free_capital, total_capital,
stoploss, available_capital
)
return round(position_size, 15)
def stoploss(self, pair: str) -> float:
if pair in self._cached_pairs:
@@ -168,7 +192,6 @@ class Edge():
"""
Filters out and sorts "pairs" according to Edge calculated pairs
"""
final = []
for pair, info in self._cached_pairs.items():
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
@@ -176,12 +199,14 @@ class Edge():
pair in pairs:
final.append(pair)
if final:
logger.info('Edge validated only %s', final)
else:
logger.info('Edge removed all pairs as no pair with minimum expectancy was found !')
if self._final_pairs != final:
self._final_pairs = final
if self._final_pairs:
logger.info('Edge validated only %s', self._final_pairs)
else:
logger.info('Edge removed all pairs as no pair with minimum expectancy was found !')
return final
return self._final_pairs
def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
"""
@@ -202,9 +227,11 @@ class Edge():
# 0.05% is 0.0005
# fee = 0.001
stake = self.config.get('stake_amount')
# we set stake amount to an arbitrary amount.
# as it doesn't change the calculation.
# all returned values are relative. they are percentages.
stake = 0.015
fee = self.fee
open_fee = fee / 2
close_fee = fee / 2

View File

@@ -302,7 +302,12 @@ class FreqtradeBot(object):
:return: float: Stake Amount
"""
if self.edge:
stake_amount = self.edge.stake_amount(pair)
return self.edge.stake_amount(
pair,
self.wallets.get_free(self.config['stake_currency']),
self.wallets.get_total(self.config['stake_currency']),
Trade.total_open_trades_stakes()
)
else:
stake_amount = self.config['stake_amount']

View File

@@ -14,6 +14,7 @@ from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy import func
from sqlalchemy.pool import StaticPool
from freqtrade import OperationalException
@@ -349,3 +350,14 @@ class Trade(_DECL_BASE):
)
profit_percent = (close_trade_price / open_trade_price) - 1
return float(f"{profit_percent:.8f}")
@staticmethod
def total_open_trades_stakes() -> float:
"""
Calculates total invested amount in open trades
in stake currency
"""
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\
.filter(Trade.is_open.is_(True))\
.scalar()
return total_open_stake_amount or 0

View File

@@ -10,6 +10,7 @@ import arrow
import pytest
from telegram import Chat, Message, Update
from freqtrade import constants
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
from freqtrade.exchange import Exchange
from freqtrade.edge import Edge, PairInfo
@@ -63,7 +64,6 @@ def patch_edge(mocker) -> None:
'LTC/BTC': PairInfo(-0.21, 0.66, 3.71, 0.50, 1.71, 11, 20),
}
))
mocker.patch('freqtrade.edge.Edge.stoploss', MagicMock(return_value=-0.20))
mocker.patch('freqtrade.edge.Edge.calculate', MagicMock(return_value=True))
@@ -788,10 +788,13 @@ def buy_order_fee():
@pytest.fixture(scope="function")
def edge_conf(default_conf):
default_conf['max_open_trades'] = -1
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
default_conf['edge'] = {
"enabled": True,
"process_throttle_secs": 1800,
"calculate_since_number_of_days": 14,
"capital_available_percentage": 0.5,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,

View File

@@ -123,9 +123,9 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None:
assert res.close_time == _get_frame_time_from_offset(trade.close_tick)
def test_adjust(mocker, default_conf):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy)
def test_adjust(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
return_value={
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
@@ -138,9 +138,9 @@ def test_adjust(mocker, default_conf):
assert(edge.adjust(pairs) == ['E/F', 'C/D'])
def test_stoploss(mocker, default_conf):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy)
def test_stoploss(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
return_value={
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
@@ -152,9 +152,9 @@ def test_stoploss(mocker, default_conf):
assert edge.stoploss('E/F') == -0.01
def test_nonexisting_stoploss(mocker, default_conf):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy)
def test_nonexisting_stoploss(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
return_value={
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
@@ -164,6 +164,42 @@ def test_nonexisting_stoploss(mocker, default_conf):
assert edge.stoploss('N/O') == -0.1
def test_stake_amount(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
return_value={
'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60),
}
))
free = 100
total = 100
in_trade = 25
assert edge.stake_amount('E/F', free, total, in_trade) == 31.25
free = 20
total = 100
in_trade = 25
assert edge.stake_amount('E/F', free, total, in_trade) == 20
free = 0
total = 100
in_trade = 25
assert edge.stake_amount('E/F', free, total, in_trade) == 0
def test_nonexisting_stake_amount(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
return_value={
'E/F': PairInfo(-0.11, 0.66, 3.71, 0.50, 1.71, 10, 60),
}
))
# should use strategy stoploss
assert edge.stake_amount('N/O', 1, 2, 1) == 0.15
def _validate_ohlc(buy_ohlc_sell_matrice):
for index, ohlc in enumerate(buy_ohlc_sell_matrice):
# if not high < open < low or not high < close < low
@@ -246,12 +282,12 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
return pairdata
def test_edge_process_downloaded_data(mocker, default_conf):
default_conf['datadir'] = None
freqtrade = get_patched_freqtradebot(mocker, default_conf)
def test_edge_process_downloaded_data(mocker, edge_conf):
edge_conf['datadir'] = None
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
assert edge.calculate()
assert len(edge._cached_pairs) == 2

View File

@@ -260,8 +260,8 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
patch_edge(mocker)
freqtrade = FreqtradeBot(edge_conf)
assert freqtrade._get_trade_stake_amount('NEO/BTC') == (0.001 * 0.01) / 0.20
assert freqtrade._get_trade_stake_amount('LTC/BTC') == (0.001 * 0.01) / 0.20
assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20
assert freqtrade._get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21
def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, edge_conf) -> None:
@@ -342,6 +342,39 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets,
assert freqtrade.handle_trade(trade) is False
def test_total_open_trades_stakes(mocker, default_conf, ticker,
limit_buy_order, fee, markets) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
default_conf['stake_amount'] = 0.0000098751
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
assert trade is not None
assert trade.stake_amount == 0.0000098751
assert trade.is_open
assert trade.open_date is not None
freqtrade.create_trade()
trade = Trade.query.order_by(Trade.id.desc()).first()
assert trade is not None
assert trade.stake_amount == 0.0000098751
assert trade.is_open
assert trade.open_date is not None
assert Trade.total_open_trades_stakes() == 1.97502e-05
def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)

View File

@@ -58,6 +58,8 @@ def test_sync_wallet_at_boot(mocker, default_conf):
assert freqtrade.wallets.wallets['GAS'].used == 0.1
assert freqtrade.wallets.wallets['GAS'].total == 0.260439
assert freqtrade.wallets.get_free('GAS') == 0.270739
assert freqtrade.wallets.get_used('GAS') == 0.1
assert freqtrade.wallets.get_total('GAS') == 0.260439
def test_sync_wallet_missing_data(mocker, default_conf):

View File

@@ -40,6 +40,28 @@ class Wallets(object):
else:
return 0
def get_used(self, currency) -> float:
if self.exchange._conf['dry_run']:
return 999.9
balance = self.wallets.get(currency)
if balance and balance.used:
return balance.used
else:
return 0
def get_total(self, currency) -> float:
if self.exchange._conf['dry_run']:
return 999.9
balance = self.wallets.get(currency)
if balance and balance.total:
return balance.total
else:
return 0
def update(self) -> None:
balances = self.exchange.get_balances()