RF-Slice 2: ResolutionFarming-Persistenz (EF/MySQL) + Repos + Migration
- Entities RfCandidate/RfPosition/RfClosedTrade (+ RfSettings aus Slice 1). - ResolutionFarmingDbContext: Tabellen rf_settings/rf_candidates/rf_positions/ rf_closed_trades. Autoincrement-PKs (Identity) fuer Candidate/ClosedTrade von Anfang an (Lehre aus dem CopyTrading-TradeId-Problem), zusammengesetzter PK (AccountId,TokenId) fuer Positions, Indizes + Decimal-Precision. - 4 Repos (Settings/Candidate/Position/ClosedTrade) mit serverseitigen Aggregaten (RealizedPnlSince fuer Kill-Switch, CountOpenedSince fuer Tages-Drossel). - Modul registriert DbContextFactory + Repos. - Design-Time-Factory nutzt die fest gepinnte Server-Version -> Migration wurde OHNE DB-Verbindung generiert (kein Zugriff auf die produktive DB). Anwenden per 'dotnet ef database update' bewusst im Zielland/lokal durch den Nutzer. 6 neue EF-InMemory-Repo-Tests. Build 0 Fehler, 289 Tests gruen, --smoke-ui ok. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c9eb11afe4
commit
1b6e194b50
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using PolyTrader.Modules.ResolutionFarming.Models;
|
||||
using PolyTrader.Modules.ResolutionFarming.Persistence.Ef;
|
||||
using PolyTrader.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace PolyTrader.Tests
|
||||
{
|
||||
/// <summary>EF-InMemory-Tests der ResolutionFarming-Repositories (rf_-Tabellen).</summary>
|
||||
public class RfRepositoryTests
|
||||
{
|
||||
private static InMemoryContextFactory<ResolutionFarmingDbContext> Factory() =>
|
||||
new(o => new ResolutionFarmingDbContext(o));
|
||||
|
||||
// ----- Settings -----
|
||||
|
||||
[Fact]
|
||||
public void Settings_upsert_inserts_then_updates()
|
||||
{
|
||||
var f = Factory();
|
||||
var repo = new EfRfSettingsRepository(f);
|
||||
repo.Upsert(new RfSettings { AccountId = 1, MaxPerMarketUsd = 25m });
|
||||
repo.Upsert(new RfSettings { AccountId = 1, MaxPerMarketUsd = 50m });
|
||||
|
||||
Assert.Single(repo.GetAll());
|
||||
Assert.Equal(50m, repo.Get(1)!.MaxPerMarketUsd);
|
||||
Assert.Null(repo.Get(99));
|
||||
}
|
||||
|
||||
// ----- Candidates -----
|
||||
|
||||
[Fact]
|
||||
public void Candidates_getrecent_orders_desc_and_limits()
|
||||
{
|
||||
var f = Factory();
|
||||
var repo = new EfRfCandidateRepository(f);
|
||||
var t0 = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
for (int i = 0; i < 5; i++)
|
||||
repo.Insert(new RfCandidate { AccountId = 1, TokenId = "t" + i, ScannedAt = t0.AddMinutes(i) });
|
||||
repo.Insert(new RfCandidate { AccountId = 2, TokenId = "other", ScannedAt = t0.AddMinutes(99) });
|
||||
|
||||
var recent = repo.GetRecent(1, 3);
|
||||
Assert.Equal(3, recent.Count);
|
||||
Assert.Equal("t4", recent[0].TokenId); // neuester zuerst
|
||||
Assert.DoesNotContain(recent, c => c.AccountId == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Candidate_id_is_db_generated()
|
||||
{
|
||||
var f = Factory();
|
||||
var repo = new EfRfCandidateRepository(f);
|
||||
repo.Insert(new RfCandidate { AccountId = 1, TokenId = "a" });
|
||||
repo.Insert(new RfCandidate { AccountId = 1, TokenId = "b" });
|
||||
|
||||
var ids = repo.GetRecent(1, 10).Select(c => c.Id).ToList();
|
||||
Assert.Equal(2, ids.Distinct().Count());
|
||||
Assert.DoesNotContain(0L, ids); // Autoincrement, keine code-vergebene 0
|
||||
}
|
||||
|
||||
// ----- Positions -----
|
||||
|
||||
[Fact]
|
||||
public void Positions_upsert_find_delete_and_exposure()
|
||||
{
|
||||
var f = Factory();
|
||||
var repo = new EfRfPositionRepository(f);
|
||||
var now = DateTime.UtcNow;
|
||||
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "a", AmountUsd = 10m, OpenedAt = now });
|
||||
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "b", AmountUsd = 15m, OpenedAt = now });
|
||||
repo.Upsert(new RfPosition { AccountId = 2, TokenId = "c", AmountUsd = 99m, OpenedAt = now });
|
||||
|
||||
Assert.Equal(2, repo.GetOpen(1).Count);
|
||||
Assert.Equal(3, repo.GetAllOpen().Count);
|
||||
Assert.Equal(15m, repo.Find(1, "b")!.AmountUsd);
|
||||
|
||||
// Update über denselben zusammengesetzten Schlüssel
|
||||
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "a", AmountUsd = 12m, OpenedAt = now });
|
||||
Assert.Equal(12m, repo.Find(1, "a")!.AmountUsd);
|
||||
Assert.Equal(2, repo.GetOpen(1).Count); // kein Duplikat
|
||||
|
||||
repo.Delete(1, "a");
|
||||
Assert.Null(repo.Find(1, "a"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Positions_countopenedsince_respects_account_and_time()
|
||||
{
|
||||
var f = Factory();
|
||||
var repo = new EfRfPositionRepository(f);
|
||||
var today = DateTime.UtcNow;
|
||||
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "a", OpenedAt = today });
|
||||
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "b", OpenedAt = today.AddDays(-2) });
|
||||
repo.Upsert(new RfPosition { AccountId = 2, TokenId = "c", OpenedAt = today });
|
||||
|
||||
Assert.Equal(1, repo.CountOpenedSince(1, today.AddHours(-1)));
|
||||
}
|
||||
|
||||
// ----- ClosedTrades -----
|
||||
|
||||
[Fact]
|
||||
public void ClosedTrades_insert_find_and_pnl_sum()
|
||||
{
|
||||
var f = Factory();
|
||||
var repo = new EfRfClosedTradeRepository(f);
|
||||
var since = new DateTime(2026, 7, 9, 0, 0, 0, DateTimeKind.Utc);
|
||||
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "a", RealizedPnl = 5m, ClosedAt = since.AddHours(1) });
|
||||
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "b", RealizedPnl = -8m, ClosedAt = since.AddHours(2) });
|
||||
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "c", RealizedPnl = 100m, ClosedAt = since.AddDays(-1) }); // vor dem Fenster
|
||||
repo.Insert(new RfClosedTrade { AccountId = 2, TokenId = "d", RealizedPnl = 50m, ClosedAt = since.AddHours(1) });
|
||||
|
||||
Assert.Equal(-3m, repo.RealizedPnlSince(1, since)); // 5 - 8, ohne 100 (zu alt), ohne Account 2
|
||||
Assert.Equal(2, repo.Find(t => t.RealizedPnl > 0 && t.AccountId == 1).Count); // a (5) + c (100)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user