Repository: freqtrade/freqtrade
Pull request: Feature market direction
<!-- Thank you for sending your pull request. But first, have you included
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
-->
## Summary

Adds new user set optional field which represents market direction, as well as a telegram command allowing users to change this field.

Solve the issue: #8011 

## Quick changelog

- Added new command ` \marketdir <param>` to change the marketdir variable

## What's new?
- Added new field `market_direction `to the `IStrategy `class 
- Added a new enum `MarketDirection`
- Added new command ` \marketdir <param>` to change the marketdir variable
- Added a test to check if the functionality works as intended

The goal of this PR is to allow users to have an internal variable they can control to signify what they think the market conditions are. The default value for this if not set by the user is `MarketDirection.NONE` meaning if a user has not chosen to set up this value, it will not hold any meaning or influence anything else.



Problem statement:
New variable in Strategy class that can be changed via telegram when bot is running
Variable name :
market_direction = "long" , "short" , ("even" or "side" - one of these 2, which you think fits better as description when market doesn't have a clear direction) 
Default startup value will be set in the strategy class, with one of the above 3 parameter.

Access and change value (in running instance) via telegram through command:
/marketdir long
/marketdir short
/marketdir side
so methods that will use market_direction or self.market_direction variable will get the updated (via telegram) value.

This will allow to easy change strategy behavior from phone when you don't have access to PC, when market condition are changing.

Hints from issue discussion:
while i understand the intend - i don't think that's something we'll be adding / implementing - at least not anytime soon.

The way you propose fits exactly your needs (which is understandable) - but i don't see that generalize well for most strategies.

it will be too restricted for most usecases (maybe i'd like to change this more fain-grained), or a pointless feature for most others.

If any - it'll be strategy parameters that can be modified remotely - though not via telegram, but via the UI (it's way too complex to use through telegram efficiently).
I don't agree with your statements about utility of this feature; but if you exclude telegram usage for it from the start, it makes no sense for me to debate about benefits of this feature.
I see the benefit of that feature. There are times the market direction changes suddenly and you just wish you could switch the strategy that has worked well in a certain direction. This particularly because it is kinda difficult to find a balanced strategy that works in both directions. 
You think it's beneficial - but you'll soon discover how limited it is - and find it effectively useless as it won't cover most cases.
It's a variable with 2 values - 90% of the strategies will want more.

It's also something that cannot be tested upfront - so all strategies using that will automatically fly blind (you can't rely on backtests for strategies that use this).

I've marked it as "good first issue" for now - so i'm happy to get a Pull Request for it - but i'll definitely not look at this myself - and will close this issue if we're not getting a PR (which someone also intends to finish) within a 3 months - as i'll read that as "there is no interest".
it would be an easy and safe way to make a change inside a running strategy when you don't have a PC available...the limitation depends on each skill to build around it...like all other methods and parameters
It could be argued that "market_direction" isn't a "real" thing. It's a value based on a human hunch or external information, and it should presumably be better served by TA or some other quantitative indicator. I see other problems with it, e.g. you forget that you changed it from long to short and bad trades start to happen. Making changes to something as complex as a running strategy without easy access to a PC sounds dangerous to me.
Then you should not use it. It's not mandatory. Should not affect actual strategies that people are using.
All other methods and parameter have same problem..if used wrong..you get bad trades.
Hey! 

I'm new to open source and would like to take on this issue. Is it currently open?
sure is, feel free to give it a go.

I am confirming some things to ensure I'm on the right track. I've added a new var to the `interface.py` file which keeps track of the market direction, is this the correct place to do this? Also, I have added a new enum type to signify the market direction. Currently, I have 4 values of the enum which map to "long", "short", "even", and "none". I think a "none" option should be included, as setting any of the other values as the default seems wrong as they may not apply to the user and the market state at the time.

Commit message: fixed formatting

