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
commit aa579bafa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 197 additions and 45 deletions

View File

@ -57,7 +57,7 @@
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"total_capital_in_stake_currency": 0.5,
"capital_available_percentage": 0.5,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,

View File

@ -59,7 +59,7 @@
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"total_capital_in_stake_currency": 0.5,
"capital_available_percentage": 0.5,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,

View File

@ -68,7 +68,8 @@
"edge": {
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 2,
"calculate_since_number_of_days": 7,
"capital_available_percentage": 0.5,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,

View File

@ -82,9 +82,7 @@ Edge dictates the stake amount for each trade to the bot according to the follow
Allowed capital at risk is calculated as follows:
**allowed capital at risk** = **total capital** X **allowed risk per trade**
**total capital** is your stake amount.
**allowed capital at risk** = **capital_available_percentage** X **allowed risk per trade**
**Stoploss** is calculated as described above against historical data.
@ -92,14 +90,20 @@ Your position size then will be:
**position size** = **allowed capital at risk** / **stoploss**
Example:
Let's say your stake amount is 3 ETH, you would allow 1% of risk for each trade. thus your allowed capital at risk would be **3 x 0.01 = 0.03 ETH**. Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.03 / 0.02= 1.5ETH**.<br/>
Example:<br/>
Let's say the stake currency is ETH and you have 10 ETH on the exchange, your **capital_available_percentage** is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**. <br/>
Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5ETH**.<br/>
Bot takes a position of 2.5ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on BTC/ETH market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25ETH (call it trade 2).<br/>
Note that available capital for trading didnt change for trade 2 even if you had already trade 1. The available capital doesnt mean the free amount on your wallet.<br/>
Now you have two trades open. The Bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5ETH**. But there are already 4ETH blocked in two previous trades. So the position size for this third trade would be 1ETH.<br/>
Available capital doesnt change before a position is sold. Lets assume that trade 1 receives a sell signal and it is sold with a profit of 1ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5ETH. <br/>
So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75**.
## Configurations
Edge has following configurations:
#### enabled
If true, then Edge will run periodically<br/>
If true, then Edge will run periodically.<br/>
(default to false)
#### process_throttle_secs
@ -108,19 +112,24 @@ How often should Edge run in seconds? <br/>
#### calculate_since_number_of_days
Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy
Note that it downloads historical data so increasing this number would lead to slowing down the bot<br/>
Note that it downloads historical data so increasing this number would lead to slowing down the bot.<br/>
(default to 7)
#### capital_available_percentage
This is the percentage of the total capital on exchange in stake currency. <br/>
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.<br/>
(default to 0.5)
#### allowed_risk
Percentage of allowed risk per trade<br/>
Percentage of allowed risk per trade.<br/>
(default to 0.01 [1%])
#### stoploss_range_min
Minimum stoploss <br/>
Minimum stoploss.<br/>
(default to -0.01)
#### stoploss_range_max
Maximum stoploss <br/>
Maximum stoploss.<br/>
(default to -0.10)
#### stoploss_range_step

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