diff --git a/core/forms.py b/core/forms.py index acb911f..c36c8d9 100644 --- a/core/forms.py +++ b/core/forms.py @@ -122,6 +122,7 @@ class StrategyForm(RestrictedFormMixin, ModelForm): "time_in_force", "entry_signals", "exit_signals", + "trend_signals", "enabled", "take_profit_percent", "stop_loss_percent", @@ -138,8 +139,9 @@ class StrategyForm(RestrictedFormMixin, ModelForm): "trading_times": "When the strategy will place new trades.", "order_type": "Market: Buy/Sell at the current market price. Limit: Buy/Sell at a specified price. Limits protect you more against market slippage.", "time_in_force": "The time in force controls how the order is executed.", - "entry_signals": "The entry signals to attach to this strategy. Callbacks received to these signals will trigger a trade.", - "exit_signals": "The exit signals to attach to this strategy. Callbacks received to these signals will close all trades for the symbol on the account.", + "entry_signals": "Callbacks received to these signals will trigger a trade.", + "exit_signals": "Callbacks received to these signals will close all trades for the symbol on the account.", + "trend_signals": "Callbacks received to these signals will limit the trading direction of the given symbol to the callback direction until further notice.", "enabled": "Whether the strategy is enabled.", "take_profit_percent": "The take profit will be set at this percentage above/below the entry price.", "stop_loss_percent": "The stop loss will be set at this percentage above/below the entry price.", @@ -161,6 +163,12 @@ class StrategyForm(RestrictedFormMixin, ModelForm): help_text=Meta.help_texts["exit_signals"], required=False, ) + trend_signals = forms.ModelMultipleChoiceField( + queryset=Signal.objects.all(), + widget=forms.CheckboxSelectMultiple, + help_text=Meta.help_texts["trend_signals"], + required=False, + ) trading_times = forms.ModelMultipleChoiceField( queryset=TradingTime.objects.all(), widget=forms.CheckboxSelectMultiple ) diff --git a/core/lib/market.py b/core/lib/market.py index 3800f6f..3f4eab5 100644 --- a/core/lib/market.py +++ b/core/lib/market.py @@ -296,7 +296,7 @@ def execute_strategy(callback, strategy, func): :param strategy: Strategy object """ - # Only check times for entries. We can always exit trades. + # Only check times for entries. We can always exit trades and set trends. if func == "entry": # Check if we can trade now! now_utc = datetime.utcnow() @@ -380,6 +380,31 @@ def execute_strategy(callback, strategy, func): response = account.client.close_position(side, symbol) log.debug(f"Close position response: {response}") return + + # Set the trend + elif func == "trend": + if strategy.trends is None: + strategy.trends = {} + strategy.trends[symbol] = direction + strategy.save() + log.debug(f"Set trend for {symbol}: {direction}") + return + + # Check if we are trading against the trend + if strategy.trend_signals is not None: + if strategy.trends is None: + log.debug("Refusing to trade with no trend signals received") + return + if symbol not in strategy.trends: + log.debug("Refusing to trade asset without established trend") + return + else: + if strategy.trends[symbol] != direction: + log.debug("Refusing to trade against the trend") + return + else: + log.debug(f"Trend check passed for {symbol} - {direction}") + type = strategy.order_type # Get the account's balance in the native account currency @@ -443,6 +468,17 @@ def execute_strategy(callback, strategy, func): def process_callback(callback): log.info(f"Received callback for {callback.hook} - {callback.signal}") + # Scan for trend + log.debug("Scanning for trend strategies...") + strategies = Strategy.objects.filter(trend_signals=callback.signal, enabled=True) + log.debug(f"Matched strategies: {strategies}") + for strategy in strategies: + log.debug(f"Executing strategy {strategy}") + if callback.hook.user != strategy.user: + log.error("Ownership differs between callback and strategy.") + continue + execute_strategy(callback, strategy, func="trend") + # Scan for entry log.debug("Scanning for entry strategies...") strategies = Strategy.objects.filter(entry_signals=callback.signal, enabled=True) diff --git a/core/migrations/0043_strategy_trend_signals.py b/core/migrations/0043_strategy_trend_signals.py new file mode 100644 index 0000000..045f6bf --- /dev/null +++ b/core/migrations/0043_strategy_trend_signals.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2022-12-06 19:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0042_trade_information'), + ] + + operations = [ + migrations.AddField( + model_name='strategy', + name='trend_signals', + field=models.ManyToManyField(blank=True, related_name='trend_strategies', to='core.signal'), + ), + ] diff --git a/core/migrations/0044_strategy_trends.py b/core/migrations/0044_strategy_trends.py new file mode 100644 index 0000000..c037287 --- /dev/null +++ b/core/migrations/0044_strategy_trends.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2022-12-06 19:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0043_strategy_trend_signals'), + ] + + operations = [ + migrations.AddField( + model_name='strategy', + name='trends', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index 0aa0d97..3dc3cc6 100644 --- a/core/models.py +++ b/core/models.py @@ -334,6 +334,9 @@ class Strategy(models.Model): exit_signals = models.ManyToManyField( Signal, related_name="exit_strategies", blank=True ) + trend_signals = models.ManyToManyField( + Signal, related_name="trend_strategies", blank=True + ) enabled = models.BooleanField(default=False) take_profit_percent = models.FloatField(default=1.5) stop_loss_percent = models.FloatField(default=1.0) @@ -341,6 +344,7 @@ class Strategy(models.Model): price_slippage_percent = models.FloatField(default=2.5) callback_price_deviation_percent = models.FloatField(default=0.5) trade_size_percent = models.FloatField(default=0.5) + trends = models.JSONField(null=True, blank=True) class Meta: verbose_name_plural = "strategies"