Fix API offset limit 400 Bad Request, implement requested UI/UX improvements
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
public class JobRepository : IJobRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public JobRepository(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<BackgroundJob?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.BackgroundJobs.Include(j => j.Trader).FirstOrDefaultAsync(j => j.Id == id, ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<BackgroundJob>> GetAllAsync(int skip = 0, int take = 50, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.BackgroundJobs
|
||||
.Include(j => j.Trader)
|
||||
.OrderByDescending(j => j.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<BackgroundJob?> GetNextPendingJobAsync(JobType type, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.BackgroundJobs
|
||||
.Where(j => j.JobType == type && j.Status == JobStatus.Pending)
|
||||
.OrderBy(j => j.CreatedAt)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
||||
|
||||
public async Task AddAsync(BackgroundJob job, CancellationToken ct = default)
|
||||
{
|
||||
_db.BackgroundJobs.Add(job);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(BackgroundJob job, CancellationToken ct = default)
|
||||
{
|
||||
_db.BackgroundJobs.Update(job);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -141,8 +141,8 @@ public class MarketRepository : IMarketRepository
|
||||
|
||||
if (existingEventsMap.TryGetValue(ev.PlatformEventId, out var existing))
|
||||
{
|
||||
existing.Slug = ev.Slug;
|
||||
existing.Title = ev.Title;
|
||||
existing.Slug = ev.Slug!;
|
||||
existing.Title = ev.Title!;
|
||||
existing.Description = ev.Description;
|
||||
existing.ImageUrl = ev.ImageUrl;
|
||||
existing.Tags = ev.Tags;
|
||||
@@ -164,7 +164,7 @@ public class MarketRepository : IMarketRepository
|
||||
else
|
||||
{
|
||||
market.EventId = existing.Id;
|
||||
market.Event = null; // Prevent EF tracking issue
|
||||
market.Event = null!; // Prevent EF tracking issue
|
||||
existing.Markets.Add(market);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
@@ -65,7 +66,29 @@ public class TraderRepository : ITraderRepository
|
||||
{ _db.Traders.Add(trader); await _db.SaveChangesAsync(ct); }
|
||||
|
||||
public async Task UpdateAsync(Trader trader, CancellationToken ct = default)
|
||||
{ _db.Traders.Update(trader); await _db.SaveChangesAsync(ct); }
|
||||
{
|
||||
_db.Traders.Update(trader);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task UpdateRanksAsync(IEnumerable<(int TraderId, int Rank)> ranks, CancellationToken ct = default)
|
||||
{
|
||||
// Batch update ranks using raw SQL to avoid N+1 and loading entities
|
||||
var sql = new StringBuilder();
|
||||
sql.AppendLine("UPDATE TraderScores SET Rank = CASE TraderId");
|
||||
var ids = new List<int>();
|
||||
foreach (var r in ranks)
|
||||
{
|
||||
sql.AppendLine($"WHEN {r.TraderId} THEN {r.Rank}");
|
||||
ids.Add(r.TraderId);
|
||||
}
|
||||
sql.AppendLine("ELSE Rank END WHERE TraderId IN (" + string.Join(",", ids) + ");");
|
||||
|
||||
if (ids.Count > 0)
|
||||
{
|
||||
await _db.Database.ExecuteSqlRawAsync(sql.ToString(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
+1033
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBackgroundJobs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BackgroundJobs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
JobType = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Status = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
TraderId = table.Column<int>(type: "int", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
StartedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
ErrorMessage = table.Column<string>(type: "varchar(4096)", maxLength: 4096, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BackgroundJobs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BackgroundJobs_Traders_TraderId",
|
||||
column: x => x.TraderId,
|
||||
principalTable: "Traders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BackgroundJobs_JobType",
|
||||
table: "BackgroundJobs",
|
||||
column: "JobType");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BackgroundJobs_Status",
|
||||
table: "BackgroundJobs",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BackgroundJobs_TraderId",
|
||||
table: "BackgroundJobs",
|
||||
column: "TraderId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BackgroundJobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1036
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPositionCheckpoints : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "LastAppliedTradeId",
|
||||
table: "TraderPositions",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastAppliedTradeId",
|
||||
table: "TraderPositions");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1045
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMarketEnhancements : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ClosedAt",
|
||||
table: "Markets",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "FeeRateBps",
|
||||
table: "Markets",
|
||||
type: "decimal(65,30)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsNegRisk",
|
||||
table: "Markets",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClosedAt",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FeeRateBps",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsNegRisk",
|
||||
table: "Markets");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1051
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBankroll : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "CurrentBalance",
|
||||
table: "TraderAnalytics",
|
||||
type: "decimal(65,30)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "EstimatedBankroll",
|
||||
table: "TraderAnalytics",
|
||||
type: "decimal(65,30)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CurrentBalance",
|
||||
table: "TraderAnalytics");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EstimatedBankroll",
|
||||
table: "TraderAnalytics");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,9 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<DateTime?>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("ConditionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
@@ -212,10 +215,16 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Property<int>("EventId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("FeeRateBps")
|
||||
.HasColumnType("decimal(65,30)");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<bool>("IsNegRisk")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
@@ -620,6 +629,12 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("CurrentBalance")
|
||||
.HasColumnType("decimal(65,30)");
|
||||
|
||||
b.Property<decimal>("EstimatedBankroll")
|
||||
.HasColumnType("decimal(65,30)");
|
||||
|
||||
b.Property<DateTime>("LastCalculatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@@ -715,6 +730,9 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
.HasPrecision(10, 6)
|
||||
.HasColumnType("decimal(10,6)");
|
||||
|
||||
b.Property<long>("LastAppliedTradeId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("LastUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
||||
@@ -111,6 +111,16 @@ public class PolymarketApiClient
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch CLOB orderbook for a given token ID.
|
||||
/// </summary>
|
||||
public async Task<OrderBookResponse?> GetOrderBookAsync(string tokenId, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/book?token_id={tokenId}";
|
||||
_logger.LogDebug("Fetching orderbook for token: {TokenId}", tokenId);
|
||||
return await ExecuteWithRetryAsync<OrderBookResponse>(_clobClient, url, "CLOB", ct);
|
||||
}
|
||||
|
||||
private async Task<T?> ExecuteWithRetryAsync<T>(HttpClient client, string url, string endpointGroup, CancellationToken ct, int attempt = 1)
|
||||
{
|
||||
await _rateLimiter.WaitAsync(PlatformType.Polymarket, ct, endpointGroup);
|
||||
|
||||
@@ -138,6 +138,9 @@ public class GammaMarketResponse
|
||||
[JsonPropertyName("active")] public bool Active { get; set; }
|
||||
[JsonPropertyName("resolved")] public bool Resolved { get; set; }
|
||||
[JsonPropertyName("resolution_outcome")] public string? ResolutionOutcome { get; set; }
|
||||
[JsonPropertyName("negRisk")] public bool NegRisk { get; set; }
|
||||
[JsonPropertyName("closedTime")] public string? ClosedTime { get; set; }
|
||||
[JsonPropertyName("takerFee")] [JsonConverter(typeof(FlexibleDoubleConverter))] public double TakerFee { get; set; }
|
||||
|
||||
/// <summary>JSON string of outcomes, e.g. "[\"Yes\", \"No\"]"</summary>
|
||||
[JsonPropertyName("outcomes")] public string? Outcomes { get; set; }
|
||||
@@ -165,6 +168,18 @@ public class GammaEventResponse
|
||||
[JsonPropertyName("markets")] public List<GammaMarketResponse> Markets { get; set; } = [];
|
||||
}
|
||||
|
||||
public class OrderBookResponse
|
||||
{
|
||||
[JsonPropertyName("bids")] public List<OrderBookLevel> Bids { get; set; } = [];
|
||||
[JsonPropertyName("asks")] public List<OrderBookLevel> Asks { get; set; } = [];
|
||||
}
|
||||
|
||||
public class OrderBookLevel
|
||||
{
|
||||
[JsonPropertyName("price")] public string Price { get; set; } = "0";
|
||||
[JsonPropertyName("size")] public string Size { get; set; } = "0";
|
||||
}
|
||||
|
||||
public class GammaTagResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
|
||||
@@ -302,7 +302,10 @@ public class PolymarketProvider : IPlatformProvider
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsResolved = raw.Resolved || raw.Closed,
|
||||
ResolutionOutcome = raw.ResolutionOutcome,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
LastUpdatedAt = DateTime.UtcNow,
|
||||
FeeRateBps = (decimal)(raw.TakerFee * 10000),
|
||||
IsNegRisk = raw.NegRisk,
|
||||
ClosedAt = DateTime.TryParse(raw.ClosedTime, out var mct) ? mct : null
|
||||
};
|
||||
|
||||
// Parse outcomes, prices, and token IDs from JSON strings
|
||||
|
||||
@@ -70,15 +70,14 @@ public class CopytradingEstimator : ICopytradingEstimator
|
||||
var outcome = sampleTrade.MarketOutcome;
|
||||
|
||||
decimal resolutionPrice = 0;
|
||||
bool isResolved = market?.IsResolved ?? false;
|
||||
|
||||
if (isResolved)
|
||||
// Get the final outcome resolution value using the robust matcher
|
||||
var isWinner = sampleTrade.MarketOutcome != null && Predictalytics.Domain.Helpers.MarketOutcomeHelper.IsWinningOutcome(sampleTrade.MarketOutcome, market?.ResolutionOutcome);
|
||||
resolutionPrice = isWinner ? 1.0m : 0.0m;
|
||||
|
||||
// If not resolved or unknown, assume neutral or average price
|
||||
if (!market?.IsResolved ?? true)
|
||||
{
|
||||
resolutionPrice = string.Equals(market?.ResolutionOutcome, sampleTrade.Outcome, StringComparison.OrdinalIgnoreCase) ? 1.0m : 0.0m;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Live market
|
||||
resolutionPrice = outcome?.CurrentPrice ?? e.AveragePrice; // fallback to entry if unknown
|
||||
}
|
||||
|
||||
@@ -201,7 +200,7 @@ public class CopytradingEstimator : ICopytradingEstimator
|
||||
int limit = 1000;
|
||||
int offset = 0;
|
||||
|
||||
while (offset <= 4000)
|
||||
while (offset <= 3000)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -323,7 +322,7 @@ public class CopytradingEstimator : ICopytradingEstimator
|
||||
int limit = 1000;
|
||||
int offset = 0;
|
||||
|
||||
while (offset <= 4000)
|
||||
while (offset <= 3000)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -48,6 +48,13 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
.Where(tp => tp.TraderId == traderId)
|
||||
.ToDictionaryAsync(tp => tp.MarketOutcomeId, ct);
|
||||
|
||||
var analytics = trader.Analytics;
|
||||
if (analytics == null)
|
||||
{
|
||||
analytics = new TraderAnalytics { TraderId = traderId };
|
||||
_db.TraderAnalytics.Add(analytics);
|
||||
}
|
||||
|
||||
var tempPositions = new Dictionary<int, TraderPosition>();
|
||||
|
||||
var cutoff30d = DateTime.UtcNow.AddDays(-30);
|
||||
@@ -57,6 +64,9 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
var realizedPnl30d = 0m;
|
||||
var realizedPnl7d = 0m;
|
||||
var realizedPnl24h = 0m;
|
||||
|
||||
decimal currentBalance = analytics.CurrentBalance;
|
||||
decimal estimatedBankroll = analytics.EstimatedBankroll;
|
||||
|
||||
// Tracks outcomes traded within time frames
|
||||
var tradedOutcomes30d = new HashSet<int>();
|
||||
@@ -89,19 +99,26 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
MarketOutcomeId = outcomeId,
|
||||
SharesHeld = 0,
|
||||
AvgCost = 0,
|
||||
RealizedPnl = 0
|
||||
RealizedPnl = 0,
|
||||
LastAppliedTradeId = 0
|
||||
};
|
||||
}
|
||||
pos.LastUpdatedAt = DateTime.UtcNow;
|
||||
tempPositions[outcomeId] = pos;
|
||||
}
|
||||
|
||||
if (trade.Id <= pos.LastAppliedTradeId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var previousRealizedPnl = pos.RealizedPnl;
|
||||
|
||||
// Apply trade side booking rules
|
||||
switch (trade.Side)
|
||||
{
|
||||
case TradeSide.Buy:
|
||||
currentBalance -= trade.Amount;
|
||||
if (pos.SharesHeld == 0)
|
||||
{
|
||||
pos.AvgCost = trade.Price;
|
||||
@@ -118,6 +135,7 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
break;
|
||||
|
||||
case TradeSide.Sell:
|
||||
currentBalance += trade.Amount;
|
||||
var sizeToSell = Math.Min(trade.Size, pos.SharesHeld);
|
||||
pos.RealizedPnl += sizeToSell * (trade.Price - pos.AvgCost);
|
||||
pos.SharesHeld -= trade.Size;
|
||||
@@ -131,9 +149,10 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
var market = trade.MarketOutcome.Market;
|
||||
var isResolved = market?.IsResolved ?? false;
|
||||
var resolutionOutcome = market?.ResolutionOutcome;
|
||||
var isWinner = isResolved && IsWinningOutcome(trade.MarketOutcome, resolutionOutcome);
|
||||
var isWinner = isResolved && Predictalytics.Domain.Helpers.MarketOutcomeHelper.IsWinningOutcome(trade.MarketOutcome, resolutionOutcome);
|
||||
|
||||
var payout = isWinner ? 1.00m : 0.00m;
|
||||
currentBalance += (pos.SharesHeld * payout);
|
||||
pos.RealizedPnl += pos.SharesHeld * (payout - pos.AvgCost);
|
||||
pos.SharesHeld = 0;
|
||||
pos.AvgCost = 0;
|
||||
@@ -149,15 +168,46 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentBalance < 0 && Math.Abs(currentBalance) > estimatedBankroll)
|
||||
{
|
||||
estimatedBankroll = Math.Abs(currentBalance);
|
||||
}
|
||||
|
||||
pos.LastAppliedTradeId = Math.Max(pos.LastAppliedTradeId, trade.Id);
|
||||
|
||||
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
|
||||
foreach (var pos in tempPositions.Values)
|
||||
{
|
||||
if (pos.SharesHeld > 0 && pos.MarketOutcome?.Market != null)
|
||||
{
|
||||
var market = pos.MarketOutcome.Market;
|
||||
if (market.IsResolved)
|
||||
{
|
||||
var isWinner = Predictalytics.Domain.Helpers.MarketOutcomeHelper.IsWinningOutcome(pos.MarketOutcome, market.ResolutionOutcome);
|
||||
var payout = isWinner ? 1.00m : 0.00m;
|
||||
|
||||
var virtualPnlDelta = pos.SharesHeld * (payout - pos.AvgCost);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Persist new / updated positions and calculate total values
|
||||
decimal totalRealizedPnl = 0;
|
||||
decimal totalUnrealizedPnl = 0;
|
||||
@@ -199,19 +249,17 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
}
|
||||
|
||||
// Update analytics record
|
||||
var analytics = trader.Analytics;
|
||||
if (analytics == null)
|
||||
{
|
||||
analytics = new TraderAnalytics { TraderId = traderId };
|
||||
_db.TraderAnalytics.Add(analytics);
|
||||
}
|
||||
|
||||
// (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;
|
||||
|
||||
// Calculate Win Rate on Market level
|
||||
var (winRateOverall, winRate30d, winRate7d, winRate24h) = CalculateMarketWinRates(trades, tempPositions, cutoff30d, cutoff7d, cutoff24h);
|
||||
|
||||
@@ -336,18 +384,7 @@ public class PositionPnLEngine : IPositionPnLEngine
|
||||
return (winRateOverall, winRate30d, winRate7d, winRate24h);
|
||||
}
|
||||
|
||||
private static bool IsWinningOutcome(MarketOutcome outcome, string? resolutionOutcome)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(resolutionOutcome)) return false;
|
||||
|
||||
if (string.Equals(outcome.Label, resolutionOutcome, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (outcome.Label.EndsWith(" - " + resolutionOutcome, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static Dictionary<(MarketCategory, string), TraderCategoryPerformance> CalculateCategoryPerformances(
|
||||
List<Trade> trades,
|
||||
|
||||
Reference in New Issue
Block a user