Add synthApi (#27)
* Add synthApi * Put confidence for Synth proba * Update the code * Update readme * Fix bootstraping * fix github build * Update the endpoints for scenario * Add scenario and update backtest modal * Update bot modal * Update interfaces for synth * add synth to backtest * Add Kelly criterion and better signal * Update signal confidence * update doc * save leaderboard and prediction * Update nswag to generate ApiClient in the correct path * Unify the trading modal * Save miner and prediction * Update messaging and block new signal until position not close when flipping off * Rename strategies to indicators * Update doc * Update chart + add signal name * Fix signal direction * Update docker webui * remove crypto npm * Clean
This commit is contained in:
@@ -24,7 +24,7 @@ public class Backtest
|
||||
Signals = signals;
|
||||
Candles = candles;
|
||||
WalletBalances = new List<KeyValuePair<DateTime, decimal>>();
|
||||
StrategiesValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
|
||||
// Initialize start and end dates if candles are provided
|
||||
if (candles != null && candles.Count > 0)
|
||||
@@ -55,7 +55,7 @@ public class Backtest
|
||||
[Required] public List<KeyValuePair<DateTime, decimal>> WalletBalances { get; set; }
|
||||
[Required] public MoneyManagement OptimizedMoneyManagement { get; set; }
|
||||
[Required] public User User { get; set; }
|
||||
[Required] public Dictionary<IndicatorType, IndicatorsResultBase> StrategiesValues { get; set; }
|
||||
[Required] public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; }
|
||||
[Required] public double Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Managing.Domain.Bots;
|
||||
|
||||
public class BotBackup
|
||||
{
|
||||
public BotType BotType { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public User User { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Risk;
|
||||
using Managing.Domain.Scenarios;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
@@ -20,6 +21,13 @@ public class TradingBotConfig
|
||||
[Required] public bool FlipPosition { get; set; }
|
||||
[Required] public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk management configuration for advanced probabilistic analysis and position sizing.
|
||||
/// Contains all configurable parameters for Expected Utility Theory, Kelly Criterion, and probability thresholds.
|
||||
/// If null, default risk management settings will be used.
|
||||
/// </summary>
|
||||
public RiskManagement RiskManagement { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The scenario object containing all strategies. When provided, this takes precedence over ScenarioName.
|
||||
/// This allows running backtests without requiring scenarios to be saved in the database.
|
||||
@@ -52,4 +60,27 @@ public class TradingBotConfig
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool FlipOnlyWhenInProfit { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth API for probabilistic price forecasts and risk assessment.
|
||||
/// When true, the bot will use Synth predictions for signal filtering, position risk assessment, and position monitoring.
|
||||
/// When false, the bot operates in traditional mode without Synth predictions.
|
||||
/// The actual Synth configuration is managed centrally in SynthPredictionService.
|
||||
/// </summary>
|
||||
public bool UseSynthApi { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for position sizing adjustments and risk assessment
|
||||
/// </summary>
|
||||
public bool UseForPositionSizing { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for signal filtering
|
||||
/// </summary>
|
||||
public bool UseForSignalFiltering { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
|
||||
/// </summary>
|
||||
public bool UseForDynamicStopLoss { get; set; } = true;
|
||||
}
|
||||
192
src/Managing.Domain/Risk/RiskManagement.cs
Normal file
192
src/Managing.Domain/Risk/RiskManagement.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Managing.Common;
|
||||
|
||||
namespace Managing.Domain.Risk;
|
||||
|
||||
/// <summary>
|
||||
/// Risk management configuration for trading bots
|
||||
/// Contains all configurable risk parameters for probabilistic analysis and position sizing
|
||||
/// </summary>
|
||||
public class RiskManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Threshold for adverse probability in signal validation (default: 20%)
|
||||
/// Signals with SL probability above this threshold may be filtered out
|
||||
/// Range: 0.05 (5%) to 0.50 (50%)
|
||||
/// </summary>
|
||||
[Range(0.05, 0.50)]
|
||||
[Required]
|
||||
public decimal AdverseProbabilityThreshold { get; set; } = 0.20m;
|
||||
|
||||
/// <summary>
|
||||
/// Threshold for favorable probability in signal validation (default: 30%)
|
||||
/// Used for additional signal filtering and confidence assessment
|
||||
/// Range: 0.10 (10%) to 0.70 (70%)
|
||||
/// </summary>
|
||||
[Range(0.10, 0.70)]
|
||||
[Required]
|
||||
public decimal FavorableProbabilityThreshold { get; set; } = 0.30m;
|
||||
|
||||
/// <summary>
|
||||
/// Risk aversion parameter for Expected Utility calculations (default: 1.0)
|
||||
/// Higher values = more risk-averse behavior in utility calculations
|
||||
/// Range: 0.1 (risk-seeking) to 5.0 (highly risk-averse)
|
||||
/// </summary>
|
||||
[Range(0.1, 5.0)]
|
||||
[Required]
|
||||
public decimal RiskAversion { get; set; } = 1.0m;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum Kelly Criterion fraction to consider a trade favorable (default: 1%)
|
||||
/// Trades with Kelly fraction below this threshold are considered unfavorable
|
||||
/// Range: 0.5% to 10%
|
||||
/// </summary>
|
||||
[Range(0.005, 0.10)]
|
||||
[Required]
|
||||
public decimal KellyMinimumThreshold { get; set; } = 0.01m;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum Kelly Criterion fraction cap for practical risk management (default: 25%)
|
||||
/// Prevents over-allocation even when Kelly suggests higher percentages
|
||||
/// Range: 5% to 50%
|
||||
/// </summary>
|
||||
[Range(0.05, 0.50)]
|
||||
[Required]
|
||||
public decimal KellyMaximumCap { get; set; } = 0.25m;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum acceptable liquidation probability for position risk assessment (default: 10%)
|
||||
/// Positions with higher liquidation risk may be blocked or reduced
|
||||
/// Range: 5% to 30%
|
||||
/// </summary>
|
||||
[Range(0.05, 0.30)]
|
||||
[Required]
|
||||
public decimal MaxLiquidationProbability { get; set; } = 0.10m;
|
||||
|
||||
/// <summary>
|
||||
/// Time horizon in hours for signal validation analysis (default: 24 hours)
|
||||
/// Longer horizons provide more stable predictions but less responsive signals
|
||||
/// Range: 1 hour to 168 hours (1 week)
|
||||
/// </summary>
|
||||
[Range(1, 168)]
|
||||
[Required]
|
||||
public int SignalValidationTimeHorizonHours { get; set; } = 24;
|
||||
|
||||
/// <summary>
|
||||
/// Time horizon in hours for position risk monitoring (default: 6 hours)
|
||||
/// Shorter horizons for more frequent risk updates on open positions
|
||||
/// Range: 1 hour to 48 hours
|
||||
/// </summary>
|
||||
[Range(1, 48)]
|
||||
[Required]
|
||||
public int PositionMonitoringTimeHorizonHours { get; set; } = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Probability threshold for issuing position risk warnings (default: 20%)
|
||||
/// Positions exceeding this liquidation risk will trigger warnings
|
||||
/// Range: 10% to 40%
|
||||
/// </summary>
|
||||
[Range(0.10, 0.40)]
|
||||
[Required]
|
||||
public decimal PositionWarningThreshold { get; set; } = 0.20m;
|
||||
|
||||
/// <summary>
|
||||
/// Probability threshold for automatic position closure (default: 50%)
|
||||
/// Positions exceeding this liquidation risk will be automatically closed
|
||||
/// Range: 30% to 80%
|
||||
/// </summary>
|
||||
[Range(0.30, 0.80)]
|
||||
[Required]
|
||||
public decimal PositionAutoCloseThreshold { get; set; } = 0.50m;
|
||||
|
||||
/// <summary>
|
||||
/// Fractional Kelly multiplier for conservative position sizing (default: 1.0)
|
||||
/// Values less than 1.0 implement fractional Kelly (e.g., 0.5 = half-Kelly)
|
||||
/// Range: 0.1 to 1.0
|
||||
/// </summary>
|
||||
[Range(0.1, 1.0)]
|
||||
[Required]
|
||||
public decimal KellyFractionalMultiplier { get; set; } = 1.0m;
|
||||
|
||||
/// <summary>
|
||||
/// Risk tolerance level affecting overall risk calculations
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Expected Utility Theory for decision making
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool UseExpectedUtility { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Kelly Criterion for position sizing recommendations
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool UseKellyCriterion { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the risk management configuration is coherent
|
||||
/// </summary>
|
||||
/// <returns>True if configuration is valid, false otherwise</returns>
|
||||
public bool IsConfigurationValid()
|
||||
{
|
||||
// Ensure favorable threshold is higher than adverse threshold
|
||||
if (FavorableProbabilityThreshold <= AdverseProbabilityThreshold)
|
||||
return false;
|
||||
|
||||
// Ensure Kelly minimum is less than maximum
|
||||
if (KellyMinimumThreshold >= KellyMaximumCap)
|
||||
return false;
|
||||
|
||||
// Ensure warning threshold is less than auto-close threshold
|
||||
if (PositionWarningThreshold >= PositionAutoCloseThreshold)
|
||||
return false;
|
||||
|
||||
// Ensure signal validation horizon is longer than position monitoring
|
||||
if (SignalValidationTimeHorizonHours < PositionMonitoringTimeHorizonHours)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a preset configuration based on risk tolerance level
|
||||
/// </summary>
|
||||
/// <param name="tolerance">Risk tolerance level</param>
|
||||
/// <returns>Configured RiskManagement instance</returns>
|
||||
public static RiskManagement GetPresetConfiguration(Enums.RiskToleranceLevel tolerance)
|
||||
{
|
||||
return tolerance switch
|
||||
{
|
||||
Enums.RiskToleranceLevel.Conservative => new RiskManagement
|
||||
{
|
||||
AdverseProbabilityThreshold = 0.15m,
|
||||
FavorableProbabilityThreshold = 0.40m,
|
||||
RiskAversion = 2.0m,
|
||||
KellyMinimumThreshold = 0.02m,
|
||||
KellyMaximumCap = 0.15m,
|
||||
MaxLiquidationProbability = 0.08m,
|
||||
PositionWarningThreshold = 0.15m,
|
||||
PositionAutoCloseThreshold = 0.35m,
|
||||
KellyFractionalMultiplier = 0.5m,
|
||||
RiskTolerance = tolerance
|
||||
},
|
||||
Enums.RiskToleranceLevel.Aggressive => new RiskManagement
|
||||
{
|
||||
AdverseProbabilityThreshold = 0.30m,
|
||||
FavorableProbabilityThreshold = 0.25m,
|
||||
RiskAversion = 0.5m,
|
||||
KellyMinimumThreshold = 0.005m,
|
||||
KellyMaximumCap = 0.40m,
|
||||
MaxLiquidationProbability = 0.15m,
|
||||
PositionWarningThreshold = 0.30m,
|
||||
PositionAutoCloseThreshold = 0.70m,
|
||||
KellyFractionalMultiplier = 1.0m,
|
||||
RiskTolerance = tolerance
|
||||
},
|
||||
_ => new RiskManagement { RiskTolerance = tolerance } // Moderate (default values)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -108,15 +108,21 @@ public static class TradingBox
|
||||
}
|
||||
}
|
||||
|
||||
// Keep only the latest signal per indicator to avoid count mismatch
|
||||
var latestSignalsPerIndicator = signalOnCandles
|
||||
.GroupBy(s => s.IndicatorName)
|
||||
.Select(g => g.OrderByDescending(s => s.Date).First())
|
||||
.ToHashSet();
|
||||
|
||||
// Remove the restrictive requirement that ALL strategies must produce signals
|
||||
// Instead, let ComputeSignals handle the logic based on what we have
|
||||
if (!signalOnCandles.Any())
|
||||
if (!latestSignalsPerIndicator.Any())
|
||||
{
|
||||
return null; // No signals from any strategy
|
||||
}
|
||||
|
||||
var data = newCandles.First();
|
||||
return ComputeSignals(strategies, signalOnCandles, MiscExtensions.ParseEnum<Ticker>(data.Ticker),
|
||||
return ComputeSignals(strategies, latestSignalsPerIndicator, MiscExtensions.ParseEnum<Ticker>(data.Ticker),
|
||||
data.Timeframe, config);
|
||||
}
|
||||
|
||||
@@ -136,51 +142,88 @@ public static class TradingBox
|
||||
}
|
||||
|
||||
// Check if all strategies produced signals - this is required for composite signals
|
||||
if (signalOnCandles.Count != strategies.Count)
|
||||
var strategyNames = strategies.Select(s => s.Name).ToHashSet();
|
||||
var signalIndicatorNames = signalOnCandles.Select(s => s.IndicatorName).ToHashSet();
|
||||
|
||||
if (!strategyNames.SetEquals(signalIndicatorNames))
|
||||
{
|
||||
// Not all strategies produced signals - composite signal requires all strategies to contribute
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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();
|
||||
var signals = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList();
|
||||
var trendSignals = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList();
|
||||
var contextSignals = signalOnCandles.Where(s => s.SignalType == SignalType.Context).ToList();
|
||||
|
||||
// Context validation - evaluates market conditions based on confidence levels
|
||||
if (!ValidateContextStrategies(strategies, contextStrategies, config))
|
||||
if (!ValidateContextStrategies(strategies, contextSignals, config))
|
||||
{
|
||||
return null; // Context strategies are blocking the trade
|
||||
}
|
||||
|
||||
// Trend analysis - evaluate overall market direction
|
||||
var trendDirection = EvaluateTrendDirection(trendStrategies, config);
|
||||
// Check for 100% agreement across ALL signals (no threshold voting)
|
||||
var allDirectionalSignals = signalOnCandles
|
||||
.Where(s => s.Direction != TradeDirection.None && s.SignalType != SignalType.Context).ToList();
|
||||
|
||||
// Signal analysis - evaluate entry signals
|
||||
var signalDirection = EvaluateSignalDirection(signalStrategies, config);
|
||||
if (!allDirectionalSignals.Any())
|
||||
{
|
||||
return null; // No directional signals available
|
||||
}
|
||||
|
||||
// Determine final direction and confidence
|
||||
var (finalDirection, confidence) =
|
||||
DetermineFinalSignal(signalDirection, trendDirection, signalStrategies, trendStrategies, config);
|
||||
// Require 100% agreement - all signals must have the same direction
|
||||
var lastSignalDirection = allDirectionalSignals.Last().Direction;
|
||||
if (!allDirectionalSignals.All(s => s.Direction == lastSignalDirection))
|
||||
{
|
||||
return null; // Signals are not in complete agreement
|
||||
}
|
||||
|
||||
if (finalDirection == TradeDirection.None || confidence < config.MinimumConfidence)
|
||||
var finalDirection = lastSignalDirection;
|
||||
|
||||
// Calculate confidence based on the average confidence of all signals
|
||||
var averageConfidence = CalculateAverageConfidence(allDirectionalSignals);
|
||||
|
||||
if (finalDirection == TradeDirection.None || averageConfidence < config.MinimumConfidence)
|
||||
{
|
||||
return null; // No valid signal or below minimum confidence
|
||||
}
|
||||
|
||||
// Create composite signal
|
||||
var lastSignal = signalStrategies.LastOrDefault() ??
|
||||
trendStrategies.LastOrDefault() ?? contextStrategies.LastOrDefault();
|
||||
var lastSignal = signals.LastOrDefault() ??
|
||||
trendSignals.LastOrDefault() ?? contextSignals.LastOrDefault();
|
||||
|
||||
return new Signal(
|
||||
ticker,
|
||||
finalDirection,
|
||||
confidence,
|
||||
averageConfidence,
|
||||
lastSignal?.Candle,
|
||||
lastSignal?.Date ?? DateTime.UtcNow,
|
||||
lastSignal?.Exchange ?? config.DefaultExchange,
|
||||
IndicatorType.Composite,
|
||||
SignalType.Signal);
|
||||
SignalType.Signal, "Aggregated");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the average confidence level from a list of signals
|
||||
/// </summary>
|
||||
private static Confidence CalculateAverageConfidence(List<Signal> signals)
|
||||
{
|
||||
if (!signals.Any())
|
||||
{
|
||||
return Confidence.None;
|
||||
}
|
||||
|
||||
// Convert confidence enum to numeric values for averaging
|
||||
var confidenceValues = signals.Select(s => (int)s.Confidence).ToList();
|
||||
var averageValue = confidenceValues.Average();
|
||||
|
||||
// Round to nearest confidence level
|
||||
var roundedValue = Math.Round(averageValue);
|
||||
|
||||
// Ensure the value is within valid confidence enum range
|
||||
roundedValue = Math.Max(0, Math.Min(3, roundedValue));
|
||||
|
||||
return (Confidence)(int)roundedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -73,7 +73,7 @@ public class StDevContext : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
var test = new IndicatorsResultBase()
|
||||
{
|
||||
@@ -119,7 +119,7 @@ public class StDevContext : Indicator
|
||||
candleSignal,
|
||||
candleSignal.Date,
|
||||
candleSignal.Exchange,
|
||||
Type, SignalType);
|
||||
Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Managing.Domain.Strategies
|
||||
FixedSizeQueue<Candle> Candles { get; set; }
|
||||
|
||||
List<Signal> Run();
|
||||
IndicatorsResultBase GetStrategyValues();
|
||||
IndicatorsResultBase GetIndicatorValues();
|
||||
void UpdateCandles(HashSet<Candle> newCandles);
|
||||
string GetName();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Managing.Core.FixedSizedQueue;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Scenarios;
|
||||
@@ -19,7 +20,7 @@ namespace Managing.Domain.Strategies
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
[JsonIgnore] public FixedSizeQueue<Candle> Candles { get; set; }
|
||||
[JsonIgnore] [IgnoreDataMember] public FixedSizeQueue<Candle> Candles { get; set; }
|
||||
public IndicatorType Type { get; set; }
|
||||
public SignalType SignalType { get; set; }
|
||||
public int MinimumHistory { get; set; }
|
||||
@@ -38,7 +39,7 @@ namespace Managing.Domain.Strategies
|
||||
return new List<Signal>();
|
||||
}
|
||||
|
||||
public virtual IndicatorsResultBase GetStrategyValues()
|
||||
public virtual IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase();
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ namespace Managing.Domain.Strategies
|
||||
[Required] public IndicatorType IndicatorType { get; set; }
|
||||
[Required] public SignalType SignalType { get; set; }
|
||||
public User User { get; set; }
|
||||
[Required] public string IndicatorName { get; set; }
|
||||
|
||||
public Signal(Ticker ticker, TradeDirection direction, Confidence confidence, Candle candle, DateTime date,
|
||||
TradingExchanges exchange, IndicatorType indicatorType, SignalType signalType, User user = null)
|
||||
TradingExchanges exchange, IndicatorType indicatorType, SignalType signalType, string indicatorName,
|
||||
User user = null)
|
||||
{
|
||||
Direction = direction;
|
||||
Confidence = confidence;
|
||||
@@ -34,10 +36,11 @@ namespace Managing.Domain.Strategies
|
||||
Status = SignalStatus.WaitingForPosition;
|
||||
IndicatorType = indicatorType;
|
||||
User = user;
|
||||
IndicatorName = indicatorName;
|
||||
SignalType = signalType;
|
||||
|
||||
Identifier =
|
||||
$"{IndicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}";
|
||||
SignalType = signalType;
|
||||
$"{indicatorName}-{indicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}";
|
||||
}
|
||||
|
||||
public void SetConfidence(Confidence confidence)
|
||||
|
||||
@@ -40,7 +40,7 @@ public class ChandelierExitIndicator : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -113,7 +113,8 @@ public class ChandelierExitIndicator : Indicator
|
||||
candleSignal,
|
||||
candleSignal.Date,
|
||||
candleSignal.Exchange,
|
||||
Type, SignalType);
|
||||
Type, SignalType,
|
||||
Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -19,7 +19,7 @@ public class DualEmaCrossIndicator : EmaBaseIndicator
|
||||
MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -104,7 +104,7 @@ public class DualEmaCrossIndicator : EmaBaseIndicator
|
||||
private void AddSignal(CandleDualEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -16,7 +16,7 @@ public class EmaCrossIndicator : EmaBaseIndicator
|
||||
Period = period;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -68,7 +68,7 @@ public class EmaCrossIndicator : EmaBaseIndicator
|
||||
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -89,7 +89,7 @@ public class LaggingSTC : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
|
||||
return new IndicatorsResultBase
|
||||
@@ -130,7 +130,8 @@ public class LaggingSTC : Indicator
|
||||
candleSignal,
|
||||
candleSignal.Date,
|
||||
candleSignal.Exchange,
|
||||
Type, SignalType);
|
||||
Type, SignalType,
|
||||
Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -59,7 +59,7 @@ public class MacdCrossIndicator : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -96,7 +96,7 @@ public class MacdCrossIndicator : Indicator
|
||||
Confidence confidence)
|
||||
{
|
||||
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -49,7 +49,7 @@ public class RsiDivergenceConfirmIndicator : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -233,7 +233,7 @@ public class RsiDivergenceConfirmIndicator : Indicator
|
||||
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -52,7 +52,7 @@ public class RsiDivergenceIndicator : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -206,7 +206,7 @@ public class RsiDivergenceIndicator : Indicator
|
||||
private void AddSignal(CandleRsi candleSignal, TradeDirection direction)
|
||||
{
|
||||
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, Confidence.Low,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
|
||||
if (Signals.Count(s => s.Identifier == signal.Identifier) < 1)
|
||||
{
|
||||
|
||||
@@ -64,7 +64,7 @@ public class StcIndicator : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
if (FastPeriods != null && SlowPeriods != null)
|
||||
{
|
||||
@@ -110,7 +110,8 @@ public class StcIndicator : Indicator
|
||||
candleSignal,
|
||||
candleSignal.Date,
|
||||
candleSignal.Exchange,
|
||||
Type, SignalType);
|
||||
Type, SignalType,
|
||||
Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -157,7 +157,7 @@ public class SuperTrendCrossEma : Indicator
|
||||
return superTrends;
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -171,7 +171,7 @@ public class SuperTrendCrossEma : Indicator
|
||||
{
|
||||
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date,
|
||||
candleSignal.Exchange, Type, SignalType);
|
||||
candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -61,7 +61,7 @@ public class SuperTrendIndicator : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -99,7 +99,7 @@ public class SuperTrendIndicator : Indicator
|
||||
{
|
||||
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date,
|
||||
candleSignal.Exchange, Type, SignalType);
|
||||
candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Managing.Domain.Strategies.Signals
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class EmaTrendIndicator : EmaBaseIndicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -65,7 +65,7 @@ public class EmaTrendIndicator : EmaBaseIndicator
|
||||
public void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
|
||||
{
|
||||
var signal = new Signal(MiscExtensions.ParseEnum<Ticker>(candleSignal.Ticker), direction, confidence,
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType);
|
||||
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
@@ -65,7 +65,7 @@ public class StochRsiTrendIndicator : Indicator
|
||||
}
|
||||
}
|
||||
|
||||
public override IndicatorsResultBase GetStrategyValues()
|
||||
public override IndicatorsResultBase GetIndicatorValues()
|
||||
{
|
||||
return new IndicatorsResultBase()
|
||||
{
|
||||
@@ -108,7 +108,8 @@ public class StochRsiTrendIndicator : Indicator
|
||||
candleSignal.Date,
|
||||
candleSignal.Exchange,
|
||||
Type,
|
||||
SignalType);
|
||||
SignalType,
|
||||
Name);
|
||||
if (!Signals.Any(s => s.Identifier == signal.Identifier))
|
||||
{
|
||||
Signals.AddItem(signal);
|
||||
|
||||
36
src/Managing.Domain/Synth/Models/MinerInfo.cs
Normal file
36
src/Managing.Domain/Synth/Models/MinerInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a miner on the Synth API leaderboard
|
||||
/// </summary>
|
||||
public class MinerInfo
|
||||
{
|
||||
[JsonPropertyName("coldkey")]
|
||||
public string Coldkey { get; set; }
|
||||
|
||||
[JsonPropertyName("emission")]
|
||||
public decimal Emission { get; set; }
|
||||
|
||||
[JsonPropertyName("incentive")]
|
||||
public decimal Incentive { get; set; }
|
||||
|
||||
[JsonPropertyName("neuron_uid")]
|
||||
public int NeuronUid { get; set; }
|
||||
|
||||
[JsonPropertyName("pruning_score")]
|
||||
public decimal PruningScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rank value from API (decimal representing the ranking score)
|
||||
/// </summary>
|
||||
[JsonPropertyName("rank")]
|
||||
public decimal Rank { get; set; }
|
||||
|
||||
[JsonPropertyName("stake")]
|
||||
public decimal Stake { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_at")]
|
||||
public string UpdatedAt { get; set; }
|
||||
}
|
||||
32
src/Managing.Domain/Synth/Models/MinerPrediction.cs
Normal file
32
src/Managing.Domain/Synth/Models/MinerPrediction.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the prediction data from a single miner
|
||||
/// Contains multiple simulated price paths and the miner's information
|
||||
/// </summary>
|
||||
public class MinerPrediction
|
||||
{
|
||||
public string Asset { get; set; }
|
||||
[JsonPropertyName("miner_uid")] public int MinerUid { get; set; }
|
||||
[JsonPropertyName("num_simulations")] public int NumSimulations { get; set; }
|
||||
public List<List<PricePoint>> Prediction { get; set; } = new();
|
||||
[JsonPropertyName("start_time")] public string StartTime { get; set; }
|
||||
[JsonPropertyName("time_increment")] public int TimeIncrement { get; set; }
|
||||
[JsonPropertyName("time_length")] public int TimeLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Complete miner information including rank, stake, incentive, etc.
|
||||
/// This is populated after fetching predictions by mapping MinerUid to MinerInfo.NeuronUid
|
||||
/// </summary>
|
||||
public MinerInfo? MinerInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the StartTime string to DateTime for easier manipulation
|
||||
/// </summary>
|
||||
public DateTime GetStartDateTime()
|
||||
{
|
||||
return DateTime.TryParse(StartTime, out var dateTime) ? dateTime : DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
18
src/Managing.Domain/Synth/Models/PricePoint.cs
Normal file
18
src/Managing.Domain/Synth/Models/PricePoint.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a price at a specific time within a simulated path
|
||||
/// </summary>
|
||||
public class PricePoint
|
||||
{
|
||||
public decimal Price { get; set; }
|
||||
public string Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the Time string to DateTime for easier manipulation
|
||||
/// </summary>
|
||||
public DateTime GetDateTime()
|
||||
{
|
||||
return DateTime.TryParse(Time, out var dateTime) ? dateTime : DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
403
src/Managing.Domain/Synth/Models/SignalValidationResult.cs
Normal file
403
src/Managing.Domain/Synth/Models/SignalValidationResult.cs
Normal file
@@ -0,0 +1,403 @@
|
||||
using Managing.Domain.Risk;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of Synth signal validation containing comprehensive analysis data
|
||||
/// </summary>
|
||||
public class SignalValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Overall confidence level of the signal based on TP vs SL probability analysis
|
||||
/// </summary>
|
||||
public Confidence Confidence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw stop loss probability (0.0 to 1.0)
|
||||
/// </summary>
|
||||
public decimal StopLossProbability { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw take profit probability (0.0 to 1.0)
|
||||
/// </summary>
|
||||
public decimal TakeProfitProbability { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculated ratio of Take Profit Probability / Stop Loss Probability
|
||||
/// Higher values indicate more favorable risk/reward
|
||||
/// </summary>
|
||||
public decimal TpSlRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the signal should be blocked based on risk analysis
|
||||
/// True when confidence is None or adverse probability is too high
|
||||
/// </summary>
|
||||
public bool IsBlocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Threshold used for adverse probability evaluation
|
||||
/// </summary>
|
||||
public decimal AdverseProbabilityThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional context information about the validation
|
||||
/// </summary>
|
||||
public string ValidationContext { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Time horizon used for the probability calculations (in seconds)
|
||||
/// </summary>
|
||||
public int TimeHorizonSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether custom thresholds were used in the analysis
|
||||
/// </summary>
|
||||
public bool UsedCustomThresholds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Monetary gain if take profit is reached (positive value)
|
||||
/// </summary>
|
||||
public decimal TakeProfitGain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Monetary loss if stop loss is hit (positive value representing loss amount)
|
||||
/// </summary>
|
||||
public decimal StopLossLoss { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected Monetary Value: (TP_Gain * TP_Prob) - (SL_Loss * SL_Prob)
|
||||
/// Positive values indicate favorable expected outcomes
|
||||
/// </summary>
|
||||
public decimal ExpectedMonetaryValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected Utility using logarithmic utility function for risk-adjusted decision making
|
||||
/// Higher values indicate more desirable risk-adjusted outcomes
|
||||
/// </summary>
|
||||
public decimal ExpectedUtility { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk-adjusted return ratio (Expected Utility / Risk)
|
||||
/// Higher values indicate better risk-adjusted opportunities
|
||||
/// </summary>
|
||||
public decimal UtilityRiskRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Kelly Criterion fraction - optimal percentage of capital to allocate (0.0 to 1.0)
|
||||
/// Based on Kelly formula: f* = (bp - q) / b, where b = payoff ratio, p = win probability, q = loss probability
|
||||
/// Values above 0.25 (25%) are typically capped for practical risk management
|
||||
/// </summary>
|
||||
public decimal KellyFraction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Capped Kelly Fraction for practical risk management (typically max 25% of capital)
|
||||
/// </summary>
|
||||
public decimal KellyCappedFraction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Win/Loss ratio used in Kelly calculation (TakeProfitGain / StopLossLoss)
|
||||
/// </summary>
|
||||
public decimal WinLossRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Kelly Criterion assessment indicating the quality of the opportunity
|
||||
/// </summary>
|
||||
public string KellyAssessment { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Risk tolerance level affecting overall risk calculations
|
||||
/// </summary>
|
||||
public RiskToleranceLevel RiskTolerance { get; set; } = RiskToleranceLevel.Moderate;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Expected Utility Theory for decision making
|
||||
/// </summary>
|
||||
public bool UseExpectedUtility { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Kelly Criterion for position sizing recommendations
|
||||
/// </summary>
|
||||
public bool UseKellyCriterion { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Trading balance used for utility calculations (from TradingBotConfig.BotTradingBalance)
|
||||
/// Represents the actual capital allocated to this trading bot
|
||||
/// </summary>
|
||||
public decimal TradingBalance { get; private set; } = 10000m;
|
||||
|
||||
/// <summary>
|
||||
/// Risk aversion parameter used for utility calculations (configured from RiskManagement)
|
||||
/// </summary>
|
||||
public decimal ConfiguredRiskAversion { get; private set; } = 1.0m;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Expected Monetary Value and Expected Utility using configured risk parameters
|
||||
/// </summary>
|
||||
/// <param name="tradingBalance">Actual trading balance allocated to the bot</param>
|
||||
/// <param name="riskConfig">Complete risk management configuration</param>
|
||||
public void CalculateExpectedMetrics(decimal tradingBalance, RiskManagement riskConfig)
|
||||
{
|
||||
// Store configured values for reference
|
||||
TradingBalance = tradingBalance;
|
||||
ConfiguredRiskAversion = riskConfig.RiskAversion;
|
||||
|
||||
// Calculate Expected Monetary Value
|
||||
// EMV = (TP_Gain * TP_Prob) - (SL_Loss * SL_Prob)
|
||||
ExpectedMonetaryValue = (TakeProfitGain * TakeProfitProbability) - (StopLossLoss * StopLossProbability);
|
||||
|
||||
// Calculate Expected Utility using logarithmic utility function
|
||||
// This accounts for diminishing marginal utility and risk aversion
|
||||
ExpectedUtility = CalculateLogarithmicExpectedUtility();
|
||||
|
||||
// Calculate utility-to-risk ratio for ranking opportunities
|
||||
var totalRisk = StopLossLoss > 0 ? StopLossLoss : 1m; // Avoid division by zero
|
||||
UtilityRiskRatio = ExpectedUtility / totalRisk;
|
||||
|
||||
// Calculate Kelly Criterion for optimal position sizing using full risk config
|
||||
CalculateKellyCriterion(riskConfig);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Expected Utility using logarithmic utility function
|
||||
/// U(x) = ln(tradingBalance + x) for gains, ln(tradingBalance - x) for losses
|
||||
/// Uses the actual trading balance and configured risk aversion
|
||||
/// </summary>
|
||||
/// <returns>Expected utility value</returns>
|
||||
private decimal CalculateLogarithmicExpectedUtility()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use actual trading balance and configured risk aversion
|
||||
var baseCapital = TradingBalance > 0 ? TradingBalance : 10000m;
|
||||
var riskAversion = ConfiguredRiskAversion > 0 ? ConfiguredRiskAversion : 1.0m;
|
||||
|
||||
// Calculate utility of TP outcome: U(tradingBalance + gain)
|
||||
var tpOutcome = baseCapital + TakeProfitGain;
|
||||
var tpUtility = tpOutcome > 0 ? (decimal)Math.Log((double)tpOutcome) / riskAversion : decimal.MinValue;
|
||||
|
||||
// Calculate utility of SL outcome: U(tradingBalance - loss)
|
||||
var slOutcome = baseCapital - StopLossLoss;
|
||||
var slUtility = slOutcome > 0 ? (decimal)Math.Log((double)slOutcome) / riskAversion : decimal.MinValue;
|
||||
|
||||
// Calculate utility of no-change outcome (neither TP nor SL hit)
|
||||
var noChangeProb = Math.Max(0m, 1m - TakeProfitProbability - StopLossProbability);
|
||||
var noChangeUtility = (decimal)Math.Log((double)baseCapital) / riskAversion;
|
||||
|
||||
// Expected Utility = Sum of (Utility * Probability) for all outcomes
|
||||
var expectedUtility = (tpUtility * TakeProfitProbability) +
|
||||
(slUtility * StopLossProbability) +
|
||||
(noChangeUtility * noChangeProb);
|
||||
|
||||
return expectedUtility;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Return conservative utility value on calculation errors
|
||||
return decimal.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Kelly Criterion for optimal position sizing
|
||||
/// Kelly Formula: f* = (bp - q) / b
|
||||
/// Where: b = payoff ratio (win/loss), p = win probability, q = loss probability
|
||||
/// </summary>
|
||||
/// <param name="riskConfig">Complete risk management configuration</param>
|
||||
private void CalculateKellyCriterion(RiskManagement riskConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Calculate Win/Loss Ratio (b in Kelly formula)
|
||||
WinLossRatio = StopLossLoss > 0 ? TakeProfitGain / StopLossLoss : 0m;
|
||||
|
||||
// Handle edge cases
|
||||
if (WinLossRatio <= 0 || TakeProfitProbability <= 0)
|
||||
{
|
||||
KellyFraction = 0m;
|
||||
KellyCappedFraction = 0m;
|
||||
KellyAssessment = "No Position - Unfavorable risk/reward ratio";
|
||||
return;
|
||||
}
|
||||
|
||||
// Kelly Formula: f* = (bp - q) / b
|
||||
// Where:
|
||||
// b = WinLossRatio (TakeProfitGain / StopLossLoss)
|
||||
// p = TakeProfitProbability
|
||||
// q = StopLossProbability
|
||||
var numerator = (WinLossRatio * TakeProfitProbability) - StopLossProbability;
|
||||
var kellyFraction = numerator / WinLossRatio;
|
||||
|
||||
// Ensure Kelly fraction is not negative (would indicate unfavorable bet)
|
||||
KellyFraction = Math.Max(0m, kellyFraction);
|
||||
|
||||
// Apply fractional Kelly multiplier
|
||||
KellyFraction *= riskConfig.KellyFractionalMultiplier;
|
||||
|
||||
// Apply practical cap for risk management
|
||||
KellyCappedFraction = Math.Min(KellyFraction, riskConfig.KellyMaximumCap);
|
||||
|
||||
// Generate Kelly assessment using the configured threshold
|
||||
KellyAssessment = GenerateKellyAssessment(riskConfig);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Safe defaults on calculation errors
|
||||
KellyFraction = 0m;
|
||||
KellyCappedFraction = 0m;
|
||||
WinLossRatio = 0m;
|
||||
KellyAssessment = "Calculation Error - No position recommended";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a descriptive assessment of the Kelly Criterion result
|
||||
/// </summary>
|
||||
/// <param name="riskConfig">Risk management configuration containing Kelly thresholds</param>
|
||||
/// <returns>Human-readable Kelly assessment</returns>
|
||||
private string GenerateKellyAssessment(RiskManagement riskConfig)
|
||||
{
|
||||
if (KellyFraction <= 0)
|
||||
return "No Position - Negative or zero Kelly fraction";
|
||||
|
||||
if (KellyFraction < riskConfig.KellyMinimumThreshold)
|
||||
return $"Below Threshold - Kelly {KellyFraction:P2} < {riskConfig.KellyMinimumThreshold:P2} minimum";
|
||||
|
||||
if (KellyFraction < 0.05m) // 1-5%
|
||||
return "Small Position - Low but positive edge";
|
||||
|
||||
if (KellyFraction < 0.10m) // 5-10%
|
||||
return "Moderate Position - Reasonable edge";
|
||||
|
||||
if (KellyFraction < 0.25m) // 10-25%
|
||||
return "Large Position - Strong edge detected";
|
||||
|
||||
if (KellyFraction < 0.50m) // 25-50%
|
||||
return "Very Large Position - Exceptional edge (CAPPED for safety)";
|
||||
|
||||
// Above 50%
|
||||
return "Extreme Position - Extraordinary edge (HEAVILY CAPPED for safety)";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed Kelly Criterion analysis including fractional betting recommendations
|
||||
/// </summary>
|
||||
/// <param name="totalCapital">Total available capital for position sizing</param>
|
||||
/// <returns>Detailed Kelly analysis with dollar amounts</returns>
|
||||
public string GetDetailedKellyAnalysis(decimal totalCapital = 100000m)
|
||||
{
|
||||
var recommendedAmount = KellyCappedFraction * totalCapital;
|
||||
var uncappedAmount = KellyFraction * totalCapital;
|
||||
|
||||
var analysis = $"Kelly Analysis:\n" +
|
||||
$"• Win/Loss Ratio: {WinLossRatio:F2}:1\n" +
|
||||
$"• Optimal Kelly %: {KellyFraction:P2}\n" +
|
||||
$"• Capped Kelly %: {KellyCappedFraction:P2}\n" +
|
||||
$"• Recommended Amount: ${recommendedAmount:N0}\n";
|
||||
|
||||
if (KellyFraction > KellyCappedFraction)
|
||||
{
|
||||
analysis += $"• Uncapped Amount: ${uncappedAmount:N0} (RISK WARNING)\n";
|
||||
}
|
||||
|
||||
analysis += $"• Assessment: {KellyAssessment}";
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates fractional Kelly betting for more conservative position sizing
|
||||
/// </summary>
|
||||
/// <param name="fraction">Fraction of Kelly to use (e.g., 0.5 for half-Kelly)</param>
|
||||
/// <returns>Fractional Kelly allocation percentage</returns>
|
||||
public decimal GetFractionalKelly(decimal fraction = 0.5m)
|
||||
{
|
||||
if (fraction < 0 || fraction > 1) fraction = 0.5m; // Default to half-Kelly
|
||||
return KellyFraction * fraction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if the Kelly Criterion suggests this is a profitable opportunity
|
||||
/// </summary>
|
||||
/// <param name="riskConfig">Risk management configuration containing Kelly thresholds</param>
|
||||
/// <returns>True if Kelly fraction is above the configured threshold</returns>
|
||||
public bool IsKellyFavorable(RiskManagement riskConfig)
|
||||
{
|
||||
return KellyFraction > riskConfig.KellyMinimumThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alternative utility calculation using square root utility (less risk-averse than logarithmic)
|
||||
/// Uses the actual trading balance from the bot configuration
|
||||
/// </summary>
|
||||
/// <returns>Expected utility using square root function</returns>
|
||||
public decimal CalculateSquareRootExpectedUtility()
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseCapital = TradingBalance > 0 ? TradingBalance : 10000m;
|
||||
|
||||
// Square root utility: U(x) = sqrt(x)
|
||||
var tpOutcome = baseCapital + TakeProfitGain;
|
||||
var tpUtility = tpOutcome > 0 ? (decimal)Math.Sqrt((double)tpOutcome) : 0m;
|
||||
|
||||
var slOutcome = Math.Max(0m, baseCapital - StopLossLoss);
|
||||
var slUtility = (decimal)Math.Sqrt((double)slOutcome);
|
||||
|
||||
var noChangeProb = Math.Max(0m, 1m - TakeProfitProbability - StopLossProbability);
|
||||
var noChangeUtility = (decimal)Math.Sqrt((double)baseCapital);
|
||||
|
||||
return (tpUtility * TakeProfitProbability) +
|
||||
(slUtility * StopLossProbability) +
|
||||
(noChangeUtility * noChangeProb);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a risk assessment based on Expected Utility Theory
|
||||
/// </summary>
|
||||
/// <returns>Descriptive risk assessment</returns>
|
||||
public string GetUtilityRiskAssessment()
|
||||
{
|
||||
if (ExpectedMonetaryValue > 0 && ExpectedUtility > 0)
|
||||
return "Favorable - Positive expected value and utility";
|
||||
|
||||
if (ExpectedMonetaryValue > 0 && ExpectedUtility <= 0)
|
||||
return "Cautious - Positive expected value but negative risk-adjusted utility";
|
||||
|
||||
if (ExpectedMonetaryValue <= 0 && ExpectedUtility > 0)
|
||||
return "Risk-Seeking - Negative expected value but positive utility (unusual)";
|
||||
|
||||
return "Unfavorable - Negative expected value and utility";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a result indicating Synth is disabled
|
||||
/// </summary>
|
||||
public static SignalValidationResult CreateDisabledResult(Confidence originalConfidence)
|
||||
{
|
||||
return new SignalValidationResult
|
||||
{
|
||||
Confidence = originalConfidence,
|
||||
IsBlocked = false,
|
||||
ValidationContext = "Synth API disabled"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a result for error scenarios
|
||||
/// </summary>
|
||||
public static SignalValidationResult CreateErrorResult(Confidence fallbackConfidence, string errorContext)
|
||||
{
|
||||
return new SignalValidationResult
|
||||
{
|
||||
Confidence = fallbackConfidence,
|
||||
IsBlocked = false,
|
||||
ValidationContext = $"Error in validation: {errorContext}"
|
||||
};
|
||||
}
|
||||
}
|
||||
64
src/Managing.Domain/Synth/Models/SynthConfiguration.cs
Normal file
64
src/Managing.Domain/Synth/Models/SynthConfiguration.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration settings for Synth API integration
|
||||
/// </summary>
|
||||
public class SynthConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to enable Synth API integration
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Number of top miners to fetch predictions from (default: 10)
|
||||
/// </summary>
|
||||
public int TopMinersCount { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Time increment in seconds for predictions (default: 300 = 5 minutes)
|
||||
/// </summary>
|
||||
public int TimeIncrement { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Default time length in seconds for predictions (default: 86400 = 24 hours)
|
||||
/// </summary>
|
||||
public int DefaultTimeLength { get; set; } = 86400;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum acceptable liquidation probability threshold (0.0 to 1.0)
|
||||
/// If liquidation probability exceeds this, position opening may be blocked
|
||||
/// </summary>
|
||||
public decimal MaxLiquidationProbability { get; set; } = 0.10m; // 10%
|
||||
|
||||
/// <summary>
|
||||
/// Cache duration for predictions in minutes (default: 5 minutes)
|
||||
/// </summary>
|
||||
public int PredictionCacheDurationMinutes { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for position sizing adjustments
|
||||
/// </summary>
|
||||
public bool UseForPositionSizing { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for signal filtering
|
||||
/// </summary>
|
||||
public bool UseForSignalFiltering { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
|
||||
/// </summary>
|
||||
public bool UseForDynamicStopLoss { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Validates the configuration
|
||||
/// </summary>
|
||||
public bool IsValid()
|
||||
{
|
||||
return !IsEnabled || (TopMinersCount > 0 &&
|
||||
TimeIncrement > 0 &&
|
||||
DefaultTimeLength > 0 &&
|
||||
MaxLiquidationProbability >= 0 && MaxLiquidationProbability <= 1);
|
||||
}
|
||||
}
|
||||
57
src/Managing.Domain/Synth/Models/SynthMinersLeaderboard.cs
Normal file
57
src/Managing.Domain/Synth/Models/SynthMinersLeaderboard.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a cached leaderboard entry for Synth miners
|
||||
/// Used for MongoDB persistence to avoid repeated API calls
|
||||
/// </summary>
|
||||
public class SynthMinersLeaderboard
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this leaderboard entry
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Asset symbol (e.g., "BTC", "ETH")
|
||||
/// </summary>
|
||||
public string Asset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time increment used for this leaderboard data
|
||||
/// </summary>
|
||||
public int TimeIncrement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Signal date for which this leaderboard was retrieved (for backtests)
|
||||
/// Null for live trading data
|
||||
/// </summary>
|
||||
public DateTime? SignalDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is backtest data or live data
|
||||
/// </summary>
|
||||
public bool IsBacktest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of miners in the leaderboard
|
||||
/// </summary>
|
||||
public List<MinerInfo> Miners { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// When this leaderboard data was created/stored
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a cache key for this leaderboard entry
|
||||
/// </summary>
|
||||
public string GetCacheKey()
|
||||
{
|
||||
var key = $"{Asset}_{TimeIncrement}";
|
||||
if (IsBacktest && SignalDate.HasValue)
|
||||
{
|
||||
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
72
src/Managing.Domain/Synth/Models/SynthMinersPredictions.cs
Normal file
72
src/Managing.Domain/Synth/Models/SynthMinersPredictions.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents cached prediction data from Synth miners
|
||||
/// Used for MongoDB persistence to avoid repeated API calls
|
||||
/// </summary>
|
||||
public class SynthMinersPredictions
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this predictions entry
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Asset symbol (e.g., "BTC", "ETH")
|
||||
/// </summary>
|
||||
public string Asset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time increment used for these predictions
|
||||
/// </summary>
|
||||
public int TimeIncrement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time length (horizon) for these predictions in seconds
|
||||
/// </summary>
|
||||
public int TimeLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Signal date for which these predictions were retrieved (for backtests)
|
||||
/// Null for live trading data
|
||||
/// </summary>
|
||||
public DateTime? SignalDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is backtest data or live data
|
||||
/// </summary>
|
||||
public bool IsBacktest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of miner UIDs these predictions are from
|
||||
/// </summary>
|
||||
public List<int> MinerUids { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The actual prediction data from miners
|
||||
/// </summary>
|
||||
public List<MinerPrediction> Predictions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// When this prediction data was fetched from the API
|
||||
/// </summary>
|
||||
public DateTime FetchedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this prediction data was created/stored
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a cache key for this predictions entry
|
||||
/// </summary>
|
||||
public string GetCacheKey()
|
||||
{
|
||||
var key = $"{Asset}_{TimeIncrement}_{TimeLength}";
|
||||
if (IsBacktest && SignalDate.HasValue)
|
||||
{
|
||||
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
67
src/Managing.Domain/Synth/Models/SynthPrediction.cs
Normal file
67
src/Managing.Domain/Synth/Models/SynthPrediction.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents cached prediction data from a single Synth miner
|
||||
/// Used for MongoDB persistence to avoid repeated API calls and reduce document size
|
||||
/// </summary>
|
||||
public class SynthPrediction
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this prediction entry
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Asset symbol (e.g., "BTC", "ETH")
|
||||
/// </summary>
|
||||
public string Asset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Miner UID that provided this prediction
|
||||
/// </summary>
|
||||
public int MinerUid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time increment used for this prediction
|
||||
/// </summary>
|
||||
public int TimeIncrement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time length (horizon) for this prediction in seconds
|
||||
/// </summary>
|
||||
public int TimeLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Signal date for which this prediction was retrieved (for backtests)
|
||||
/// Null for live trading data
|
||||
/// </summary>
|
||||
public DateTime? SignalDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is backtest data or live data
|
||||
/// </summary>
|
||||
public bool IsBacktest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The actual prediction data from the miner
|
||||
/// </summary>
|
||||
public MinerPrediction Prediction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this prediction data was created/stored
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a cache key for this prediction entry
|
||||
/// </summary>
|
||||
public string GetCacheKey()
|
||||
{
|
||||
var key = $"{Asset}_{TimeIncrement}_{TimeLength}_{MinerUid}";
|
||||
if (IsBacktest && SignalDate.HasValue)
|
||||
{
|
||||
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
13
src/Managing.Domain/Synth/Models/SynthRiskResult.cs
Normal file
13
src/Managing.Domain/Synth/Models/SynthRiskResult.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Managing.Domain.Synth.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result of Synth risk monitoring
|
||||
/// </summary>
|
||||
public class SynthRiskResult
|
||||
{
|
||||
public decimal LiquidationProbability { get; set; }
|
||||
public bool ShouldWarn { get; set; }
|
||||
public bool ShouldAutoClose { get; set; }
|
||||
public string WarningMessage { get; set; }
|
||||
public string EmergencyMessage { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user