From 338fe333a9bac02cc4ffc80eb8e0505ae7da94de Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Nov 2021 20:11:04 +0100 Subject: [PATCH 1/8] Allow forcebuy to specify order_type --- freqtrade/freqtradebot.py | 10 ++++------ freqtrade/rpc/api_server/api_schemas.py | 22 ++++++++++++++++------ freqtrade/rpc/api_server/api_v1.py | 5 +++-- freqtrade/rpc/rpc.py | 8 ++++++-- tests/rpc/test_rpc.py | 2 +- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index db0453cd7..57d5e0528 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -466,8 +466,8 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False - def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: + def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *, + order_type: Optional[str] = None, buy_tag: Optional[str] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -510,10 +510,8 @@ class FreqtradeBot(LoggingMixin): f"{stake_amount} ...") amount = stake_amount / enter_limit_requested - order_type = self.strategy.order_types['buy'] - if forcebuy: - # Forcebuy can define a different ordertype - order_type = self.strategy.order_types.get('forcebuy', order_type) + if not order_type: + order_type = self.strategy.order_types['buy'] if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 268d50fdb..ed483b18d 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,4 +1,5 @@ from datetime import date, datetime +from enum import Enum from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel @@ -131,13 +132,21 @@ class UnfilledTimeout(BaseModel): exit_timeout_count: Optional[int] +class OrderTypeValues(Enum): + limit = 'limit' + market = 'market' + + class Config: + use_enum_values = True + + class OrderTypes(BaseModel): - buy: str - sell: str - emergencysell: Optional[str] - forcesell: Optional[str] - forcebuy: Optional[str] - stoploss: str + buy: OrderTypeValues + sell: OrderTypeValues + emergencysell: Optional[OrderTypeValues] + forcesell: Optional[OrderTypeValues] + forcebuy: Optional[OrderTypeValues] + stoploss: OrderTypeValues stoploss_on_exchange: bool stoploss_on_exchange_interval: Optional[int] @@ -274,6 +283,7 @@ class Logs(BaseModel): class ForceBuyPayload(BaseModel): pair: str price: Optional[float] + ordertype: Optional[OrderTypeValues] class ForceSellPayload(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 0467e4705..6fc135820 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -29,7 +29,8 @@ logger = logging.getLogger(__name__) # API version # Pre-1.1, no version was provided # Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen. -API_VERSION = 1.1 +# 1.11: forcebuy accepts new option with ordertype +API_VERSION = 1.11 # Public API, requires no auth. router_public = APIRouter() @@ -129,7 +130,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): - trade = rpc._rpc_forcebuy(payload.pair, payload.price) + trade = rpc._rpc_forcebuy(payload.pair, payload.price, payload.ordertype) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 28585e4e8..fc1c0c777 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -692,7 +692,8 @@ class RPC: self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} - def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: + def _rpc_forcebuy(self, pair: str, price: Optional[float], + order_type: Optional[str] = None) -> Optional[Trade]: """ Handler for forcebuy Buys a pair trade at the given or current price @@ -720,7 +721,10 @@ class RPC: stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) # execute buy - if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True): + if not order_type: + order_type = self._freqtrade.strategy.order_types.get( + 'forcebuy', self._freqtrade.strategy.order_types['buy']) + if self._freqtrade.execute_entry(pair, stakeamount, price, order_type=order_type): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 2852ada81..b6fe1c691 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1093,7 +1093,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'): rpc._rpc_forcebuy(pair, 0.0001) pair = 'XRP/BTC' - trade = rpc._rpc_forcebuy(pair, 0.0001) + trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit') assert isinstance(trade, Trade) assert trade.pair == pair assert trade.open_rate == 0.0001 From 80ed5283b24096168a441f0890fabb2075c5d929 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:10:18 +0100 Subject: [PATCH 2/8] Add forcesell market/limit distinction --- freqtrade/freqtradebot.py | 18 ++++++++---------- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 6 +++--- freqtrade/rpc/rpc.py | 10 +++++++--- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 57d5e0528..a6d1b36b9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -467,7 +467,7 @@ class FreqtradeBot(LoggingMixin): return False def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *, - order_type: Optional[str] = None, buy_tag: Optional[str] = None) -> bool: + ordertype: Optional[str] = None, buy_tag: Optional[str] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -510,8 +510,7 @@ class FreqtradeBot(LoggingMixin): f"{stake_amount} ...") amount = stake_amount / enter_limit_requested - if not order_type: - order_type = self.strategy.order_types['buy'] + order_type = ordertype or self.strategy.order_types['buy'] if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, @@ -866,7 +865,7 @@ class FreqtradeBot(LoggingMixin): logger.info( f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. ' f'Tag: {exit_tag if exit_tag is not None else "None"}') - self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag) + self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag=exit_tag) return True return False @@ -1079,7 +1078,10 @@ class FreqtradeBot(LoggingMixin): trade: Trade, limit: float, sell_reason: SellCheckTuple, - exit_tag: Optional[str] = None) -> bool: + *, + exit_tag: Optional[str] = None, + ordertype: Optional[str] = None, + ) -> bool: """ Executes a trade exit for the given trade and limit :param trade: Trade instance @@ -1117,14 +1119,10 @@ class FreqtradeBot(LoggingMixin): except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - order_type = self.strategy.order_types[sell_type] + order_type = ordertype or self.strategy.order_types[sell_type] if sell_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason.sell_type == SellType.FORCE_SELL: - # Force sells (default to the sell_type defined in the strategy, - # but we allow this value to be changed) - order_type = self.strategy.order_types.get("forcesell", order_type) amount = self._safe_exit_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['sell'] diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index ed483b18d..d0e772848 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -288,6 +288,7 @@ class ForceBuyPayload(BaseModel): class ForceSellPayload(BaseModel): tradeid: str + ordertype: Optional[OrderTypeValues] class BlacklistPayload(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 6fc135820..1fd4ca74b 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) # API version # Pre-1.1, no version was provided # Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen. -# 1.11: forcebuy accepts new option with ordertype +# 1.11: forcebuy and forcesell accept ordertype API_VERSION = 1.11 # Public API, requires no auth. @@ -130,7 +130,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): - trade = rpc._rpc_forcebuy(payload.pair, payload.price, payload.ordertype) + trade = rpc._rpc_forcebuy(payload.pair, payload.price, payload.ordertype.value) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) @@ -140,7 +140,7 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_forcesell(payload.tradeid) + return rpc._rpc_forcesell(payload.tradeid, payload.ordertype.value) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fc1c0c777..c21890b7d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -640,7 +640,7 @@ class RPC: return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} - def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]: + def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]: """ Handler for forcesell . Sells the given trade at current price @@ -664,7 +664,11 @@ class RPC: current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side="sell") sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) - self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason) + order_type = ordertype or self._freqtrade.strategy.order_types.get( + "forcesell", self._freqtrade.strategy.order_types["sell"]) + + self._freqtrade.execute_trade_exit( + trade, current_rate, sell_reason, ordertype=order_type) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: @@ -724,7 +728,7 @@ class RPC: if not order_type: order_type = self._freqtrade.strategy.order_types.get( 'forcebuy', self._freqtrade.strategy.order_types['buy']) - if self._freqtrade.execute_entry(pair, stakeamount, price, order_type=order_type): + if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade From bc52b3db56b02f448ccef3d6a8220b01849fc9ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:26:14 +0100 Subject: [PATCH 3/8] Properly handle None values via API --- freqtrade/rpc/api_server/api_v1.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 1fd4ca74b..65b6941e2 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -130,7 +130,8 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): - trade = rpc._rpc_forcebuy(payload.pair, payload.price, payload.ordertype.value) + ordertype = payload.ordertype.value if payload.ordertype else None + trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) @@ -140,7 +141,8 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_forcesell(payload.tradeid, payload.ordertype.value) + ordertype = payload.ordertype.value if payload.ordertype else None + return rpc._rpc_forcesell(payload.tradeid, ordertype) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) From 6ca6f62509122d06fcbf4a9d435a8dc96a27bbad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:39:10 +0100 Subject: [PATCH 4/8] Remove duplicate code in optimize_reports --- freqtrade/optimize/optimize_reports.py | 50 +++----------------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c4002fcbe..dcd6b4e1f 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -46,20 +46,11 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]: '.2f', 'd', 's', 's'] -def _get_line_header(first_column: str, stake_currency: str) -> List[str]: +def _get_line_header(first_column: str, stake_currency: str, direction: str = 'Buys') -> List[str]: """ Generate header lines (goes in line with _generate_result_line()) """ - return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', - f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Win Draw Loss Win%'] - - -def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]: - """ - Generate header lines (goes in line with _generate_result_line()) - """ - return [first_column, 'Sells', 'Avg Profit %', 'Cum Profit %', + return [first_column, direction, 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', 'Win Draw Loss Win%'] @@ -156,7 +147,7 @@ def generate_tag_metrics(tag_type: str, if skip_nan and result['profit_abs'].isnull().all(): continue - tabular_data.append(_generate_tag_result_line(result, starting_balance, tag)) + tabular_data.append(_generate_result_line(result, starting_balance, tag)) # Sort by total profit %: tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) @@ -168,39 +159,6 @@ def generate_tag_metrics(tag_type: str, return [] -def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: - """ - Generate one result dict, with "first_column" as key. - """ - profit_sum = result['profit_ratio'].sum() - # (end-capital - starting capital) / starting capital - profit_total = result['profit_abs'].sum() / starting_balance - - return { - 'key': first_column, - 'trades': len(result), - 'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0, - 'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0, - 'profit_sum': profit_sum, - 'profit_sum_pct': round(profit_sum * 100.0, 2), - 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': profit_total, - 'profit_total_pct': round(profit_total * 100.0, 2), - 'duration_avg': str(timedelta( - minutes=round(result['trade_duration'].mean())) - ) if not result.empty else '0:00', - # 'duration_max': str(timedelta( - # minutes=round(result['trade_duration'].max())) - # ) if not result.empty else '0:00', - # 'duration_min': str(timedelta( - # minutes=round(result['trade_duration'].min())) - # ) if not result.empty else '0:00', - 'wins': len(result[result['profit_abs'] > 0]), - 'draws': len(result[result['profit_abs'] == 0]), - 'losses': len(result[result['profit_abs'] < 0]), - } - - def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results @@ -631,7 +589,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr if(tag_type == "buy_tag"): headers = _get_line_header("TAG", stake_currency) else: - headers = _get_line_header_sell("TAG", stake_currency) + headers = _get_line_header("TAG", stake_currency, 'Sells') floatfmt = _get_line_floatfmt(stake_currency) output = [ [ From a629777890a9fd2d10cf39eff32ad999964a42ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:53:05 +0100 Subject: [PATCH 5/8] Improve test coverage in telegram module --- tests/rpc/test_rpc_telegram.py | 64 +++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ce3b044be..6c32e59fc 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -24,6 +24,7 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC +from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.telegram import Telegram, authorized_only from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist) @@ -1186,8 +1187,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: assert fbuy_mock.call_count == 1 -def test_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1216,8 +1217,8 @@ def test_performance_handle(default_conf, update, ticker, fee, assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] -def test_buy_tag_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_buy_tag_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -1240,15 +1241,27 @@ def test_buy_tag_performance_handle(default_conf, update, ticker, fee, trade.close_date = datetime.utcnow() trade.is_open = False - - telegram._buy_tag_performance(update=update, context=MagicMock()) + context = MagicMock() + telegram._buy_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Buy Tag Performance' in msg_mock.call_args_list[0][0][0] assert 'TESTBUY\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + context.args = [trade.pair] + telegram._buy_tag_performance(update=update, context=context) + assert msg_mock.call_count == 2 -def test_sell_reason_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: + msg_mock.reset_mock() + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_buy_tag_performance', + side_effect=RPCException('Error')) + telegram._buy_tag_performance(update=update, context=MagicMock()) + + assert msg_mock.call_count == 1 + assert "Error" in msg_mock.call_args_list[0][0][0] + + +def test_telegram_sell_reason_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -1271,15 +1284,27 @@ def test_sell_reason_performance_handle(default_conf, update, ticker, fee, trade.close_date = datetime.utcnow() trade.is_open = False - - telegram._sell_reason_performance(update=update, context=MagicMock()) + context = MagicMock() + telegram._sell_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Sell Reason Performance' in msg_mock.call_args_list[0][0][0] assert 'TESTSELL\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + context.args = [trade.pair] + + telegram._sell_reason_performance(update=update, context=context) + assert msg_mock.call_count == 2 + + msg_mock.reset_mock() + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_sell_reason_performance', + side_effect=RPCException('Error')) + telegram._sell_reason_performance(update=update, context=MagicMock()) + + assert msg_mock.call_count == 1 + assert "Error" in msg_mock.call_args_list[0][0][0] -def test_mix_tag_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -1305,12 +1330,25 @@ def test_mix_tag_performance_handle(default_conf, update, ticker, fee, trade.close_date = datetime.utcnow() trade.is_open = False - telegram._mix_tag_performance(update=update, context=MagicMock()) + context = MagicMock() + telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] assert ('TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0]) + context.args = [trade.pair] + telegram._mix_tag_performance(update=update, context=context) + assert msg_mock.call_count == 2 + + msg_mock.reset_mock() + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_mix_tag_performance', + side_effect=RPCException('Error')) + telegram._mix_tag_performance(update=update, context=MagicMock()) + + assert msg_mock.call_count == 1 + assert "Error" in msg_mock.call_args_list[0][0][0] + def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( From 409a80176320a7d934eb5159faf35cb2a1ae9989 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 19:30:49 +0100 Subject: [PATCH 6/8] Fix caching problem in refresh_ohlcv closes #5978 --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 19ad4e4b6..5fa852eb0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1294,7 +1294,7 @@ class Exchange: cached_pairs = [] # Gather coroutines to run for pair, timeframe in set(pair_list): - if ((pair, timeframe) not in self._klines + if ((pair, timeframe) not in self._klines or not cache or self._now_is_time_to_refresh(pair, timeframe)): if not since_ms and self.required_candle_call_count > 1: # Multiple calls for one pair - to get more history diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 12b11ff3d..b642b3fa2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1667,12 +1667,21 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: assert len(res) == len(pairs) assert exchange._api_async.fetch_ohlcv.call_count == 0 + exchange.required_candle_call_count = 1 assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, " f"timeframe {pairs[0][1]} ...", caplog) res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')], cache=False) assert len(res) == 3 + assert exchange._api_async.fetch_ohlcv.call_count == 3 + + # Test the same again, should NOT return from cache! + exchange._api_async.fetch_ohlcv.reset_mock() + res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')], + cache=False) + assert len(res) == 3 + assert exchange._api_async.fetch_ohlcv.call_count == 3 @pytest.mark.asyncio From 6429205d3920c7c3a7f9c4ce85903ce11b2b4a3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 19:53:37 +0100 Subject: [PATCH 7/8] Improve Notebook documentation to include Dataprovider fix #5975 --- docs/strategy_analysis_example.md | 4 +++- freqtrade/templates/strategy_analysis_example.ipynb | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index dd7e07824..90d8d8800 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -50,7 +50,9 @@ candles.head() ```python # Load strategy using values set above from freqtrade.resolvers import StrategyResolver +from freqtrade.data.dataprovider import DataProvider strategy = StrategyResolver.load_strategy(config) +strategy.dp = DataProvider(config, None, None) # Generate buy/sell signals using strategy df = strategy.analyze_ticker(candles, {'pair': pair}) @@ -228,7 +230,7 @@ graph = generate_candlestick_graph(pair=pair, # Show graph inline # graph.show() -# Render graph in a separate window +# Render graph in a seperate window graph.show(renderer="browser") ``` diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 99720ae6e..3b937d1c5 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -79,7 +79,9 @@ "source": [ "# Load strategy using values set above\n", "from freqtrade.resolvers import StrategyResolver\n", + "from freqtrade.data.dataprovider import DataProvider\n", "strategy = StrategyResolver.load_strategy(config)\n", + "strategy.dp = DataProvider(config, None, None)\n", "\n", "# Generate buy/sell signals using strategy\n", "df = strategy.analyze_ticker(candles, {'pair': pair})\n", From cf5ff9257d432d170af14c4881fc3d098caa817d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 19:39:43 +0100 Subject: [PATCH 8/8] Add plotconfig as property documentation and sample --- docs/plotting.md | 110 +++++++++++++----- freqtrade/templates/base_strategy.py.j2 | 1 + .../subtemplates/plot_config_full.j2 | 30 ++--- 3 files changed, 97 insertions(+), 44 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 9fae38504..b2d7654f6 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -164,16 +164,17 @@ The resulting plot will have the following elements: An advanced plot configuration can be specified in the strategy in the `plot_config` parameter. -Additional features when using plot_config include: +Additional features when using `plot_config` include: * Specify colors per indicator * Specify additional subplots -* Specify indicator pairs to fill area in between +* Specify indicator pairs to fill area in between The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult. It also allows multiple subplots to display both MACD and RSI at the same time. Plot type can be configured using `type` key. Possible types are: + * `scatter` corresponding to `plotly.graph_objects.Scatter` class (default). * `bar` corresponding to `plotly.graph_objects.Bar` class. @@ -182,40 +183,89 @@ Extra parameters to `plotly.graph_objects.*` constructor can be specified in `pl Sample configuration with inline comments explaining the process: ``` python - plot_config = { - 'main_plot': { - # Configuration for main plot indicators. - # Specifies `ema10` to be red, and `ema50` to be a shade of gray - 'ema10': {'color': 'red'}, - 'ema50': {'color': '#CCCCCC'}, - # By omitting color, a random color is selected. - 'sar': {}, - # fill area between senkou_a and senkou_b - 'senkou_a': { - 'color': 'green', #optional - 'fill_to': 'senkou_b', - 'fill_label': 'Ichimoku Cloud', #optional - 'fill_color': 'rgba(255,76,46,0.2)', #optional - }, - # plot senkou_b, too. Not only the area to it. - 'senkou_b': {} +@property +def plot_config(self): + """ + There are a lot of solutions how to build the return dictionary. + The only important point is the return value. + Example: + plot_config = {'main_plot': {}, 'subplots': {}} + + """ + plot_config = {} + plot_config['main_plot'] = { + # Configuration for main plot indicators. + # Assumes 2 parameters, emashort and emalong to be specified. + f'ema_{self.emashort.value}': {'color': 'red'}, + f'ema_{self.emalong.value}': {'color': '#CCCCCC'}, + # By omitting color, a random color is selected. + 'sar': {}, + # fill area between senkou_a and senkou_b + 'senkou_a': { + 'color': 'green', #optional + 'fill_to': 'senkou_b', + 'fill_label': 'Ichimoku Cloud', #optional + 'fill_color': 'rgba(255,76,46,0.2)', #optional }, - 'subplots': { - # Create subplot MACD - "MACD": { - 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, - 'macdsignal': {'color': 'orange'}, - 'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}} - }, - # Additional subplot RSI - "RSI": { - 'rsi': {'color': 'red'} - } + # plot senkou_b, too. Not only the area to it. + 'senkou_b': {} + } + plot_config['subplots'] = { + # Create subplot MACD + "MACD": { + 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, + 'macdsignal': {'color': 'orange'}, + 'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}} + }, + # Additional subplot RSI + "RSI": { + 'rsi': {'color': 'red'} } } + return plot_config ``` +??? Note "As attribute (former method)" + Assigning plot_config is also possible as Attribute (this used to be the default way). + This has the disadvantage that strategy parameters are not available, preventing certain configurations from working. + + ``` python + plot_config = { + 'main_plot': { + # Configuration for main plot indicators. + # Specifies `ema10` to be red, and `ema50` to be a shade of gray + 'ema10': {'color': 'red'}, + 'ema50': {'color': '#CCCCCC'}, + # By omitting color, a random color is selected. + 'sar': {}, + # fill area between senkou_a and senkou_b + 'senkou_a': { + 'color': 'green', #optional + 'fill_to': 'senkou_b', + 'fill_label': 'Ichimoku Cloud', #optional + 'fill_color': 'rgba(255,76,46,0.2)', #optional + }, + # plot senkou_b, too. Not only the area to it. + 'senkou_b': {} + }, + 'subplots': { + # Create subplot MACD + "MACD": { + 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, + 'macdsignal': {'color': 'orange'}, + 'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}} + }, + # Additional subplot RSI + "RSI": { + 'rsi': {'color': 'red'} + } + } + } + + ``` + + !!! Note The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, `macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy. diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 7f5399672..035468d58 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -87,6 +87,7 @@ class {{ strategy }}(IStrategy): 'sell': 'gtc' } {{ plot_config | indent(4) }} + def informative_pairs(self): """ Define additional, informative pair/interval combinations to be cached from the exchange. diff --git a/freqtrade/templates/subtemplates/plot_config_full.j2 b/freqtrade/templates/subtemplates/plot_config_full.j2 index ab02c7892..e3f9e7ca0 100644 --- a/freqtrade/templates/subtemplates/plot_config_full.j2 +++ b/freqtrade/templates/subtemplates/plot_config_full.j2 @@ -1,18 +1,20 @@ -plot_config = { - # Main plot indicators (Moving averages, ...) - 'main_plot': { - 'tema': {}, - 'sar': {'color': 'white'}, - }, - 'subplots': { - # Subplots - each dict defines one additional plot - "MACD": { - 'macd': {'color': 'blue'}, - 'macdsignal': {'color': 'orange'}, +@property +def plot_config(self): + return { + # Main plot indicators (Moving averages, ...) + 'main_plot': { + 'tema': {}, + 'sar': {'color': 'white'}, }, - "RSI": { - 'rsi': {'color': 'red'}, + 'subplots': { + # Subplots - each dict defines one additional plot + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + "RSI": { + 'rsi': {'color': 'red'}, + } } } -}