Initial commit: Predictalytics solution

Clean Architecture .NET 8 solution (Domain/Application/Infrastructure/Api/Worker/WinFormsHost)
for analyzing Polymarket traders for copytrading/strategy-replication candidates.

Includes EF Core InitialBaseline migration and DB secrets removed from source/config
in preparation for version control.
This commit is contained in:
Richard
2026-07-01 19:53:29 +02:00
commit afb251acfc
107 changed files with 9613 additions and 0 deletions
@@ -0,0 +1,38 @@
using Predictalytics.Domain.Enums;
namespace Predictalytics.Domain.Entities;
/// <summary>
/// An alert triggered by the system based on configurable rules.
/// </summary>
public class Alert
{
public int Id { get; set; }
/// <summary>Type of alert.</summary>
public AlertType Type { get; set; }
/// <summary>Platform where the triggering event occurred.</summary>
public PlatformType Platform { get; set; }
/// <summary>Optional reference to the trader who triggered this alert.</summary>
public int? TraderId { get; set; }
/// <summary>Alert title / headline.</summary>
public string Title { get; set; } = string.Empty;
/// <summary>Detailed message describing the alert.</summary>
public string Message { get; set; } = string.Empty;
/// <summary>Severity: 1=Low, 2=Medium, 3=High, 4=Critical.</summary>
public int Severity { get; set; } = 1;
/// <summary>Whether the alert has been read/acknowledged.</summary>
public bool IsRead { get; set; }
/// <summary>When the alert was created.</summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation
public Trader? Trader { get; set; }
}
@@ -0,0 +1,42 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Predictalytics.Domain.Entities;
public class TraderAnalytics
{
[Key, ForeignKey("Trader")]
public int TraderId { get; set; }
public DateTime LastCalculatedAt { get; set; } = DateTime.UtcNow;
public decimal OverallPnL { get; set; }
public decimal OverallWinRate { get; set; }
public decimal PnL30d { get; set; }
public decimal WinRate30d { get; set; }
public decimal PnL7d { get; set; }
public decimal WinRate7d { get; set; }
public decimal PnL24h { get; set; }
public decimal WinRate24h { get; set; }
// Navigation
public virtual Trader Trader { get; set; } = null!;
}
public class MarketAnalytics
{
[Key, ForeignKey("Market")]
public int MarketId { get; set; }
public DateTime LastCalculatedAt { get; set; } = DateTime.UtcNow;
public decimal BotActivityScore { get; set; }
public int UniqueTradersCount { get; set; }
public decimal AverageTradeSize { get; set; }
// Navigation
public virtual Market Market { get; set; } = null!;
}
@@ -0,0 +1,69 @@
using Predictalytics.Domain.Enums;
namespace Predictalytics.Domain.Entities;
/// <summary>
/// Represents a prediction market (event/question).
/// </summary>
public class Market
{
public int Id { get; set; }
/// <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>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; }
/// <summary>Market image URL.</summary>
public string? ImageUrl { get; set; }
/// <summary>The question being predicted.</summary>
public string Question { get; set; } = string.Empty;
/// <summary>Category / tag (e.g. "Politics", "Crypto", "Sports").</summary>
public string Category { get; set; } = string.Empty;
/// <summary>Current total volume traded.</summary>
public decimal Volume { get; set; }
/// <summary>Current liquidity.</summary>
public decimal Liquidity { get; set; }
/// <summary>The time when trading opened for this market.</summary>
public DateTime? StartDate { get; set; }
/// <summary>When the market closes / resolves.</summary>
public DateTime? EndDate { get; set; }
/// <summary>Whether the market has been resolved.</summary>
public bool IsResolved { get; set; }
/// <summary>Resolution outcome (if resolved).</summary>
public string? ResolutionOutcome { get; set; }
/// <summary>When this market 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 market data was refreshed.</summary>
public DateTime? LastUpdatedAt { get; set; }
/// <summary>Last time market trades were polled for trader discovery.</summary>
public DateTime? LastTradesUpdatedAt { get; set; }
// Navigation
public ICollection<MarketOutcome> Outcomes { get; set; } = new List<MarketOutcome>();
public virtual MarketAnalytics? Analytics { get; set; }
}
@@ -0,0 +1,32 @@
namespace Predictalytics.Domain.Entities;
/// <summary>
/// Represents a single tradeable outcome within a market.
/// For example, "Yes" and "No" on a binary market, or named outcomes on a multi-outcome market.
/// The TokenId (clobTokenId on Polymarket) is the key link between trades and outcomes.
/// </summary>
public class MarketOutcome
{
public int Id { get; set; }
/// <summary>Foreign key to the parent market.</summary>
public int MarketId { get; set; }
/// <summary>Human-readable outcome label (e.g. "Yes", "No", "Trump", "Biden").</summary>
public string Label { get; set; } = string.Empty;
/// <summary>Positional index of this outcome within the market (0-based).</summary>
public int OutcomeIndex { get; set; }
/// <summary>
/// Platform-specific token identifier for this outcome.
/// On Polymarket this is the clobTokenId — the key used in trade asset_id fields.
/// </summary>
public string TokenId { get; set; } = string.Empty;
/// <summary>Current price of this outcome (0.00 to 1.00).</summary>
public decimal CurrentPrice { get; set; }
// Navigation
public Market Market { get; set; } = null!;
}
@@ -0,0 +1,15 @@
using Predictalytics.Domain.Enums;
namespace Predictalytics.Domain.Entities;
public class PlatformConfig
{
public PlatformType Id { get; set; }
public string Name { get; set; } = "";
public string DisplayName { get; set; } = "";
public bool IsActive { get; set; } = true;
public string? BaseUrl { get; set; }
public string? SettingsJson { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,82 @@
using Predictalytics.Domain.Enums;
using System.ComponentModel.DataAnnotations.Schema;
namespace Predictalytics.Domain.Entities;
/// <summary>
/// Represents a single trade executed by a trader on a prediction market.
/// </summary>
public class Trade
{
public long Id { get; set; }
/// <summary>Foreign key to the trader who made this trade.</summary>
public int TraderId { get; set; }
/// <summary>Platform where the trade occurred.</summary>
public PlatformType Platform { get; set; }
/// <summary>
/// Platform-specific trade ID for deduplication.
/// Format: "{txHash}_{assetId}_{side}" (max ~143 chars for new trades).
/// Legacy trades may include wallet address as 2nd segment.
/// </summary>
public string PlatformTradeId { get; set; } = string.Empty;
/// <summary>
/// Platform-specific market identifier string (conditionId on Polymarket, address on Limitless).
/// Kept as VARCHAR(66) for cross-referencing during import and reconciliation.
/// After full linking, prefer DbMarketId.
/// </summary>
public string MarketId { get; set; } = string.Empty;
/// <summary>
/// Foreign key to the internal Markets table. Populated during import when the market is known.
/// Null for trades whose market has not yet been synced.
/// </summary>
public int? DbMarketId { get; set; }
/// <summary>
/// Platform-specific token/asset ID (clobTokenId on Polymarket). Links to MarketOutcome.TokenId.
/// Kept as VARCHAR(66) until MarketOutcomeId is resolved.
/// </summary>
public string AssetId { get; set; } = string.Empty;
/// <summary>Foreign key to the resolved MarketOutcome (nullable until resolved via market sync).</summary>
public int? MarketOutcomeId { get; set; }
/// <summary>The outcome the trader bet on (e.g. "Yes", "No").</summary>
public string Outcome { get; set; } = string.Empty;
/// <summary>Buy or Sell.</summary>
public TradeSide Side { get; set; }
/// <summary>Price per share at execution (0.00 to 1.00 on Polymarket).</summary>
public decimal Price { get; set; }
/// <summary>Number of shares/tokens traded.</summary>
public decimal Size { get; set; }
/// <summary>Total notional value in USD.</summary>
public decimal Amount { get; set; }
/// <summary>When the trade was executed on the platform.</summary>
public DateTime ExecutedAt { get; set; }
/// <summary>Transaction hash (for blockchain-based platforms).</summary>
public string? TransactionHash { get; set; }
// ── Transient (not persisted) ──────────────────────────────────────────
/// <summary>
/// Wallet address of the trader, set by the provider during data ingestion.
/// NOT stored in the database — used transiently for trader discovery in MarketHistoryWorker.
/// </summary>
[NotMapped]
public string? TransientWallet { get; set; }
// ── Navigation ────────────────────────────────────────────────────────
public Trader Trader { get; set; } = null!;
public MarketOutcome? MarketOutcome { get; set; }
public Market? DbMarket { get; set; }
}
@@ -0,0 +1,69 @@
using Predictalytics.Domain.Enums;
namespace Predictalytics.Domain.Entities;
/// <summary>
/// Represents a trader on a prediction market platform.
/// Identity is composite: PlatformType + PlatformUserId (e.g. wallet address on Polymarket).
/// </summary>
public class Trader
{
public int Id { get; set; }
/// <summary>The platform this trader belongs to.</summary>
public PlatformType Platform { get; set; }
/// <summary>Platform-specific user identifier (e.g. Ethereum wallet address for Polymarket).</summary>
public string PlatformUserId { get; set; } = string.Empty;
/// <summary>Display name / alias (can be auto-discovered or manually set).</summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>Optional notes about the trader.</summary>
public string? Notes { get; set; }
/// <summary>Whether this trader was auto-discovered or manually added.</summary>
public bool IsAutoDiscovered { get; set; }
/// <summary>Current tier classification.</summary>
public TraderTier Tier { get; set; } = TraderTier.Unknown;
/// <summary>Classified strategy type from deep-dive analysis.</summary>
public StrategyType Strategy { get; set; } = StrategyType.Unknown;
/// <summary>Whether the trader shows bot-like behavior.</summary>
public bool IsSuspectedBot { get; set; }
/// <summary>Manual priority override (null = use calculated score).</summary>
public int? ManualPriorityOverride { get; set; }
/// <summary>When this trader was first tracked.</summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>When data was last polled for this trader.</summary>
public DateTime? LastPolledAt { get; set; }
/// <summary>When the trader's trade history was last fully synced. Used for cooldown.</summary>
public DateTime? LastTradesUpdatedAt { get; set; }
/// <summary>Whether the initial full historical trade import is complete.</summary>
public bool IsInitialImportComplete { get; set; }
/// <summary>When the API first returned an error (e.g. 404 for deleted account).</summary>
public DateTime? LastApiErrorAt { get; set; }
/// <summary>Total estimated PnL across all resolved markets.</summary>
public decimal TotalPnl { get; set; }
/// <summary>Win rate as a percentage (0-100).</summary>
public decimal WinRate { get; set; }
/// <summary>Total number of trades tracked.</summary>
public int TotalTrades { get; set; }
// Navigation properties
public ICollection<Trade> Trades { get; set; } = new List<Trade>();
public TraderScore? CurrentScore { get; set; }
public virtual TraderAnalytics? Analytics { get; set; }
public ICollection<WatchlistEntry> WatchlistEntries { get; set; } = new List<WatchlistEntry>();
}
@@ -0,0 +1,37 @@
namespace Predictalytics.Domain.Entities;
/// <summary>
/// Calculated priority and quality score for a trader.
/// Updated periodically by the scoring engine.
/// </summary>
public class TraderScore
{
public int Id { get; set; }
/// <summary>Foreign key to trader.</summary>
public int TraderId { get; set; }
/// <summary>Activity score: frequency, recency, and consistency of trading (0-100).</summary>
public decimal ActivityScore { get; set; }
/// <summary>Quality score: win rate, ROI, risk management quality (0-100).</summary>
public decimal QualityScore { get; set; }
/// <summary>Combined weighted score (0-100).</summary>
public decimal CombinedScore { get; set; }
/// <summary>Volume score: size of positions relative to market (0-100).</summary>
public decimal VolumeScore { get; set; }
/// <summary>Timing score: how well-timed entries and exits are (0-100).</summary>
public decimal TimingScore { get; set; }
/// <summary>Overall rank among all tracked traders.</summary>
public int Rank { get; set; }
/// <summary>When this score was last calculated.</summary>
public DateTime CalculatedAt { get; set; } = DateTime.UtcNow;
// Navigation
public Trader Trader { get; set; } = null!;
}
@@ -0,0 +1,27 @@
namespace Predictalytics.Domain.Entities;
/// <summary>
/// A trader that has been added to the user's watchlist for prioritized tracking.
/// </summary>
public class WatchlistEntry
{
public int Id { get; set; }
/// <summary>Foreign key to the watched trader.</summary>
public int TraderId { get; set; }
/// <summary>User-defined label for this watchlist entry.</summary>
public string Label { get; set; } = string.Empty;
/// <summary>Optional notes about why this trader is watched.</summary>
public string? Notes { get; set; }
/// <summary>Whether to receive alerts for this trader's activity.</summary>
public bool AlertsEnabled { get; set; } = true;
/// <summary>When this entry was added to the watchlist.</summary>
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
// Navigation
public Trader Trader { get; set; } = null!;
}