@
Rework insider feed: system-level InsiderWatch + dedicated view (no watchlist writes) Watchlists will become per-user once the product is offered commercially, so the system must not auto-add/remove traders there. Decouple insider tracking entirely: - New system-owned entity InsiderWatch (TraderId unique, FirstDetectedAt, LastAlertedTradeAt) + IInsiderWatchRepository; migration AddInsiderWatch. - AlertService.EvaluateInsiderWatchAsync now maintains InsiderWatch (not the watchlist): registers each possible_insider wallet, seeds the high-water mark at detection time, and fires one InsiderActivity alert per new trade. Dedup via LastAlertedTradeAt. - Dedicated "Insider" view: GET /api/traders/insiders + InsiderDto + a new Insider-Radar page (sorted by market-surprise). Read-only, separate from watchlist. - Revert the WatchlistEntry.LastInsiderAlertAt field + its migration (unapplied); drop the now-unused IWatchlistRepository.UpdateAsync. - Tests updated to assert InsiderWatch registry (and that no WatchlistEntry is created). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -20,6 +20,39 @@ public static class TraderEndpoints
|
||||
group.MapGet("/showcases", async (IAnalyticsService svc, CancellationToken ct) =>
|
||||
Results.Ok(await svc.GetShowcasesAsync(ct)));
|
||||
|
||||
// Dedicated Insider view: system-level list of possible-insider wallets (NOT a user watchlist).
|
||||
group.MapGet("/insiders", async (Predictalytics.Infrastructure.Data.AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var traders = await db.Traders
|
||||
.Include(t => t.Traits)
|
||||
.Include(t => t.Analytics)
|
||||
.Include(t => t.CurrentScore)
|
||||
.Where(t => t.Traits.Any(tr => tr.Trait == "possible_insider"))
|
||||
.ToListAsync(ct);
|
||||
|
||||
var detectedAt = (await db.InsiderWatches.ToListAsync(ct))
|
||||
.ToDictionary(i => i.TraderId, i => i.FirstDetectedAt);
|
||||
|
||||
var result = traders
|
||||
.Select(t => new Predictalytics.Application.DTOs.InsiderDto(
|
||||
t.Id,
|
||||
t.Platform.ToString(),
|
||||
t.DisplayName,
|
||||
t.PlatformUserId,
|
||||
t.CurrentScore?.CombinedScore ?? 0,
|
||||
t.Analytics?.CopytradingCopyabilityScore ?? 0,
|
||||
t.WinRate,
|
||||
t.TotalPnl,
|
||||
t.TotalTrades,
|
||||
t.Traits.FirstOrDefault(tr => tr.Trait == "possible_insider")?.Value ?? 0,
|
||||
detectedAt.TryGetValue(t.Id, out var d) ? d : (DateTime?)null,
|
||||
t.LastPolledAt))
|
||||
.OrderByDescending(i => i.SurpriseValue)
|
||||
.ToList();
|
||||
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
group.MapGet("/traits", async (Predictalytics.Infrastructure.Data.AppDbContext db, CancellationToken ct) =>
|
||||
Results.Ok(await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync(db.TraderTraits.Select(t => t.Trait).Distinct(), ct)));
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
<div class="nav-dot"></div>
|
||||
<span>Watchlist</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-page="insiders">
|
||||
<div class="nav-dot"></div>
|
||||
<span>Insider</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-page="markets">
|
||||
<div class="nav-dot"></div>
|
||||
<span>Märkte</span>
|
||||
@@ -321,6 +325,36 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3b. Insider View (system-level, read-only) -->
|
||||
<section class="page" id="page-insiders">
|
||||
<div class="page-title-wrap">
|
||||
<div>
|
||||
<h1 class="page-title">Insider-Radar</h1>
|
||||
<div class="page-subtitle">Wallets mit statistisch unplausibel guten Longshot-Treffern (possible_insider). Systemweit erkannt — unabhängig von deiner Watchlist.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="table-wrap">
|
||||
<table class="data-table" id="insidersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Trader</th>
|
||||
<th>Plattform</th>
|
||||
<th class="num-col">Surprise ↓</th>
|
||||
<th class="num-col">Score</th>
|
||||
<th class="num-col">Copyability</th>
|
||||
<th class="num-col">Win Rate</th>
|
||||
<th class="num-col">PnL</th>
|
||||
<th class="num-col">Trades</th>
|
||||
<th>Erkannt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="insidersBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Markets List View -->
|
||||
<section class="page" id="page-markets">
|
||||
<div class="page-title-wrap">
|
||||
|
||||
@@ -28,6 +28,7 @@ document.querySelectorAll('.nav-item[data-page]').forEach(item => {
|
||||
if (page === 'markets') loadMarkets();
|
||||
if (page === 'jobs') loadJobs();
|
||||
if (page === 'watchlist') loadWatchlist();
|
||||
if (page === 'insiders') loadInsiders();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -514,6 +515,32 @@ async function loadTraders() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadInsiders() {
|
||||
const tbody = document.getElementById('insidersBody');
|
||||
const data = await api('/api/traders/insiders');
|
||||
if (!Array.isArray(data)) {
|
||||
tbody.innerHTML = '<tr><td colspan="9">⚠ Insider-Liste konnte nicht geladen werden (siehe Konsole / Server-Log).</td></tr>';
|
||||
return;
|
||||
}
|
||||
if (!data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="9"><div class="empty-state"><p>Noch keine possible-insider Wallets erkannt.</p></div></td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = data.map(t => `
|
||||
<tr onclick="viewTrader(${t.id})">
|
||||
<td><strong>${t.displayName}</strong></td>
|
||||
<td>${t.platform}</td>
|
||||
<td class="num-col"><strong title="Market-Surprise: -log10 der Longshot-Trefferwahrscheinlichkeit">${Number(t.surpriseValue).toFixed(1)}</strong></td>
|
||||
<td class="num-col">${Number(t.combinedScore).toFixed(1)}</td>
|
||||
<td class="num-col">${Number(t.copytradingCopyabilityScore || 0).toFixed(1)}</td>
|
||||
<td class="num-col">${fmt.pct(t.winRate)}</td>
|
||||
<td class="num-col">${fmt.pnl(t.totalPnl)}</td>
|
||||
<td class="num-col">${fmt.num(t.totalTrades)}</td>
|
||||
<td>${t.firstDetectedAt ? fmt.time(t.firstDetectedAt) : '—'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadWatchlist() {
|
||||
const tbody = document.getElementById('watchlistBody');
|
||||
const data = await api(`/api/watchlist`);
|
||||
|
||||
@@ -23,18 +23,18 @@ public class AlertServiceTests
|
||||
new AlertRepository(db),
|
||||
new TradeRepository(db, NullLogger<TradeRepository>.Instance),
|
||||
new TraderRepository(db),
|
||||
new WatchlistRepository(db),
|
||||
new InsiderWatchRepository(db),
|
||||
NullLogger<AlertService>.Instance);
|
||||
|
||||
[Fact]
|
||||
public async Task InsiderWatch_AutoAddsInsider_WithoutAlertingOnHistory()
|
||||
public async Task InsiderWatch_RegistersInsider_WithoutAlertingOnHistory()
|
||||
{
|
||||
using var db = CreateDbContext();
|
||||
|
||||
var trader = new Trader { Id = 1, PlatformUserId = "0xI", DisplayName = "QuietWhale" };
|
||||
trader.Traits.Add(new TraderTrait { TraderId = 1, Trait = "possible_insider", Value = 4.2m });
|
||||
db.Traders.Add(trader);
|
||||
// A historical trade (predates the auto-add) must NOT produce an alert.
|
||||
// A historical trade (predates detection) must NOT produce an alert.
|
||||
db.Trades.Add(new Trade
|
||||
{
|
||||
Id = 10, TraderId = 1, DbMarketId = 100, Platform = PlatformType.Polymarket,
|
||||
@@ -46,7 +46,9 @@ public class AlertServiceTests
|
||||
var svc = CreateService(db);
|
||||
await svc.EvaluateInsiderWatchAsync();
|
||||
|
||||
Assert.Single(db.WatchlistEntries.Where(w => w.TraderId == 1));
|
||||
// Registered in the system-level insider registry, NOT the user watchlist.
|
||||
Assert.Single(db.InsiderWatches.Where(i => i.TraderId == 1));
|
||||
Assert.Empty(db.WatchlistEntries);
|
||||
Assert.Empty(db.Alerts.Where(a => a.Type == AlertType.InsiderActivity));
|
||||
}
|
||||
|
||||
@@ -58,13 +60,14 @@ public class AlertServiceTests
|
||||
var trader = new Trader { Id = 2, PlatformUserId = "0xJ", DisplayName = "Insider2" };
|
||||
trader.Traits.Add(new TraderTrait { TraderId = 2, Trait = "possible_insider", Value = 5m });
|
||||
db.Traders.Add(trader);
|
||||
// Already watched, added an hour ago.
|
||||
db.WatchlistEntries.Add(new WatchlistEntry
|
||||
// Already registered an hour ago; high-water mark predates the new trade.
|
||||
db.InsiderWatches.Add(new InsiderWatch
|
||||
{
|
||||
Id = 5, TraderId = 2, Label = "watched", AlertsEnabled = true,
|
||||
AddedAt = DateTime.UtcNow.AddHours(-1)
|
||||
Id = 3, TraderId = 2,
|
||||
FirstDetectedAt = DateTime.UtcNow.AddHours(-1),
|
||||
LastAlertedTradeAt = DateTime.UtcNow.AddHours(-1)
|
||||
});
|
||||
// A trade placed AFTER the entry was added -> should alert exactly once.
|
||||
// A trade placed AFTER the high-water mark -> should alert exactly once.
|
||||
db.Trades.Add(new Trade
|
||||
{
|
||||
Id = 20, TraderId = 2, DbMarketId = 200, Platform = PlatformType.Polymarket,
|
||||
@@ -82,8 +85,8 @@ public class AlertServiceTests
|
||||
Assert.Equal(2, alerts[0].TraderId);
|
||||
|
||||
// High-water mark advanced; a second run must not re-alert.
|
||||
var entry = db.WatchlistEntries.First(w => w.Id == 5);
|
||||
Assert.NotNull(entry.LastInsiderAlertAt);
|
||||
var watch = db.InsiderWatches.First(i => i.Id == 3);
|
||||
Assert.True(watch.LastAlertedTradeAt > DateTime.UtcNow.AddMinutes(-25));
|
||||
|
||||
await svc.EvaluateInsiderWatchAsync();
|
||||
Assert.Single(db.Alerts.Where(a => a.Type == AlertType.InsiderActivity));
|
||||
|
||||
@@ -147,3 +147,19 @@ public record TraderCorrelationDto(
|
||||
decimal IntersectionRatioB,
|
||||
decimal AgreementRatio
|
||||
);
|
||||
|
||||
/// <summary>One row of the dedicated Insider view (system-level, not a user watchlist).</summary>
|
||||
public record InsiderDto(
|
||||
int Id,
|
||||
string Platform,
|
||||
string DisplayName,
|
||||
string PlatformUserId,
|
||||
decimal CombinedScore,
|
||||
decimal CopytradingCopyabilityScore,
|
||||
decimal WinRate,
|
||||
decimal TotalPnl,
|
||||
int TotalTrades,
|
||||
decimal SurpriseValue,
|
||||
DateTime? FirstDetectedAt,
|
||||
DateTime? LastPolledAt
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ public class AlertService : IAlertService
|
||||
private readonly IAlertRepository _alertRepo;
|
||||
private readonly ITradeRepository _tradeRepo;
|
||||
private readonly ITraderRepository _traderRepo;
|
||||
private readonly IWatchlistRepository _watchlistRepo;
|
||||
private readonly IInsiderWatchRepository _insiderRepo;
|
||||
private readonly ILogger<AlertService> _logger;
|
||||
|
||||
// Alert thresholds (configurable in future)
|
||||
@@ -28,13 +28,13 @@ public class AlertService : IAlertService
|
||||
IAlertRepository alertRepo,
|
||||
ITradeRepository tradeRepo,
|
||||
ITraderRepository traderRepo,
|
||||
IWatchlistRepository watchlistRepo,
|
||||
IInsiderWatchRepository insiderRepo,
|
||||
ILogger<AlertService> logger)
|
||||
{
|
||||
_alertRepo = alertRepo;
|
||||
_tradeRepo = tradeRepo;
|
||||
_traderRepo = traderRepo;
|
||||
_watchlistRepo = watchlistRepo;
|
||||
_insiderRepo = insiderRepo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -70,11 +70,12 @@ public class AlertService : IAlertService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insider-Follow feed: keeps every <c>possible_insider</c> wallet on the watchlist and fires a
|
||||
/// high-severity alert for each new trade one of them places. These wallets trade rarely, so a
|
||||
/// single new trade is the strongest copy signal we have. Dedup is via
|
||||
/// <see cref="WatchlistEntry.LastInsiderAlertAt"/>; a freshly auto-added wallet is baselined at
|
||||
/// its <see cref="WatchlistEntry.AddedAt"/> so historical trades never trigger a backlog of alerts.
|
||||
/// Insider-Follow feed: maintains the system-level <see cref="InsiderWatch"/> registry for every
|
||||
/// <c>possible_insider</c> wallet and fires a high-severity alert for each new trade one of them
|
||||
/// places. These wallets trade rarely, so a single new trade is the strongest copy signal we have.
|
||||
/// This is deliberately decoupled from (per-user) watchlists. Dedup is via
|
||||
/// <see cref="InsiderWatch.LastAlertedTradeAt"/>; a freshly detected wallet is seeded at detection
|
||||
/// time so historical (backfilled) trades never trigger a backlog of alerts.
|
||||
/// </summary>
|
||||
public async Task EvaluateInsiderWatchAsync(CancellationToken ct = default)
|
||||
{
|
||||
@@ -82,27 +83,24 @@ public class AlertService : IAlertService
|
||||
|
||||
foreach (var trader in insiders)
|
||||
{
|
||||
var entry = trader.WatchlistEntries.FirstOrDefault();
|
||||
var watch = await _insiderRepo.GetByTraderIdAsync(trader.Id, ct);
|
||||
|
||||
// Auto-add newly detected insiders; baseline at now so we don't alert on their history.
|
||||
if (entry == null)
|
||||
// Register newly detected insiders; seed the high-water mark at "now" so we don't alert
|
||||
// on their history. No alerts on the detection cycle itself.
|
||||
if (watch == null)
|
||||
{
|
||||
await _watchlistRepo.AddAsync(new WatchlistEntry
|
||||
await _insiderRepo.AddAsync(new InsiderWatch
|
||||
{
|
||||
TraderId = trader.Id,
|
||||
Label = "Auto: Possible Insider",
|
||||
Notes = "Automatisch aufgenommen (possible_insider-Trait).",
|
||||
AlertsEnabled = true
|
||||
FirstDetectedAt = DateTime.UtcNow,
|
||||
LastAlertedTradeAt = DateTime.UtcNow
|
||||
}, ct);
|
||||
_logger.LogInformation("👁 Insider-Watch: auto-added {Trader} to watchlist", trader.DisplayName);
|
||||
_logger.LogInformation("👁 Insider-Watch: detected new possible-insider {Trader}", trader.DisplayName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.AlertsEnabled) continue;
|
||||
|
||||
var since = entry.LastInsiderAlertAt ?? entry.AddedAt;
|
||||
var recent = await _tradeRepo.GetByTraderIdAsync(trader.Id, 0, 50, ct);
|
||||
var newTrades = recent.Where(t => t.ExecutedAt > since).OrderBy(t => t.ExecutedAt).ToList();
|
||||
var newTrades = recent.Where(t => t.ExecutedAt > watch.LastAlertedTradeAt).OrderBy(t => t.ExecutedAt).ToList();
|
||||
if (newTrades.Count == 0) continue;
|
||||
|
||||
foreach (var trade in newTrades)
|
||||
@@ -121,8 +119,8 @@ public class AlertService : IAlertService
|
||||
}, ct);
|
||||
}
|
||||
|
||||
entry.LastInsiderAlertAt = newTrades.Max(t => t.ExecutedAt);
|
||||
await _watchlistRepo.UpdateAsync(entry, ct);
|
||||
watch.LastAlertedTradeAt = newTrades.Max(t => t.ExecutedAt);
|
||||
await _insiderRepo.UpdateAsync(watch, ct);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Predictalytics.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// System-level registry of wallets flagged with the <c>possible_insider</c> trait. This is a
|
||||
/// global, system-owned list — deliberately separate from per-user <see cref="WatchlistEntry"/>
|
||||
/// watchlists (which will become user-specific once the product is offered commercially). It backs
|
||||
/// the dedicated "Insider" view and carries the high-water mark used to de-duplicate activity alerts.
|
||||
/// </summary>
|
||||
public class InsiderWatch
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>Foreign key to the flagged trader (unique — one row per trader).</summary>
|
||||
public int TraderId { get; set; }
|
||||
|
||||
/// <summary>When this wallet was first detected as a possible insider.</summary>
|
||||
public DateTime FirstDetectedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// High-water mark for activity alerts: the ExecutedAt of the newest trade already alerted on.
|
||||
/// Seeded at detection time so historical (backfilled) trades never trigger a backlog of alerts.
|
||||
/// </summary>
|
||||
public DateTime LastAlertedTradeAt { get; set; }
|
||||
|
||||
// Navigation
|
||||
public Trader Trader { get; set; } = null!;
|
||||
}
|
||||
@@ -22,13 +22,6 @@ public class WatchlistEntry
|
||||
/// <summary>When this entry was added to the watchlist.</summary>
|
||||
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// High-water mark for insider-activity alerts: the ExecutedAt of the newest trade already
|
||||
/// alerted on. Null until the first insider alert fires; new-trade detection uses
|
||||
/// <see cref="AddedAt"/> as the baseline so we never alert on backfilled history.
|
||||
/// </summary>
|
||||
public DateTime? LastInsiderAlertAt { get; set; }
|
||||
|
||||
// Navigation
|
||||
public Trader Trader { get; set; } = null!;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
|
||||
namespace Predictalytics.Domain.Interfaces;
|
||||
|
||||
public interface IInsiderWatchRepository
|
||||
{
|
||||
Task<IReadOnlyList<InsiderWatch>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<InsiderWatch?> GetByTraderIdAsync(int traderId, CancellationToken ct = default);
|
||||
Task AddAsync(InsiderWatch entry, CancellationToken ct = default);
|
||||
Task UpdateAsync(InsiderWatch entry, CancellationToken ct = default);
|
||||
}
|
||||
@@ -7,6 +7,5 @@ public interface IWatchlistRepository
|
||||
Task<IReadOnlyList<WatchlistEntry>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<WatchlistEntry?> GetByTraderIdAsync(int traderId, CancellationToken ct = default);
|
||||
Task AddAsync(WatchlistEntry entry, CancellationToken ct = default);
|
||||
Task UpdateAsync(WatchlistEntry entry, CancellationToken ct = default);
|
||||
Task RemoveAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class AppDbContext : DbContext
|
||||
public DbSet<BackgroundJob> BackgroundJobs => Set<BackgroundJob>();
|
||||
public DbSet<TraderTrait> TraderTraits => Set<TraderTrait>();
|
||||
public DbSet<TraderWindowMetrics> TraderWindowMetrics => Set<TraderWindowMetrics>();
|
||||
public DbSet<InsiderWatch> InsiderWatches => Set<InsiderWatch>();
|
||||
|
||||
private readonly bool _isReadOnly;
|
||||
|
||||
@@ -207,6 +208,14 @@ public class AppDbContext : DbContext
|
||||
e.HasOne(w => w.Trader).WithMany(t => t.WatchlistEntries).HasForeignKey(w => w.TraderId);
|
||||
});
|
||||
|
||||
// InsiderWatch (system-level, one row per flagged trader)
|
||||
mb.Entity<InsiderWatch>(e =>
|
||||
{
|
||||
e.HasKey(i => i.Id);
|
||||
e.HasIndex(i => i.TraderId).IsUnique();
|
||||
e.HasOne(i => i.Trader).WithMany().HasForeignKey(i => i.TraderId);
|
||||
});
|
||||
|
||||
// Alert
|
||||
mb.Entity<Alert>(e =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Data.Repositories;
|
||||
|
||||
public class InsiderWatchRepository : IInsiderWatchRepository
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public InsiderWatchRepository(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<IReadOnlyList<InsiderWatch>> GetAllAsync(CancellationToken ct = default)
|
||||
=> await _db.InsiderWatches.ToListAsync(ct);
|
||||
|
||||
public async Task<InsiderWatch?> GetByTraderIdAsync(int traderId, CancellationToken ct = default)
|
||||
=> await _db.InsiderWatches.FirstOrDefaultAsync(i => i.TraderId == traderId, ct);
|
||||
|
||||
public async Task AddAsync(InsiderWatch entry, CancellationToken ct = default)
|
||||
{ _db.InsiderWatches.Add(entry); await _db.SaveChangesAsync(ct); }
|
||||
|
||||
public async Task UpdateAsync(InsiderWatch entry, CancellationToken ct = default)
|
||||
{ _db.InsiderWatches.Update(entry); await _db.SaveChangesAsync(ct); }
|
||||
}
|
||||
@@ -48,7 +48,8 @@ public class TraderRepository : ITraderRepository
|
||||
public async Task<IReadOnlyList<Trader>> GetByTraitAsync(string trait, CancellationToken ct = default)
|
||||
=> await _db.Traders
|
||||
.Include(t => t.Traits)
|
||||
.Include(t => t.WatchlistEntries)
|
||||
.Include(t => t.Analytics)
|
||||
.Include(t => t.CurrentScore)
|
||||
.Where(t => t.Traits.Any(tr => tr.Trait == trait))
|
||||
.ToListAsync(ct);
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@ public class WatchlistRepository : IWatchlistRepository
|
||||
public async Task AddAsync(WatchlistEntry entry, CancellationToken ct = default)
|
||||
{ _db.WatchlistEntries.Add(entry); await _db.SaveChangesAsync(ct); }
|
||||
|
||||
public async Task UpdateAsync(WatchlistEntry entry, CancellationToken ct = default)
|
||||
{ _db.WatchlistEntries.Update(entry); await _db.SaveChangesAsync(ct); }
|
||||
|
||||
public async Task RemoveAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var e = await _db.WatchlistEntries.FindAsync(new object[] { id }, ct);
|
||||
|
||||
@@ -66,6 +66,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<ITradeRepository, TradeRepository>();
|
||||
services.AddScoped<IMarketRepository, MarketRepository>();
|
||||
services.AddScoped<IWatchlistRepository, WatchlistRepository>();
|
||||
services.AddScoped<IInsiderWatchRepository, InsiderWatchRepository>();
|
||||
services.AddScoped<IAlertRepository, AlertRepository>();
|
||||
services.AddScoped<IJobRepository, JobRepository>();
|
||||
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddWatchlistLastInsiderAlertAt : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "LastInsiderAlertAt",
|
||||
table: "WatchlistEntries",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LastInsiderAlertAt",
|
||||
table: "WatchlistEntries");
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-5
@@ -12,8 +12,8 @@ using Predictalytics.Infrastructure.Data;
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260723164157_AddWatchlistLastInsiderAlertAt")]
|
||||
partial class AddWatchlistLastInsiderAlertAt
|
||||
[Migration("20260723184034_AddInsiderWatch")]
|
||||
partial class AddInsiderWatch
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -181,6 +181,31 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.InsiderWatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("FirstDetectedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("LastAlertedTradeAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TraderId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InsiderWatches");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1041,9 +1066,6 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<DateTime?>("LastInsiderAlertAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
@@ -1078,6 +1100,17 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.InsiderWatch", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany()
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Event", "Event")
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Predictalytics.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddInsiderWatch : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InsiderWatches",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
TraderId = table.Column<int>(type: "int", nullable: false),
|
||||
FirstDetectedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
LastAlertedTradeAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_InsiderWatches", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_InsiderWatches_Traders_TraderId",
|
||||
column: x => x.TraderId,
|
||||
principalTable: "Traders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InsiderWatches_TraderId",
|
||||
table: "InsiderWatches",
|
||||
column: "TraderId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "InsiderWatches");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,31 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.InsiderWatch", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("FirstDetectedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("LastAlertedTradeAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TraderId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InsiderWatches");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1038,9 +1063,6 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<DateTime?>("LastInsiderAlertAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
@@ -1075,6 +1097,17 @@ namespace Predictalytics.Infrastructure.Migrations
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.InsiderWatch", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Trader", "Trader")
|
||||
.WithMany()
|
||||
.HasForeignKey("TraderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Trader");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Predictalytics.Domain.Entities.Market", b =>
|
||||
{
|
||||
b.HasOne("Predictalytics.Domain.Entities.Event", "Event")
|
||||
|
||||
Reference in New Issue
Block a user