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:
Oda
2025-07-03 00:13:42 +07:00
committed by GitHub
parent 453806356d
commit a547c4a040
103 changed files with 9916 additions and 810 deletions

View File

@@ -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>

View File

@@ -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; }

View File

@@ -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;
}

View 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)
};
}
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -52,7 +52,7 @@ namespace Managing.Domain.Strategies.Signals
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
throw new NotImplementedException();
}

View File

@@ -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);

View File

@@ -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);

View 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; }
}

View 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;
}
}

View 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;
}
}

View 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}"
};
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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; }
}