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:
Richard
2026-07-23 20:52:37 +02:00
parent 8b9b34342f
commit 2bec11d0a9
19 changed files with 342 additions and 82 deletions
@@ -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)));
+34
View File
@@ -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">
+27
View File
@@ -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>();
@@ -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");
}
}
}
@@ -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")