Optimize strategies (#23)

* Update Tradingbox + set limit price

* Change discord message
This commit is contained in:
Oda
2025-05-30 09:00:27 +02:00
committed by GitHub
parent a31dff3f22
commit faafedbdd9
9 changed files with 695 additions and 59 deletions

View File

@@ -7,23 +7,82 @@ using static Managing.Common.Enums;
namespace Managing.Domain.Shared.Helpers;
/// <summary>
/// Configuration for strategy combination logic
/// </summary>
public class StrategyComboConfig
{
/// <summary>
/// Minimum percentage of trend strategies that must agree for strong trend (default: 66%)
/// </summary>
public decimal TrendStrongAgreementThreshold { get; set; } = 0.66m;
/// <summary>
/// Minimum percentage of signal strategies that must agree (default: 50%)
/// </summary>
public decimal SignalAgreementThreshold { get; set; } = 0.5m;
/// <summary>
/// Whether to allow signal strategies to override conflicting trends (default: true)
/// This is useful for trend reversal signals
/// </summary>
public bool AllowSignalTrendOverride { get; set; } = true;
/// <summary>
/// Minimum confidence level to return a signal (default: Low)
/// </summary>
public Confidence MinimumConfidence { get; set; } = Confidence.Low;
/// <summary>
/// Minimum confidence level required from context strategies (default: Medium)
/// Context strategies evaluate market conditions - higher requirements mean more conservative trading
/// </summary>
public Confidence MinimumContextConfidence { get; set; } = Confidence.Medium;
/// <summary>
/// Default exchange to use when signals don't specify one
/// </summary>
public TradingExchanges DefaultExchange { get; set; } = TradingExchanges.Binance;
}
public static class TradingBox
{
private static readonly StrategyComboConfig _defaultConfig = new();
public static Signal GetSignal(HashSet<Candle> newCandles, HashSet<IStrategy> strategies,
HashSet<Signal> previousSignal, int? loopbackPeriod = 1)
{
return GetSignal(newCandles, strategies, previousSignal, _defaultConfig, loopbackPeriod);
}
public static Signal GetSignal(HashSet<Candle> newCandles, HashSet<IStrategy> strategies,
HashSet<Signal> previousSignal, StrategyComboConfig config, int? loopbackPeriod = 1)
{
var signalOnCandles = new HashSet<Signal>();
var limitedCandles = newCandles.ToList().TakeLast(600).ToList();
foreach (var strategy in strategies)
{
strategy.UpdateCandles(limitedCandles.ToHashSet());
var signals = strategy.Run();
if (signals == null || signals.Count == 0) continue;
if (signals == null || signals.Count == 0)
{
// For trend and context strategies, lack of signal might be meaningful
// Signal strategies are expected to be sparse, so we continue
if (strategy.SignalType == SignalType.Signal)
{
continue;
}
// For trend strategies, no signal might mean neutral trend
// For context strategies, no signal might mean no restrictions
// We'll let the ComputeSignals method handle these cases
continue;
}
// Ensure limitedCandles is ordered chronologically
var orderedCandles = limitedCandles.OrderBy(c => c.Date).ToList();
var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1;
var candleLoopback = orderedCandles.TakeLast(loopback).ToList();
@@ -49,63 +108,208 @@ public static class TradingBox
}
}
if (signalOnCandles.Count != strategies.Count)
return null;
// Remove the restrictive requirement that ALL strategies must produce signals
// Instead, let ComputeSignals handle the logic based on what we have
if (!signalOnCandles.Any())
{
return null; // No signals from any strategy
}
var data = newCandles.First();
return ComputeSignals(strategies, signalOnCandles, MiscExtensions.ParseEnum<Ticker>(data.Ticker),
data.Timeframe);
data.Timeframe, config);
}
public static Signal ComputeSignals(HashSet<IStrategy> strategies, HashSet<Signal> signalOnCandles, Ticker ticker,
Timeframe timeframe)
{
Signal signal = null;
if (strategies.Count > 1)
{
var trendSignal = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList();
var signals = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList();
var contextStrategiesCount = strategies.Count(s => s.SignalType == SignalType.Context);
var validContext = true;
return ComputeSignals(strategies, signalOnCandles, ticker, timeframe, _defaultConfig);
}
if (contextStrategiesCount > 0 &&
signalOnCandles.Count(s => s.SignalType == SignalType.Context) != contextStrategiesCount)
{
validContext = false;
}
if (signals.All(s => s.Direction == TradeDirection.Long) &&
trendSignal.All(t => t.Direction == TradeDirection.Long) && validContext)
{
signal = new Signal(
ticker,
TradeDirection.Long,
Confidence.High,
signals.Last().Candle,
signals.Last().Date,
signals.Last().Exchange,
StrategyType.Composite, SignalType.Signal);
}
else if (signals.All(s => s.Direction == TradeDirection.Short) &&
trendSignal.All(t => t.Direction == TradeDirection.Short) && validContext)
{
signal = new Signal(
ticker,
TradeDirection.Short,
Confidence.High,
signals.Last().Candle,
signals.Last().Date,
signals.Last().Exchange,
StrategyType.Composite, SignalType.Signal);
}
}
else
public static Signal ComputeSignals(HashSet<IStrategy> strategies, HashSet<Signal> signalOnCandles, Ticker ticker,
Timeframe timeframe, StrategyComboConfig config)
{
if (strategies.Count == 1)
{
// Only one strategy, we just add the single signal to the bot
signal = signalOnCandles.Single();
// Only one strategy, return the single signal
return signalOnCandles.Single();
}
return signal;
// Group signals by type for analysis
var signalStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList();
var trendStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList();
var contextStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Context).ToList();
// Context validation - evaluates market conditions based on confidence levels
if (!ValidateContextStrategies(strategies, contextStrategies, config))
{
return null; // Context strategies are blocking the trade
}
// Trend analysis - evaluate overall market direction
var trendDirection = EvaluateTrendDirection(trendStrategies, config);
// Signal analysis - evaluate entry signals
var signalDirection = EvaluateSignalDirection(signalStrategies, config);
// Determine final direction and confidence
var (finalDirection, confidence) = DetermineFinalSignal(signalDirection, trendDirection, signalStrategies, trendStrategies, config);
if (finalDirection == TradeDirection.None || confidence < config.MinimumConfidence)
{
return null; // No valid signal or below minimum confidence
}
// Create composite signal
var lastSignal = signalStrategies.LastOrDefault() ?? trendStrategies.LastOrDefault() ?? contextStrategies.LastOrDefault();
return new Signal(
ticker,
finalDirection,
confidence,
lastSignal?.Candle,
lastSignal?.Date ?? DateTime.UtcNow,
lastSignal?.Exchange ?? config.DefaultExchange,
StrategyType.Composite,
SignalType.Signal);
}
/// <summary>
/// Validates context strategies based on confidence levels indicating market condition quality
/// </summary>
private static bool ValidateContextStrategies(HashSet<IStrategy> allStrategies, List<Signal> contextSignals, StrategyComboConfig config)
{
var contextStrategiesCount = allStrategies.Count(s => s.SignalType == SignalType.Context);
if (contextStrategiesCount == 0)
{
return true; // No context strategies, no restrictions
}
// Check if we have signals from all context strategies
if (contextSignals.Count != contextStrategiesCount)
{
return false; // Missing context information
}
// All context strategies must meet minimum confidence requirements
// This ensures market conditions are suitable for trading
return contextSignals.All(s => s.Confidence >= config.MinimumContextConfidence);
}
/// <summary>
/// Evaluates trend direction using majority voting with neutral handling
/// </summary>
private static TradeDirection EvaluateTrendDirection(List<Signal> trendSignals, StrategyComboConfig config)
{
if (!trendSignals.Any())
{
return TradeDirection.None; // No trend information available
}
var longCount = trendSignals.Count(s => s.Direction == TradeDirection.Long);
var shortCount = trendSignals.Count(s => s.Direction == TradeDirection.Short);
var neutralCount = trendSignals.Count(s => s.Direction == TradeDirection.None);
// Strong trend agreement using configurable threshold
var totalTrend = trendSignals.Count;
if (longCount > totalTrend * config.TrendStrongAgreementThreshold)
return TradeDirection.Long;
if (shortCount > totalTrend * config.TrendStrongAgreementThreshold)
return TradeDirection.Short;
// Moderate trend agreement (> 50% but <= strong threshold)
if (longCount > shortCount && longCount > neutralCount)
return TradeDirection.Long;
if (shortCount > longCount && shortCount > neutralCount)
return TradeDirection.Short;
// No clear trend or too many neutrals
return TradeDirection.None;
}
/// <summary>
/// Evaluates signal direction using weighted majority voting
/// </summary>
private static TradeDirection EvaluateSignalDirection(List<Signal> signalStrategies, StrategyComboConfig config)
{
if (!signalStrategies.Any())
{
return TradeDirection.None; // No signal strategies
}
// For signal strategies, we need stronger agreement since they're rare and should be precise
var longCount = signalStrategies.Count(s => s.Direction == TradeDirection.Long);
var shortCount = signalStrategies.Count(s => s.Direction == TradeDirection.Short);
// Use configurable agreement threshold for signals
var totalSignals = signalStrategies.Count;
if (longCount > totalSignals * config.SignalAgreementThreshold)
return TradeDirection.Long;
if (shortCount > totalSignals * config.SignalAgreementThreshold)
return TradeDirection.Short;
return TradeDirection.None;
}
/// <summary>
/// Determines final signal direction and confidence based on signal and trend analysis
/// </summary>
private static (TradeDirection Direction, Confidence Confidence) DetermineFinalSignal(
TradeDirection signalDirection,
TradeDirection trendDirection,
List<Signal> signalStrategies,
List<Signal> trendStrategies,
StrategyComboConfig config)
{
// Priority 1: If we have signal strategies, they take precedence
if (signalDirection != TradeDirection.None)
{
// Signal strategies have fired - check if trend supports or conflicts
if (trendDirection == signalDirection)
{
// Perfect alignment - signal and trend agree
return (signalDirection, Confidence.High);
}
else if (trendDirection == TradeDirection.None)
{
// No trend information or neutral trend - medium confidence
return (signalDirection, Confidence.Medium);
}
else if (config.AllowSignalTrendOverride)
{
// Trend conflicts with signal but we allow override
// This could be a trend reversal signal
return (signalDirection, Confidence.Low);
}
else
{
// Trend conflicts and we don't allow override
return (TradeDirection.None, Confidence.None);
}
}
// Priority 2: Only trend strategies available
if (trendDirection != TradeDirection.None)
{
// Calculate confidence based on trend strength
var totalTrend = trendStrategies.Count;
var majorityDirection = trendDirection == TradeDirection.Long
? trendStrategies.Count(s => s.Direction == TradeDirection.Long)
: trendStrategies.Count(s => s.Direction == TradeDirection.Short);
var agreementPercentage = (decimal)majorityDirection / totalTrend;
if (agreementPercentage >= 0.8m)
return (trendDirection, Confidence.High);
else if (agreementPercentage >= config.TrendStrongAgreementThreshold)
return (trendDirection, Confidence.Medium);
else
return (trendDirection, Confidence.Low);
}
// No valid signal found
return (TradeDirection.None, Confidence.None);
}
public static MoneyManagement GetBestMoneyManagement(List<Candle> candles, List<Position> positions,