Implement A3, A4, A5, B3: Add MarketOutcomePriceSnapshot, implement Polymarket CLOB prices-history endpoint, improve strategy classification, introduce CopytradingScore, and decouple/optimize scoring pipeline into a separate worker
This commit is contained in:
@@ -10,6 +10,7 @@ public record TraderDto(
|
|||||||
string Tier,
|
string Tier,
|
||||||
string Strategy,
|
string Strategy,
|
||||||
decimal CombinedScore,
|
decimal CombinedScore,
|
||||||
|
decimal CopytradingScore,
|
||||||
decimal WinRate,
|
decimal WinRate,
|
||||||
decimal TotalPnl,
|
decimal TotalPnl,
|
||||||
int TotalTrades,
|
int TotalTrades,
|
||||||
@@ -36,6 +37,7 @@ public record TraderDetailDto(
|
|||||||
decimal VolumeScore,
|
decimal VolumeScore,
|
||||||
decimal TimingScore,
|
decimal TimingScore,
|
||||||
decimal CombinedScore,
|
decimal CombinedScore,
|
||||||
|
decimal CopytradingScore,
|
||||||
int Rank,
|
int Rank,
|
||||||
bool IsOnWatchlist,
|
bool IsOnWatchlist,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
|
|||||||
@@ -16,15 +16,16 @@ public class AnalyticsService : IAnalyticsService
|
|||||||
private readonly IWatchlistRepository _watchlistRepo;
|
private readonly IWatchlistRepository _watchlistRepo;
|
||||||
private readonly IMarketRepository _marketRepo;
|
private readonly IMarketRepository _marketRepo;
|
||||||
private readonly IDiscoveryService _discovery;
|
private readonly IDiscoveryService _discovery;
|
||||||
|
private readonly IEnumerable<IPlatformProvider> _providers;
|
||||||
private readonly ILogger<AnalyticsService> _logger;
|
private readonly ILogger<AnalyticsService> _logger;
|
||||||
|
|
||||||
public AnalyticsService(ITraderRepository traderRepo, ITradeRepository tradeRepo,
|
public AnalyticsService(ITraderRepository traderRepo, ITradeRepository tradeRepo,
|
||||||
IAlertRepository alertRepo, IWatchlistRepository watchlistRepo, IMarketRepository marketRepo,
|
IAlertRepository alertRepo, IWatchlistRepository watchlistRepo, IMarketRepository marketRepo,
|
||||||
IDiscoveryService discovery, ILogger<AnalyticsService> logger)
|
IDiscoveryService discovery, IEnumerable<IPlatformProvider> providers, ILogger<AnalyticsService> logger)
|
||||||
{
|
{
|
||||||
_traderRepo = traderRepo; _tradeRepo = tradeRepo;
|
_traderRepo = traderRepo; _tradeRepo = tradeRepo;
|
||||||
_alertRepo = alertRepo; _watchlistRepo = watchlistRepo; _marketRepo = marketRepo;
|
_alertRepo = alertRepo; _watchlistRepo = watchlistRepo; _marketRepo = marketRepo;
|
||||||
_discovery = discovery; _logger = logger;
|
_discovery = discovery; _providers = providers; _logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DashboardDto> GetDashboardAsync(CancellationToken ct = default)
|
public async Task<DashboardDto> GetDashboardAsync(CancellationToken ct = default)
|
||||||
@@ -69,7 +70,66 @@ public class AnalyticsService : IAnalyticsService
|
|||||||
var trader = await _traderRepo.GetByIdAsync(traderId, ct);
|
var trader = await _traderRepo.GetByIdAsync(traderId, ct);
|
||||||
if (trader == null) return null;
|
if (trader == null) return null;
|
||||||
var trades = await _tradeRepo.GetByTraderIdAsync(traderId, 0, 500, ct);
|
var trades = await _tradeRepo.GetByTraderIdAsync(traderId, 0, 500, ct);
|
||||||
var analysis = PerformDeepDive(trader, trades);
|
|
||||||
|
// Fetch or load price snapshots for outcomes
|
||||||
|
var outcomeIds = trades
|
||||||
|
.Where(t => t.MarketOutcomeId.HasValue)
|
||||||
|
.Select(t => t.MarketOutcomeId!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var snapshotsByOutcome = new Dictionary<int, List<MarketOutcomePriceSnapshot>>();
|
||||||
|
|
||||||
|
foreach (var outcomeId in outcomeIds)
|
||||||
|
{
|
||||||
|
var existing = await _marketRepo.GetPriceSnapshotsAsync(outcomeId, ct);
|
||||||
|
|
||||||
|
var needsFetch = existing.Count == 0;
|
||||||
|
if (existing.Count > 0)
|
||||||
|
{
|
||||||
|
var latestSnapshot = existing.MaxBy(ps => ps.Timestamp);
|
||||||
|
var firstTradeWithOutcome = trades.FirstOrDefault(t => t.MarketOutcomeId == outcomeId && t.MarketOutcome != null);
|
||||||
|
var isResolved = firstTradeWithOutcome?.MarketOutcome?.Market?.IsResolved ?? false;
|
||||||
|
if (!isResolved && (DateTime.UtcNow - (latestSnapshot?.Timestamp ?? DateTime.MinValue)).TotalHours >= 24)
|
||||||
|
{
|
||||||
|
needsFetch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsFetch)
|
||||||
|
{
|
||||||
|
var firstTradeWithOutcome = trades.FirstOrDefault(t => t.MarketOutcomeId == outcomeId && t.MarketOutcome != null);
|
||||||
|
if (firstTradeWithOutcome?.MarketOutcome != null)
|
||||||
|
{
|
||||||
|
var outcome = firstTradeWithOutcome.MarketOutcome;
|
||||||
|
var provider = _providers.FirstOrDefault(p => p.Platform == firstTradeWithOutcome.Platform);
|
||||||
|
if (provider != null && !string.IsNullOrEmpty(outcome.TokenId))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fetched = await provider.GetPriceHistoryAsync(outcome.TokenId, ct);
|
||||||
|
if (fetched.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var f in fetched)
|
||||||
|
{
|
||||||
|
f.MarketOutcomeId = outcomeId;
|
||||||
|
}
|
||||||
|
await _marketRepo.SavePriceSnapshotsAsync(outcomeId, fetched, ct);
|
||||||
|
existing = fetched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to fetch price history for outcome {OutcomeId}", outcomeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotsByOutcome[outcomeId] = existing.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var analysis = PerformDeepDive(trader, trades, snapshotsByOutcome);
|
||||||
var tradeDtos = trades.Take(100).Select(MapTradeDto).ToList();
|
var tradeDtos = trades.Take(100).Select(MapTradeDto).ToList();
|
||||||
return new TraderDeepDiveDto(traderId, trader.DisplayName, trader.Platform,
|
return new TraderDeepDiveDto(traderId, trader.DisplayName, trader.Platform,
|
||||||
analysis.ClassifiedStrategy, analysis.IsSuspectedBot, analysis.AvgHoldDurationHours,
|
analysis.ClassifiedStrategy, analysis.IsSuspectedBot, analysis.AvgHoldDurationHours,
|
||||||
@@ -130,7 +190,7 @@ public class AnalyticsService : IAnalyticsService
|
|||||||
trader.Notes, trader.Tier.ToString(), trader.Strategy.ToString(), trader.IsSuspectedBot, trader.ManualPriorityOverride,
|
trader.Notes, trader.Tier.ToString(), trader.Strategy.ToString(), trader.IsSuspectedBot, trader.ManualPriorityOverride,
|
||||||
trader.WinRate, trader.TotalPnl, trader.TotalTrades,
|
trader.WinRate, trader.TotalPnl, trader.TotalTrades,
|
||||||
s?.ActivityScore ?? 0, s?.QualityScore ?? 0, s?.VolumeScore ?? 0, s?.TimingScore ?? 0,
|
s?.ActivityScore ?? 0, s?.QualityScore ?? 0, s?.VolumeScore ?? 0, s?.TimingScore ?? 0,
|
||||||
s?.CombinedScore ?? 0, s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt,
|
s?.CombinedScore ?? 0, s?.CopytradingScore ?? 0, s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt,
|
||||||
trades.Select(MapTradeDto).ToList());
|
trades.Select(MapTradeDto).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +250,10 @@ public class AnalyticsService : IAnalyticsService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private TraderAnalysis PerformDeepDive(Trader trader, IReadOnlyList<Trade> trades)
|
private TraderAnalysis PerformDeepDive(
|
||||||
|
Trader trader,
|
||||||
|
IReadOnlyList<Trade> trades,
|
||||||
|
Dictionary<int, List<MarketOutcomePriceSnapshot>> snapshotsByOutcome)
|
||||||
{
|
{
|
||||||
if (trades.Count == 0)
|
if (trades.Count == 0)
|
||||||
return new TraderAnalysis(trader.Id, StrategyType.Unknown, false, 0, 0, 0, 0, 50, 50, 50,
|
return new TraderAnalysis(trader.Id, StrategyType.Unknown, false, 0, 0, 0, 0, 50, 50, 50,
|
||||||
@@ -220,11 +283,120 @@ public class AnalyticsService : IAnalyticsService
|
|||||||
var hedgingRate = marketsTraded > 0 ? (decimal)hedgeGroups.Count() / marketsTraded * 100 : 0;
|
var hedgingRate = marketsTraded > 0 ? (decimal)hedgeGroups.Count() / marketsTraded * 100 : 0;
|
||||||
var strategy = avgSize > 10000 ? StrategyType.Whale : hedgingRate > 30 ? StrategyType.Hedger :
|
var strategy = avgSize > 10000 ? StrategyType.Whale : hedgingRate > 30 ? StrategyType.Hedger :
|
||||||
botIndicators.Count > 0 ? StrategyType.Bot : StrategyType.Unknown;
|
botIndicators.Count > 0 ? StrategyType.Bot : StrategyType.Unknown;
|
||||||
return new TraderAnalysis(trader.Id, strategy, botIndicators.Count > 1, 0, avgSize, marketsTraded,
|
|
||||||
hedgingRate, 50, 50, 50, botIndicators.ToArray(),
|
// Calculate holding duration
|
||||||
|
var holdDuration = (decimal)CalculateAvgHoldDuration(trades.ToList());
|
||||||
|
|
||||||
|
// Calculate Entry and Exit qualities
|
||||||
|
var entryQualities = new List<decimal>();
|
||||||
|
var exitQualities = new List<decimal>();
|
||||||
|
|
||||||
|
foreach (var trade in trades)
|
||||||
|
{
|
||||||
|
if (trade.MarketOutcomeId == null) continue;
|
||||||
|
var outcomeId = trade.MarketOutcomeId.Value;
|
||||||
|
|
||||||
|
if (snapshotsByOutcome.TryGetValue(outcomeId, out var snapshots) && snapshots.Count > 0)
|
||||||
|
{
|
||||||
|
var subsequent = snapshots
|
||||||
|
.Where(ps => ps.Timestamp > trade.ExecutedAt && ps.Timestamp <= trade.ExecutedAt.AddDays(7))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (subsequent.Count > 0)
|
||||||
|
{
|
||||||
|
var avgSubsequentPrice = subsequent.Average(ps => ps.Price);
|
||||||
|
if (trade.Side == TradeSide.Buy)
|
||||||
|
{
|
||||||
|
var entryQuality = 50m + ((avgSubsequentPrice - trade.Price) / Math.Max(trade.Price, 0.01m)) * 100m;
|
||||||
|
entryQualities.Add(Math.Clamp(entryQuality, 0m, 100m));
|
||||||
|
}
|
||||||
|
else if (trade.Side == TradeSide.Sell)
|
||||||
|
{
|
||||||
|
var exitQuality = 50m + ((trade.Price - avgSubsequentPrice) / Math.Max(trade.Price, 0.01m)) * 100m;
|
||||||
|
exitQualities.Add(Math.Clamp(exitQuality, 0m, 100m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalEntryQuality = entryQualities.Count > 0 ? Math.Round(entryQualities.Average(), 2) : 50m;
|
||||||
|
var finalExitQuality = exitQualities.Count > 0 ? Math.Round(exitQualities.Average(), 2) : 50m;
|
||||||
|
|
||||||
|
decimal finalTimingAccuracy;
|
||||||
|
if (entryQualities.Count > 0 && exitQualities.Count > 0)
|
||||||
|
{
|
||||||
|
finalTimingAccuracy = Math.Round((entryQualities.Average() + exitQualities.Average()) / 2m, 2);
|
||||||
|
}
|
||||||
|
else if (entryQualities.Count > 0)
|
||||||
|
{
|
||||||
|
finalTimingAccuracy = Math.Round(entryQualities.Average(), 2);
|
||||||
|
}
|
||||||
|
else if (exitQualities.Count > 0)
|
||||||
|
{
|
||||||
|
finalTimingAccuracy = Math.Round(exitQualities.Average(), 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finalTimingAccuracy = 50m;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TraderAnalysis(trader.Id, strategy, botIndicators.Count > 1, holdDuration, avgSize, marketsTraded,
|
||||||
|
hedgingRate, finalTimingAccuracy, finalEntryQuality, finalExitQuality, botIndicators.ToArray(),
|
||||||
$"{trader.DisplayName}: {strategy}, {marketsTraded} markets, avg ${avgSize:N0}");
|
$"{trader.DisplayName}: {strategy}, {marketsTraded} markets, avg ${avgSize:N0}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static double CalculateAvgHoldDuration(List<Trade> trades)
|
||||||
|
{
|
||||||
|
var outcomeBuys = new Dictionary<int, List<(DateTime ExecutedAt, decimal Size)>>();
|
||||||
|
double totalWeightedHours = 0;
|
||||||
|
decimal totalMatchedSize = 0;
|
||||||
|
|
||||||
|
foreach (var t in trades)
|
||||||
|
{
|
||||||
|
if (t.MarketOutcomeId == null) continue;
|
||||||
|
var oid = t.MarketOutcomeId.Value;
|
||||||
|
|
||||||
|
if (t.Side == TradeSide.Buy)
|
||||||
|
{
|
||||||
|
if (!outcomeBuys.TryGetValue(oid, out var list))
|
||||||
|
{
|
||||||
|
list = new List<(DateTime, decimal)>();
|
||||||
|
outcomeBuys[oid] = list;
|
||||||
|
}
|
||||||
|
list.Add((t.ExecutedAt, t.Size));
|
||||||
|
}
|
||||||
|
else if (t.Side == TradeSide.Sell || t.Side == TradeSide.Redeem)
|
||||||
|
{
|
||||||
|
if (outcomeBuys.TryGetValue(oid, out var list) && list.Count > 0)
|
||||||
|
{
|
||||||
|
var sellSizeRemaining = t.Size;
|
||||||
|
while (sellSizeRemaining > 0 && list.Count > 0)
|
||||||
|
{
|
||||||
|
var buy = list[0];
|
||||||
|
var matchedSize = Math.Min(sellSizeRemaining, buy.Size);
|
||||||
|
var hours = (t.ExecutedAt - buy.ExecutedAt).TotalHours;
|
||||||
|
if (hours < 0) hours = 0;
|
||||||
|
|
||||||
|
totalWeightedHours += hours * (double)matchedSize;
|
||||||
|
totalMatchedSize += matchedSize;
|
||||||
|
|
||||||
|
sellSizeRemaining -= matchedSize;
|
||||||
|
if (matchedSize >= buy.Size)
|
||||||
|
{
|
||||||
|
list.RemoveAt(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
list[0] = (buy.ExecutedAt, buy.Size - matchedSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalMatchedSize > 0 ? totalWeightedHours / (double)totalMatchedSize : 0;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task TriggerTradeSyncAsync(int traderId, CancellationToken ct = default)
|
public async Task TriggerTradeSyncAsync(int traderId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var trader = await _traderRepo.GetByIdAsync(traderId, ct);
|
var trader = await _traderRepo.GetByIdAsync(traderId, ct);
|
||||||
@@ -247,7 +419,7 @@ public class AnalyticsService : IAnalyticsService
|
|||||||
|
|
||||||
private static TraderDto MapTraderDto(Trader t, HashSet<int> wIds) => new(
|
private static TraderDto MapTraderDto(Trader t, HashSet<int> wIds) => new(
|
||||||
t.Id, t.Platform.ToString(), t.PlatformUserId, t.DisplayName, t.Tier.ToString(), t.Strategy.ToString(),
|
t.Id, t.Platform.ToString(), t.PlatformUserId, t.DisplayName, t.Tier.ToString(), t.Strategy.ToString(),
|
||||||
t.CurrentScore?.CombinedScore ?? 0, t.WinRate, t.TotalPnl, t.TotalTrades,
|
t.CurrentScore?.CombinedScore ?? 0, t.CurrentScore?.CopytradingScore ?? 0, t.WinRate, t.TotalPnl, t.TotalTrades,
|
||||||
wIds.Contains(t.Id), t.IsSuspectedBot, t.LastPolledAt);
|
wIds.Contains(t.Id), t.IsSuspectedBot, t.LastPolledAt);
|
||||||
|
|
||||||
private static TradeDto MapTradeDto(Trade t) => new(
|
private static TradeDto MapTradeDto(Trade t) => new(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Predictalytics.Application.Interfaces;
|
using Predictalytics.Application.Interfaces;
|
||||||
using Predictalytics.Domain.Entities;
|
using Predictalytics.Domain.Entities;
|
||||||
|
using Predictalytics.Domain.Enums;
|
||||||
using Predictalytics.Domain.Interfaces;
|
using Predictalytics.Domain.Interfaces;
|
||||||
using Predictalytics.Domain.ValueObjects;
|
using Predictalytics.Domain.ValueObjects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -68,6 +69,7 @@ public class ScoringService : IScoringService
|
|||||||
traderScore.VolumeScore = volumeScore;
|
traderScore.VolumeScore = volumeScore;
|
||||||
traderScore.TimingScore = timingScore;
|
traderScore.TimingScore = timingScore;
|
||||||
traderScore.CombinedScore = combined;
|
traderScore.CombinedScore = combined;
|
||||||
|
traderScore.CopytradingScore = CalculateCopytradingScore(trader, trades);
|
||||||
traderScore.CalculatedAt = DateTime.UtcNow;
|
traderScore.CalculatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
trader.CurrentScore = traderScore;
|
trader.CurrentScore = traderScore;
|
||||||
@@ -91,18 +93,40 @@ public class ScoringService : IScoringService
|
|||||||
foreach (var trader in traders)
|
foreach (var trader in traders)
|
||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested) break;
|
if (ct.IsCancellationRequested) break;
|
||||||
var score = await CalculateScoreAsync(trader.Id, ct);
|
|
||||||
scored.Add((trader.Id, score.EffectiveScore));
|
var needsScoring = trader.CurrentScore == null ||
|
||||||
|
trader.LastTradesUpdatedAt == null ||
|
||||||
|
trader.CurrentScore.CalculatedAt < trader.LastTradesUpdatedAt;
|
||||||
|
|
||||||
|
if (needsScoring)
|
||||||
|
{
|
||||||
|
var score = await CalculateScoreAsync(trader.Id, ct);
|
||||||
|
scored.Add((trader.Id, score.EffectiveScore));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scored.Add((trader.Id, trader.CurrentScore!.CombinedScore));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ranks
|
// Update ranks
|
||||||
foreach (var (id, _) in scored.OrderByDescending(s => s.Score))
|
foreach (var (id, _) in scored.OrderByDescending(s => s.Score))
|
||||||
{
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
|
||||||
var trader = await _traderRepo.GetByIdAsync(id, ct);
|
var trader = await _traderRepo.GetByIdAsync(id, ct);
|
||||||
if (trader?.CurrentScore != null)
|
if (trader?.CurrentScore != null)
|
||||||
{
|
{
|
||||||
trader.CurrentScore.Rank = rank++;
|
var newRank = rank++;
|
||||||
await _traderRepo.UpdateAsync(trader, ct);
|
if (trader.CurrentScore.Rank != newRank)
|
||||||
|
{
|
||||||
|
trader.CurrentScore.Rank = newRank;
|
||||||
|
await _traderRepo.UpdateAsync(trader, ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Even if rank is unchanged, rank needs incrementing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,4 +205,52 @@ public class ScoringService : IScoringService
|
|||||||
|
|
||||||
return Math.Min(Math.Round(timeSpread + consistencyScore, 2), 100);
|
return Math.Min(Math.Round(timeSpread + consistencyScore, 2), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private decimal CalculateCopytradingScore(Trader trader, IReadOnlyList<Trade> trades)
|
||||||
|
{
|
||||||
|
if (trades.Count == 0) return 0;
|
||||||
|
|
||||||
|
decimal score = 100;
|
||||||
|
|
||||||
|
// 1. Bot/Scalper Penalty
|
||||||
|
if (trader.IsSuspectedBot || trader.Strategy == StrategyType.Bot)
|
||||||
|
{
|
||||||
|
score -= 60;
|
||||||
|
}
|
||||||
|
else if (trader.Strategy == StrategyType.Scalper)
|
||||||
|
{
|
||||||
|
score -= 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Volume/Slippage Penalty
|
||||||
|
var avgAmount = trades.Average(t => t.Amount);
|
||||||
|
if (avgAmount > 10000)
|
||||||
|
{
|
||||||
|
score -= 20;
|
||||||
|
}
|
||||||
|
else if (avgAmount > 5000)
|
||||||
|
{
|
||||||
|
score -= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Track Record length reward/penalty
|
||||||
|
if (trader.TotalTrades < 5)
|
||||||
|
{
|
||||||
|
score -= 40;
|
||||||
|
}
|
||||||
|
else if (trader.TotalTrades < 20)
|
||||||
|
{
|
||||||
|
score -= 15;
|
||||||
|
}
|
||||||
|
else if (trader.TotalTrades > 100)
|
||||||
|
{
|
||||||
|
score += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. WinRate contribution
|
||||||
|
var winRateEffect = (trader.WinRate - 50m) * 0.8m;
|
||||||
|
score += winRateEffect;
|
||||||
|
|
||||||
|
return Math.Clamp(Math.Round(score, 2), 0, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Predictalytics.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a historical price snapshot of a market outcome at a specific time.
|
||||||
|
/// Used to calculate timing quality, entry/exit quality, and performance trends.
|
||||||
|
/// </summary>
|
||||||
|
public class MarketOutcomePriceSnapshot
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Foreign key to the market outcome.</summary>
|
||||||
|
public int MarketOutcomeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The timestamp of this price snapshot.</summary>
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The price of the outcome at the given timestamp (0.00 to 1.00).</summary>
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
// Navigation property
|
||||||
|
public MarketOutcome MarketOutcome { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Predictalytics.Domain.Entities;
|
namespace Predictalytics.Domain.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculated priority and quality score for a trader.
|
/// Calculated priority and quality score for a trader.
|
||||||
@@ -29,6 +29,9 @@ public class TraderScore
|
|||||||
/// <summary>Overall rank among all tracked traders.</summary>
|
/// <summary>Overall rank among all tracked traders.</summary>
|
||||||
public int Rank { get; set; }
|
public int Rank { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Copytrading suitability score (0-100).</summary>
|
||||||
|
public decimal CopytradingScore { get; set; }
|
||||||
|
|
||||||
/// <summary>When this score was last calculated.</summary>
|
/// <summary>When this score was last calculated.</summary>
|
||||||
public DateTime CalculatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CalculatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,6 @@ public interface IMarketRepository
|
|||||||
Task UpdateAsync(Market market, CancellationToken ct = default);
|
Task UpdateAsync(Market market, CancellationToken ct = default);
|
||||||
Task<Market?> GetByIdAsync(int id, CancellationToken ct = default);
|
Task<Market?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||||
Task<IReadOnlyList<Market>> SearchAsync(string query, int take = 20, CancellationToken ct = default);
|
Task<IReadOnlyList<Market>> SearchAsync(string query, int take = 20, CancellationToken ct = default);
|
||||||
|
Task<IReadOnlyList<MarketOutcomePriceSnapshot>> GetPriceSnapshotsAsync(int marketOutcomeId, CancellationToken ct = default);
|
||||||
|
Task SavePriceSnapshotsAsync(int marketOutcomeId, IEnumerable<MarketOutcomePriceSnapshot> snapshots, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public interface IPlatformProvider
|
|||||||
|
|
||||||
/// <summary>Fetch recent trades that occurred on a specific market.</summary>
|
/// <summary>Fetch recent trades that occurred on a specific market.</summary>
|
||||||
Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 50, CancellationToken ct = default);
|
Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 50, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>Fetch historical prices for an outcome token.</summary>
|
||||||
|
Task<IReadOnlyList<MarketOutcomePriceSnapshot>> GetPriceHistoryAsync(string tokenId, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class AppDbContext : DbContext
|
|||||||
public DbSet<TraderAnalytics> TraderAnalytics => Set<TraderAnalytics>();
|
public DbSet<TraderAnalytics> TraderAnalytics => Set<TraderAnalytics>();
|
||||||
public DbSet<MarketAnalytics> MarketAnalytics => Set<MarketAnalytics>();
|
public DbSet<MarketAnalytics> MarketAnalytics => Set<MarketAnalytics>();
|
||||||
public DbSet<TraderPosition> TraderPositions => Set<TraderPosition>();
|
public DbSet<TraderPosition> TraderPositions => Set<TraderPosition>();
|
||||||
|
public DbSet<MarketOutcomePriceSnapshot> MarketOutcomePriceSnapshots => Set<MarketOutcomePriceSnapshot>();
|
||||||
|
|
||||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ public class AppDbContext : DbContext
|
|||||||
e.Property(s => s.CombinedScore).HasPrecision(8, 4);
|
e.Property(s => s.CombinedScore).HasPrecision(8, 4);
|
||||||
e.Property(s => s.VolumeScore).HasPrecision(8, 4);
|
e.Property(s => s.VolumeScore).HasPrecision(8, 4);
|
||||||
e.Property(s => s.TimingScore).HasPrecision(8, 4);
|
e.Property(s => s.TimingScore).HasPrecision(8, 4);
|
||||||
|
e.Property(s => s.CopytradingScore).HasPrecision(8, 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
// WatchlistEntry
|
// WatchlistEntry
|
||||||
@@ -166,5 +168,14 @@ public class AppDbContext : DbContext
|
|||||||
e.HasOne(tp => tp.Trader).WithMany(t => t.Positions).HasForeignKey(tp => tp.TraderId).OnDelete(DeleteBehavior.Cascade);
|
e.HasOne(tp => tp.Trader).WithMany(t => t.Positions).HasForeignKey(tp => tp.TraderId).OnDelete(DeleteBehavior.Cascade);
|
||||||
e.HasOne(tp => tp.MarketOutcome).WithMany().HasForeignKey(tp => tp.MarketOutcomeId).OnDelete(DeleteBehavior.Cascade);
|
e.HasOne(tp => tp.MarketOutcome).WithMany().HasForeignKey(tp => tp.MarketOutcomeId).OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// MarketOutcomePriceSnapshot
|
||||||
|
mb.Entity<MarketOutcomePriceSnapshot>(e =>
|
||||||
|
{
|
||||||
|
e.HasKey(ps => ps.Id);
|
||||||
|
e.HasIndex(ps => new { ps.MarketOutcomeId, ps.Timestamp });
|
||||||
|
e.Property(ps => ps.Price).HasPrecision(10, 6);
|
||||||
|
e.HasOne(ps => ps.MarketOutcome).WithMany().HasForeignKey(ps => ps.MarketOutcomeId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,4 +198,23 @@ public class MarketRepository : IMarketRepository
|
|||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<MarketOutcomePriceSnapshot>> GetPriceSnapshotsAsync(int marketOutcomeId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _db.MarketOutcomePriceSnapshots
|
||||||
|
.Where(ps => ps.MarketOutcomeId == marketOutcomeId)
|
||||||
|
.OrderBy(ps => ps.Timestamp)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SavePriceSnapshotsAsync(int marketOutcomeId, IEnumerable<MarketOutcomePriceSnapshot> snapshots, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var existing = await _db.MarketOutcomePriceSnapshots
|
||||||
|
.Where(ps => ps.MarketOutcomeId == marketOutcomeId)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
_db.MarketOutcomePriceSnapshots.RemoveRange(existing);
|
||||||
|
|
||||||
|
_db.MarketOutcomePriceSnapshots.AddRange(snapshots);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+737
@@ -0,0 +1,737 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Predictalytics.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Predictalytics.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260703091721_AddMarketOutcomePriceSnapshot")]
|
||||||
|
partial class AddMarketOutcomePriceSnapshot
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.11")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRead")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("varchar(4096)");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Severity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("varchar(512)");
|
||||||
|
|
||||||
|
b.Property<int?>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId");
|
||||||
|
|
||||||
|
b.ToTable("Alerts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DbCreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("varchar(4096)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("EventSlug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("varchar(512)");
|
||||||
|
|
||||||
|
b.Property<string>("ImageUrl")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsResolved")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastTradesUpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastUpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Liquidity")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("MarketSlug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("varchar(512)");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PlatformMarketId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("Question")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("ResolutionOutcome")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Volume")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Platform", "PlatformMarketId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Markets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("MarketId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("AverageTradeSize")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("BotActivityScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastCalculatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("UniqueTradersCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("MarketId");
|
||||||
|
|
||||||
|
b.ToTable("MarketAnalytics");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentPrice")
|
||||||
|
.HasPrecision(18, 8)
|
||||||
|
.HasColumnType("decimal(18,8)");
|
||||||
|
|
||||||
|
b.Property<string>("Label")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<int>("MarketId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("OutcomeIndex")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TokenId");
|
||||||
|
|
||||||
|
b.HasIndex("MarketId", "OutcomeIndex")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MarketOutcomes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("MarketOutcomeId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Price")
|
||||||
|
.HasPrecision(10, 6)
|
||||||
|
.HasColumnType("decimal(10,6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MarketOutcomeId", "Timestamp");
|
||||||
|
|
||||||
|
b.ToTable("MarketOutcomePriceSnapshots");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("BaseUrl")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("SettingsJson")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PlatformConfigs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("AssetId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("varchar(80)");
|
||||||
|
|
||||||
|
b.Property<int?>("DbMarketId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExecutedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("MarketId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(66)
|
||||||
|
.HasColumnType("varchar(66)");
|
||||||
|
|
||||||
|
b.Property<int?>("MarketOutcomeId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Outcome")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PlatformTradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Price")
|
||||||
|
.HasPrecision(10, 6)
|
||||||
|
.HasColumnType("decimal(10,6)");
|
||||||
|
|
||||||
|
b.Property<int>("Side")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(14, 6)
|
||||||
|
.HasColumnType("decimal(14,6)");
|
||||||
|
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("TransactionHash")
|
||||||
|
.HasMaxLength(66)
|
||||||
|
.HasColumnType("varchar(66)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetId");
|
||||||
|
|
||||||
|
b.HasIndex("DbMarketId");
|
||||||
|
|
||||||
|
b.HasIndex("ExecutedAt");
|
||||||
|
|
||||||
|
b.HasIndex("MarketOutcomeId");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId");
|
||||||
|
|
||||||
|
b.HasIndex("Platform", "PlatformTradeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Trades");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAutoDiscovered")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsInitialImportComplete")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSuspectedBot")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastApiErrorAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastPolledAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastTradesUpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int?>("ManualPriorityOverride")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PlatformUserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<int>("Strategy")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Tier")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("TotalPnl")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<int>("TotalTrades")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("WinRate")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Platform", "PlatformUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Traders");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastCalculatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("OverallPnL")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("OverallWinRate")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PnL24h")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PnL30d")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PnL7d")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("WinRate24h")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("WinRate30d")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("WinRate7d")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.HasKey("TraderId");
|
||||||
|
|
||||||
|
b.ToTable("TraderAnalytics");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("AvgCost")
|
||||||
|
.HasPrecision(10, 6)
|
||||||
|
.HasColumnType("decimal(10,6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("MarketOutcomeId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("RealizedPnl")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("SharesHeld")
|
||||||
|
.HasPrecision(14, 6)
|
||||||
|
.HasColumnType("decimal(14,6)");
|
||||||
|
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MarketOutcomeId");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId", "MarketOutcomeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TraderPositions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("ActivityScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CalculatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("CombinedScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("QualityScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<int>("Rank")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("TimingScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("VolumeScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TraderScores");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("AlertsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Label")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("WatchlistEntries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||||
|
.WithOne("Analytics")
|
||||||
|
.HasForeignKey("Predictalytics.Domain.Entities.MarketAnalytics", "MarketId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Market");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||||
|
.WithMany("Outcomes")
|
||||||
|
.HasForeignKey("MarketId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Market");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MarketOutcomeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MarketOutcome");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DbMarketId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MarketOutcomeId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithMany("Trades")
|
||||||
|
.HasForeignKey("TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("DbMarket");
|
||||||
|
|
||||||
|
b.Navigation("MarketOutcome");
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithOne("Analytics")
|
||||||
|
.HasForeignKey("Predictalytics.Domain.Entities.TraderAnalytics", "TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MarketOutcomeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithMany("Positions")
|
||||||
|
.HasForeignKey("TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MarketOutcome");
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithOne("CurrentScore")
|
||||||
|
.HasForeignKey("Predictalytics.Domain.Entities.TraderScore", "TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithMany("WatchlistEntries")
|
||||||
|
.HasForeignKey("TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Analytics");
|
||||||
|
|
||||||
|
b.Navigation("Outcomes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Analytics");
|
||||||
|
|
||||||
|
b.Navigation("CurrentScore");
|
||||||
|
|
||||||
|
b.Navigation("Positions");
|
||||||
|
|
||||||
|
b.Navigation("Trades");
|
||||||
|
|
||||||
|
b.Navigation("WatchlistEntries");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Predictalytics.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMarketOutcomePriceSnapshot : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MarketOutcomePriceSnapshots",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
MarketOutcomeId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
Price = table.Column<decimal>(type: "decimal(10,6)", precision: 10, scale: 6, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MarketOutcomePriceSnapshots", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_MarketOutcomePriceSnapshots_MarketOutcomes_MarketOutcomeId",
|
||||||
|
column: x => x.MarketOutcomeId,
|
||||||
|
principalTable: "MarketOutcomes",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MarketOutcomePriceSnapshots_MarketOutcomeId_Timestamp",
|
||||||
|
table: "MarketOutcomePriceSnapshots",
|
||||||
|
columns: new[] { "MarketOutcomeId", "Timestamp" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MarketOutcomePriceSnapshots");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+741
@@ -0,0 +1,741 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Predictalytics.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Predictalytics.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260703091905_AddCopytradingScore")]
|
||||||
|
partial class AddCopytradingScore
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.11")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRead")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("varchar(4096)");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Severity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("varchar(512)");
|
||||||
|
|
||||||
|
b.Property<int?>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId");
|
||||||
|
|
||||||
|
b.ToTable("Alerts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DbCreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("varchar(4096)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("EventSlug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("varchar(512)");
|
||||||
|
|
||||||
|
b.Property<string>("ImageUrl")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsResolved")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastTradesUpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastUpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Liquidity")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("MarketSlug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("varchar(512)");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PlatformMarketId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("Question")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("ResolutionOutcome")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Volume")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Platform", "PlatformMarketId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Markets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("MarketId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("AverageTradeSize")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("BotActivityScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastCalculatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("UniqueTradersCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("MarketId");
|
||||||
|
|
||||||
|
b.ToTable("MarketAnalytics");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("CurrentPrice")
|
||||||
|
.HasPrecision(18, 8)
|
||||||
|
.HasColumnType("decimal(18,8)");
|
||||||
|
|
||||||
|
b.Property<string>("Label")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<int>("MarketId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("OutcomeIndex")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TokenId");
|
||||||
|
|
||||||
|
b.HasIndex("MarketId", "OutcomeIndex")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MarketOutcomes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("MarketOutcomeId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Price")
|
||||||
|
.HasPrecision(10, 6)
|
||||||
|
.HasColumnType("decimal(10,6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MarketOutcomeId", "Timestamp");
|
||||||
|
|
||||||
|
b.ToTable("MarketOutcomePriceSnapshots");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("BaseUrl")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("SettingsJson")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PlatformConfigs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<string>("AssetId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("varchar(80)");
|
||||||
|
|
||||||
|
b.Property<int?>("DbMarketId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExecutedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("MarketId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(66)
|
||||||
|
.HasColumnType("varchar(66)");
|
||||||
|
|
||||||
|
b.Property<int?>("MarketOutcomeId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Outcome")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PlatformTradeId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Price")
|
||||||
|
.HasPrecision(10, 6)
|
||||||
|
.HasColumnType("decimal(10,6)");
|
||||||
|
|
||||||
|
b.Property<int>("Side")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(14, 6)
|
||||||
|
.HasColumnType("decimal(14,6)");
|
||||||
|
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("TransactionHash")
|
||||||
|
.HasMaxLength(66)
|
||||||
|
.HasColumnType("varchar(66)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetId");
|
||||||
|
|
||||||
|
b.HasIndex("DbMarketId");
|
||||||
|
|
||||||
|
b.HasIndex("ExecutedAt");
|
||||||
|
|
||||||
|
b.HasIndex("MarketOutcomeId");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId");
|
||||||
|
|
||||||
|
b.HasIndex("Platform", "PlatformTradeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Trades");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAutoDiscovered")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsInitialImportComplete")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSuspectedBot")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastApiErrorAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastPolledAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastTradesUpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int?>("ManualPriorityOverride")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PlatformUserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)");
|
||||||
|
|
||||||
|
b.Property<int>("Strategy")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Tier")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("TotalPnl")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<int>("TotalTrades")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("WinRate")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Platform", "PlatformUserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Traders");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastCalculatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("OverallPnL")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("OverallWinRate")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PnL24h")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PnL30d")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PnL7d")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("WinRate24h")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("WinRate30d")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("WinRate7d")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.HasKey("TraderId");
|
||||||
|
|
||||||
|
b.ToTable("TraderAnalytics");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("AvgCost")
|
||||||
|
.HasPrecision(10, 6)
|
||||||
|
.HasColumnType("decimal(10,6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("MarketOutcomeId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("RealizedPnl")
|
||||||
|
.HasPrecision(18, 4)
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("SharesHeld")
|
||||||
|
.HasPrecision(14, 6)
|
||||||
|
.HasColumnType("decimal(14,6)");
|
||||||
|
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MarketOutcomeId");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId", "MarketOutcomeId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TraderPositions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("ActivityScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CalculatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal>("CombinedScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("CopytradingScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("QualityScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<int>("Rank")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("TimingScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("VolumeScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("TraderScores");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("AlertsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Label")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("varchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("TraderId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TraderId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("WatchlistEntries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Alert", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||||
|
.WithOne("Analytics")
|
||||||
|
.HasForeignKey("Predictalytics.Domain.Entities.MarketAnalytics", "MarketId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Market");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcome", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||||
|
.WithMany("Outcomes")
|
||||||
|
.HasForeignKey("MarketId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Market");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MarketOutcomeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MarketOutcome");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DbMarketId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MarketOutcomeId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithMany("Trades")
|
||||||
|
.HasForeignKey("TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("DbMarket");
|
||||||
|
|
||||||
|
b.Navigation("MarketOutcome");
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderAnalytics", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithOne("Analytics")
|
||||||
|
.HasForeignKey("Predictalytics.Domain.Entities.TraderAnalytics", "TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MarketOutcomeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithMany("Positions")
|
||||||
|
.HasForeignKey("TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MarketOutcome");
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderScore", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithOne("CurrentScore")
|
||||||
|
.HasForeignKey("Predictalytics.Domain.Entities.TraderScore", "TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.WatchlistEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||||
|
.WithMany("WatchlistEntries")
|
||||||
|
.HasForeignKey("TraderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Trader");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Analytics");
|
||||||
|
|
||||||
|
b.Navigation("Outcomes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trader", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Analytics");
|
||||||
|
|
||||||
|
b.Navigation("CurrentScore");
|
||||||
|
|
||||||
|
b.Navigation("Positions");
|
||||||
|
|
||||||
|
b.Navigation("Trades");
|
||||||
|
|
||||||
|
b.Navigation("WatchlistEntries");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Predictalytics.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddCopytradingScore : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "CopytradingScore",
|
||||||
|
table: "TraderScores",
|
||||||
|
type: "decimal(8,4)",
|
||||||
|
precision: 8,
|
||||||
|
scale: 4,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CopytradingScore",
|
||||||
|
table: "TraderScores");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -213,6 +213,31 @@ namespace Predictalytics.Infrastructure.Migrations
|
|||||||
b.ToTable("MarketOutcomes");
|
b.ToTable("MarketOutcomes");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("MarketOutcomeId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Price")
|
||||||
|
.HasPrecision(10, 6)
|
||||||
|
.HasColumnType("decimal(10,6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MarketOutcomeId", "Timestamp");
|
||||||
|
|
||||||
|
b.ToTable("MarketOutcomePriceSnapshots");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
|
modelBuilder.Entity("Predictalytics.Domain.Entities.PlatformConfig", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -505,6 +530,10 @@ namespace Predictalytics.Infrastructure.Migrations
|
|||||||
.HasPrecision(8, 4)
|
.HasPrecision(8, 4)
|
||||||
.HasColumnType("decimal(8,4)");
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
|
b.Property<decimal>("CopytradingScore")
|
||||||
|
.HasPrecision(8, 4)
|
||||||
|
.HasColumnType("decimal(8,4)");
|
||||||
|
|
||||||
b.Property<decimal>("QualityScore")
|
b.Property<decimal>("QualityScore")
|
||||||
.HasPrecision(8, 4)
|
.HasPrecision(8, 4)
|
||||||
.HasColumnType("decimal(8,4)");
|
.HasColumnType("decimal(8,4)");
|
||||||
@@ -596,6 +625,17 @@ namespace Predictalytics.Infrastructure.Migrations
|
|||||||
b.Navigation("Market");
|
b.Navigation("Market");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketOutcomePriceSnapshot", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MarketOutcomeId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("MarketOutcome");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
modelBuilder.Entity("Predictalytics.Domain.Entities.Trade", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
|
b.HasOne("Predictalytics.Domain.Entities.Market", "DbMarket")
|
||||||
|
|||||||
@@ -39,4 +39,7 @@ public class AzuroProvider : IPlatformProvider
|
|||||||
|
|
||||||
public Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 50, CancellationToken ct = default)
|
public Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 50, CancellationToken ct = default)
|
||||||
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<Trade>>(Array.Empty<Trade>()); }
|
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<Trade>>(Array.Empty<Trade>()); }
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<MarketOutcomePriceSnapshot>> GetPriceHistoryAsync(string tokenId, CancellationToken ct = default)
|
||||||
|
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<MarketOutcomePriceSnapshot>>(Array.Empty<MarketOutcomePriceSnapshot>()); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,6 +222,11 @@ public class LimitlessProvider : IPlatformProvider
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<MarketOutcomePriceSnapshot>> GetPriceHistoryAsync(string tokenId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<MarketOutcomePriceSnapshot>>(Array.Empty<MarketOutcomePriceSnapshot>());
|
||||||
|
}
|
||||||
|
|
||||||
private Market MapLimitlessMarket(LimitlessMarketResponse raw)
|
private Market MapLimitlessMarket(LimitlessMarketResponse raw)
|
||||||
{
|
{
|
||||||
var market = new Market
|
var market = new Market
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Predictalytics.Application.Interfaces;
|
using Predictalytics.Application.Interfaces;
|
||||||
using Predictalytics.Domain.Enums;
|
using Predictalytics.Domain.Enums;
|
||||||
@@ -13,11 +14,13 @@ public class PolymarketApiClient
|
|||||||
{
|
{
|
||||||
private readonly HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
private readonly HttpClient _gammaClient;
|
private readonly HttpClient _gammaClient;
|
||||||
|
private readonly HttpClient _clobClient;
|
||||||
private readonly IRateLimiter _rateLimiter;
|
private readonly IRateLimiter _rateLimiter;
|
||||||
private readonly ILogger<PolymarketApiClient> _logger;
|
private readonly ILogger<PolymarketApiClient> _logger;
|
||||||
|
|
||||||
private const string DataApiBase = "https://data-api.polymarket.com";
|
private const string DataApiBase = "https://data-api.polymarket.com";
|
||||||
private const string GammaApiBase = "https://gamma-api.polymarket.com";
|
private const string GammaApiBase = "https://gamma-api.polymarket.com";
|
||||||
|
private const string ClobApiBase = "https://clob.polymarket.com";
|
||||||
|
|
||||||
public PolymarketApiClient(IHttpClientFactory httpFactory, IRateLimiter rateLimiter, ILogger<PolymarketApiClient> logger)
|
public PolymarketApiClient(IHttpClientFactory httpFactory, IRateLimiter rateLimiter, ILogger<PolymarketApiClient> logger)
|
||||||
{
|
{
|
||||||
@@ -29,6 +32,10 @@ public class PolymarketApiClient
|
|||||||
_gammaClient.BaseAddress = new Uri(GammaApiBase);
|
_gammaClient.BaseAddress = new Uri(GammaApiBase);
|
||||||
_gammaClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
_gammaClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||||
|
|
||||||
|
_clobClient = httpFactory.CreateClient("PolymarketClob");
|
||||||
|
_clobClient.BaseAddress = new Uri(ClobApiBase);
|
||||||
|
_clobClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||||
|
|
||||||
_rateLimiter = rateLimiter;
|
_rateLimiter = rateLimiter;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -161,4 +168,22 @@ public class PolymarketApiClient
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<PriceHistoryEntry>> GetPricesHistoryAsync(string clobTokenId, string interval = "6h", CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var url = $"/prices-history?market={clobTokenId}&interval={interval}";
|
||||||
|
var result = await ExecuteWithRetryAsync<PolymarketPriceHistoryResponse>(_clobClient, url, ct);
|
||||||
|
return result?.History ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PolymarketPriceHistoryResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("history")] public List<PriceHistoryEntry> History { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PriceHistoryEntry
|
||||||
|
{
|
||||||
|
[JsonPropertyName("t")] public long Timestamp { get; set; }
|
||||||
|
[JsonPropertyName("p")] public double Price { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,4 +295,20 @@ public class PolymarketProvider : IPlatformProvider
|
|||||||
|
|
||||||
return TradeSide.Unknown;
|
return TradeSide.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<MarketOutcomePriceSnapshot>> GetPriceHistoryAsync(string tokenId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
using var _ = PlatformLogContext.Push(PlatformName);
|
||||||
|
_logger.LogDebug("Fetching price history for CLOB Token {TokenId}", tokenId);
|
||||||
|
|
||||||
|
var rawHistory = await _api.GetPricesHistoryAsync(tokenId, "6h", ct);
|
||||||
|
_logger.LogInformation("Fetched {Count} price history entries for {TokenId}", rawHistory.Count, tokenId);
|
||||||
|
|
||||||
|
return rawHistory.Select(r => new MarketOutcomePriceSnapshot
|
||||||
|
{
|
||||||
|
Price = (decimal)r.Price,
|
||||||
|
Timestamp = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
||||||
|
MarketOutcomeId = 0
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public static class DependencyInjection
|
|||||||
services.AddHostedService<ReportingWorker>();
|
services.AddHostedService<ReportingWorker>();
|
||||||
services.AddHostedService<TraderCleanupWorker>();
|
services.AddHostedService<TraderCleanupWorker>();
|
||||||
services.AddHostedService<TraderAnalyticsWorker>();
|
services.AddHostedService<TraderAnalyticsWorker>();
|
||||||
|
services.AddHostedService<ScoringAndAlertsWorker>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,14 +154,7 @@ public class PollingWorker : BackgroundService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate scores and evaluate alerts
|
|
||||||
using (var scope = _services.CreateScope())
|
|
||||||
{
|
|
||||||
var scoringService = scope.ServiceProvider.GetRequiredService<IScoringService>();
|
|
||||||
var alertService = scope.ServiceProvider.GetRequiredService<IAlertService>();
|
|
||||||
await scoringService.RecalculateAllScoresAsync(stoppingToken);
|
|
||||||
await alertService.EvaluateAlertsAsync(stoppingToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning("✅ Polling cycle complete. Next in 60s.");
|
_logger.LogWarning("✅ Polling cycle complete. Next in 60s.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Predictalytics.Application.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Predictalytics.Worker.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Background service that periodically recalculates scores and ranks for all traders
|
||||||
|
/// and evaluates system alerts. Decoupled from the 60-second polling cycle.
|
||||||
|
/// </summary>
|
||||||
|
public class ScoringAndAlertsWorker : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly ILogger<ScoringAndAlertsWorker> _logger;
|
||||||
|
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(15);
|
||||||
|
|
||||||
|
public ScoringAndAlertsWorker(IServiceProvider services, ILogger<ScoringAndAlertsWorker> logger)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("📈 ScoringAndAlertsWorker started (recalculation interval: {Interval}m)", _checkInterval.TotalMinutes);
|
||||||
|
await Task.Delay(10000, stoppingToken); // Let system initialize
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("📈 ScoringAndAlertsWorker: Starting recalculation cycle...");
|
||||||
|
|
||||||
|
using (var scope = _services.CreateScope())
|
||||||
|
{
|
||||||
|
var scoringService = scope.ServiceProvider.GetRequiredService<IScoringService>();
|
||||||
|
var alertService = scope.ServiceProvider.GetRequiredService<IAlertService>();
|
||||||
|
|
||||||
|
await scoringService.RecalculateAllScoresAsync(stoppingToken);
|
||||||
|
await alertService.EvaluateAlertsAsync(stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("📈 ScoringAndAlertsWorker: Recalculation cycle complete.");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// App shutting down
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error in ScoringAndAlertsWorker execution cycle");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(_checkInterval, stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("📈 ScoringAndAlertsWorker stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user