Enhance UI, add AI integration, improve logging and database stats
This commit is contained in:
@@ -35,6 +35,12 @@ public static class TraderEndpoints
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
group.MapPost("/{id:int}/ai-analysis", async (int id, bool manual, IAiStrategyAnalysisService aiSvc, CancellationToken ct) =>
|
||||
{
|
||||
var summary = await aiSvc.AnalyzeTraderStrategyAsync(id, manual, ct);
|
||||
return Results.Ok(new { summary });
|
||||
});
|
||||
|
||||
group.MapPost("/", async (string platform, string wallet, IAnalyticsService svc, CancellationToken ct) =>
|
||||
{
|
||||
var id = await svc.AddTraderAsync(platform, wallet, ct);
|
||||
|
||||
@@ -188,7 +188,8 @@
|
||||
<button class="btn-sm" id="btn-refresh-trader" style="padding:8px 16px; background:var(--bg-input)">Sync History</button>
|
||||
</div>
|
||||
<div class="detail-grid">
|
||||
<div class="card detail-sidebar">
|
||||
<div class="detail-sidebar">
|
||||
<div class="card">
|
||||
<div class="stat-group">
|
||||
<div class="stat-label">Platform</div>
|
||||
<div class="stat-value" id="td-platform">—</div>
|
||||
@@ -206,6 +207,16 @@
|
||||
<div class="stat-value" id="td-strategy">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="margin-top: 16px;">
|
||||
<div class="stat-group">
|
||||
<div class="stat-label" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
AI Analysis
|
||||
<button class="btn-sm" id="btn-ai-analysis" style="padding:4px 8px; font-size:12px; background:var(--bg-input);">Run Analysis</button>
|
||||
</div>
|
||||
<div class="stat-value small" id="td-ai-summary" style="margin-top: 8px; font-weight:normal; line-height:1.4;">Not analyzed yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-main">
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card"><div class="metric-label">Win Rate</div><div class="metric-value" id="td-winrate">—</div></div>
|
||||
|
||||
@@ -149,7 +149,12 @@ async function api(endpoint) {
|
||||
// ─── Format Helpers ───
|
||||
const fmt = {
|
||||
usd: v => { if (v === null || v === undefined) return '$0'; const n = Number(v); return n >= 1000000 ? `$${(n/1000000).toFixed(1)}M` : n >= 1000 ? `$${(n/1000).toFixed(1)}K` : `$${n.toFixed(0)}`; },
|
||||
pct: v => { if (v === null || v === undefined) return '0%'; return `${Number(v).toFixed(1)}%`; },
|
||||
pct: v => {
|
||||
if (v === null || v === undefined) return '0%';
|
||||
const n = Number(v);
|
||||
const color = n > 55 ? 'var(--pnl-positive)' : n < 45 ? 'var(--pnl-negative)' : 'var(--text)';
|
||||
return `<span style="color:${color};font-weight:${n>55||n<45?'600':'normal'};">${n.toFixed(1)}%</span>`;
|
||||
},
|
||||
num: v => { if (v === null || v === undefined) return '0'; return Number(v).toLocaleString(); },
|
||||
time: v => { if (!v) return '—'; const d = new Date(v); const now = new Date(); const diff = (now - d) / 1000;
|
||||
if (diff < 60) return `${Math.floor(diff)}s ago`;
|
||||
@@ -207,15 +212,15 @@ async function loadDashboard() {
|
||||
// Top Traders table
|
||||
const tbody = document.getElementById('topTradersBody');
|
||||
tbody.innerHTML = data.topTraders.map((t, i) => `
|
||||
<tr onclick="viewTrader(${t.id})" style="cursor:pointer">
|
||||
<td>${i + 1}</td>
|
||||
<td><strong>${t.displayName}</strong></td>
|
||||
<td>${t.platform}</td>
|
||||
<td><strong>${Number(t.combinedScore).toFixed(1)}</strong></td>
|
||||
<td>${fmt.pct(t.winRate)}</td>
|
||||
<td>${fmt.pnl(t.totalPnl)}</td>
|
||||
<td>${fmt.tier(t.tier)}</td>
|
||||
<td>${fmt.num(t.totalTrades)}</td>
|
||||
<tr>
|
||||
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${i + 1}</td>
|
||||
<td><strong>${t.platform === 'Polymarket' ? `<a href="https://polymarket.com/profile/${t.platformUserId}" target="_blank" style="color:var(--text);text-decoration:underline;">${t.displayName}</a>` : t.displayName}</strong></td>
|
||||
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${t.platform}</td>
|
||||
<td onclick="viewTrader(${t.id})" style="cursor:pointer"><strong>${Number(t.combinedScore).toFixed(1)}</strong></td>
|
||||
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.pct(t.winRate)}</td>
|
||||
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.pnl(t.totalPnl)}</td>
|
||||
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.tier(t.tier)}</td>
|
||||
<td onclick="viewTrader(${t.id})" style="cursor:pointer">${fmt.num(t.totalTrades)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
@@ -282,7 +287,7 @@ async function loadTraders() {
|
||||
tbody.innerHTML = data.map((t, i) => `
|
||||
<tr>
|
||||
<td>${i + 1}</td>
|
||||
<td><strong>${t.displayName}</strong></td>
|
||||
<td><strong>${t.platform === 'Polymarket' ? `<a href="https://polymarket.com/profile/${t.platformUserId}" target="_blank" style="color:var(--text);text-decoration:underline;">${t.displayName}</a>` : t.displayName}</strong></td>
|
||||
<td>${t.platform}</td>
|
||||
<td><strong>${Number(t.combinedScore).toFixed(1)}</strong></td>
|
||||
<td>${fmt.pct(t.winRate)}</td>
|
||||
@@ -350,10 +355,14 @@ async function viewTrader(id) {
|
||||
document.getElementById('td-pnl').innerHTML = fmt.pnl(t.totalPnl);
|
||||
document.getElementById('td-trades').textContent = fmt.num(t.totalTrades);
|
||||
document.getElementById('td-score').textContent = Number(t.combinedScore).toFixed(1);
|
||||
document.getElementById('td-ai-summary').textContent = t.aiStrategySummary || 'Not analyzed yet.';
|
||||
|
||||
const refreshBtn = document.getElementById('btn-refresh-trader');
|
||||
refreshBtn.onclick = () => manualUpdateTrader(id);
|
||||
|
||||
const aiBtn = document.getElementById('btn-ai-analysis');
|
||||
aiBtn.onclick = () => triggerAiAnalysis(id, true);
|
||||
|
||||
const tbody = document.getElementById('td-tradesBody');
|
||||
tbody.innerHTML = t.recentTrades.map(tr => `
|
||||
<tr>
|
||||
@@ -428,3 +437,24 @@ setInterval(() => {
|
||||
const activePage = document.querySelector('.page.active');
|
||||
if (activePage?.id === 'page-dashboard') loadDashboard();
|
||||
}, 30000);
|
||||
|
||||
async function triggerAiAnalysis(id, manual) {
|
||||
const btn = document.getElementById('btn-ai-analysis');
|
||||
const oldText = btn.textContent;
|
||||
btn.textContent = 'Analyzing...';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await api(`/api/traders/${id}/ai-analysis?manual=${manual}`, { method: 'POST' });
|
||||
if (res && res.summary) {
|
||||
document.getElementById('td-ai-summary').textContent = res.summary;
|
||||
} else {
|
||||
alert('Analysis failed or returned empty.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Analysis error: ' + e);
|
||||
} finally {
|
||||
btn.textContent = oldText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class AnalyticsServiceTests
|
||||
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
|
||||
db.Traders.Add(trader);
|
||||
|
||||
var market = new Market { Id = 10, PlatformMarketId = "pm1", Question = "Q?" };
|
||||
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?" };
|
||||
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.50m };
|
||||
market.Outcomes.Add(outcome);
|
||||
db.Markets.Add(market);
|
||||
|
||||
@@ -28,7 +28,7 @@ public class PositionPnLEngineTests
|
||||
var pnlEngine = new PositionPnLEngine(db, NullLogger<PositionPnLEngine>.Instance);
|
||||
|
||||
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
|
||||
var market = new Market { Id = 10, PlatformMarketId = "pm1", Question = "Q?" };
|
||||
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?" };
|
||||
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.60m };
|
||||
market.Outcomes.Add(outcome);
|
||||
|
||||
@@ -69,7 +69,7 @@ public class PositionPnLEngineTests
|
||||
var pnlEngine = new PositionPnLEngine(db, NullLogger<PositionPnLEngine>.Instance);
|
||||
|
||||
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
|
||||
var market = new Market { Id = 10, PlatformMarketId = "pm1", Question = "Q?" };
|
||||
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?" };
|
||||
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.60m };
|
||||
market.Outcomes.Add(outcome);
|
||||
|
||||
@@ -113,7 +113,7 @@ public class PositionPnLEngineTests
|
||||
var pnlEngine = new PositionPnLEngine(db, NullLogger<PositionPnLEngine>.Instance);
|
||||
|
||||
var trader = new Trader { Id = 1, PlatformUserId = "0x1", DisplayName = "Trader 1" };
|
||||
var market = new Market { Id = 10, PlatformMarketId = "pm1", Question = "Q?", IsResolved = true, ResolutionOutcome = "Yes" };
|
||||
var market = new Market { Id = 10, PlatformMarketId = 1L, Question = "Q?", IsResolved = true, ResolutionOutcome = "Yes" };
|
||||
var outcome = new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 1.00m };
|
||||
market.Outcomes.Add(outcome);
|
||||
|
||||
|
||||
@@ -42,5 +42,6 @@ public record TraderDetailDto(
|
||||
bool IsOnWatchlist,
|
||||
DateTime CreatedAt,
|
||||
DateTime? LastPolledAt,
|
||||
string? AiStrategySummary,
|
||||
IReadOnlyList<TradeDto> RecentTrades
|
||||
);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Predictalytics.Application.Interfaces;
|
||||
|
||||
public interface IAiStrategyAnalysisService
|
||||
{
|
||||
Task<string> AnalyzeTraderStrategyAsync(int traderId, bool manual, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Predictalytics.Application.Interfaces;
|
||||
|
||||
public interface IOpenRouterApiClient
|
||||
{
|
||||
Task<string> GenerateChatCompletionAsync(string prompt, bool useManualModel = false, CancellationToken ct = default);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ public interface IPlatformStatisticsService
|
||||
void TrackMarketSync(PlatformType platform, int count = 1);
|
||||
void TrackTraderDiscovery(PlatformType platform, int count = 1);
|
||||
void TrackTradeActivity(PlatformType platform, int count = 1);
|
||||
void TrackDuplicateError(PlatformType platform, int count = 1);
|
||||
|
||||
Dictionary<PlatformType, PlatformStats> GetAndResetStats();
|
||||
}
|
||||
@@ -16,4 +17,5 @@ public class PlatformStats
|
||||
public int MarketsSynced { get; set; }
|
||||
public int TradersDiscovered { get; set; }
|
||||
public int TradesProcessed { get; set; }
|
||||
public int DuplicateErrors { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ namespace Predictalytics.Application.Interfaces;
|
||||
public interface IRateLimiter
|
||||
{
|
||||
/// <summary>Wait until a request can be made to the given platform.</summary>
|
||||
Task WaitAsync(PlatformType platform, CancellationToken ct = default);
|
||||
Task WaitAsync(PlatformType platform, CancellationToken ct = default, string endpointGroup = "Default");
|
||||
|
||||
/// <summary>Check if a request can be made immediately.</summary>
|
||||
bool CanMakeRequest(PlatformType platform);
|
||||
bool CanMakeRequest(PlatformType platform, string endpointGroup = "Default");
|
||||
|
||||
/// <summary>Report that a 429 Too Many Requests was received.</summary>
|
||||
void ReportRateLimitExceeded(PlatformType platform, TimeSpan? retryAfter = null);
|
||||
void ReportRateLimitExceeded(PlatformType platform, TimeSpan? retryAfter = null, string endpointGroup = "Default");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Predictalytics.Application.Interfaces;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
|
||||
namespace Predictalytics.Application.Services;
|
||||
|
||||
public class AiStrategyAnalysisService : IAiStrategyAnalysisService
|
||||
{
|
||||
private readonly ITraderRepository _traderRepo;
|
||||
private readonly ITradeRepository _tradeRepo;
|
||||
private readonly IOpenRouterApiClient _openRouter;
|
||||
private readonly ILogger<AiStrategyAnalysisService> _logger;
|
||||
|
||||
public AiStrategyAnalysisService(
|
||||
ITraderRepository traderRepo,
|
||||
ITradeRepository tradeRepo,
|
||||
IOpenRouterApiClient openRouter,
|
||||
ILogger<AiStrategyAnalysisService> logger)
|
||||
{
|
||||
_traderRepo = traderRepo;
|
||||
_tradeRepo = tradeRepo;
|
||||
_openRouter = openRouter;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<string> AnalyzeTraderStrategyAsync(int traderId, bool manual, CancellationToken ct = default)
|
||||
{
|
||||
var trader = await _traderRepo.GetByIdAsync(traderId, ct);
|
||||
if (trader == null) return "Trader not found.";
|
||||
|
||||
var trades = await _tradeRepo.GetByTraderIdAsync(traderId, 0, 500, ct);
|
||||
|
||||
var prompt = BuildTraderContext(trader, trades);
|
||||
|
||||
_logger.LogInformation("Sending {Model} AI analysis request for trader {TraderName}", manual ? "Manual (Claude)" : "Auto (Gemini)", trader.DisplayName);
|
||||
|
||||
var result = await _openRouter.GenerateChatCompletionAsync(prompt, manual, ct);
|
||||
|
||||
trader.AiStrategySummary = result;
|
||||
trader.AiStrategyUpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _traderRepo.UpdateAsync(trader, ct);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string BuildTraderContext(Trader trader, IReadOnlyList<Trade> trades)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Analyze the following prediction market trader.");
|
||||
sb.AppendLine($"Name: {trader.DisplayName}");
|
||||
sb.AppendLine($"Platform: {trader.Platform}");
|
||||
sb.AppendLine($"Total PnL: ${trader.TotalPnl:F2}");
|
||||
sb.AppendLine($"Win Rate: {trader.WinRate:F2}%");
|
||||
sb.AppendLine($"Total Trades: {trader.TotalTrades}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("Recent Trades:");
|
||||
foreach (var trade in trades.OrderByDescending(t => t.ExecutedAt).Take(50))
|
||||
{
|
||||
sb.AppendLine($"- {trade.ExecutedAt:yyyy-MM-dd}: {trade.Side} {trade.Size:F0} shares of '{trade.Outcome}' @ ${trade.Price:F2} (Total: ${trade.Amount:F2})");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Based on these stats and recent trades, provide a concise summary of their strategy (e.g. Value investor, Arbitrageur, News-driven, Degen). Highlight their main strengths and weaknesses. Keep it under 100 words.");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,7 @@ public class AnalyticsService : IAnalyticsService
|
||||
trader.WinRate, trader.TotalPnl, trader.TotalTrades,
|
||||
s?.ActivityScore ?? 0, s?.QualityScore ?? 0, s?.VolumeScore ?? 0, s?.TimingScore ?? 0,
|
||||
s?.CombinedScore ?? 0, s?.CopytradingScore ?? 0, s?.Rank ?? 0, wl != null, trader.CreatedAt, trader.LastPolledAt,
|
||||
trader.AiStrategySummary,
|
||||
trades.Select(MapTradeDto).ToList());
|
||||
}
|
||||
|
||||
@@ -204,14 +205,14 @@ public class AnalyticsService : IAnalyticsService
|
||||
if (recentTrades.Count == 0)
|
||||
{
|
||||
// Fallback: trades ingested before DbMarketId backfill
|
||||
recentTrades = await _tradeRepo.GetByMarketIdAsync(market.PlatformMarketId, 0, 50, ct);
|
||||
recentTrades = await _tradeRepo.GetByMarketIdAsync(market.ConditionId, 0, 50, ct);
|
||||
}
|
||||
|
||||
return new MarketDetailDto
|
||||
{
|
||||
Id = market.Id,
|
||||
Platform = market.Platform.ToString(),
|
||||
PlatformMarketId = market.PlatformMarketId,
|
||||
PlatformMarketId = market.ConditionId,
|
||||
Question = market.Question,
|
||||
Description = market.Description,
|
||||
Category = market.Category,
|
||||
|
||||
@@ -26,6 +26,12 @@ public class PlatformStatisticsService : IPlatformStatisticsService
|
||||
lock (stats) stats.TradesProcessed += count;
|
||||
}
|
||||
|
||||
public void TrackDuplicateError(PlatformType platform, int count = 1)
|
||||
{
|
||||
var stats = _stats.GetOrAdd(platform, _ => new PlatformStats());
|
||||
lock (stats) stats.DuplicateErrors += count;
|
||||
}
|
||||
|
||||
public Dictionary<PlatformType, PlatformStats> GetAndResetStats()
|
||||
{
|
||||
var result = new Dictionary<PlatformType, PlatformStats>();
|
||||
|
||||
@@ -9,30 +9,35 @@ namespace Predictalytics.Application.Services;
|
||||
/// </summary>
|
||||
public class RateLimiterService : IRateLimiter
|
||||
{
|
||||
private readonly ConcurrentDictionary<PlatformType, SemaphoreSlim> _semaphores = new();
|
||||
private readonly ConcurrentDictionary<PlatformType, DateTime> _lastRequest = new();
|
||||
private readonly ConcurrentDictionary<PlatformType, DateTime> _blockedUntil = new();
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphores = new();
|
||||
private readonly ConcurrentDictionary<string, DateTime> _lastRequest = new();
|
||||
private readonly ConcurrentDictionary<string, DateTime> _blockedUntil = new();
|
||||
|
||||
// Minimum delay between requests per platform (milliseconds)
|
||||
private static readonly Dictionary<PlatformType, int> PlatformDelays = new()
|
||||
private static readonly Dictionary<string, int> Delays = new()
|
||||
{
|
||||
{ PlatformType.Polymarket, 200 },
|
||||
{ PlatformType.Limitless, 500 },
|
||||
{ PlatformType.Azuro, 1000 },
|
||||
{ PlatformType.Myriad, 1000 },
|
||||
{ PlatformType.PredictFun, 1000 },
|
||||
{ PlatformType.Kalshi, 500 },
|
||||
{ PlatformType.Stake, 1000 }
|
||||
{ "Polymarket-Gamma", 35 }, // ~28 req/s (Gamma /markets limit is 30/s)
|
||||
{ "Polymarket-Data", 55 }, // ~18 req/s (Data /trades limit is 20/s)
|
||||
{ "Polymarket-Clob", 15 }, // ~66 req/s (Clob /prices limit is 100/s)
|
||||
{ "Polymarket-Default", 0 }, // Bypass worker-level limiting for Polymarket, handled in ApiClient
|
||||
|
||||
{ "Limitless-Default", 500 },
|
||||
{ "Azuro-Default", 1000 },
|
||||
{ "Myriad-Default", 1000 },
|
||||
{ "PredictFun-Default", 1000 },
|
||||
{ "Kalshi-Default", 500 },
|
||||
{ "Stake-Default", 1000 }
|
||||
};
|
||||
|
||||
public async Task WaitAsync(PlatformType platform, CancellationToken ct = default)
|
||||
public async Task WaitAsync(PlatformType platform, CancellationToken ct = default, string endpointGroup = "Default")
|
||||
{
|
||||
var sem = _semaphores.GetOrAdd(platform, _ => new SemaphoreSlim(1, 1));
|
||||
var key = $"{platform}-{endpointGroup}";
|
||||
if (!Delays.ContainsKey(key)) key = $"{platform}-Default";
|
||||
|
||||
var sem = _semaphores.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
await sem.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
// 1. Check if we are currently blocked due to a 429
|
||||
if (_blockedUntil.TryGetValue(platform, out var blockedUntil))
|
||||
if (_blockedUntil.TryGetValue(key, out var blockedUntil))
|
||||
{
|
||||
var waitTime = blockedUntil - DateTime.UtcNow;
|
||||
if (waitTime > TimeSpan.Zero)
|
||||
@@ -41,32 +46,37 @@ public class RateLimiterService : IRateLimiter
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Normal token bucket delay
|
||||
if (_lastRequest.TryGetValue(platform, out var last))
|
||||
if (_lastRequest.TryGetValue(key, out var last))
|
||||
{
|
||||
var delayMs = PlatformDelays.GetValueOrDefault(platform, 1000);
|
||||
var delayMs = Delays.GetValueOrDefault(key, 1000);
|
||||
var elapsed = (DateTime.UtcNow - last).TotalMilliseconds;
|
||||
if (elapsed < delayMs)
|
||||
await Task.Delay((int)(delayMs - elapsed), ct);
|
||||
}
|
||||
_lastRequest[platform] = DateTime.UtcNow;
|
||||
_lastRequest[key] = DateTime.UtcNow;
|
||||
}
|
||||
finally { sem.Release(); }
|
||||
}
|
||||
|
||||
public bool CanMakeRequest(PlatformType platform)
|
||||
public bool CanMakeRequest(PlatformType platform, string endpointGroup = "Default")
|
||||
{
|
||||
if (_blockedUntil.TryGetValue(platform, out var blockedUntil) && blockedUntil > DateTime.UtcNow)
|
||||
var key = $"{platform}-{endpointGroup}";
|
||||
if (!Delays.ContainsKey(key)) key = $"{platform}-Default";
|
||||
|
||||
if (_blockedUntil.TryGetValue(key, out var blockedUntil) && blockedUntil > DateTime.UtcNow)
|
||||
return false;
|
||||
|
||||
if (!_lastRequest.TryGetValue(platform, out var last)) return true;
|
||||
var delayMs = PlatformDelays.GetValueOrDefault(platform, 1000);
|
||||
if (!_lastRequest.TryGetValue(key, out var last)) return true;
|
||||
var delayMs = Delays.GetValueOrDefault(key, 1000);
|
||||
return (DateTime.UtcNow - last).TotalMilliseconds >= delayMs;
|
||||
}
|
||||
|
||||
public void ReportRateLimitExceeded(PlatformType platform, TimeSpan? retryAfter = null)
|
||||
public void ReportRateLimitExceeded(PlatformType platform, TimeSpan? retryAfter = null, string endpointGroup = "Default")
|
||||
{
|
||||
var key = $"{platform}-{endpointGroup}";
|
||||
if (!Delays.ContainsKey(key)) key = $"{platform}-Default";
|
||||
|
||||
var penalty = retryAfter ?? TimeSpan.FromSeconds(30);
|
||||
_blockedUntil[platform] = DateTime.UtcNow.Add(penalty);
|
||||
_blockedUntil[key] = DateTime.UtcNow.Add(penalty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Predictalytics.Domain.Enums;
|
||||
|
||||
namespace Predictalytics.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a prediction market event, which can contain multiple Markets (Questions).
|
||||
/// </summary>
|
||||
public class Event
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>Platform this event belongs to.</summary>
|
||||
public PlatformType Platform { get; set; }
|
||||
|
||||
/// <summary>Platform-specific numeric identifier for the event.</summary>
|
||||
public long PlatformEventId { get; set; }
|
||||
|
||||
/// <summary>URL-friendly slug for the event.</summary>
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Event title.</summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Detailed event description.</summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>Event image URL.</summary>
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>The time when the event starts.</summary>
|
||||
public DateTime? StartDate { get; set; }
|
||||
|
||||
/// <summary>The time when the event ends.</summary>
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
/// <summary>Comma-separated tags (categories) associated with the event.</summary>
|
||||
public string Tags { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Whether the event is currently active.</summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>Whether the event is closed.</summary>
|
||||
public bool IsClosed { get; set; }
|
||||
|
||||
/// <summary>When this event was created on the platform.</summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>When this record was first saved to our database.</summary>
|
||||
public DateTime DbCreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Last time event data was refreshed.</summary>
|
||||
public DateTime? LastUpdatedAt { get; set; }
|
||||
|
||||
// Navigation
|
||||
public ICollection<Market> Markets { get; set; } = new List<Market>();
|
||||
}
|
||||
@@ -12,15 +12,22 @@ public class Market
|
||||
/// <summary>Platform this market belongs to.</summary>
|
||||
public PlatformType Platform { get; set; }
|
||||
|
||||
/// <summary>Platform-specific market identifier (conditionId on Polymarket).</summary>
|
||||
public string PlatformMarketId { get; set; } = string.Empty;
|
||||
/// <summary>The Event this market belongs to.</summary>
|
||||
public int EventId { get; set; }
|
||||
public virtual Event Event { get; set; }
|
||||
|
||||
/// <summary>Platform-specific numeric market identifier.</summary>
|
||||
public long PlatformMarketId { get; set; }
|
||||
|
||||
/// <summary>Platform-specific blockchain market identifier (conditionId on Polymarket).</summary>
|
||||
public string ConditionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Smart contract question ID.</summary>
|
||||
public string QuestionId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>URL-friendly slug for the market.</summary>
|
||||
public string MarketSlug { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>URL-friendly slug for the parent event.</summary>
|
||||
public string EventSlug { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Detailed market description / resolution criteria.</summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
@@ -36,6 +43,9 @@ public class Market
|
||||
/// <summary>Current total volume traded.</summary>
|
||||
public decimal Volume { get; set; }
|
||||
|
||||
/// <summary>Current 24h volume.</summary>
|
||||
public decimal Volume24h { get; set; }
|
||||
|
||||
/// <summary>Current liquidity.</summary>
|
||||
public decimal Liquidity { get; set; }
|
||||
|
||||
|
||||
@@ -66,6 +66,19 @@ public class Trade
|
||||
/// <summary>Transaction hash (for blockchain-based platforms).</summary>
|
||||
public string? TransactionHash { get; set; }
|
||||
|
||||
// ── Context Enrichment (AI Strategy Detection) ───────────
|
||||
|
||||
/// <summary>Market price 1 minute before trade execution.</summary>
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal? PreTradePrice1m { get; set; }
|
||||
|
||||
/// <summary>Market price 1 minute after trade execution.</summary>
|
||||
[Column(TypeName = "decimal(18,4)")]
|
||||
public decimal? PostTradePrice1m { get; set; }
|
||||
|
||||
/// <summary>Indicates if high-res price context was fetched.</summary>
|
||||
public bool IsContextEnriched { get; set; }
|
||||
|
||||
// ── Transient (not persisted) ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -58,6 +58,12 @@ public class Trader
|
||||
/// <summary>Win rate as a percentage (0-100).</summary>
|
||||
public decimal WinRate { get; set; }
|
||||
|
||||
/// <summary>AI-generated strategy summary based on trade history.</summary>
|
||||
public string? AiStrategySummary { get; set; }
|
||||
|
||||
/// <summary>When the AI strategy summary was last updated.</summary>
|
||||
public DateTime? AiStrategyUpdatedAt { get; set; }
|
||||
|
||||
/// <summary>Total number of trades tracked.</summary>
|
||||
public int TotalTrades { get; set; }
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ public interface IMarketRepository
|
||||
Task<IReadOnlyList<MarketOutcome>> GetOutcomesByTokenIdsAsync(IEnumerable<string> tokenIds, CancellationToken ct = default);
|
||||
Task AddOrUpdateAsync(Market market, CancellationToken ct = default);
|
||||
Task AddOrUpdateRangeAsync(IEnumerable<Market> markets, CancellationToken ct = default);
|
||||
Task AddOrUpdateEventsAsync(IEnumerable<Event> events, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<Market>> GetActiveAsync(int count = 50, CancellationToken ct = default);
|
||||
Task<int> GetCountAsync(CancellationToken ct = default);
|
||||
Task<IReadOnlyList<Market>> GetMarketsDueForTradeUpdateAsync(int cooldownHours, int limit, CancellationToken ct = default);
|
||||
|
||||
@@ -30,8 +30,8 @@ public interface IPlatformProvider
|
||||
/// <summary>Fetch market metadata by platform-specific market ID.</summary>
|
||||
Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Fetch a batch of markets with their outcomes for bulk sync.</summary>
|
||||
Task<IReadOnlyList<Market>> GetMarketsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default);
|
||||
/// <summary>Fetch a batch of events with their markets and outcomes for bulk sync.</summary>
|
||||
Task<IReadOnlyList<Event>> GetEventsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Fetch top holders for a market to discover new traders.</summary>
|
||||
Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default);
|
||||
|
||||
@@ -19,4 +19,5 @@ public interface ITradeRepository
|
||||
Task<IReadOnlyList<Trade>> GetOrphanedTradesAsync(int limit, CancellationToken ct = default);
|
||||
Task<HashSet<string>> GetKnownPlatformTradeIdsAsync(PlatformType platform, int traderId, IEnumerable<string> platformTradeIds, CancellationToken ct = default);
|
||||
Task UpdateAsync(Trade trade, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<Trade>> GetTradesForContextEnrichmentAsync(int limit, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -22,5 +22,8 @@ public interface ITraderRepository
|
||||
/// <summary>Get traders that haven't been polled in a long time or have a prolonged API error for cleanup.</summary>
|
||||
Task<IReadOnlyList<Trader>> GetTradersForCleanupAsync(DateTime inactiveSince, DateTime errorSince, int take = 50, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Get traders ordered by LastPolledAt to ensure round-robin polling of all traders.</summary>
|
||||
Task<IReadOnlyList<Trader>> GetTradersForPollingAsync(int take, CancellationToken ct = default);
|
||||
|
||||
Task<IReadOnlyList<Trader>> SearchAsync(string query, int take = 20, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Enums;
|
||||
|
||||
namespace Predictalytics.Domain.ValueObjects;
|
||||
|
||||
@@ -23,14 +23,15 @@ public record PriorityScore(
|
||||
|
||||
/// <summary>
|
||||
/// Determine tier based on effective score.
|
||||
/// To reach Diamond, the score must be exceptionally high (top 1%).
|
||||
/// </summary>
|
||||
public TraderTier DetermineTier() => EffectiveScore switch
|
||||
{
|
||||
>= 90 => TraderTier.Diamond,
|
||||
>= 75 => TraderTier.Platinum,
|
||||
>= 60 => TraderTier.Gold,
|
||||
>= 40 => TraderTier.Silver,
|
||||
>= 20 => TraderTier.Bronze,
|
||||
>= 95 => TraderTier.Diamond,
|
||||
>= 85 => TraderTier.Platinum,
|
||||
>= 70 => TraderTier.Gold,
|
||||
>= 50 => TraderTier.Silver,
|
||||
>= 30 => TraderTier.Bronze,
|
||||
_ => TraderTier.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ public class AppDbContext : DbContext
|
||||
{
|
||||
public DbSet<Trader> Traders => Set<Trader>();
|
||||
public DbSet<Trade> Trades => Set<Trade>();
|
||||
public DbSet<Event> Events => Set<Event>();
|
||||
public DbSet<Market> Markets => Set<Market>();
|
||||
public DbSet<MarketOutcome> MarketOutcomes => Set<MarketOutcome>();
|
||||
public DbSet<TraderScore> TraderScores => Set<TraderScore>();
|
||||
@@ -66,19 +67,34 @@ public class AppDbContext : DbContext
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// Event
|
||||
mb.Entity<Event>(e =>
|
||||
{
|
||||
e.HasKey(ev => ev.Id);
|
||||
e.HasIndex(ev => new { ev.Platform, ev.PlatformEventId }).IsUnique();
|
||||
e.Property(ev => ev.Slug).HasMaxLength(512);
|
||||
e.Property(ev => ev.Title).HasMaxLength(1024);
|
||||
e.Property(ev => ev.Description).HasMaxLength(4096);
|
||||
e.Property(ev => ev.ImageUrl).HasMaxLength(1024);
|
||||
e.Property(ev => ev.Tags).HasMaxLength(1024);
|
||||
e.HasMany(ev => ev.Markets).WithOne(m => m.Event).HasForeignKey(m => m.EventId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// Market
|
||||
mb.Entity<Market>(e =>
|
||||
{
|
||||
e.HasKey(m => m.Id);
|
||||
e.HasIndex(m => new { m.Platform, m.PlatformMarketId }).IsUnique();
|
||||
e.Property(m => m.PlatformMarketId).HasMaxLength(256);
|
||||
e.Property(m => m.ConditionId).HasMaxLength(256);
|
||||
e.Property(m => m.QuestionId).HasMaxLength(256);
|
||||
e.Property(m => m.MarketSlug).HasMaxLength(512);
|
||||
e.Property(m => m.EventSlug).HasMaxLength(512);
|
||||
e.Property(m => m.Question).HasMaxLength(1024);
|
||||
e.Property(m => m.Description).HasMaxLength(4096);
|
||||
e.Property(m => m.ImageUrl).HasMaxLength(1024);
|
||||
e.Property(m => m.Category).HasMaxLength(128);
|
||||
e.Property(m => m.Volume).HasPrecision(18, 4);
|
||||
e.Property(m => m.Volume24h).HasPrecision(18, 4);
|
||||
e.Property(m => m.Liquidity).HasPrecision(18, 4);
|
||||
e.HasMany(m => m.Outcomes).WithOne(o => o.Market).HasForeignKey(o => o.MarketId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
@@ -14,8 +14,8 @@ public class MarketRepository : IMarketRepository
|
||||
public MarketRepository(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<Market?> GetByPlatformIdAsync(PlatformType platform, string platformMarketId, CancellationToken ct = default)
|
||||
=> await _db.Markets.Include(m => m.Outcomes)
|
||||
.FirstOrDefaultAsync(m => m.Platform == platform && m.PlatformMarketId == platformMarketId, ct);
|
||||
=> await _db.Markets.Include(m => m.Outcomes).Include(m => m.Event)
|
||||
.FirstOrDefaultAsync(m => m.Platform == platform && m.ConditionId == platformMarketId, ct);
|
||||
|
||||
public async Task<MarketOutcome?> GetOutcomeByTokenIdAsync(string tokenId, CancellationToken ct = default)
|
||||
=> await _db.MarketOutcomes.Include(o => o.Market)
|
||||
@@ -34,7 +34,7 @@ public class MarketRepository : IMarketRepository
|
||||
TruncateMarketStrings(market);
|
||||
|
||||
var existing = await _db.Markets.Include(m => m.Outcomes)
|
||||
.FirstOrDefaultAsync(m => m.Platform == market.Platform && m.PlatformMarketId == market.PlatformMarketId, ct);
|
||||
.FirstOrDefaultAsync(m => m.Platform == market.Platform && m.ConditionId == market.ConditionId, ct);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
@@ -42,6 +42,11 @@ public class MarketRepository : IMarketRepository
|
||||
}
|
||||
else
|
||||
{
|
||||
if (market.Event == null && market.EventId == 0)
|
||||
{
|
||||
// Fallback to avoid foreign key exceptions if event is entirely missing
|
||||
market.Event = new Event { Platform = market.Platform, PlatformEventId = market.PlatformMarketId, Slug = "unknown", Title = "Unknown" };
|
||||
}
|
||||
_db.Markets.Add(market);
|
||||
}
|
||||
|
||||
@@ -55,9 +60,9 @@ public class MarketRepository : IMarketRepository
|
||||
|
||||
public async Task AddOrUpdateRangeAsync(IEnumerable<Market> markets, CancellationToken ct = default)
|
||||
{
|
||||
// Deduplicate input by PlatformMarketId to avoid processing the same ID twice in one call
|
||||
// Deduplicate input by ConditionId to avoid processing the same ID twice in one call
|
||||
var marketList = markets
|
||||
.GroupBy(m => new { m.Platform, m.PlatformMarketId })
|
||||
.GroupBy(m => new { m.Platform, m.ConditionId })
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
@@ -72,25 +77,28 @@ public class MarketRepository : IMarketRepository
|
||||
{
|
||||
var currentBatch = marketList.Skip(i).Take(subBatchSize).ToList();
|
||||
var platform = currentBatch.First().Platform;
|
||||
var ids = currentBatch.Select(m => m.PlatformMarketId).ToList();
|
||||
var ids = currentBatch.Select(m => m.ConditionId).ToList();
|
||||
|
||||
// Fetch all existing markets in this batch at once
|
||||
var existingMarkets = await _db.Markets.Include(m => m.Outcomes)
|
||||
.Where(m => m.Platform == platform && ids.Contains(m.PlatformMarketId))
|
||||
.Where(m => m.Platform == platform && ids.Contains(m.ConditionId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
var existingMap = existingMarkets.ToDictionary(m => m.PlatformMarketId);
|
||||
var existingMap = existingMarkets.ToDictionary(m => m.ConditionId);
|
||||
|
||||
foreach (var market in currentBatch)
|
||||
{
|
||||
TruncateMarketStrings(market);
|
||||
|
||||
if (existingMap.TryGetValue(market.PlatformMarketId, out var existing))
|
||||
if (existingMap.TryGetValue(market.ConditionId, out var existing))
|
||||
{
|
||||
UpdateMarketFields(existing, market);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (market.Event == null && market.EventId == 0)
|
||||
{
|
||||
market.Event = new Event { Platform = market.Platform, PlatformEventId = market.PlatformMarketId, Slug = "unknown", Title = "Unknown" };
|
||||
}
|
||||
_db.Markets.Add(market);
|
||||
}
|
||||
}
|
||||
@@ -104,21 +112,96 @@ public class MarketRepository : IMarketRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddOrUpdateEventsAsync(IEnumerable<Event> events, CancellationToken ct = default)
|
||||
{
|
||||
var eventList = events.GroupBy(e => new { e.Platform, e.PlatformEventId }).Select(g => g.First()).ToList();
|
||||
if (!eventList.Any()) return;
|
||||
|
||||
await _syncSemaphore.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
const int subBatchSize = 100;
|
||||
for (int i = 0; i < eventList.Count; i += subBatchSize)
|
||||
{
|
||||
var currentBatch = eventList.Skip(i).Take(subBatchSize).ToList();
|
||||
var platform = currentBatch.First().Platform;
|
||||
var eventIds = currentBatch.Select(e => e.PlatformEventId).ToList();
|
||||
|
||||
var existingEvents = await _db.Events
|
||||
.Include(e => e.Markets).ThenInclude(m => m.Outcomes)
|
||||
.Where(e => e.Platform == platform && eventIds.Contains(e.PlatformEventId))
|
||||
.ToListAsync(ct);
|
||||
|
||||
var existingEventsMap = existingEvents.ToDictionary(e => e.PlatformEventId);
|
||||
|
||||
foreach (var ev in currentBatch)
|
||||
{
|
||||
if (ev.Slug != null && ev.Slug.Length > 512) ev.Slug = ev.Slug[..512];
|
||||
if (ev.Title != null && ev.Title.Length > 1024) ev.Title = ev.Title[..1024];
|
||||
|
||||
if (existingEventsMap.TryGetValue(ev.PlatformEventId, out var existing))
|
||||
{
|
||||
existing.Slug = ev.Slug;
|
||||
existing.Title = ev.Title;
|
||||
existing.Description = ev.Description;
|
||||
existing.ImageUrl = ev.ImageUrl;
|
||||
existing.Tags = ev.Tags;
|
||||
existing.StartDate = ev.StartDate;
|
||||
existing.EndDate = ev.EndDate;
|
||||
existing.IsActive = ev.IsActive;
|
||||
existing.IsClosed = ev.IsClosed;
|
||||
existing.LastUpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Upsert markets inside event
|
||||
foreach (var market in ev.Markets)
|
||||
{
|
||||
TruncateMarketStrings(market);
|
||||
var existingMarket = existing.Markets.FirstOrDefault(m => m.ConditionId == market.ConditionId);
|
||||
if (existingMarket != null)
|
||||
{
|
||||
UpdateMarketFields(existingMarket, market);
|
||||
}
|
||||
else
|
||||
{
|
||||
market.EventId = existing.Id;
|
||||
market.Event = null; // Prevent EF tracking issue
|
||||
existing.Markets.Add(market);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var m in ev.Markets) TruncateMarketStrings(m);
|
||||
_db.Events.Add(ev);
|
||||
}
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMarketFields(Market existing, Market updated)
|
||||
{
|
||||
existing.Question = updated.Question;
|
||||
existing.MarketSlug = updated.MarketSlug;
|
||||
existing.EventSlug = updated.EventSlug;
|
||||
existing.PlatformMarketId = updated.PlatformMarketId;
|
||||
existing.QuestionId = updated.QuestionId;
|
||||
existing.Description = updated.Description;
|
||||
existing.ImageUrl = updated.ImageUrl;
|
||||
existing.Category = updated.Category;
|
||||
existing.Volume = updated.Volume;
|
||||
existing.Volume24h = updated.Volume24h;
|
||||
existing.Liquidity = updated.Liquidity;
|
||||
existing.StartDate = updated.StartDate;
|
||||
existing.EndDate = updated.EndDate;
|
||||
existing.IsResolved = updated.IsResolved;
|
||||
existing.ResolutionOutcome = updated.ResolutionOutcome;
|
||||
existing.CreatedAt = updated.CreatedAt; // Platform creation date
|
||||
existing.CreatedAt = updated.CreatedAt;
|
||||
existing.LastUpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Upsert outcomes
|
||||
@@ -146,7 +229,6 @@ public class MarketRepository : IMarketRepository
|
||||
market.Question = StringHelper.Truncate(market.Question, 1024) ?? "";
|
||||
market.Description = StringHelper.Truncate(market.Description, 4096);
|
||||
market.MarketSlug = StringHelper.Truncate(market.MarketSlug, 512) ?? "";
|
||||
market.EventSlug = StringHelper.Truncate(market.EventSlug, 512) ?? "";
|
||||
market.ImageUrl = StringHelper.Truncate(market.ImageUrl, 1024);
|
||||
market.Category = StringHelper.Truncate(market.Category, 128) ?? "";
|
||||
|
||||
@@ -192,7 +274,7 @@ public class MarketRepository : IMarketRepository
|
||||
|
||||
return await _db.Markets.Include(m => m.Outcomes)
|
||||
.Where(m => m.Question.Contains(query) ||
|
||||
m.PlatformMarketId.Contains(query) ||
|
||||
m.ConditionId.Contains(query) ||
|
||||
m.Id.ToString() == query)
|
||||
.OrderByDescending(m => m.Volume)
|
||||
.Take(take)
|
||||
|
||||
@@ -92,13 +92,13 @@ public class TradeRepository : ITradeRepository
|
||||
public async Task<HashSet<string>> GetKnownPlatformTradeIdsAsync(PlatformType platform, int traderId, IEnumerable<string> platformTradeIds, CancellationToken ct = default)
|
||||
{
|
||||
var idList = platformTradeIds.ToList();
|
||||
if (idList.Count == 0) return new HashSet<string>();
|
||||
if (idList.Count == 0) return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var ids = await _db.Trades
|
||||
.Where(t => t.Platform == platform && t.TraderId == traderId && idList.Contains(t.PlatformTradeId))
|
||||
.Where(t => t.Platform == platform && idList.Contains(t.PlatformTradeId))
|
||||
.Select(t => t.PlatformTradeId)
|
||||
.ToListAsync(ct);
|
||||
return new HashSet<string>(ids);
|
||||
return new HashSet<string>(ids, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Trade trade, CancellationToken ct = default)
|
||||
@@ -111,4 +111,19 @@ public class TradeRepository : ITradeRepository
|
||||
_db.Trades.Update(trade);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Trade>> GetTradesForContextEnrichmentAsync(int limit, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.Trades
|
||||
.Include(t => t.Trader)
|
||||
.Include(t => t.Trader.CurrentScore)
|
||||
.Include(t => t.Trader.WatchlistEntries)
|
||||
.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))
|
||||
.OrderByDescending(t => t.ExecutedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,14 @@ public class TraderRepository : ITraderRepository
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Trader>> GetTradersForPollingAsync(int take, CancellationToken ct = default)
|
||||
{
|
||||
return await _db.Traders
|
||||
.OrderBy(t => t.LastPolledAt)
|
||||
.Take(take)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Trader>> SearchAsync(string query, int take = 20, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) return Array.Empty<Trader>();
|
||||
|
||||
@@ -84,6 +84,8 @@ public static class DependencyInjection
|
||||
|
||||
services.AddSingleton<PolymarketApiClient>();
|
||||
services.AddSingleton<LimitlessApiClient>();
|
||||
services.AddHttpClient<Predictalytics.Application.Interfaces.IOpenRouterApiClient, Predictalytics.Infrastructure.Providers.OpenRouter.OpenRouterApiClient>();
|
||||
services.AddScoped<Predictalytics.Application.Interfaces.IAiStrategyAnalysisService, Predictalytics.Application.Services.AiStrategyAnalysisService>();
|
||||
services.AddSingleton<IPlatformProvider, PolymarketProvider>();
|
||||
services.AddSingleton<IPlatformProvider, LimitlessProvider>();
|
||||
services.AddSingleton<IPlatformProvider, AzuroProvider>();
|
||||
|
||||
Generated
+750
@@ -0,0 +1,750 @@
|
||||
// <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("20260703100444_AddTradePriceContext")]
|
||||
partial class AddTradePriceContext
|
||||
{
|
||||
/// <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<bool>("IsContextEnriched")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
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?>("PostTradePrice1m")
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal?>("PreTradePrice1m")
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
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,49 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTradePriceContext : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsContextEnriched",
|
||||
table: "Trades",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "PostTradePrice1m",
|
||||
table: "Trades",
|
||||
type: "decimal(18,4)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "PreTradePrice1m",
|
||||
table: "Trades",
|
||||
type: "decimal(18,4)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsContextEnriched",
|
||||
table: "Trades");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PostTradePrice1m",
|
||||
table: "Trades");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PreTradePrice1m",
|
||||
table: "Trades");
|
||||
}
|
||||
}
|
||||
}
|
||||
+844
@@ -0,0 +1,844 @@
|
||||
// <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("20260703114907_AddEventsAndTags")]
|
||||
partial class AddEventsAndTags
|
||||
{
|
||||
/// <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.Event", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
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>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsClosed")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("LastUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("PlatformEventId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Platform", "PlatformEventId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
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<string>("ConditionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
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<int>("EventId")
|
||||
.HasColumnType("int");
|
||||
|
||||
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<long>("PlatformMarketId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<string>("QuestionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
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.Property<decimal>("Volume24h")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
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<bool>("IsContextEnriched")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
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?>("PostTradePrice1m")
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal?>("PreTradePrice1m")
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
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.Market", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Event", "Event")
|
||||
.WithMany("Markets")
|
||||
.HasForeignKey("EventId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Event");
|
||||
});
|
||||
|
||||
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.Event", b =>
|
||||
{
|
||||
b.Navigation("Markets");
|
||||
});
|
||||
|
||||
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,167 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEventsAndTags : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("SET FOREIGN_KEY_CHECKS=0; TRUNCATE TABLE TraderPositions; TRUNCATE TABLE Trades; TRUNCATE TABLE MarketOutcomes; TRUNCATE TABLE Markets; SET FOREIGN_KEY_CHECKS=1;");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EventSlug",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.AlterColumn<long>(
|
||||
name: "PlatformMarketId",
|
||||
table: "Markets",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "varchar(256)",
|
||||
oldMaxLength: 256)
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ConditionId",
|
||||
table: "Markets",
|
||||
type: "varchar(256)",
|
||||
maxLength: 256,
|
||||
nullable: false,
|
||||
defaultValue: "")
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "EventId",
|
||||
table: "Markets",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "QuestionId",
|
||||
table: "Markets",
|
||||
type: "varchar(256)",
|
||||
maxLength: 256,
|
||||
nullable: false,
|
||||
defaultValue: "")
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "Volume24h",
|
||||
table: "Markets",
|
||||
type: "decimal(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Events",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Platform = table.Column<int>(type: "int", nullable: false),
|
||||
PlatformEventId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Slug = table.Column<string>(type: "varchar(512)", maxLength: 512, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Title = table.Column<string>(type: "varchar(1024)", maxLength: 1024, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Description = table.Column<string>(type: "varchar(4096)", maxLength: 4096, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ImageUrl = table.Column<string>(type: "varchar(1024)", maxLength: 1024, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
StartDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
EndDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
Tags = table.Column<string>(type: "varchar(1024)", maxLength: 1024, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
IsClosed = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
DbCreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
LastUpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Events", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Markets_EventId",
|
||||
table: "Markets",
|
||||
column: "EventId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Events_Platform_PlatformEventId",
|
||||
table: "Events",
|
||||
columns: new[] { "Platform", "PlatformEventId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Markets_Events_EventId",
|
||||
table: "Markets",
|
||||
column: "EventId",
|
||||
principalTable: "Events",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Markets_Events_EventId",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Events");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Markets_EventId",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ConditionId",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EventId",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "QuestionId",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Volume24h",
|
||||
table: "Markets");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "PlatformMarketId",
|
||||
table: "Markets",
|
||||
type: "varchar(256)",
|
||||
maxLength: 256,
|
||||
nullable: false,
|
||||
oldClrType: typeof(long),
|
||||
oldType: "bigint")
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "EventSlug",
|
||||
table: "Markets",
|
||||
type: "varchar(512)",
|
||||
maxLength: 512,
|
||||
nullable: false,
|
||||
defaultValue: "")
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+850
@@ -0,0 +1,850 @@
|
||||
// <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("20260704120722_AddAiStrategyFields")]
|
||||
partial class AddAiStrategyFields
|
||||
{
|
||||
/// <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.Event", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
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>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsClosed")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("LastUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("PlatformEventId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Platform", "PlatformEventId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
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<string>("ConditionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
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<int>("EventId")
|
||||
.HasColumnType("int");
|
||||
|
||||
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<long>("PlatformMarketId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<string>("QuestionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
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.Property<decimal>("Volume24h")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
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<bool>("IsContextEnriched")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
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?>("PostTradePrice1m")
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal?>("PreTradePrice1m")
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
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<string>("AiStrategySummary")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime?>("AiStrategyUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
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.Market", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Event", "Event")
|
||||
.WithMany("Markets")
|
||||
.HasForeignKey("EventId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Event");
|
||||
});
|
||||
|
||||
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.Event", b =>
|
||||
{
|
||||
b.Navigation("Markets");
|
||||
});
|
||||
|
||||
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,40 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAiStrategyFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AiStrategySummary",
|
||||
table: "Traders",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "AiStrategyUpdatedAt",
|
||||
table: "Traders",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AiStrategySummary",
|
||||
table: "Traders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AiStrategyUpdatedAt",
|
||||
table: "Traders");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.ToTable("Alerts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -75,11 +75,6 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@@ -93,11 +88,85 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("EventSlug")
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsClosed")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("LastUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("PlatformEventId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Platform", "PlatformEventId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
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<string>("ConditionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
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<int>("EventId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
@@ -123,16 +192,19 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PlatformMarketId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
b.Property<long>("PlatformMarketId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<string>("QuestionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<string>("ResolutionOutcome")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
@@ -143,8 +215,14 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("Volume24h")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.HasIndex("Platform", "PlatformMarketId")
|
||||
.IsUnique();
|
||||
|
||||
@@ -297,6 +375,9 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Property<DateTime>("ExecutedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsContextEnriched")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("MarketId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(66)
|
||||
@@ -318,6 +399,12 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<decimal?>("PostTradePrice1m")
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal?>("PreTradePrice1m")
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(10, 6)
|
||||
.HasColumnType("decimal(10,6)");
|
||||
@@ -362,6 +449,12 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AiStrategySummary")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime?>("AiStrategyUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
@@ -603,6 +696,17 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Event", "Event")
|
||||
.WithMany("Markets")
|
||||
.HasForeignKey("EventId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Event");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.MarketAnalytics", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Market", "Market")
|
||||
@@ -713,6 +817,11 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Event", b =>
|
||||
{
|
||||
b.Navigation("Markets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.Navigation("Analytics");
|
||||
|
||||
@@ -31,8 +31,8 @@ public class AzuroProvider : IPlatformProvider
|
||||
public Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); _logger.LogWarning("Provider not yet implemented"); return Task.FromResult<Market?>(null); }
|
||||
|
||||
public Task<IReadOnlyList<Market>> GetMarketsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<Market>>(Array.Empty<Market>()); }
|
||||
public Task<IReadOnlyList<Event>> GetEventsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<Event>>(Array.Empty<Event>()); }
|
||||
|
||||
public Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default)
|
||||
{ using var _ = PlatformLogContext.Push(PlatformName); return Task.FromResult<IReadOnlyList<DiscoveredTrader>>(Array.Empty<DiscoveredTrader>()); }
|
||||
|
||||
@@ -23,6 +23,7 @@ public class LimitlessApiClient
|
||||
var url = $"markets/active?limit={Math.Min(limit, 25)}"; // Offset is not supported by this endpoint, limit max 25
|
||||
try
|
||||
{
|
||||
|
||||
var response = await _client.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -45,6 +46,7 @@ public class LimitlessApiClient
|
||||
var url = $"markets/{addressOrSlug}";
|
||||
try
|
||||
{
|
||||
|
||||
var response = await _client.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
return await response.Content.ReadFromJsonAsync<LimitlessMarketResponse>(cancellationToken: ct);
|
||||
@@ -61,6 +63,7 @@ public class LimitlessApiClient
|
||||
var url = $"portfolio/{walletAddress}/positions";
|
||||
try
|
||||
{
|
||||
|
||||
var response = await _client.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -82,6 +85,7 @@ public class LimitlessApiClient
|
||||
var url = $"markets/{slug}/events?limit={limit}";
|
||||
try
|
||||
{
|
||||
|
||||
var response = await _client.GetAsync(url, ct);
|
||||
if (!response.IsSuccessStatusCode) return [];
|
||||
var result = await response.Content.ReadFromJsonAsync<LimitlessEventsResponse>(cancellationToken: ct);
|
||||
|
||||
@@ -164,17 +164,33 @@ public class LimitlessProvider : IPlatformProvider
|
||||
return MapLimitlessMarket(raw);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Market>> GetMarketsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
public async Task<IReadOnlyList<Event>> GetEventsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
int.TryParse(cursor, out var offset);
|
||||
|
||||
// Since Limitless /markets/active doesn't support offset, we only return the first page.
|
||||
// Returning data for offset > 0 would cause an infinite loop in MarketSyncWorker.
|
||||
if (offset > 0) return [];
|
||||
var offset = 0;
|
||||
if (!string.IsNullOrEmpty(cursor) && int.TryParse(cursor, out var parsed))
|
||||
offset = parsed;
|
||||
|
||||
_logger.LogInformation("Fetching markets batch (limit={Limit}, offset={Offset})", limit, offset);
|
||||
var raw = await _api.GetActiveMarketsAsync(limit, offset, ct);
|
||||
return raw.Select(MapLimitlessMarket).ToList();
|
||||
_logger.LogInformation("Fetched {Count} markets from Limitless API", raw.Count);
|
||||
|
||||
var events = new List<Event>();
|
||||
foreach (var r in raw)
|
||||
{
|
||||
var m = MapLimitlessMarket(r);
|
||||
events.Add(new Event
|
||||
{
|
||||
Platform = PlatformType.Limitless,
|
||||
PlatformEventId = m.PlatformMarketId, // Use market ID as Event ID
|
||||
Slug = "limitless-" + m.ConditionId,
|
||||
Title = m.Question,
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
LastUpdatedAt = DateTime.UtcNow,
|
||||
Markets = new List<Market> { m }
|
||||
});
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default)
|
||||
@@ -229,12 +245,13 @@ public class LimitlessProvider : IPlatformProvider
|
||||
|
||||
private Market MapLimitlessMarket(LimitlessMarketResponse raw)
|
||||
{
|
||||
var conditionId = raw.Address ?? raw.Slug ?? Guid.NewGuid().ToString();
|
||||
var market = new Market
|
||||
{
|
||||
Platform = PlatformType.Limitless,
|
||||
PlatformMarketId = raw.Address ?? raw.Slug ?? "",
|
||||
ConditionId = conditionId,
|
||||
PlatformMarketId = GetStableHashCode(conditionId),
|
||||
MarketSlug = raw.Slug ?? "",
|
||||
EventSlug = "", // Limitless doesn't seem to have a clear Event/Market split in this model
|
||||
Question = raw.Title ?? "",
|
||||
Description = raw.Description ?? "",
|
||||
Category = raw.Categories?.FirstOrDefault() ?? "",
|
||||
@@ -279,4 +296,15 @@ public class LimitlessProvider : IPlatformProvider
|
||||
if (sideStr == "0" || sideStr == "BUY") return TradeSide.Buy;
|
||||
return TradeSide.Sell;
|
||||
}
|
||||
|
||||
private static long GetStableHashCode(string str)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
long hash = 23;
|
||||
foreach (char c in str)
|
||||
hash = hash * 31 + c;
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Predictalytics.Application.Interfaces;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Providers.OpenRouter;
|
||||
|
||||
public class OpenRouterApiClient : IOpenRouterApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly ILogger<OpenRouterApiClient> _logger;
|
||||
|
||||
public OpenRouterApiClient(HttpClient httpClient, IConfiguration config, ILogger<OpenRouterApiClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
|
||||
var baseUrl = _config["OpenRouter:BaseUrl"] ?? "https://openrouter.ai/api/v1";
|
||||
var apiKey = _config["OpenRouter:ApiKey"];
|
||||
|
||||
_httpClient.BaseAddress = new Uri(baseUrl.EndsWith("/") ? baseUrl : baseUrl + "/");
|
||||
if (!string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
}
|
||||
// OpenRouter recommends adding a referer and title for ranking
|
||||
_httpClient.DefaultRequestHeaders.Add("HTTP-Referer", "http://localhost");
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Title", "Predictalytics");
|
||||
}
|
||||
|
||||
public async Task<string> GenerateChatCompletionAsync(string prompt, bool useManualModel = false, CancellationToken ct = default)
|
||||
{
|
||||
var model = useManualModel
|
||||
? _config["OpenRouter:ManualAnalysisModel"] ?? "anthropic/claude-3-opus"
|
||||
: _config["OpenRouter:DefaultModel"] ?? "google/gemini-flash-1.5";
|
||||
|
||||
var requestBody = new
|
||||
{
|
||||
model = model,
|
||||
messages = new[]
|
||||
{
|
||||
new { role = "system", content = "You are an expert crypto and prediction market analyst. You analyze a trader's history and deduce their strategy, strengths, and weaknesses." },
|
||||
new { role = "user", content = prompt }
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("chat/completions", requestBody, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<OpenRouterResponse>(cancellationToken: ct);
|
||||
return result?.Choices?[0]?.Message?.Content ?? "No response generated.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to generate chat completion from OpenRouter using model {Model}", model);
|
||||
return $"Error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private class OpenRouterResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public Choice[]? Choices { get; set; }
|
||||
}
|
||||
|
||||
private class Choice
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public Message? Message { get; set; }
|
||||
}
|
||||
|
||||
private class Message
|
||||
{
|
||||
[JsonPropertyName("content")]
|
||||
public string? Content { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -43,39 +43,39 @@ public class PolymarketApiClient
|
||||
public async Task<List<PolymarketTradeResponse>> GetTradesAsync(string walletAddress, int limit = 1000, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/activity?user={walletAddress}&limit={limit}";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, ct) ?? [];
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, "Data", ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<List<PolymarketTradeResponse>> GetMarketTradesAsync(string conditionId, int limit = 1000, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/trades?condition_id={conditionId}&limit={limit}";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, ct) ?? [];
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, "Data", ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<List<PolymarketPositionResponse>> GetPositionsAsync(string walletAddress, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/positions?user={walletAddress}&sizeThreshold=0.1&sortBy=CURRENT&sortOrder=DESC";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketPositionResponse>>(_client, url, ct) ?? [];
|
||||
return await ExecuteWithRetryAsync<List<PolymarketPositionResponse>>(_client, url, "Data", ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<GammaMarketResponse?> GetMarketAsync(string conditionId, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/markets?condition_id={conditionId}";
|
||||
var results = await ExecuteWithRetryAsync<List<GammaMarketResponse>>(_gammaClient, url, ct);
|
||||
var results = await ExecuteWithRetryAsync<List<GammaMarketResponse>>(_gammaClient, url, "Gamma", ct);
|
||||
return results?.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch a batch of markets from the Gamma API with pagination.
|
||||
/// Fetch a batch of events (and their nested markets) from the Gamma API with pagination.
|
||||
/// Supports offset-based pagination via the offset parameter.
|
||||
/// </summary>
|
||||
public async Task<List<GammaMarketResponse>> GetMarketsAsync(int limit = 1000, int offset = 0, bool includeClosed = false, CancellationToken ct = default)
|
||||
public async Task<List<GammaEventResponse>> GetEventsAsync(int limit = 100, int offset = 0, bool includeClosed = false, CancellationToken ct = default)
|
||||
{
|
||||
var activeOnly = !includeClosed;
|
||||
var url = $"/markets?limit={limit}&offset={offset}&active={activeOnly.ToString().ToLower()}&closed={includeClosed.ToString().ToLower()}";
|
||||
_logger.LogDebug("Fetching markets: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<GammaMarketResponse>>(_gammaClient, url, ct);
|
||||
_logger.LogInformation("Fetched {Count} markets (offset={Offset}, closed={Closed})", result?.Count ?? 0, offset, includeClosed);
|
||||
var url = $"/events?limit={limit}&offset={offset}&active={activeOnly.ToString().ToLower()}&closed={includeClosed.ToString().ToLower()}";
|
||||
_logger.LogDebug("Fetching events: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<GammaEventResponse>>(_gammaClient, url, "Gamma", ct);
|
||||
_logger.LogInformation("Fetched {Count} events (offset={Offset}, closed={Closed})", result?.Count ?? 0, offset, includeClosed);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public class PolymarketApiClient
|
||||
{
|
||||
var url = $"/holders?market={conditionId}&limit={limit}";
|
||||
_logger.LogDebug("Fetching holders: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<HoldersResponse>>(_client, url, ct);
|
||||
var result = await ExecuteWithRetryAsync<List<HoldersResponse>>(_client, url, "Data", ct);
|
||||
_logger.LogInformation("Fetched holders for {Market}: {Count} token groups",
|
||||
conditionId.Length > 12 ? conditionId[..12] + "..." : conditionId, result?.Count ?? 0);
|
||||
return result ?? [];
|
||||
@@ -106,13 +106,15 @@ public class PolymarketApiClient
|
||||
{
|
||||
var url = $"/v1/leaderboard?limit={Math.Min(limit, 50)}&time_period={timePeriod}&order_by={orderBy}&category={category}";
|
||||
_logger.LogDebug("Fetching leaderboard: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<LeaderboardEntry>>(_client, url, ct);
|
||||
var result = await ExecuteWithRetryAsync<List<LeaderboardEntry>>(_client, url, "Data", ct);
|
||||
_logger.LogInformation("Leaderboard returned {Count} entries", result?.Count ?? 0);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
private async Task<T?> ExecuteWithRetryAsync<T>(HttpClient client, string url, CancellationToken ct, int attempt = 1)
|
||||
private async Task<T?> ExecuteWithRetryAsync<T>(HttpClient client, string url, string endpointGroup, CancellationToken ct, int attempt = 1)
|
||||
{
|
||||
await _rateLimiter.WaitAsync(PlatformType.Polymarket, ct, endpointGroup);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync(url, ct);
|
||||
@@ -135,15 +137,14 @@ public class PolymarketApiClient
|
||||
waitTime = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
_logger.LogWarning("⚠️ Rate limit exceeded (429) for Polymarket. Pausing for {WaitTime}s...", (int)waitTime.TotalSeconds);
|
||||
_logger.LogWarning("⚠️ Rate limit exceeded (429) for Polymarket {Group}. Pausing for {WaitTime}s...", endpointGroup, (int)waitTime.TotalSeconds);
|
||||
|
||||
_rateLimiter.ReportRateLimitExceeded(PlatformType.Polymarket, waitTime);
|
||||
_rateLimiter.ReportRateLimitExceeded(PlatformType.Polymarket, waitTime, endpointGroup);
|
||||
|
||||
if (attempt < 3)
|
||||
{
|
||||
await _rateLimiter.WaitAsync(PlatformType.Polymarket, ct);
|
||||
_logger.LogWarning("Retrying {Url} (attempt {NextAttempt})...", url, attempt + 1);
|
||||
return await ExecuteWithRetryAsync<T>(client, url, ct, attempt + 1);
|
||||
return await ExecuteWithRetryAsync<T>(client, url, endpointGroup, ct, attempt + 1);
|
||||
}
|
||||
|
||||
return default;
|
||||
@@ -172,7 +173,7 @@ public class PolymarketApiClient
|
||||
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);
|
||||
var result = await ExecuteWithRetryAsync<PolymarketPriceHistoryResponse>(_clobClient, url, "Clob", ct);
|
||||
return result?.History ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ public class GammaMarketResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("conditionId")] public string ConditionId { get; set; } = "";
|
||||
[JsonPropertyName("questionID")] public string QuestionId { get; set; } = "";
|
||||
[JsonPropertyName("question")] public string Question { get; set; } = "";
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; } = "";
|
||||
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||
@@ -121,11 +122,16 @@ public class GammaMarketResponse
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Volume { get; set; }
|
||||
|
||||
[JsonPropertyName("volume24hr")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Volume24hr { get; set; }
|
||||
|
||||
[JsonPropertyName("liquidityNum")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Liquidity { get; set; }
|
||||
|
||||
[JsonPropertyName("endDateIso")] public string? EndDate { get; set; }
|
||||
[JsonPropertyName("endDateIso")] public string? EndDateIso { get; set; }
|
||||
[JsonPropertyName("endDate")] public string? EndDate { get; set; }
|
||||
[JsonPropertyName("startDate")] public string? StartDate { get; set; }
|
||||
[JsonPropertyName("createdAt")] public string? CreatedAt { get; set; }
|
||||
[JsonPropertyName("closed")] public bool Closed { get; set; }
|
||||
@@ -136,7 +142,7 @@ public class GammaMarketResponse
|
||||
/// <summary>JSON string of outcomes, e.g. "[\"Yes\", \"No\"]"</summary>
|
||||
[JsonPropertyName("outcomes")] public string? Outcomes { get; set; }
|
||||
|
||||
/// <summary>JSON string of outcome prices, e.g. "[\"0.55\", \"0.45\"]"</summary>
|
||||
/// <summary>JSON string of outcomePrices, e.g. "[\"0.55\", \"0.45\"]"</summary>
|
||||
[JsonPropertyName("outcomePrices")] public string? OutcomePrices { get; set; }
|
||||
|
||||
/// <summary>JSON string of CLOB token IDs, e.g. "[\"12345...\", \"67890...\"]"</summary>
|
||||
@@ -148,6 +154,22 @@ public class GammaEventResponse
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; } = "";
|
||||
[JsonPropertyName("title")] public string Title { get; set; } = "";
|
||||
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||
[JsonPropertyName("image")] public string? Image { get; set; }
|
||||
[JsonPropertyName("startDate")] public string? StartDate { get; set; }
|
||||
[JsonPropertyName("endDate")] public string? EndDate { get; set; }
|
||||
[JsonPropertyName("createdAt")] public string? CreatedAt { get; set; }
|
||||
[JsonPropertyName("active")] public bool Active { get; set; }
|
||||
[JsonPropertyName("closed")] public bool Closed { get; set; }
|
||||
[JsonPropertyName("tags")] public List<GammaTagResponse> Tags { get; set; } = [];
|
||||
[JsonPropertyName("markets")] public List<GammaMarketResponse> Markets { get; set; } = [];
|
||||
}
|
||||
|
||||
public class GammaTagResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("label")] public string Label { get; set; } = "";
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; } = "";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
@@ -32,17 +32,19 @@ public class PolymarketProvider : IPlatformProvider
|
||||
|
||||
var mappedTrades = raw.Select(r =>
|
||||
{
|
||||
var wallet = r.User ?? r.ProxyWallet ?? "";
|
||||
var wallet = !string.IsNullOrEmpty(r.User) ? r.User :
|
||||
!string.IsNullOrEmpty(r.ProxyWallet) ? r.ProxyWallet :
|
||||
platformUserId;
|
||||
var side = MapTradeSide(r);
|
||||
var sideStr = side.ToString().ToUpperInvariant();
|
||||
// Compact format: {txHash}_{assetId}_{side} — no wallet in ID to reduce index size.
|
||||
// Wallet passed transiently via TransientWallet [NotMapped] for MarketHistoryWorker.
|
||||
// Format: {txHash}_{wallet}_{assetId}_{side}
|
||||
// Wallet must be included to avoid cross-user collisions in the global IX_Trades_Platform_PlatformTradeId index.
|
||||
return new Trade
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
|
||||
? $"{r.Timestamp}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash}_{r.Asset}_{sideStr}",
|
||||
? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
|
||||
MarketId = r.ConditionId ?? "",
|
||||
AssetId = r.Asset ?? "",
|
||||
Outcome = r.Outcome ?? "",
|
||||
@@ -51,13 +53,13 @@ public class PolymarketProvider : IPlatformProvider
|
||||
Size = (decimal)r.Size,
|
||||
Amount = (decimal)(r.Price * r.Size),
|
||||
ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
||||
TransactionHash = r.TransactionHash,
|
||||
TransactionHash = r.TransactionHash?.ToLowerInvariant(),
|
||||
TraderId = 0,
|
||||
TransientWallet = wallet,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return mappedTrades.GroupBy(t => t.PlatformTradeId).Select(g => g.First()).ToList();
|
||||
return mappedTrades.GroupBy(t => t.PlatformTradeId, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -75,8 +77,8 @@ public class PolymarketProvider : IPlatformProvider
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
|
||||
? $"{r.Timestamp}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash}_{r.Asset}_{sideStr}",
|
||||
? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
|
||||
MarketId = r.ConditionId ?? "",
|
||||
AssetId = r.Asset ?? "",
|
||||
Outcome = r.Outcome ?? "",
|
||||
@@ -127,33 +129,76 @@ public class PolymarketProvider : IPlatformProvider
|
||||
public async Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogDebug("Fetching market {MarketId}", platformMarketId);
|
||||
var raw = await _api.GetMarketAsync(platformMarketId, ct);
|
||||
if (raw == null)
|
||||
if (raw == null || string.IsNullOrEmpty(raw.ConditionId)) return null;
|
||||
|
||||
var parentTags = "";
|
||||
if (raw.Events != null && raw.Events.Count > 0)
|
||||
{
|
||||
_logger.LogWarning("Market {MarketId} not found", platformMarketId);
|
||||
return null;
|
||||
var ev = raw.Events[0];
|
||||
parentTags = ev.Tags != null ? string.Join(", ", ev.Tags.Select(t => t.Label)) : "";
|
||||
}
|
||||
var market = MapGammaMarket(raw, parentTags);
|
||||
|
||||
// Map the parent Event if available in the Market response
|
||||
if (raw.Events != null && raw.Events.Count > 0)
|
||||
{
|
||||
var rawEv = raw.Events[0];
|
||||
long.TryParse(rawEv.Id, out var numericEventId);
|
||||
market.Event = new Event
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformEventId = numericEventId,
|
||||
Slug = rawEv.Slug,
|
||||
Title = rawEv.Title,
|
||||
Description = rawEv.Description,
|
||||
ImageUrl = rawEv.Image,
|
||||
StartDate = DateTime.TryParse(rawEv.StartDate, out var esd) ? esd : null,
|
||||
EndDate = DateTime.TryParse(rawEv.EndDate, out var eed) ? eed : null,
|
||||
CreatedAt = DateTime.TryParse(rawEv.CreatedAt, out var ecd) ? ecd : DateTime.UtcNow,
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsActive = rawEv.Active,
|
||||
IsClosed = rawEv.Closed,
|
||||
Tags = rawEv.Tags != null && rawEv.Tags.Count > 0 ? string.Join(", ", rawEv.Tags.Select(t => t.Label)) : string.Empty,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback empty event if missing (should rarely happen for valid Polymarket markets)
|
||||
market.Event = new Event
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
Slug = "unknown-" + market.ConditionId,
|
||||
Title = "Unknown Event",
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation("Fetched market: {Question}", raw.Question);
|
||||
return MapGammaMarket(raw);
|
||||
return market;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Market>> GetMarketsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
public async Task<IReadOnlyList<Event>> GetEventsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
int offset = 0;
|
||||
if (!string.IsNullOrEmpty(cursor) && int.TryParse(cursor, out var parsed))
|
||||
offset = parsed;
|
||||
|
||||
_logger.LogInformation("Fetching markets batch (limit={Limit}, offset={Offset}, includeClosed={Closed})", limit, offset, includeClosed);
|
||||
var raw = await _api.GetMarketsAsync(limit, offset, includeClosed, ct);
|
||||
_logger.LogInformation("Fetched {Count} markets from Gamma API", raw.Count);
|
||||
_logger.LogInformation("Fetching events batch (limit={Limit}, offset={Offset}, includeClosed={Closed})", limit, offset, includeClosed);
|
||||
var rawEvents = await _api.GetEventsAsync(limit, offset, includeClosed, ct);
|
||||
_logger.LogInformation("Fetched {Count} events from Gamma API", rawEvents.Count);
|
||||
|
||||
return raw
|
||||
.Where(m => !string.IsNullOrEmpty(m.ConditionId) && !string.IsNullOrEmpty(m.ClobTokenIds))
|
||||
.Select(MapGammaMarket)
|
||||
.ToList();
|
||||
var events = new List<Event>();
|
||||
foreach (var rawEvent in rawEvents)
|
||||
{
|
||||
var ev = MapGammaEvent(rawEvent);
|
||||
events.Add(ev);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default)
|
||||
@@ -192,31 +237,67 @@ public class PolymarketProvider : IPlatformProvider
|
||||
|
||||
// ── Private helpers ──────────────────────────────────────────
|
||||
|
||||
private Market MapGammaMarket(GammaMarketResponse raw)
|
||||
private Event MapGammaEvent(GammaEventResponse rawEvent)
|
||||
{
|
||||
var eventSlug = "";
|
||||
if (raw.Events != null && raw.Events.Count > 0 && !string.IsNullOrEmpty(raw.Events[0].Slug))
|
||||
long.TryParse(rawEvent.Id, out var numericId);
|
||||
|
||||
var ev = new Event
|
||||
{
|
||||
eventSlug = raw.Events[0].Slug;
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformEventId = numericId,
|
||||
Slug = rawEvent.Slug,
|
||||
Title = rawEvent.Title,
|
||||
Description = rawEvent.Description,
|
||||
ImageUrl = rawEvent.Image,
|
||||
StartDate = DateTime.TryParse(rawEvent.StartDate, out var sd) ? sd : null,
|
||||
EndDate = DateTime.TryParse(rawEvent.EndDate, out var ed) ? ed : null,
|
||||
CreatedAt = DateTime.TryParse(rawEvent.CreatedAt, out var cd) ? cd : DateTime.UtcNow,
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsActive = rawEvent.Active,
|
||||
IsClosed = rawEvent.Closed,
|
||||
Tags = rawEvent.Tags != null && rawEvent.Tags.Count > 0
|
||||
? string.Join(", ", rawEvent.Tags.Select(t => t.Label))
|
||||
: string.Empty,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
if (rawEvent.Markets != null)
|
||||
{
|
||||
foreach (var rawMarket in rawEvent.Markets)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rawMarket.ConditionId) || string.IsNullOrEmpty(rawMarket.ClobTokenIds)) continue;
|
||||
|
||||
var market = MapGammaMarket(rawMarket, ev.Tags ?? "");
|
||||
ev.Markets.Add(market);
|
||||
}
|
||||
}
|
||||
|
||||
return ev;
|
||||
}
|
||||
|
||||
private Market MapGammaMarket(GammaMarketResponse raw, string parentTags = "")
|
||||
{
|
||||
long.TryParse(raw.Id, out var marketNumericId);
|
||||
|
||||
var market = new Market
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformMarketId = raw.ConditionId,
|
||||
PlatformMarketId = marketNumericId,
|
||||
ConditionId = raw.ConditionId,
|
||||
QuestionId = raw.QuestionId,
|
||||
MarketSlug = raw.Slug,
|
||||
EventSlug = eventSlug,
|
||||
Description = raw.Description,
|
||||
ImageUrl = raw.Image,
|
||||
Question = raw.Question,
|
||||
Category = raw.Category,
|
||||
Category = string.IsNullOrWhiteSpace(raw.Category) ? parentTags : raw.Category,
|
||||
Volume = (decimal)raw.Volume,
|
||||
Volume24h = (decimal)raw.Volume24hr,
|
||||
Liquidity = (decimal)raw.Liquidity,
|
||||
StartDate = DateTime.TryParse(raw.StartDate, out var sd) ? sd : null,
|
||||
EndDate = DateTime.TryParse(raw.EndDate, out var ed) ? ed : null,
|
||||
CreatedAt = DateTime.TryParse(raw.CreatedAt, out var cd) ? cd : DateTime.UtcNow,
|
||||
StartDate = DateTime.TryParse(raw.StartDate, out var msd) ? msd : null,
|
||||
EndDate = DateTime.TryParse(raw.EndDate ?? raw.EndDateIso, out var med) ? med : null,
|
||||
CreatedAt = DateTime.TryParse(raw.CreatedAt, out var mcd) ? mcd : DateTime.UtcNow,
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsResolved = raw.Resolved || raw.Closed, // Prefer resolved flag
|
||||
IsResolved = raw.Resolved || raw.Closed,
|
||||
ResolutionOutcome = raw.ResolutionOutcome,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
+193
-17
@@ -21,6 +21,8 @@ partial class MainForm
|
||||
btn_serverstart = new ToolStripButton();
|
||||
btn_localWebserver = new ToolStripButton();
|
||||
statusStrip1 = new StatusStrip();
|
||||
label_apiRatelimit = new ToolStripStatusLabel();
|
||||
label_buildVersion = new ToolStripStatusLabel();
|
||||
tabControl1 = new TabControl();
|
||||
tabPage_terminal = new TabPage();
|
||||
rtb_terminal = new RichTextBox();
|
||||
@@ -34,8 +36,7 @@ partial class MainForm
|
||||
developmentToolStripMenuItem = new ToolStripMenuItem();
|
||||
btn_dbReset = new ToolStripMenuItem();
|
||||
btn_syncmarkets = new ToolStripMenuItem();
|
||||
label_apiRatelimit = new ToolStripStatusLabel();
|
||||
label_buildVersion = new ToolStripStatusLabel();
|
||||
btn_dbUpdate = new ToolStripMenuItem();
|
||||
toolStrip1.SuspendLayout();
|
||||
statusStrip1.SuspendLayout();
|
||||
tabControl1.SuspendLayout();
|
||||
@@ -76,6 +77,21 @@ partial class MainForm
|
||||
statusStrip1.Size = new Size(1864, 32);
|
||||
statusStrip1.TabIndex = 1;
|
||||
//
|
||||
// label_apiRatelimit
|
||||
//
|
||||
label_apiRatelimit.Name = "label_apiRatelimit";
|
||||
label_apiRatelimit.Size = new Size(1782, 25);
|
||||
label_apiRatelimit.Spring = true;
|
||||
label_apiRatelimit.Text = "API: OK";
|
||||
label_apiRatelimit.TextAlign = ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// label_buildVersion
|
||||
//
|
||||
label_buildVersion.Name = "label_buildVersion";
|
||||
label_buildVersion.Size = new Size(67, 25);
|
||||
label_buildVersion.Text = "Build: -";
|
||||
label_buildVersion.TextAlign = ContentAlignment.MiddleRight;
|
||||
//
|
||||
// tabControl1
|
||||
//
|
||||
tabControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
@@ -150,18 +166,182 @@ partial class MainForm
|
||||
// btn_logfolder
|
||||
//
|
||||
btn_logfolder.Name = "btn_logfolder";
|
||||
btn_logfolder.Size = new Size(261, 34);
|
||||
btn_logfolder.Text = "Show Logfolder";
|
||||
btn_logfolder.Click += btn_logfolder_Click;
|
||||
//
|
||||
// btn_openbrowser
|
||||
toolStrip1 = new ToolStrip();
|
||||
btn_serverstart = new ToolStripButton();
|
||||
btn_localWebserver = new ToolStripButton();
|
||||
statusStrip1 = new StatusStrip();
|
||||
label_apiRatelimit = new ToolStripStatusLabel();
|
||||
label_dbSize = new ToolStripStatusLabel();
|
||||
label_buildVersion = new ToolStripStatusLabel();
|
||||
tabControl1 = new TabControl();
|
||||
tabPage_terminal = new TabPage();
|
||||
rtb_terminal = new RichTextBox();
|
||||
tabPage2 = new TabPage();
|
||||
pg_settings = new PropertyGrid();
|
||||
menuStrip1 = new MenuStrip();
|
||||
filesToolStripMenuItem = new ToolStripMenuItem();
|
||||
editToolStripMenuItem = new ToolStripMenuItem();
|
||||
btn_logfolder = new ToolStripMenuItem();
|
||||
btn_openbrowser = new ToolStripMenuItem();
|
||||
developmentToolStripMenuItem = new ToolStripMenuItem();
|
||||
btn_dbReset = new ToolStripMenuItem();
|
||||
btn_syncmarkets = new ToolStripMenuItem();
|
||||
btn_dbUpdate = new ToolStripMenuItem();
|
||||
toolStrip1.SuspendLayout();
|
||||
statusStrip1.SuspendLayout();
|
||||
tabControl1.SuspendLayout();
|
||||
tabPage_terminal.SuspendLayout();
|
||||
tabPage2.SuspendLayout();
|
||||
menuStrip1.SuspendLayout();
|
||||
SuspendLayout();
|
||||
//
|
||||
// toolStrip1
|
||||
//
|
||||
toolStrip1.ImageScalingSize = new Size(24, 24);
|
||||
toolStrip1.Items.AddRange(new ToolStripItem[] { btn_serverstart, btn_localWebserver });
|
||||
toolStrip1.Location = new Point(0, 33);
|
||||
toolStrip1.Name = "toolStrip1";
|
||||
toolStrip1.Size = new Size(1864, 34);
|
||||
toolStrip1.TabIndex = 0;
|
||||
//
|
||||
// btn_serverstart
|
||||
//
|
||||
btn_serverstart.ImageTransparentColor = Color.Magenta;
|
||||
btn_serverstart.Name = "btn_serverstart";
|
||||
btn_serverstart.Size = new Size(127, 29);
|
||||
btn_serverstart.Text = "▶ Start Server";
|
||||
//
|
||||
// btn_localWebserver
|
||||
//
|
||||
btn_localWebserver.ImageTransparentColor = Color.Magenta;
|
||||
btn_localWebserver.Name = "btn_localWebserver";
|
||||
btn_localWebserver.Size = new Size(161, 29);
|
||||
btn_localWebserver.Text = "▶ Start Webserver";
|
||||
//
|
||||
// statusStrip1
|
||||
//
|
||||
statusStrip1.ImageScalingSize = new Size(24, 24);
|
||||
statusStrip1.Items.AddRange(new ToolStripItem[] { label_apiRatelimit, label_dbSize, label_buildVersion });
|
||||
statusStrip1.Location = new Point(0, 1000);
|
||||
statusStrip1.Name = "statusStrip1";
|
||||
statusStrip1.Size = new Size(1864, 32);
|
||||
statusStrip1.TabIndex = 1;
|
||||
//
|
||||
// label_apiRatelimit
|
||||
//
|
||||
label_apiRatelimit.Name = "label_apiRatelimit";
|
||||
label_apiRatelimit.Size = new Size(1600, 25);
|
||||
label_apiRatelimit.Spring = true;
|
||||
label_apiRatelimit.Text = "API: OK";
|
||||
label_apiRatelimit.TextAlign = ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// label_dbSize
|
||||
//
|
||||
label_dbSize.Name = "label_dbSize";
|
||||
label_dbSize.Size = new Size(150, 25);
|
||||
label_dbSize.Text = "DB Size: -";
|
||||
label_dbSize.TextAlign = ContentAlignment.MiddleRight;
|
||||
//
|
||||
// label_buildVersion
|
||||
//
|
||||
label_buildVersion.Name = "label_buildVersion";
|
||||
label_buildVersion.Size = new Size(67, 25);
|
||||
label_buildVersion.Text = "Build: -";
|
||||
label_buildVersion.TextAlign = ContentAlignment.MiddleRight;
|
||||
//
|
||||
// tabControl1
|
||||
//
|
||||
tabControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
tabControl1.Controls.Add(tabPage_terminal);
|
||||
tabControl1.Controls.Add(tabPage2);
|
||||
tabControl1.Location = new Point(0, 61);
|
||||
tabControl1.Name = "tabControl1";
|
||||
tabControl1.SelectedIndex = 0;
|
||||
tabControl1.Size = new Size(1864, 946);
|
||||
tabControl1.TabIndex = 2;
|
||||
//
|
||||
// tabPage_terminal
|
||||
//
|
||||
tabPage_terminal.Controls.Add(rtb_terminal);
|
||||
tabPage_terminal.Location = new Point(4, 34);
|
||||
tabPage_terminal.Name = "tabPage_terminal";
|
||||
tabPage_terminal.Padding = new Padding(3);
|
||||
tabPage_terminal.Size = new Size(1856, 908);
|
||||
tabPage_terminal.TabIndex = 0;
|
||||
tabPage_terminal.Text = "Terminal";
|
||||
tabPage_terminal.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// rtb_terminal
|
||||
//
|
||||
rtb_terminal.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
rtb_terminal.Location = new Point(3, 6);
|
||||
rtb_terminal.Name = "rtb_terminal";
|
||||
rtb_terminal.Size = new Size(1847, 896);
|
||||
rtb_terminal.TabIndex = 0;
|
||||
rtb_terminal.Text = "";
|
||||
//
|
||||
// tabPage2
|
||||
//
|
||||
tabPage2.Controls.Add(pg_settings);
|
||||
tabPage2.Location = new Point(4, 34);
|
||||
tabPage2.Name = "tabPage2";
|
||||
tabPage2.Padding = new Padding(3);
|
||||
tabPage2.Size = new Size(1856, 908);
|
||||
tabPage2.TabIndex = 1;
|
||||
tabPage2.Text = "Settings";
|
||||
tabPage2.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// pg_settings
|
||||
//
|
||||
pg_settings.Location = new Point(3, 6);
|
||||
pg_settings.Name = "pg_settings";
|
||||
pg_settings.Size = new Size(1850, 896);
|
||||
pg_settings.TabIndex = 0;
|
||||
//
|
||||
// menuStrip1
|
||||
//
|
||||
menuStrip1.ImageScalingSize = new Size(24, 24);
|
||||
menuStrip1.Items.AddRange(new ToolStripItem[] { filesToolStripMenuItem, editToolStripMenuItem, developmentToolStripMenuItem });
|
||||
menuStrip1.Location = new Point(0, 0);
|
||||
menuStrip1.Name = "menuStrip1";
|
||||
menuStrip1.Size = new Size(1864, 33);
|
||||
menuStrip1.TabIndex = 3;
|
||||
//
|
||||
// filesToolStripMenuItem
|
||||
//
|
||||
filesToolStripMenuItem.Name = "filesToolStripMenuItem";
|
||||
filesToolStripMenuItem.Size = new Size(62, 29);
|
||||
filesToolStripMenuItem.Text = "Files";
|
||||
//
|
||||
// editToolStripMenuItem
|
||||
//
|
||||
editToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { btn_logfolder, btn_openbrowser });
|
||||
editToolStripMenuItem.Name = "editToolStripMenuItem";
|
||||
editToolStripMenuItem.Size = new Size(58, 29);
|
||||
editToolStripMenuItem.Text = "Edit";
|
||||
//
|
||||
// btn_logfolder
|
||||
//
|
||||
btn_logfolder.Name = "btn_logfolder";
|
||||
btn_logfolder.Size = new Size(261, 34);
|
||||
btn_logfolder.Text = "Show Logfolder";
|
||||
btn_logfolder.Click += btn_logfolder_Click;
|
||||
//
|
||||
// btn_openbrowser
|
||||
//
|
||||
btn_openbrowser.Name = "btn_openbrowser";
|
||||
btn_openbrowser.Size = new Size(261, 34);
|
||||
btn_openbrowser.Text = "Show Local WebUI";
|
||||
btn_openbrowser.Click += btn_openbrowser_Click;
|
||||
//
|
||||
// developmentToolStripMenuItem
|
||||
//
|
||||
developmentToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { btn_dbReset, btn_syncmarkets });
|
||||
developmentToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { btn_dbReset, btn_syncmarkets, btn_dbUpdate });
|
||||
developmentToolStripMenuItem.Name = "developmentToolStripMenuItem";
|
||||
developmentToolStripMenuItem.Size = new Size(135, 29);
|
||||
developmentToolStripMenuItem.Text = "Development";
|
||||
@@ -169,28 +349,22 @@ partial class MainForm
|
||||
// btn_dbReset
|
||||
//
|
||||
btn_dbReset.Name = "btn_dbReset";
|
||||
btn_dbReset.Size = new Size(270, 34);
|
||||
btn_dbReset.Text = "reset TradesDB";
|
||||
//
|
||||
// btn_syncmarkets
|
||||
//
|
||||
btn_syncmarkets.Name = "btn_syncmarkets";
|
||||
btn_syncmarkets.Text = "Sync Marketsa";
|
||||
btn_syncmarkets.Size = new Size(270, 34);
|
||||
btn_syncmarkets.Text = "Sync Markets";
|
||||
btn_syncmarkets.Click += syncMarketsaToolStripMenuItem_Click;
|
||||
//
|
||||
// label_apiRatelimit
|
||||
// btn_dbUpdate
|
||||
//
|
||||
label_apiRatelimit.Name = "label_apiRatelimit";
|
||||
label_apiRatelimit.Size = new Size(1670, 25);
|
||||
label_apiRatelimit.Spring = true;
|
||||
label_apiRatelimit.Text = "API: OK";
|
||||
label_apiRatelimit.TextAlign = ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// label_buildVersion
|
||||
//
|
||||
label_buildVersion.Name = "label_buildVersion";
|
||||
label_buildVersion.Size = new Size(179, 25);
|
||||
label_buildVersion.Text = "Build: -";
|
||||
label_buildVersion.TextAlign = ContentAlignment.MiddleRight;
|
||||
btn_dbUpdate.Name = "btn_dbUpdate";
|
||||
btn_dbUpdate.Size = new Size(270, 34);
|
||||
btn_dbUpdate.Text = "UpdateDB";
|
||||
btn_dbUpdate.Click += btn_dbUpdate_Click;
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
@@ -237,5 +411,7 @@ partial class MainForm
|
||||
private ToolStripMenuItem btn_syncmarkets;
|
||||
private PropertyGrid pg_settings;
|
||||
private ToolStripStatusLabel label_apiRatelimit;
|
||||
private ToolStripStatusLabel label_dbSize;
|
||||
private ToolStripStatusLabel label_buildVersion;
|
||||
private ToolStripMenuItem btn_dbUpdate;
|
||||
}
|
||||
|
||||
@@ -59,10 +59,16 @@ public partial class MainForm : Form
|
||||
btn_serverstart.Click += Btn_serverstart_Click;
|
||||
btn_localWebserver.Click += Btn_localWebserver_Click;
|
||||
btn_syncmarkets.Click += syncMarketsaToolStripMenuItem_Click;
|
||||
btn_dbUpdate.Click += btn_dbUpdate_Click;
|
||||
|
||||
Log.Information("MainForm initialized. Ready.");
|
||||
Log.Information("Press 'Start Server' to begin polling & discovery.");
|
||||
Log.Information("Press 'Start Local Webserver' to launch the WebUI on http://localhost:{Port}", _settings.WebserverPort);
|
||||
|
||||
_ = UpdateDbSizeAsync();
|
||||
var dbSizeTimer = new System.Windows.Forms.Timer { Interval = 6 * 60 * 60 * 1000 };
|
||||
dbSizeTimer.Tick += async (s, e) => await UpdateDbSizeAsync();
|
||||
dbSizeTimer.Start();
|
||||
}
|
||||
|
||||
private async void Btn_serverstart_Click(object? sender, EventArgs e)
|
||||
@@ -193,11 +199,67 @@ public partial class MainForm : Form
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Manual market sync failed");
|
||||
MessageBox.Show($"Market sync failed: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
MessageBox.Show($"Error syncing markets: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
btn_syncmarkets.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void btn_dbUpdate_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (_workerRunning)
|
||||
{
|
||||
MessageBox.Show("Database update cannot be run while background workers are running.",
|
||||
"Workers Busy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
btn_dbUpdate.Enabled = false;
|
||||
Log.Information("Manual database update triggered...");
|
||||
await _webServer!.UpdateDatabaseAsync();
|
||||
Log.Information("Database updated successfully.");
|
||||
MessageBox.Show("Database update completed successfully.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Manual database update failed");
|
||||
MessageBox.Show($"Database update failed: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
btn_dbUpdate.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateDbSizeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Build the connection string
|
||||
var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder(_settings.ConnectionString);
|
||||
if (string.IsNullOrWhiteSpace(csBuilder.Database))
|
||||
return; // Not ready or valid yet
|
||||
|
||||
using var conn = new MySqlConnector.MySqlConnection(_settings.ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT SUM(data_length + index_length) / 1024 / 1024 FROM information_schema.tables WHERE table_schema = DATABASE();";
|
||||
|
||||
var result = await cmd.ExecuteScalarAsync();
|
||||
if (result != DBNull.Value && result != null)
|
||||
{
|
||||
var sizeMb = Convert.ToDouble(result);
|
||||
this.Invoke(() => label_dbSize.Text = $"DB Size: {sizeMb:F2} MB");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Invoke(() => label_dbSize.Text = "DB Size: Error");
|
||||
Log.Debug(ex, "Failed to fetch DB size for status bar");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ internal static class Program
|
||||
.MinimumLevel.Override("Microsoft.Hosting", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
|
||||
// Suppress duplicate entry EF errors completely from logging
|
||||
.Filter.ByExcluding(e => e.Exception != null && e.Exception.ToString().Contains("Duplicate entry"))
|
||||
|
||||
// ── Console (simple) ──
|
||||
.WriteTo.Console(outputTemplate: simpleTemplate, restrictedToMinimumLevel: LogEventLevel.Warning)
|
||||
|
||||
|
||||
@@ -21,6 +21,14 @@ public class EmbeddedWebServer
|
||||
public string? ConnectionString { get; set; }
|
||||
public bool DbConnectionDebug { get; set; }
|
||||
|
||||
public async Task UpdateDatabaseAsync()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
Predictalytics.Infrastructure.DependencyInjection.AddPredictalytics(services, new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(), ConnectionString, DbConnectionDebug);
|
||||
var provider = services.BuildServiceProvider();
|
||||
await Predictalytics.Infrastructure.DependencyInjection.EnsureDatabaseAsync(provider, DbConnectionDebug);
|
||||
}
|
||||
|
||||
public async Task StartWebServerAsync(int port = 5000)
|
||||
{
|
||||
lock (_lock) { if (_app != null) return; }
|
||||
@@ -161,25 +169,25 @@ public class EmbeddedWebServer
|
||||
.First(x => x.Platform == p.Platform);
|
||||
|
||||
await rateLimiter.WaitAsync(provider.Platform, ct);
|
||||
var markets = await provider.GetMarketsAsync(batchSize, offset.ToString(), includeClosed, ct);
|
||||
var events = await provider.GetEventsAsync(batchSize, offset.ToString(), includeClosed, ct);
|
||||
|
||||
if (markets == null || markets.Count == 0)
|
||||
if (events == null || events.Count == 0)
|
||||
{
|
||||
Log.Warning("[{Platform}] No more markets found at offset {Offset} (includeClosed={Closed}). Ending pass.", provider.PlatformName, offset, includeClosed);
|
||||
Log.Warning("[{Platform}] No more events found at offset {Offset} (includeClosed={Closed}). Ending pass.", provider.PlatformName, offset, includeClosed);
|
||||
break;
|
||||
}
|
||||
|
||||
await marketRepo.AddOrUpdateRangeAsync(markets, ct);
|
||||
await marketRepo.AddOrUpdateEventsAsync(events, ct);
|
||||
|
||||
totalSynced += markets.Count;
|
||||
totalSynced += events.Count;
|
||||
offset += batchSize;
|
||||
|
||||
if (totalSynced % 500 == 0 || markets.Count < batchSize)
|
||||
Log.Information("[{Platform}] Synced {Total} markets so far (offset={Offset}, includeClosed={Closed})...", provider.PlatformName, totalSynced, offset, includeClosed);
|
||||
if (totalSynced % 500 == 0 || events.Count < batchSize)
|
||||
Log.Information("[{Platform}] Synced {Total} events so far (offset={Offset}, includeClosed={Closed})...", provider.PlatformName, totalSynced, offset, includeClosed);
|
||||
|
||||
if (markets.Count < batchSize)
|
||||
if (events.Count < batchSize)
|
||||
{
|
||||
Log.Warning("[{Platform}] Batch was smaller than limit ({Count}/{Limit}), assuming end of list.", provider.PlatformName, markets.Count, batchSize);
|
||||
Log.Warning("[{Platform}] Batch was smaller than limit ({Count}/{Limit}), assuming end of list.", provider.PlatformName, events.Count, batchSize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,11 @@
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
},
|
||||
"OpenRouter": {
|
||||
"ApiKey": "",
|
||||
"BaseUrl": "https://openrouter.ai/api/v1",
|
||||
"DefaultModel": "google/gemini-flash-1.5",
|
||||
"ManualAnalysisModel": "anthropic/claude-3-opus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public static class DependencyInjection
|
||||
services.AddHostedService<TraderAnalyticsWorker>();
|
||||
services.AddHostedService<ScoringAndAlertsWorker>();
|
||||
services.AddHostedService<TradeRetentionWorker>();
|
||||
services.AddHostedService<TradeContextEnrichmentWorker>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class MarketHistoryWorker : BackgroundService
|
||||
private readonly ILogger<MarketHistoryWorker> _logger;
|
||||
|
||||
private const int CooldownHours = 6;
|
||||
private const int MarketsPerCycle = 10;
|
||||
private const int MarketsPerCycle = 50;
|
||||
private const int TradesPerFetch = 100;
|
||||
|
||||
public MarketHistoryWorker(IServiceProvider services, ILogger<MarketHistoryWorker> logger)
|
||||
@@ -64,7 +64,7 @@ public class MarketHistoryWorker : BackgroundService
|
||||
using var platformCtx = PlatformLogContext.Push(provider.PlatformName);
|
||||
await rateLimiter.WaitAsync(market.Platform, stoppingToken);
|
||||
|
||||
var trades = await provider.GetMarketTradesAsync(market.PlatformMarketId, TradesPerFetch, stoppingToken);
|
||||
var trades = await provider.GetMarketTradesAsync(market.ConditionId, TradesPerFetch, stoppingToken);
|
||||
|
||||
// Extract unique wallet addresses from TransientWallet [NotMapped].
|
||||
// Providers set this during in-memory mapping; it is NOT stored in the DB.
|
||||
|
||||
@@ -66,15 +66,15 @@ public class MarketSyncWorker : BackgroundService
|
||||
.First(x => x.Platform == p.Platform);
|
||||
|
||||
await rateLimiter.WaitAsync(provider.Platform, stoppingToken);
|
||||
var markets = await provider.GetMarketsAsync(batchSize, offset.ToString(), includeClosed, stoppingToken);
|
||||
var events = await provider.GetEventsAsync(batchSize, offset.ToString(), includeClosed, stoppingToken);
|
||||
|
||||
if (markets.Count == 0) break;
|
||||
if (events.Count == 0) break;
|
||||
|
||||
await marketRepo.AddOrUpdateRangeAsync(markets, stoppingToken);
|
||||
await marketRepo.AddOrUpdateEventsAsync(events, stoppingToken);
|
||||
|
||||
passSynced += markets.Count;
|
||||
cycleTotalSynced += markets.Count;
|
||||
_statsService.TrackMarketSync(provider.Platform, markets.Count);
|
||||
passSynced += events.Count;
|
||||
cycleTotalSynced += events.Count;
|
||||
_statsService.TrackMarketSync(provider.Platform, events.Count);
|
||||
offset += batchSize;
|
||||
|
||||
if (passSynced % 500 == 0)
|
||||
|
||||
@@ -36,7 +36,7 @@ public class PollingWorker : BackgroundService
|
||||
using (var scope = _services.CreateScope())
|
||||
{
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ITraderRepository>();
|
||||
tradersToProcess = await repo.GetAllAsync(take: 100, ct: stoppingToken);
|
||||
tradersToProcess = await repo.GetTradersForPollingAsync(take: 100, ct: stoppingToken);
|
||||
}
|
||||
|
||||
_logger.LogWarning("📊 Polling {Count} traders...", tradersToProcess.Count);
|
||||
@@ -121,7 +121,7 @@ public class PollingWorker : BackgroundService
|
||||
{
|
||||
// Final deduplication of the batch itself
|
||||
var uniqueNewTrades = newTrades
|
||||
.GroupBy(tr => tr.PlatformTradeId)
|
||||
.GroupBy(tr => tr.PlatformTradeId, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
@@ -135,7 +135,7 @@ public class PollingWorker : BackgroundService
|
||||
}
|
||||
catch (Exception ex) when (ex.ToString().Contains("Duplicate entry") || (ex.InnerException?.Message.Contains("Duplicate entry") ?? false))
|
||||
{
|
||||
_logger.LogWarning("{Trader}: Skipping batch due to duplicate entries (likely already imported)", trader.DisplayName);
|
||||
_statsService.TrackDuplicateError(trader.Platform, 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -52,8 +52,9 @@ public class ReportingWorker : BackgroundService
|
||||
|
||||
foreach (var (platform, stats) in statsMap)
|
||||
{
|
||||
_logger.LogWarning("[{Platform}] Markets: {M} | New Traders: {T} | Activities: {A}",
|
||||
platform, stats.MarketsSynced, stats.TradersDiscovered, stats.TradesProcessed);
|
||||
var dupStr = stats.DuplicateErrors > 0 ? $" | Duplikatfehler: {stats.DuplicateErrors}" : "";
|
||||
_logger.LogWarning("[{Platform}] Markets: {M} | New Traders: {T} | Activities: {A}{D}",
|
||||
platform, stats.MarketsSynced, stats.TradersDiscovered, stats.TradesProcessed, dupStr);
|
||||
}
|
||||
|
||||
_logger.LogWarning("------------------------------------------------");
|
||||
|
||||
@@ -37,7 +37,7 @@ public class TopHolderDiscoveryWorker : BackgroundService
|
||||
var rateLimiter = scope.ServiceProvider.GetRequiredService<IRateLimiter>();
|
||||
|
||||
// Get top active markets by volume
|
||||
var activeMarkets = await marketRepo.GetActiveAsync(20, stoppingToken);
|
||||
var activeMarkets = await marketRepo.GetActiveAsync(100, stoppingToken);
|
||||
_logger.LogInformation("👥 Scanning top holders across {Count} active markets", activeMarkets.Count);
|
||||
|
||||
int totalDiscovered = 0;
|
||||
@@ -54,9 +54,9 @@ public class TopHolderDiscoveryWorker : BackgroundService
|
||||
using var platformCtx = PlatformLogContext.Push(provider.PlatformName);
|
||||
await rateLimiter.WaitAsync(market.Platform, stoppingToken);
|
||||
|
||||
var holders = await provider.GetTopHoldersAsync(market.PlatformMarketId, 10, stoppingToken);
|
||||
var newHolders = await provider.GetTopHoldersAsync(market.ConditionId, 50, stoppingToken);
|
||||
|
||||
foreach (var holder in holders)
|
||||
foreach (var holder in newHolders)
|
||||
{
|
||||
// Skip empty wallet addresses
|
||||
if (string.IsNullOrWhiteSpace(holder.PlatformUserId)) continue;
|
||||
@@ -95,9 +95,9 @@ public class TopHolderDiscoveryWorker : BackgroundService
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex) { _logger.LogError(ex, "TopHolderDiscoveryWorker error"); }
|
||||
|
||||
// Run every 15 minutes
|
||||
_logger.LogInformation("👥 Next top holder scan in 15 minutes.");
|
||||
await Task.Delay(TimeSpan.FromMinutes(15), stoppingToken);
|
||||
// Run every 5 minutes
|
||||
_logger.LogInformation("🚀 Next top holder scan in 5 minutes.");
|
||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation("👥 TopHolderDiscoveryWorker stopped");
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Providers.Polymarket;
|
||||
using Predictalytics.Application.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Predictalytics.Worker.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Retroactively enriches trades of top/watchlisted traders with high-resolution
|
||||
/// 1-minute price contexts immediately before and after execution.
|
||||
/// Avoids burdening the live PollingWorker.
|
||||
/// </summary>
|
||||
public class TradeContextEnrichmentWorker : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<TradeContextEnrichmentWorker> _logger;
|
||||
|
||||
public TradeContextEnrichmentWorker(IServiceProvider services, ILogger<TradeContextEnrichmentWorker> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("🧠 TradeContextEnrichmentWorker started");
|
||||
await Task.Delay(10000, stoppingToken); // Wait for app startup
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
var tradeRepo = scope.ServiceProvider.GetRequiredService<ITradeRepository>();
|
||||
var polymarketClient = scope.ServiceProvider.GetRequiredService<PolymarketApiClient>();
|
||||
var rateLimiter = scope.ServiceProvider.GetRequiredService<IRateLimiter>();
|
||||
|
||||
// Fetch a batch of unenriched trades
|
||||
var unenrichedTrades = await tradeRepo.GetTradesForContextEnrichmentAsync(500, stoppingToken);
|
||||
|
||||
if (unenrichedTrades.Count == 0)
|
||||
{
|
||||
// No work to do, sleep longer
|
||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Group by AssetId to minimize API calls (1 call per asset fetches the whole 1m history)
|
||||
var tradesByAsset = unenrichedTrades.GroupBy(t => t.AssetId).ToList();
|
||||
_logger.LogInformation("Enriching {TradeCount} trades across {AssetCount} assets...", unenrichedTrades.Count, tradesByAsset.Count);
|
||||
|
||||
int updatedCount = 0;
|
||||
|
||||
foreach (var group in tradesByAsset)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
var assetId = group.Key;
|
||||
|
||||
try
|
||||
{
|
||||
// Wait for rate limiter to respect global limits
|
||||
await rateLimiter.WaitAsync(Predictalytics.Domain.Enums.PlatformType.Polymarket, stoppingToken);
|
||||
|
||||
// Fetch the 10-minute history for the entire market (using "max" since "1m" is invalid for full history)
|
||||
var history = await polymarketClient.GetPricesHistoryAsync(assetId, "max", stoppingToken);
|
||||
if (history == null || history.Count == 0)
|
||||
{
|
||||
// If history is not available, mark as enriched to prevent infinite loops,
|
||||
// but prices remain null.
|
||||
foreach (var trade in group)
|
||||
{
|
||||
trade.IsContextEnriched = true;
|
||||
await tradeRepo.UpdateAsync(trade, stoppingToken);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Order history chronologically for safe binary search / LINQ
|
||||
var orderedHistory = history.OrderBy(h => h.Timestamp).ToList();
|
||||
|
||||
foreach (var trade in group)
|
||||
{
|
||||
var tradeTimeUnix = ((DateTimeOffset)trade.ExecutedAt).ToUnixTimeSeconds();
|
||||
|
||||
// Find the closest point BEFORE the trade (approx 1 min before)
|
||||
var prePoint = orderedHistory
|
||||
.LastOrDefault(h => h.Timestamp < tradeTimeUnix);
|
||||
|
||||
// Find the closest point AFTER the trade (approx 1 min after)
|
||||
var postPoint = orderedHistory
|
||||
.FirstOrDefault(h => h.Timestamp > tradeTimeUnix);
|
||||
|
||||
trade.PreTradePrice1m = prePoint != null ? (decimal)prePoint.Price : null;
|
||||
trade.PostTradePrice1m = postPoint != null ? (decimal)postPoint.Price : null;
|
||||
trade.IsContextEnriched = true;
|
||||
|
||||
await tradeRepo.UpdateAsync(trade, stoppingToken);
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to enrich asset {AssetId}", assetId);
|
||||
// Do NOT mark as enriched on failure, try again later
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("✅ Enriched {UpdatedCount} trades in this cycle.", updatedCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in TradeContextEnrichmentWorker loop");
|
||||
}
|
||||
|
||||
// Sleep briefly before next batch
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation("🧠 TradeContextEnrichmentWorker stopped");
|
||||
}
|
||||
}
|
||||
@@ -151,14 +151,14 @@ public class TradeHistoryWorker : BackgroundService
|
||||
|
||||
if (newTrades.Count > 0)
|
||||
{
|
||||
var uniqueNewTrades = newTrades.GroupBy(tr => tr.PlatformTradeId).Select(g => g.First()).ToList();
|
||||
var uniqueNewTrades = newTrades.GroupBy(tr => tr.PlatformTradeId, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList();
|
||||
try {
|
||||
await tradeRepo.AddRangeAsync(uniqueNewTrades, ct);
|
||||
_statsService.TrackTradeActivity(trader.Platform, uniqueNewTrades.Count);
|
||||
trader.TotalTrades += uniqueNewTrades.Count;
|
||||
_logger.LogInformation("{Trader}: {New} new trades imported", trader.DisplayName, uniqueNewTrades.Count);
|
||||
} catch (Exception ex) when (ex.ToString().Contains("Duplicate entry")) {
|
||||
_logger.LogWarning("{Trader}: Skipping batch due to duplicates", trader.DisplayName);
|
||||
} catch (Exception ex) when (ex.ToString().Contains("Duplicate entry") || (ex.InnerException?.Message.Contains("Duplicate entry") ?? false)) {
|
||||
_statsService.TrackDuplicateError(trader.Platform, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,25 +33,26 @@ public class TraderAnalyticsWorker : BackgroundService
|
||||
_logger.LogError(ex, "Error in TraderAnalyticsWorker");
|
||||
}
|
||||
|
||||
_logger.LogInformation("TraderAnalyticsWorker sleeping for 12 hours...");
|
||||
await Task.Delay(TimeSpan.FromHours(12), ct);
|
||||
_logger.LogInformation("TraderAnalyticsWorker sleeping for 2 hours...");
|
||||
await Task.Delay(TimeSpan.FromHours(2), ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAnalyticsAsync(CancellationToken ct)
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var pnlEngine = scope.ServiceProvider.GetRequiredService<IPositionPnLEngine>();
|
||||
|
||||
var cutoff30d = DateTime.UtcNow.AddDays(-30);
|
||||
List<int> traderIds;
|
||||
|
||||
using (var scope = _services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
// Find traders active in the last 30 days
|
||||
var traderIds = await db.Trades
|
||||
traderIds = await db.Trades
|
||||
.Where(t => t.ExecutedAt >= cutoff30d)
|
||||
.Select(t => t.TraderId)
|
||||
.Distinct()
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Found {Count} active traders to analyze", traderIds.Count);
|
||||
|
||||
@@ -59,6 +60,8 @@ public class TraderAnalyticsWorker : BackgroundService
|
||||
{
|
||||
try
|
||||
{
|
||||
using var traderScope = _services.CreateScope();
|
||||
var pnlEngine = traderScope.ServiceProvider.GetRequiredService<IPositionPnLEngine>();
|
||||
await pnlEngine.RecalculateTraderPositionsAsync(id, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user