Fix Estimator scoring and PnL engine cashflow
This commit is contained in:
@@ -18,6 +18,7 @@ public class AppDbContext : DbContext
|
||||
public DbSet<MarketAnalytics> MarketAnalytics => Set<MarketAnalytics>();
|
||||
public DbSet<TraderPosition> TraderPositions => Set<TraderPosition>();
|
||||
public DbSet<MarketOutcomePriceSnapshot> MarketOutcomePriceSnapshots => Set<MarketOutcomePriceSnapshot>();
|
||||
public DbSet<TraderDailySnapshot> TraderDailySnapshots => Set<TraderDailySnapshot>();
|
||||
public DbSet<TraderCategoryPerformance> TraderCategoryPerformances => Set<TraderCategoryPerformance>();
|
||||
public DbSet<TradeContext> TradeContexts => Set<TradeContext>();
|
||||
public DbSet<BackgroundJob> BackgroundJobs => Set<BackgroundJob>();
|
||||
@@ -119,12 +120,13 @@ public class AppDbContext : DbContext
|
||||
mb.Entity<TraderScore>(e =>
|
||||
{
|
||||
e.HasKey(s => s.Id);
|
||||
e.Property(s => s.ActivityScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.QualityScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.CombinedScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.VolumeScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.TimingScore).HasPrecision(8, 4);
|
||||
e.Property(s => s.CopytradingScore).HasPrecision(8, 4);
|
||||
e.HasIndex(s => s.TraderId).IsUnique();
|
||||
e.Property(s => s.ActivityScore).HasPrecision(5, 2);
|
||||
e.Property(s => s.QualityScore).HasPrecision(5, 2);
|
||||
e.Property(s => s.CombinedScore).HasPrecision(5, 2);
|
||||
e.Property(s => s.VolumeScore).HasPrecision(5, 2);
|
||||
e.Property(s => s.TimingScore).HasPrecision(5, 2);
|
||||
e.HasOne(s => s.Trader).WithOne(t => t.CurrentScore).HasForeignKey<TraderScore>(s => s.TraderId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// WatchlistEntry
|
||||
@@ -213,6 +215,16 @@ public class AppDbContext : DbContext
|
||||
e.HasOne(tp => tp.MarketOutcome).WithMany().HasForeignKey(tp => tp.MarketOutcomeId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// TraderDailySnapshot
|
||||
mb.Entity<TraderDailySnapshot>(e =>
|
||||
{
|
||||
e.HasKey(s => s.Id);
|
||||
e.HasIndex(s => new { s.TraderId, s.Date }).IsUnique();
|
||||
e.Property(s => s.TotalPnl).HasPrecision(18, 4);
|
||||
e.Property(s => s.CurrentBalance).HasPrecision(18, 4);
|
||||
e.HasOne(s => s.Trader).WithMany().HasForeignKey(s => s.TraderId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// MarketOutcomePriceSnapshot
|
||||
mb.Entity<MarketOutcomePriceSnapshot>(e =>
|
||||
{
|
||||
|
||||
@@ -2,13 +2,20 @@ using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
public class TradeRepository : ITradeRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public TradeRepository(AppDbContext db) => _db = db;
|
||||
private readonly Microsoft.Extensions.Logging.ILogger<TradeRepository> _logger;
|
||||
|
||||
public TradeRepository(AppDbContext db, Microsoft.Extensions.Logging.ILogger<TradeRepository> logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Trade?> GetByPlatformTradeIdAsync(PlatformType platform, string platformTradeId, CancellationToken ct = default)
|
||||
=> await _db.Trades.FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformTradeId == platformTradeId, ct);
|
||||
@@ -92,7 +99,8 @@ public class TradeRepository : ITradeRepository
|
||||
parameters.Add(t.IsContextEnriched);
|
||||
}
|
||||
|
||||
await _db.Database.ExecuteSqlRawAsync(sb.ToString(), parameters.ToArray(), ct);
|
||||
var rowsInserted = await _db.Database.ExecuteSqlRawAsync(sb.ToString(), parameters.ToArray(), ct);
|
||||
_logger.LogInformation("Inserted {RowsInserted} trades into the database.", rowsInserted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +149,11 @@ public class TradeRepository : ITradeRepository
|
||||
.Include(t => t.Trader)
|
||||
.Include(t => t.Trader.CurrentScore)
|
||||
.Include(t => t.Trader.WatchlistEntries)
|
||||
.Include(t => t.Trader.Analytics)
|
||||
.Where(t => !t.IsContextEnriched
|
||||
&& t.Platform == PlatformType.Polymarket
|
||||
&& t.AssetId != "")
|
||||
.Where(t => t.Trader.WatchlistEntries.Any() || (t.Trader.CurrentScore != null && t.Trader.CurrentScore.CopytradingScore > 50))
|
||||
.Where(t => t.Trader.WatchlistEntries.Any() || (t.Trader.Analytics != null && t.Trader.Analytics.CopytradingScore > 50))
|
||||
.OrderByDescending(t => t.ExecutedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct);
|
||||
|
||||
+1057
File diff suppressed because it is too large
Load Diff
+43
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTraderPositionHistoryGuards : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsHistoryPruned",
|
||||
table: "TraderPositions",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastTradeExecutedAt",
|
||||
table: "TraderPositions",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
|
||||
// Step 2.1: Global Reset to force re-calculation of all positions and fix corrupted PnL data
|
||||
migrationBuilder.Sql("UPDATE TraderPositions SET LastAppliedTradeId = 0;");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsHistoryPruned",
|
||||
table: "TraderPositions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastTradeExecutedAt",
|
||||
table: "TraderPositions");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1098
File diff suppressed because it is too large
Load Diff
+52
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTraderDailySnapshots : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TraderDailySnapshots",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
TraderId = table.Column<int>(type: "int", nullable: false),
|
||||
Date = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
TotalPnl = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
CurrentBalance = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TraderDailySnapshots", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TraderDailySnapshots_Traders_TraderId",
|
||||
column: x => x.TraderId,
|
||||
principalTable: "Traders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TraderDailySnapshots_TraderId_Date",
|
||||
table: "TraderDailySnapshots",
|
||||
columns: new[] { "TraderId", "Date" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TraderDailySnapshots");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +718,36 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.ToTable("TraderCategoryPerformances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderDailySnapshot", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("CurrentBalance")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("TotalPnl")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TraderId", "Date")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TraderDailySnapshots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -730,9 +760,15 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
.HasPrecision(10, 6)
|
||||
.HasColumnType("decimal(10,6)");
|
||||
|
||||
b.Property<bool>("IsHistoryPruned")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<long>("LastAppliedTradeId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("LastTradeExecutedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("LastUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@@ -970,6 +1006,17 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderDailySnapshot", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany()
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.TraderPosition", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.MarketOutcome", "MarketOutcome")
|
||||
|
||||
@@ -48,6 +48,30 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
.Where(tp => tp.TraderId == traderId)
|
||||
.ToDictionaryAsync(tp => tp.MarketOutcomeId, ct);
|
||||
|
||||
// Pre-flight check: Detect out-of-order unapplied trades (Bug 3) or manually reset checkpoints (Bug 2a)
|
||||
foreach (var pos in existingPositions.Values)
|
||||
{
|
||||
if (pos.IsHistoryPruned) continue; // Cannot reset if history is pruned
|
||||
|
||||
var outcomeTrades = trades.Where(t => t.MarketOutcomeId == pos.MarketOutcomeId).ToList();
|
||||
bool needsReset = pos.LastAppliedTradeId == 0 ||
|
||||
outcomeTrades.Any(t => t.Id > pos.LastAppliedTradeId && pos.LastTradeExecutedAt.HasValue && t.ExecutedAt < pos.LastTradeExecutedAt.Value);
|
||||
|
||||
if (needsReset)
|
||||
{
|
||||
if (pos.LastAppliedTradeId > 0)
|
||||
{
|
||||
_logger.LogInformation("Out-of-order unapplied trade detected for Trader {TraderId} Outcome {OutcomeId}. Resetting position.", traderId, pos.MarketOutcomeId);
|
||||
}
|
||||
|
||||
pos.SharesHeld = 0;
|
||||
pos.AvgCost = 0;
|
||||
pos.RealizedPnl = 0;
|
||||
pos.LastAppliedTradeId = 0;
|
||||
pos.LastTradeExecutedAt = null;
|
||||
}
|
||||
}
|
||||
|
||||
var analytics = trader.Analytics;
|
||||
if (analytics == null)
|
||||
{
|
||||
@@ -60,10 +84,6 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
var cutoff30d = DateTime.UtcNow.AddDays(-30);
|
||||
var cutoff7d = DateTime.UtcNow.AddDays(-7);
|
||||
var cutoff24h = DateTime.UtcNow.AddHours(-24);
|
||||
|
||||
var realizedPnl30d = 0m;
|
||||
var realizedPnl7d = 0m;
|
||||
var realizedPnl24h = 0m;
|
||||
|
||||
decimal currentBalance = analytics.CurrentBalance;
|
||||
decimal estimatedBankroll = analytics.EstimatedBankroll;
|
||||
@@ -97,10 +117,13 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
{
|
||||
TraderId = traderId,
|
||||
MarketOutcomeId = outcomeId,
|
||||
MarketOutcome = trade.MarketOutcome, // Bug 8 Fix: Assign immediately so virtual payout runs
|
||||
SharesHeld = 0,
|
||||
AvgCost = 0,
|
||||
RealizedPnl = 0,
|
||||
LastAppliedTradeId = 0
|
||||
LastAppliedTradeId = 0,
|
||||
LastTradeExecutedAt = null,
|
||||
IsHistoryPruned = false
|
||||
};
|
||||
}
|
||||
pos.LastUpdatedAt = DateTime.UtcNow;
|
||||
@@ -160,6 +183,25 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
|
||||
case TradeSide.Split:
|
||||
case TradeSide.Merge:
|
||||
var cashEquivalent = Math.Abs(trade.Size) * trade.Price;
|
||||
if (trade.Size > 0)
|
||||
{
|
||||
currentBalance -= cashEquivalent;
|
||||
var totalCost = (pos.SharesHeld * pos.AvgCost) + cashEquivalent;
|
||||
var totalShares = pos.SharesHeld + trade.Size;
|
||||
pos.AvgCost = totalShares > 0 ? totalCost / totalShares : 0;
|
||||
pos.SharesHeld = totalShares;
|
||||
}
|
||||
else if (trade.Size < 0)
|
||||
{
|
||||
var absSize = Math.Abs(trade.Size);
|
||||
currentBalance += cashEquivalent;
|
||||
var splitSizeToSell = Math.Min(absSize, pos.SharesHeld);
|
||||
pos.RealizedPnl += splitSizeToSell * (trade.Price - pos.AvgCost);
|
||||
pos.SharesHeld -= absSize;
|
||||
if (pos.SharesHeld < 0) pos.SharesHeld = 0;
|
||||
}
|
||||
break;
|
||||
case TradeSide.AddLiquidity:
|
||||
case TradeSide.RemoveLiquidity:
|
||||
case TradeSide.Unknown:
|
||||
@@ -174,15 +216,12 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
}
|
||||
|
||||
pos.LastAppliedTradeId = Math.Max(pos.LastAppliedTradeId, trade.Id);
|
||||
if (pos.LastTradeExecutedAt == null || trade.ExecutedAt > pos.LastTradeExecutedAt.Value)
|
||||
{
|
||||
pos.LastTradeExecutedAt = trade.ExecutedAt;
|
||||
}
|
||||
|
||||
var realizedPnlDelta = pos.RealizedPnl - previousRealizedPnl;
|
||||
if (realizedPnlDelta != 0)
|
||||
{
|
||||
// This will only accumulate deltas for NEW trades.
|
||||
if (trade.ExecutedAt >= cutoff30d) realizedPnl30d += realizedPnlDelta;
|
||||
if (trade.ExecutedAt >= cutoff7d) realizedPnl7d += realizedPnlDelta;
|
||||
if (trade.ExecutedAt >= cutoff24h) realizedPnl24h += realizedPnlDelta;
|
||||
}
|
||||
}
|
||||
|
||||
// Bug 6: Virtual payout for unredeemed winning positions
|
||||
@@ -200,10 +239,6 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
pos.RealizedPnl += virtualPnlDelta;
|
||||
pos.SharesHeld = 0;
|
||||
pos.AvgCost = 0;
|
||||
|
||||
if (market.ClosedAt.HasValue && market.ClosedAt.Value >= cutoff30d) realizedPnl30d += virtualPnlDelta;
|
||||
if (market.ClosedAt.HasValue && market.ClosedAt.Value >= cutoff7d) realizedPnl7d += virtualPnlDelta;
|
||||
if (market.ClosedAt.HasValue && market.ClosedAt.Value >= cutoff24h) realizedPnl24h += virtualPnlDelta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,9 +246,6 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
// Persist new / updated positions and calculate total values
|
||||
decimal totalRealizedPnl = 0;
|
||||
decimal totalUnrealizedPnl = 0;
|
||||
decimal unrealizedPnl30d = 0;
|
||||
decimal unrealizedPnl7d = 0;
|
||||
decimal unrealizedPnl24h = 0;
|
||||
|
||||
foreach (var pos in tempPositions.Values)
|
||||
{
|
||||
@@ -222,10 +254,6 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
{
|
||||
var unrealized = pos.SharesHeld * (outcome.CurrentPrice - pos.AvgCost);
|
||||
totalUnrealizedPnl += unrealized;
|
||||
|
||||
if (tradedOutcomes30d.Contains(pos.MarketOutcomeId)) unrealizedPnl30d += unrealized;
|
||||
if (tradedOutcomes7d.Contains(pos.MarketOutcomeId)) unrealizedPnl7d += unrealized;
|
||||
if (tradedOutcomes24h.Contains(pos.MarketOutcomeId)) unrealizedPnl24h += unrealized;
|
||||
}
|
||||
totalRealizedPnl += pos.RealizedPnl;
|
||||
|
||||
@@ -239,27 +267,60 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
}
|
||||
}
|
||||
|
||||
// Remove positions for outcomes that have no trades anymore
|
||||
foreach (var outcomeId in existingPositions.Keys)
|
||||
{
|
||||
if (!tempPositions.ContainsKey(outcomeId))
|
||||
{
|
||||
_db.TraderPositions.Remove(existingPositions[outcomeId]);
|
||||
}
|
||||
}
|
||||
// Bug 7: Positions are intentionally kept even if their trades are pruned by retention policies.
|
||||
|
||||
// Update analytics record
|
||||
// (Analytics instance is already retrieved at the top of this method)
|
||||
|
||||
var overallPnl = totalRealizedPnl + totalUnrealizedPnl;
|
||||
analytics.OverallPnL = overallPnl;
|
||||
analytics.PnL30d = realizedPnl30d + unrealizedPnl30d;
|
||||
analytics.PnL7d = realizedPnl7d + unrealizedPnl7d;
|
||||
analytics.PnL24h = realizedPnl24h + unrealizedPnl24h;
|
||||
|
||||
analytics.CurrentBalance = currentBalance;
|
||||
analytics.EstimatedBankroll = estimatedBankroll;
|
||||
|
||||
// Bug 1: Calculate time windows based on historical snapshots
|
||||
var today = DateTime.UtcNow.Date;
|
||||
|
||||
// Save today's snapshot
|
||||
var todaySnapshot = await _db.TraderDailySnapshots
|
||||
.FirstOrDefaultAsync(s => s.TraderId == traderId && s.Date == today, ct);
|
||||
|
||||
if (todaySnapshot == null)
|
||||
{
|
||||
todaySnapshot = new TraderDailySnapshot
|
||||
{
|
||||
TraderId = traderId,
|
||||
Date = today,
|
||||
TotalPnl = overallPnl,
|
||||
CurrentBalance = currentBalance
|
||||
};
|
||||
_db.TraderDailySnapshots.Add(todaySnapshot);
|
||||
}
|
||||
else
|
||||
{
|
||||
todaySnapshot.TotalPnl = overallPnl;
|
||||
todaySnapshot.CurrentBalance = currentBalance;
|
||||
}
|
||||
|
||||
// Fetch historical snapshots
|
||||
var snapshot24h = await _db.TraderDailySnapshots
|
||||
.Where(s => s.TraderId == traderId && s.Date <= today.AddDays(-1))
|
||||
.OrderByDescending(s => s.Date)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
var snapshot7d = await _db.TraderDailySnapshots
|
||||
.Where(s => s.TraderId == traderId && s.Date <= today.AddDays(-7))
|
||||
.OrderByDescending(s => s.Date)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
var snapshot30d = await _db.TraderDailySnapshots
|
||||
.Where(s => s.TraderId == traderId && s.Date <= today.AddDays(-30))
|
||||
.OrderByDescending(s => s.Date)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
analytics.PnL24h = overallPnl - (snapshot24h?.TotalPnl ?? 0);
|
||||
analytics.PnL7d = overallPnl - (snapshot7d?.TotalPnl ?? 0);
|
||||
analytics.PnL30d = overallPnl - (snapshot30d?.TotalPnl ?? 0);
|
||||
|
||||
// Calculate Win Rate on Market level
|
||||
var (winRateOverall, winRate30d, winRate7d, winRate24h) = CalculateMarketWinRates(trades, tempPositions, cutoff30d, cutoff7d, cutoff24h);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user