diff --git a/config.json.example b/config.json.example index bbd9648da..323ff711e 100644 --- a/config.json.example +++ b/config.json.example @@ -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, diff --git a/config_binance.json.example b/config_binance.json.example index 7773a8c39..3d11f317a 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -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, diff --git a/config_full.json.example b/config_full.json.example index 6134e9cad..e29c020ea 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -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, diff --git a/docs/edge.md b/docs/edge.md index e5575554b..829910484 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -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**.
+Example:
+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**.
+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**.
+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).
+Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet.
+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.
+Available capital doesn’t change before a position is sold. Let’s 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.
+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
+If true, then Edge will run periodically.
(default to false) #### process_throttle_secs @@ -108,19 +112,24 @@ How often should Edge run in seconds?
#### 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
+Note that it downloads historical data so increasing this number would lead to slowing down the bot.
(default to 7) +#### capital_available_percentage +This is the percentage of the total capital on exchange in stake currency.
+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.
+(default to 0.5) + #### allowed_risk -Percentage of allowed risk per trade
+Percentage of allowed risk per trade.
(default to 0.01 [1%]) #### stoploss_range_min -Minimum stoploss
+Minimum stoploss.
(default to -0.01) #### stoploss_range_max -Maximum stoploss
+Maximum stoploss.
(default to -0.10) #### stoploss_range_step diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f8fb91240..1d12d7c6e 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -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': [ diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4cb0dbc31..49acbd3e7 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -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 diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 53c012a17..92f735a6b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -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'] diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 592a88acb..71752d58e 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -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 diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 0c38019e3..df1a1cdc4 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -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, diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index 50c4ade3d..008413ff1 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -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 diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index be84829e2..fdc152f9e 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -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) diff --git a/freqtrade/tests/test_wallets.py b/freqtrade/tests/test_wallets.py index 88366a869..8d9adc74c 100644 --- a/freqtrade/tests/test_wallets.py +++ b/freqtrade/tests/test_wallets.py @@ -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): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index bf6f8b027..59d8fa3da 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -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()