Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement strategy-controlled stake sizes #5189

Merged
merged 2 commits into from
Jul 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/strategy-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,39 @@ class AwesomeStrategy(IStrategy):

```

### Stake size management

It is possible to manage your risk by reducing or increasing stake amount when placing a new trade.

```python
class AwesomeStrategy(IStrategy):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:

dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze()

if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
if self.config['stake_amount'] == 'unlimited':
# Use entire available wallet during favorable conditions when in compounding mode.
return max_stake
else:
# Compound profits during favorable conditions instead of using a static stake.
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']

# Use default stake amount.
return proposed_stake
```

Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.

!!! Tip
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged.

!!! Tip
Returning `0` or `None` will prevent trades from being placed.

---

## Derived strategies
Expand Down
27 changes: 15 additions & 12 deletions freqtrade/freqtradebot.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,16 +424,10 @@ def create_trade(self, pair: str) -> bool:

if buy and not sell:
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
if not stake_amount:
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
return False

logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ...")

bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
if self._check_depth_of_market_buy(pair, bid_check_dom):
return self.execute_buy(pair, stake_amount)
else:
Expand Down Expand Up @@ -488,13 +482,22 @@ def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = N

min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
self.strategy.stoploss)
if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning(
f"Can't open a new trade for {pair}: stake amount "
f"is too small ({stake_amount} < {min_stake_amount})"
)

if not self.edge:
xmatthias marked this conversation as resolved.
Show resolved Hide resolved
max_stake_amount = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=datetime.now(timezone.utc),
current_rate=buy_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=max_stake_amount)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)

if not stake_amount:
return False

logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ...")

amount = stake_amount / buy_limit_requested
order_type = self.strategy.order_types['buy']
if forcebuy:
Expand Down
15 changes: 14 additions & 1 deletion freqtrade/optimize/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def _set_strategy(self, strategy: IStrategy):
"""
self.strategy: IStrategy = strategy
strategy.dp = self.dataprovider
# Attach Wallets to Strategy baseclass
IStrategy.wallets = self.wallets
# Set stoploss_on_exchange to false for backtesting,
# since a "perfect" stoploss-sell is assumed anyway
# And the regular "stoploss" function would not apply to that case
Expand Down Expand Up @@ -312,7 +314,18 @@ def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
except DependencyException:
return None
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)

min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) or 0
max_stake_amount = self.wallets.get_available_stake_amount()

stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)

if not stake_amount:
return None

order_type = self.strategy.order_types['buy']
time_in_force = self.strategy.order_time_in_force['sell']
Expand Down
17 changes: 17 additions & 0 deletions freqtrade/strategy/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,23 @@ def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_r
"""
return None

def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:
xmatthias marked this conversation as resolved.
Show resolved Hide resolved
"""
Customize stake size for each new trade. This method is not called when edge module is
enabled.

:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:return: A stake size, which is between min_stake and max_stake.
"""
return proposed_stake

def informative_pairs(self) -> ListPairsWithTimeframes:
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
Expand Down
43 changes: 38 additions & 5 deletions freqtrade/wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,22 @@ def update(self, require_update: bool = True) -> None:
def get_all_balances(self) -> Dict[str, Any]:
return self._wallets

def _get_available_stake_amount(self, val_tied_up: float) -> float:
def get_total_stake_amount(self):
"""
Return the total currently available balance in stake currency, including tied up stake and
respecting tradable_balance_ratio.
Calculated as
(<open_trade stakes> + free amount) * tradable_balance_ratio
"""
# Ensure <tradable_balance_ratio>% is used from the overall balance
# Otherwise we'd risk lowering stakes with each open trade.
# (tied up + current free) * ratio) - tied up
val_tied_up = Trade.total_open_trades_stakes()
available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) *
self._config['tradable_balance_ratio'])
return available_amount

def get_available_stake_amount(self) -> float:
"""
Return the total currently available balance in stake currency,
respecting tradable_balance_ratio.
Expand All @@ -142,9 +157,7 @@ def _get_available_stake_amount(self, val_tied_up: float) -> float:
# Ensure <tradable_balance_ratio>% is used from the overall balance
# Otherwise we'd risk lowering stakes with each open trade.
# (tied up + current free) * ratio) - tied up
available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) *
self._config['tradable_balance_ratio']) - val_tied_up
return available_amount
return self.get_total_stake_amount() - Trade.total_open_trades_stakes()

def _calculate_unlimited_stake_amount(self, available_amount: float,
val_tied_up: float) -> float:
Expand Down Expand Up @@ -193,7 +206,7 @@ def get_trade_stake_amount(self, pair: str, edge=None) -> float:
# Ensure wallets are uptodate.
self.update()
val_tied_up = Trade.total_open_trades_stakes()
available_amount = self._get_available_stake_amount(val_tied_up)
available_amount = self.get_available_stake_amount()

if edge:
stake_amount = edge.stake_amount(
Expand All @@ -209,3 +222,23 @@ def get_trade_stake_amount(self, pair: str, edge=None) -> float:
available_amount, val_tied_up)

return self._check_available_stake_amount(stake_amount, available_amount)

def _validate_stake_amount(self, pair, stake_amount, min_stake_amount):
if not stake_amount:
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
return 0

max_stake_amount = self.get_available_stake_amount()
if min_stake_amount is not None and stake_amount < min_stake_amount:
stake_amount = min_stake_amount
logger.info(
f"Stake amount for pair {pair} is too small ({stake_amount} < {min_stake_amount}), "
f"adjusting to {min_stake_amount}."
)
if stake_amount > max_stake_amount:
stake_amount = max_stake_amount
logger.info(
f"Stake amount for pair {pair} is too big ({stake_amount} > {max_stake_amount}), "
f"adjusting to {max_stake_amount}."
)
return stake_amount
11 changes: 11 additions & 0 deletions tests/optimize/test_backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,17 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
trade = backtesting._enter_trade(pair, row=row)
assert trade is not None

backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5
trade = backtesting._enter_trade(pair, row=row)
assert trade
assert trade.stake_amount == 123.5

# In case of error - use proposed stake
backtesting.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
trade = backtesting._enter_trade(pair, row=row)
assert trade
assert trade.stake_amount == 495

# Stake-amount too high!
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0)

Expand Down
2 changes: 1 addition & 1 deletion tests/rpc/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->

# Test not buying
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
freqtradebot.config['stake_amount'] = 0.0000001
freqtradebot.config['stake_amount'] = 0
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot)
pair = 'TKN/BTC'
Expand Down
41 changes: 40 additions & 1 deletion tests/test_freqtradebot.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open,


def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open,
fee, mocker) -> None:
fee, mocker, caplog) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
buy_mock = MagicMock(return_value=limit_buy_order_open)
Expand All @@ -413,6 +413,27 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord

patch_get_signal(freqtrade)

assert freqtrade.create_trade('ETH/BTC')
assert log_has_re(r"Stake amount for pair .* is too small.*", caplog)


def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open,
fee, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
buy_mock = MagicMock(return_value=limit_buy_order_open)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
buy=buy_mock,
get_fee=fee,
)

freqtrade = FreqtradeBot(default_conf)
freqtrade.config['stake_amount'] = 0

patch_get_signal(freqtrade)

assert not freqtrade.create_trade('ETH/BTC')


Expand Down Expand Up @@ -842,6 +863,24 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
assert trade.open_rate == 0.5
assert trade.stake_amount == 40.495905365

# Test with custom stake
limit_buy_order['status'] = 'open'
limit_buy_order['id'] = '556'

freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0
assert freqtrade.execute_buy(pair, stake_amount)
trade = Trade.query.all()[4]
assert trade
assert trade.stake_amount == 150

# Exception case
limit_buy_order['id'] = '557'
freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
assert freqtrade.execute_buy(pair, stake_amount)
trade = Trade.query.all()[5]
assert trade
assert trade.stake_amount == 2.0

# In case of the order is rejected and not filled at all
limit_buy_order['status'] = 'rejected'
limit_buy_order['amount'] = 90.99181073
Expand Down