Patch to review:
diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py
index 8ef53e12d28..69ef345e81c 100644
--- a/freqtrade/enums/__init__.py
+++ b/freqtrade/enums/__init__.py
@@ -5,6 +5,7 @@
 from freqtrade.enums.exittype import ExitType
 from freqtrade.enums.hyperoptstate import HyperoptState
 from freqtrade.enums.marginmode import MarginMode
+from freqtrade.enums.marketstatetype import MarketDirection
 from freqtrade.enums.ordertypevalue import OrderTypeValues
 from freqtrade.enums.pricetype import PriceType
 from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
diff --git a/freqtrade/enums/marketstatetype.py b/freqtrade/enums/marketstatetype.py
new file mode 100644
index 00000000000..8132be74a09
--- /dev/null
+++ b/freqtrade/enums/marketstatetype.py
@@ -0,0 +1,11 @@
+from enum import Enum
+
+
+class MarketDirection(Enum):
+    """
+    Enum for various market directions.
+    """
+    LONG = "long"
+    SHORT = "short"
+    EVEN = "even"
+    NONE = ''
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index fbd675d02db..60a5bcce668 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -25,7 +25,7 @@
 
 from freqtrade.__init__ import __version__
 from freqtrade.constants import DUST_PER_COIN, Config
-from freqtrade.enums import RPCMessageType, SignalDirection, TradingMode
+from freqtrade.enums import MarketDirection, RPCMessageType, SignalDirection, TradingMode
 from freqtrade.exceptions import OperationalException
 from freqtrade.misc import chunks, plural, round_coin_value
 from freqtrade.persistence import Trade
@@ -129,7 +129,7 @@ def _init_keyboard(self) -> None:
             r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
             r'/forcebuy$', r'/forcelong$', r'/forceshort$',
             r'/forcesell$', r'/forceexit$',
-            r'/edge$', r'/health$', r'/help$', r'/version$'
+            r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir$'
         ]
         # Create keys for generation
         valid_keys_print = [k.replace('$', '') for k in valid_keys]
@@ -197,6 +197,7 @@ def _init(self) -> None:
             CommandHandler('health', self._health),
             CommandHandler('help', self._help),
             CommandHandler('version', self._version),
+            CommandHandler('marketdir', self._changemarketdir)
         ]
         callbacks = [
             CallbackQueryHandler(self._status_table, pattern='update_status_table'),
@@ -1677,3 +1678,26 @@ def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
                 'TelegramError: %s! Giving up on that message.',
                 telegram_err.message
             )
+
+    @authorized_only
+    def _changemarketdir(self, update: Update, context: CallbackContext) -> None:
+        """
+        Handler for /marketdir.
+        Updates the bot's market_direction
+        :param bot: telegram bot
+        :param update: message update
+        :return: None
+        """
+        if context.args and len(context.args) == 1:
+            new_market_dir = context.args[0]
+            match new_market_dir:
+                case "long":
+                    self._rpc._freqtrade.strategy.market_direction = MarketDirection.LONG
+                case "short":
+                    self._rpc._freqtrade.strategy.market_direction = MarketDirection.SHORT
+                case "even":
+                    self._rpc._freqtrade.strategy.market_direction = MarketDirection.EVEN
+                case "none":
+                    self._rpc._freqtrade.strategy.market_direction = MarketDirection.NONE
+                case _:
+                    raise RPCException("Invalid market direction provided")
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index 1f687c196a7..96b2ac8cef9 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -12,8 +12,8 @@
 
 from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes
 from freqtrade.data.dataprovider import DataProvider
-from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection,
-                             SignalTagType, SignalType, TradingMode)
+from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, RunMode,
+                             SignalDirection, SignalTagType, SignalType, TradingMode)
 from freqtrade.exceptions import OperationalException, StrategyError
 from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
 from freqtrade.misc import remove_entry_exit_signals
@@ -122,6 +122,9 @@ class IStrategy(ABC, HyperStrategyMixin):
     # Definition of plot_config. See plotting documentation for more details.
     plot_config: Dict = {}
 
