- MarketCategoryMapper: classify from question text (the Gamma /markets
endpoint delivers neither category nor event tags, so on-demand markets
had no signal at all), match short tokens on word boundaries ("eth" no
longer hits inside "whether", "pop" not inside "popular"), widen the
keyword lists across all categories.
- UpdateMarketFields: never overwrite a tag-derived category with an
uninformative "Other" from the on-demand path.
- PositionPnLEngine: sync Trader.TotalTrades to the actual replayed row
count — the worker-side increment counters drift (INSERT IGNORE,
deletions, historic imports) and produced Trades30d > TotalTrades.
- Tests: 14 new (mapper classification + word-boundary regression,
TotalTrades sync + Trades30d invariant, category update guard via
SQLite) — suite now 32 green + 1 skip.
- FIXPLAN Teil D for the larger rebuilds (AggregatedCount column,
TraderTraits heuristics, IngestMode tiering for ultra-HF traders).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
462 lines
20 KiB
C#
462 lines
20 KiB
C#
using System.Text.Json;
|
|
using Predictalytics.Domain.Entities;
|
|
using Predictalytics.Domain.Enums;
|
|
using Predictalytics.Domain.Interfaces;
|
|
using Predictalytics.Infrastructure.Logging;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Predictalytics.Infrastructure.Providers.Polymarket;
|
|
|
|
/// <summary>
|
|
/// Full implementation of IPlatformProvider for Polymarket.
|
|
/// All log entries include Platform=Polymarket for log-file routing.
|
|
/// </summary>
|
|
public class PolymarketProvider : IPlatformProvider
|
|
{
|
|
private readonly PolymarketApiClient _api;
|
|
private readonly ILogger<PolymarketProvider> _logger;
|
|
|
|
public PlatformType Platform => PlatformType.Polymarket;
|
|
public string PlatformName => "Polymarket";
|
|
public bool IsImplemented => true;
|
|
|
|
public PolymarketProvider(PolymarketApiClient api, ILogger<PolymarketProvider> logger)
|
|
{ _api = api; _logger = logger; }
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetTraderTradesAsync(string platformUserId, int limit = 1000, CancellationToken ct = default)
|
|
{
|
|
using var _ = PlatformLogContext.Push(PlatformName);
|
|
_logger.LogDebug("Fetching trades for {Wallet} (limit={Limit})", platformUserId, limit);
|
|
var raw = await _api.GetTradesAsync(platformUserId, limit, ct);
|
|
_logger.LogInformation("Fetched {Count} trades for {Wallet}", raw.Count, platformUserId);
|
|
|
|
var mappedTrades = raw.Select(r =>
|
|
{
|
|
var wallet = !string.IsNullOrEmpty(r.User) ? r.User :
|
|
!string.IsNullOrEmpty(r.ProxyWallet) ? r.ProxyWallet :
|
|
platformUserId;
|
|
var side = MapTradeSide(r);
|
|
var sideStr = side.ToString().ToUpperInvariant();
|
|
// 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}_{wallet}_{r.Asset}_{sideStr}"
|
|
: $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
|
|
MarketId = r.ConditionId ?? "",
|
|
AssetId = r.Asset ?? "",
|
|
Outcome = r.Outcome ?? "",
|
|
Side = side,
|
|
Price = (decimal)r.Price,
|
|
Size = (decimal)r.Size,
|
|
Amount = (decimal)(r.Price * r.Size),
|
|
ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
|
TransactionHash = r.TransactionHash?.ToLowerInvariant(),
|
|
TraderId = 0,
|
|
TransientWallet = wallet,
|
|
TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym
|
|
};
|
|
}).ToList();
|
|
|
|
return mappedTrades.GroupBy(t => t.PlatformTradeId, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetTradesPagedAsync(string platformUserId, int limitPerRequest = 500, CancellationToken ct = default)
|
|
{
|
|
using var _ = PlatformLogContext.Push(PlatformName);
|
|
_logger.LogDebug("Fetching all paged trades for {Wallet}", platformUserId);
|
|
var raw = await _api.GetTradesPagedAsync(platformUserId, limitPerRequest, ct);
|
|
_logger.LogInformation("Fetched {Count} trades total for {Wallet}", raw.Count, platformUserId);
|
|
|
|
var mappedTrades = raw.Select(r =>
|
|
{
|
|
var wallet = !string.IsNullOrEmpty(r.User) ? r.User :
|
|
!string.IsNullOrEmpty(r.ProxyWallet) ? r.ProxyWallet :
|
|
platformUserId;
|
|
var side = MapTradeSide(r);
|
|
var sideStr = side.ToString().ToUpperInvariant();
|
|
return new Trade
|
|
{
|
|
Platform = PlatformType.Polymarket,
|
|
PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
|
|
? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
|
|
: $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
|
|
MarketId = r.ConditionId ?? "",
|
|
AssetId = r.Asset ?? "",
|
|
Outcome = r.Outcome ?? "",
|
|
Side = side,
|
|
Price = (decimal)r.Price,
|
|
Size = (decimal)r.Size,
|
|
Amount = (decimal)(r.Price * r.Size),
|
|
ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
|
TransactionHash = r.TransactionHash?.ToLowerInvariant(),
|
|
TraderId = 0,
|
|
TransientWallet = wallet,
|
|
TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym
|
|
};
|
|
}).ToList();
|
|
|
|
return mappedTrades.GroupBy(t => t.PlatformTradeId, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList();
|
|
}
|
|
public async Task<IReadOnlyList<Trade>> GetMarketTradesAsync(string platformMarketId, int limit = 1000, CancellationToken ct = default)
|
|
{
|
|
var raw = await _api.GetMarketTradesAsync(platformMarketId, limit, 0, ct);
|
|
_logger.LogInformation("Fetched {Count} trades for Market {Market} (limit={Limit})", raw.Count, platformMarketId, limit);
|
|
|
|
var mappedTrades = raw.Select(r =>
|
|
{
|
|
var wallet = !string.IsNullOrEmpty(r.User) ? r.User : (r.ProxyWallet ?? "");
|
|
var side = MapTradeSide(r);
|
|
var sideStr = side.ToString().ToUpperInvariant();
|
|
return new Trade
|
|
{
|
|
Platform = PlatformType.Polymarket,
|
|
PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
|
|
? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
|
|
: $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
|
|
MarketId = r.ConditionId ?? "",
|
|
AssetId = r.Asset ?? "",
|
|
Outcome = r.Outcome ?? "",
|
|
Side = side,
|
|
Price = (decimal)r.Price,
|
|
Size = (decimal)r.Size,
|
|
Amount = (decimal)(r.Price * r.Size),
|
|
ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
|
TransactionHash = r.TransactionHash,
|
|
TraderId = 0,
|
|
TransientWallet = wallet,
|
|
TransientDisplayName = !string.IsNullOrEmpty(r.Name) ? r.Name : r.Pseudonym
|
|
};
|
|
}).ToList();
|
|
|
|
return mappedTrades.GroupBy(t => t.PlatformTradeId).Select(g => g.First()).ToList();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<TraderPositionInfo>> GetTraderPositionsAsync(string platformUserId, CancellationToken ct = default)
|
|
{
|
|
using var _ = PlatformLogContext.Push(PlatformName);
|
|
_logger.LogDebug("Fetching positions for {Wallet}", platformUserId);
|
|
var raw = await _api.GetPositionsAsync(platformUserId, ct);
|
|
_logger.LogInformation("Fetched {Count} positions for {Wallet}", raw.Count, platformUserId);
|
|
|
|
return raw.Select(r => new TraderPositionInfo(
|
|
platformUserId, r.Market, r.Question, r.Outcome,
|
|
(decimal)r.Size, (decimal)r.AvgPrice,
|
|
(decimal)r.CurrentValue, (decimal)r.PercentPnl,
|
|
r.AssetId
|
|
)).ToList();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<DiscoveredTrader>> DiscoverTradersAsync(int limit = 50, CancellationToken ct = default)
|
|
{
|
|
using var _ = PlatformLogContext.Push(PlatformName);
|
|
_logger.LogInformation("Running trader discovery via v1/leaderboard (limit={Limit})...", limit);
|
|
var leaderboard = await _api.GetLeaderboardAsync(limit, ct: ct);
|
|
_logger.LogInformation("Discovery returned {Count} traders from leaderboard", leaderboard.Count);
|
|
|
|
return leaderboard.Select(e => new DiscoveredTrader(
|
|
e.ProxyWallet,
|
|
string.IsNullOrEmpty(e.UserName) ? e.ProxyWallet[..10] + "..." : e.UserName,
|
|
(decimal)e.Vol,
|
|
0, // trade count not in leaderboard API
|
|
0 // win rate computed later from trades
|
|
)).ToList();
|
|
}
|
|
|
|
public async Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default)
|
|
{
|
|
using var _ = PlatformLogContext.Push(PlatformName);
|
|
var raw = await _api.GetMarketAsync(platformMarketId, ct);
|
|
if (raw == null || string.IsNullOrEmpty(raw.ConditionId)) return null;
|
|
|
|
var parentTags = "";
|
|
if (raw.Events != null && raw.Events.Count > 0)
|
|
{
|
|
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 market;
|
|
}
|
|
|
|
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 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);
|
|
|
|
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)
|
|
{
|
|
using var _ = PlatformLogContext.Push(PlatformName);
|
|
_logger.LogInformation("Fetching top holders for market {MarketId}", platformMarketId[..12] + "...");
|
|
var holdersGroups = await _api.GetHoldersAsync(platformMarketId, limit, ct);
|
|
|
|
// Flatten all holders across token groups, deduplicate by wallet
|
|
var uniqueHolders = holdersGroups
|
|
.SelectMany(g => g.Holders)
|
|
.GroupBy(h => h.ProxyWallet)
|
|
.Select(g =>
|
|
{
|
|
var first = g.First();
|
|
var totalAmount = g.Sum(h => h.Amount);
|
|
var displayName = !string.IsNullOrEmpty(first.Name) ? first.Name
|
|
: !string.IsNullOrEmpty(first.Pseudonym) ? first.Pseudonym
|
|
: first.ProxyWallet[..10] + "...";
|
|
|
|
return new DiscoveredTrader(
|
|
first.ProxyWallet,
|
|
displayName,
|
|
(decimal)totalAmount,
|
|
0, 0
|
|
);
|
|
})
|
|
.OrderByDescending(d => d.Volume24h)
|
|
.ToList();
|
|
|
|
_logger.LogInformation("Discovered {Count} unique holders from market {MarketId}",
|
|
uniqueHolders.Count, platformMarketId[..12] + "...");
|
|
|
|
return uniqueHolders;
|
|
}
|
|
|
|
// ── Private helpers ──────────────────────────────────────────
|
|
|
|
private Event MapGammaEvent(GammaEventResponse rawEvent)
|
|
{
|
|
long.TryParse(rawEvent.Id, out var numericId);
|
|
|
|
var ev = new Event
|
|
{
|
|
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 catMap = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(raw.Category ?? "", parentTags, raw.Question ?? "");
|
|
|
|
var market = new Market
|
|
{
|
|
Platform = PlatformType.Polymarket,
|
|
PlatformMarketId = marketNumericId,
|
|
ConditionId = raw.ConditionId,
|
|
QuestionId = raw.QuestionId,
|
|
MarketSlug = raw.Slug,
|
|
Description = raw.Description,
|
|
ImageUrl = raw.Image,
|
|
Question = raw.Question,
|
|
Category = catMap.Category,
|
|
Subcategory = catMap.Subcategory,
|
|
Volume = (decimal)raw.Volume,
|
|
Volume24h = (decimal)raw.Volume24hr,
|
|
Liquidity = (decimal)raw.Liquidity,
|
|
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,
|
|
LastUpdatedAt = DateTime.UtcNow,
|
|
FeeRateBps = (decimal)(raw.TakerFee * 10000),
|
|
IsNegRisk = raw.NegRisk,
|
|
ClosedAt = DateTime.TryParse(raw.ClosedTime, out var mct) ? mct : null
|
|
};
|
|
|
|
// Parse outcomes, prices, and token IDs from JSON strings
|
|
var outcomeLabels = ParseJsonStringArray(raw.Outcomes);
|
|
var outcomePrices = ParseJsonStringArray(raw.OutcomePrices);
|
|
var tokenIds = ParseJsonStringArray(raw.ClobTokenIds);
|
|
|
|
bool pricesSnapped = false;
|
|
bool hasHighPrice = false;
|
|
bool allSnapped = outcomePrices.Count > 0;
|
|
string? snappedWinnerLabel = null;
|
|
|
|
for (int i = 0; i < outcomeLabels.Count; i++)
|
|
{
|
|
decimal price = 0;
|
|
if (i < outcomePrices.Count)
|
|
decimal.TryParse(outcomePrices[i], System.Globalization.NumberStyles.Any,
|
|
System.Globalization.CultureInfo.InvariantCulture, out price);
|
|
|
|
var label = outcomeLabels[i];
|
|
if ((label.Equals("Yes", StringComparison.OrdinalIgnoreCase) || label.Equals("No", StringComparison.OrdinalIgnoreCase))
|
|
&& !string.IsNullOrEmpty(raw.GroupItemTitle))
|
|
{
|
|
label = $"{raw.GroupItemTitle} - {label}";
|
|
}
|
|
|
|
if (price <= 0.02m || price >= 0.98m)
|
|
{
|
|
if (price >= 0.98m)
|
|
{
|
|
hasHighPrice = true;
|
|
snappedWinnerLabel = label;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
allSnapped = false;
|
|
}
|
|
|
|
string tokenId = i < tokenIds.Count ? tokenIds[i] : "";
|
|
|
|
market.Outcomes.Add(new MarketOutcome
|
|
{
|
|
Label = label,
|
|
OutcomeIndex = i,
|
|
TokenId = tokenId,
|
|
CurrentPrice = price
|
|
});
|
|
}
|
|
|
|
pricesSnapped = allSnapped && hasHighPrice;
|
|
|
|
market.IsResolved = raw.UmaResolutionStatus == "resolved" || (raw.Closed && pricesSnapped);
|
|
market.ResolutionOutcome = market.IsResolved ? snappedWinnerLabel : null;
|
|
|
|
return market;
|
|
}
|
|
|
|
private static List<string> ParseJsonStringArray(string? json)
|
|
{
|
|
if (string.IsNullOrEmpty(json)) return [];
|
|
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<List<string>>(json) ?? [];
|
|
}
|
|
catch
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private static TradeSide MapTradeSide(PolymarketTradeResponse r)
|
|
{
|
|
// Check Action/Type field first for special operations
|
|
var typeOrAction = !string.IsNullOrEmpty(r.Action) ? r.Action
|
|
: !string.IsNullOrEmpty(r.Type) ? r.Type : "";
|
|
|
|
if (!string.IsNullOrEmpty(typeOrAction))
|
|
{
|
|
if (typeOrAction.Equals("SPLIT", StringComparison.OrdinalIgnoreCase)) return TradeSide.Split;
|
|
if (typeOrAction.Equals("MERGE", StringComparison.OrdinalIgnoreCase)) return TradeSide.Merge;
|
|
if (typeOrAction.Equals("REDEEM", StringComparison.OrdinalIgnoreCase)) return TradeSide.Redeem;
|
|
if (typeOrAction.Equals("ADD_LIQUIDITY", StringComparison.OrdinalIgnoreCase)) return TradeSide.AddLiquidity;
|
|
if (typeOrAction.Equals("REMOVE_LIQUIDITY", StringComparison.OrdinalIgnoreCase)) return TradeSide.RemoveLiquidity;
|
|
// Type field can also contain BUY/SELL directly
|
|
if (typeOrAction.Equals("BUY", StringComparison.OrdinalIgnoreCase)) return TradeSide.Buy;
|
|
if (typeOrAction.Equals("SELL", StringComparison.OrdinalIgnoreCase)) return TradeSide.Sell;
|
|
}
|
|
|
|
// Side field (explicit buy/sell direction)
|
|
if (!string.IsNullOrEmpty(r.Side))
|
|
{
|
|
if (r.Side.Equals("BUY", StringComparison.OrdinalIgnoreCase)) return TradeSide.Buy;
|
|
if (r.Side.Equals("SELL", StringComparison.OrdinalIgnoreCase)) return TradeSide.Sell;
|
|
}
|
|
|
|
return TradeSide.Unknown;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<MarketOutcomePriceSnapshot>> GetPriceHistoryAsync(string tokenId, CancellationToken ct = default)
|
|
{
|
|
using var _ = PlatformLogContext.Push(PlatformName);
|
|
_logger.LogDebug("Fetching price history for CLOB Token {TokenId}", tokenId);
|
|
|
|
var rawHistory = await _api.GetPricesHistoryAsync(tokenId, "6h", ct);
|
|
_logger.LogInformation("Fetched {Count} price history entries for {TokenId}", rawHistory.Count, tokenId);
|
|
|
|
return rawHistory.Select(r => new MarketOutcomePriceSnapshot
|
|
{
|
|
Price = (decimal)r.Price,
|
|
Timestamp = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
|
MarketOutcomeId = 0
|
|
}).ToList();
|
|
}
|
|
}
|