+    # A self set parameter that represents the market direction. filled from configuration
+    market_direction: MarketDirection = MarketDirection.NONE
+
     def __init__(self, config: Config) -> None:
         self.config = config
         # Dict to determine if analysis is necessary
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 5e3c2bd18b2..01a59873435 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -20,7 +20,8 @@
 from freqtrade import __version__
 from freqtrade.constants import CANCEL_REASON
 from freqtrade.edge import PairInfo
-from freqtrade.enums import ExitType, RPCMessageType, RunMode, SignalDirection, State
+from freqtrade.enums import (ExitType, MarketDirection, RPCMessageType, RunMode, SignalDirection,
+                             State)
 from freqtrade.exceptions import OperationalException
 from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.loggers import setup_logging
@@ -106,7 +107,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
                    "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
                    "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], "
                    "['blacklist_delete', 'bl_delete'], "
-                   "['logs'], ['edge'], ['health'], ['help'], ['version']"
+                   "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir']"
                    "]")
 
     assert log_has(message_str, caplog)
@@ -2394,3 +2395,15 @@ def init_telegram(freqtradebot):
     assert log_has("using custom keyboard from config.json: "
                    "[['/daily', '/stats', '/balance', '/profit', '/profit 5'], ['/count', "
                    "'/start', '/reload_config', '/help']]", caplog)
+
+
+def test_change_market_direction(default_conf, mocker, update) -> None:
+    telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
+    assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.NONE
+    context = MagicMock()
+    context.args = ["long"]
+    telegram._changemarketdir(update, context)
+    assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG
+    context = MagicMock()
+    context.args = ["invalid"]
+    assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG


Code review comments:
diff_hunk: @@ -129,7 +129,7 @@ def _init_keyboard(self) -> None:
             r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
             r'/forcebuy$', r'/forcelong$', r'/forceshort$',
             r'/forcesell$', r'/forceexit$',
-            r'/edge$', r'/health$', r'/help$', r'/version$'
+            r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir$'
path: freqtrade/rpc/telegram.py
text: @user1:
i think this is pointless.

it'll allow you to create a button `/marketdir` (without anything after it) - but if you call `/marketdir` without argument, it'll do nothing based on the handler, which requires 1 argument.

diff_hunk: @@ -1677,3 +1678,26 @@ def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
                 'TelegramError: %s! Giving up on that message.',
                 telegram_err.message
             )
+
+    @authorized_only
+    def _changemarketdir(self, update: Update, context: CallbackContext) -> None:
+        """
+        Handler for /marketdir.
+        Updates the bot's market_direction
+        :param bot: telegram bot
+        :param update: message update
+        :return: None
+        """
+        if context.args and len(context.args) == 1:
+            new_market_dir = context.args[0]
+            match new_market_dir:
+                case "long":
+                    self._rpc._freqtrade.strategy.market_direction = MarketDirection.LONG
path: freqtrade/rpc/telegram.py
text: @user1:
conceptually, i'd probably move the actual assignment to the rpc class (which would be mostly aligned how other methods in this regards are setup).

That'll make this (eventually) available easily for other rpc methods (namely, the API) - should we decide to add support for that there as well.
Not a hard requirement - but adding this since you asked :)

diff_hunk: @@ -1677,3 +1678,26 @@ def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
                 'TelegramError: %s! Giving up on that message.',
                 telegram_err.message
             )
+
+    @authorized_only
+    def _changemarketdir(self, update: Update, context: CallbackContext) -> None:
+        """
+        Handler for /marketdir.
+        Updates the bot's market_direction
+        :param bot: telegram bot
+        :param update: message update
+        :return: None
+        """
+        if context.args and len(context.args) == 1:
+            new_market_dir = context.args[0]
+            match new_market_dir:
path: freqtrade/rpc/telegram.py
text: @user1:
You can't use match on freqtrade for now (probably not for years).

We do support python 3.8-3.10 - match was only added in 3.10, so this will fail on all prior versions (which tests confirm as "invalid syntax" errors ...