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:
Richard
2026-07-09 22:06:59 +02:00
co-authored by Claude Opus 4.8
parent c9eb11afe4
commit 1b6e194b50
12 changed files with 1396 additions and 2 deletions
@@ -0,0 +1,338 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PolyTrader.Modules.ResolutionFarming.Persistence.Ef;
#nullable disable
namespace PolyTrader.Modules.ResolutionFarming.Migrations
{
[DbContext(typeof(ResolutionFarmingDbContext))]
[Migration("20260709200552_InitialResolutionFarming")]
partial class InitialResolutionFarming
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("PolyTrader.Modules.ResolutionFarming.Models.RfCandidate", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<bool>("Accepted")
.HasColumnType("tinyint(1)");
b.Property<int>("AccountId")
.HasColumnType("int");
b.Property<decimal>("Ask")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ClusterKey")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime(6)");
b.Property<string>("EventSlug")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<int>("FeeBps")
.HasColumnType("int");
b.Property<string>("MarketQuestion")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("MarketSlug")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<decimal>("NetEdgePct")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("Outcome")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("RejectReason")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime>("ScannedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("Score")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.HasKey("Id");
b.HasIndex("ScannedAt");
b.HasIndex("AccountId", "Accepted");
b.ToTable("rf_candidates", (string)null);
});
modelBuilder.Entity("PolyTrader.Modules.ResolutionFarming.Models.RfClosedTrade", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<int>("AccountId")
.HasColumnType("int");
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<DateTime>("ClosedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ClusterKey")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<decimal>("EntryPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("ExitPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("ExitReason")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<bool>("IsDemo")
.HasColumnType("tinyint(1)");
b.Property<string>("MarketQuestion")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("MarketSlug")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime>("OpenedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Outcome")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<decimal>("PnlPercent")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("RealizedPnl")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("RedeemStatus")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<decimal>("Size")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<decimal>("TotalFees")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.HasKey("Id");
b.HasIndex("ClosedAt");
b.HasIndex("AccountId", "TokenId");
b.ToTable("rf_closed_trades", (string)null);
});
modelBuilder.Entity("PolyTrader.Modules.ResolutionFarming.Models.RfPosition", b =>
{
b.Property<int>("AccountId")
.HasColumnType("int");
b.Property<string>("TokenId")
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<decimal>("AmountUsd")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ClusterKey")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime(6)");
b.Property<int>("EntryFeeBps")
.HasColumnType("int");
b.Property<decimal>("EntryPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<bool>("IsDemo")
.HasColumnType("tinyint(1)");
b.Property<string>("MarketQuestion")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("MarketSlug")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime>("OpenedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Outcome")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<decimal>("Size")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.HasKey("AccountId", "TokenId");
b.ToTable("rf_positions", (string)null);
});
modelBuilder.Entity("PolyTrader.Modules.ResolutionFarming.Models.RfSettings", b =>
{
b.Property<int>("AccountId")
.HasColumnType("int");
b.Property<bool>("AllowTakerFallback")
.HasColumnType("tinyint(1)");
b.Property<string>("BlacklistCsv")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("CategoryWhitelistCsv")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<decimal>("DailyLossKillSwitchUsd")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<bool>("Enabled")
.HasColumnType("tinyint(1)");
b.Property<int>("MakerFillTimeoutMinutes")
.HasColumnType("int");
b.Property<int>("MaxHoursToResolution")
.HasColumnType("int");
b.Property<int>("MaxNewPositionsPerDay")
.HasColumnType("int");
b.Property<decimal>("MaxPerClusterPct")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MaxPerMarketUsd")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MaxPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MaxTotalExposurePct")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MinEdgePct")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MinPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.HasKey("AccountId");
b.ToTable("rf_settings", (string)null);
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,192 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PolyTrader.Modules.ResolutionFarming.Migrations
{
/// <inheritdoc />
public partial class InitialResolutionFarming : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "rf_candidates",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ScannedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
AccountId = table.Column<int>(type: "int", nullable: false),
TokenId = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MarketSlug = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EventSlug = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MarketQuestion = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Outcome = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Category = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ClusterKey = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Ask = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
FeeBps = table.Column<int>(type: "int", nullable: false),
NetEdgePct = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
Score = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
EndDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
Accepted = table.Column<bool>(type: "tinyint(1)", nullable: false),
RejectReason = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_rf_candidates", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "rf_closed_trades",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
AccountId = table.Column<int>(type: "int", nullable: false),
IsDemo = table.Column<bool>(type: "tinyint(1)", nullable: false),
TokenId = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MarketSlug = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MarketQuestion = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Outcome = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Category = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ClusterKey = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EntryPrice = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
ExitPrice = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
Size = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
RealizedPnl = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
PnlPercent = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
TotalFees = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
OpenedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
ClosedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
ExitReason = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
RedeemStatus = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_rf_closed_trades", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "rf_positions",
columns: table => new
{
AccountId = table.Column<int>(type: "int", nullable: false),
TokenId = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MarketSlug = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MarketQuestion = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Outcome = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Category = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ClusterKey = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EntryPrice = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
Size = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
AmountUsd = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
EntryFeeBps = table.Column<int>(type: "int", nullable: false),
IsDemo = table.Column<bool>(type: "tinyint(1)", nullable: false),
OpenedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
EndDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
Status = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_rf_positions", x => new { x.AccountId, x.TokenId });
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "rf_settings",
columns: table => new
{
AccountId = table.Column<int>(type: "int", nullable: false),
Enabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
MinPrice = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
MaxPrice = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
MaxHoursToResolution = table.Column<int>(type: "int", nullable: false),
CategoryWhitelistCsv = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
BlacklistCsv = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MinEdgePct = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
MaxPerMarketUsd = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
MaxPerClusterPct = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
MaxTotalExposurePct = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
MaxNewPositionsPerDay = table.Column<int>(type: "int", nullable: false),
DailyLossKillSwitchUsd = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
MakerFillTimeoutMinutes = table.Column<int>(type: "int", nullable: false),
AllowTakerFallback = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_rf_settings", x => x.AccountId);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_rf_candidates_AccountId_Accepted",
table: "rf_candidates",
columns: new[] { "AccountId", "Accepted" });
migrationBuilder.CreateIndex(
name: "IX_rf_candidates_ScannedAt",
table: "rf_candidates",
column: "ScannedAt");
migrationBuilder.CreateIndex(
name: "IX_rf_closed_trades_AccountId_TokenId",
table: "rf_closed_trades",
columns: new[] { "AccountId", "TokenId" });
migrationBuilder.CreateIndex(
name: "IX_rf_closed_trades_ClosedAt",
table: "rf_closed_trades",
column: "ClosedAt");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rf_candidates");
migrationBuilder.DropTable(
name: "rf_closed_trades");
migrationBuilder.DropTable(
name: "rf_positions");
migrationBuilder.DropTable(
name: "rf_settings");
}
}
}
@@ -0,0 +1,335 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PolyTrader.Modules.ResolutionFarming.Persistence.Ef;
#nullable disable
namespace PolyTrader.Modules.ResolutionFarming.Migrations
{
[DbContext(typeof(ResolutionFarmingDbContext))]
partial class ResolutionFarmingDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("PolyTrader.Modules.ResolutionFarming.Models.RfCandidate", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<bool>("Accepted")
.HasColumnType("tinyint(1)");
b.Property<int>("AccountId")
.HasColumnType("int");
b.Property<decimal>("Ask")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ClusterKey")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime(6)");
b.Property<string>("EventSlug")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<int>("FeeBps")
.HasColumnType("int");
b.Property<string>("MarketQuestion")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("MarketSlug")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<decimal>("NetEdgePct")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("Outcome")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("RejectReason")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime>("ScannedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("Score")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.HasKey("Id");
b.HasIndex("ScannedAt");
b.HasIndex("AccountId", "Accepted");
b.ToTable("rf_candidates", (string)null);
});
modelBuilder.Entity("PolyTrader.Modules.ResolutionFarming.Models.RfClosedTrade", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
b.Property<int>("AccountId")
.HasColumnType("int");
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<DateTime>("ClosedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ClusterKey")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<decimal>("EntryPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("ExitPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("ExitReason")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<bool>("IsDemo")
.HasColumnType("tinyint(1)");
b.Property<string>("MarketQuestion")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("MarketSlug")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime>("OpenedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Outcome")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<decimal>("PnlPercent")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("RealizedPnl")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("RedeemStatus")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.Property<decimal>("Size")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<decimal>("TotalFees")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.HasKey("Id");
b.HasIndex("ClosedAt");
b.HasIndex("AccountId", "TokenId");
b.ToTable("rf_closed_trades", (string)null);
});
modelBuilder.Entity("PolyTrader.Modules.ResolutionFarming.Models.RfPosition", b =>
{
b.Property<int>("AccountId")
.HasColumnType("int");
b.Property<string>("TokenId")
.HasMaxLength(120)
.HasColumnType("varchar(120)");
b.Property<decimal>("AmountUsd")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("ClusterKey")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime?>("EndDate")
.HasColumnType("datetime(6)");
b.Property<int>("EntryFeeBps")
.HasColumnType("int");
b.Property<decimal>("EntryPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<bool>("IsDemo")
.HasColumnType("tinyint(1)");
b.Property<string>("MarketQuestion")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("MarketSlug")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<DateTime>("OpenedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Outcome")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<decimal>("Size")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)");
b.HasKey("AccountId", "TokenId");
b.ToTable("rf_positions", (string)null);
});
modelBuilder.Entity("PolyTrader.Modules.ResolutionFarming.Models.RfSettings", b =>
{
b.Property<int>("AccountId")
.HasColumnType("int");
b.Property<bool>("AllowTakerFallback")
.HasColumnType("tinyint(1)");
b.Property<string>("BlacklistCsv")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("varchar(1000)");
b.Property<string>("CategoryWhitelistCsv")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<decimal>("DailyLossKillSwitchUsd")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<bool>("Enabled")
.HasColumnType("tinyint(1)");
b.Property<int>("MakerFillTimeoutMinutes")
.HasColumnType("int");
b.Property<int>("MaxHoursToResolution")
.HasColumnType("int");
b.Property<int>("MaxNewPositionsPerDay")
.HasColumnType("int");
b.Property<decimal>("MaxPerClusterPct")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MaxPerMarketUsd")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MaxPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MaxTotalExposurePct")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MinEdgePct")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.Property<decimal>("MinPrice")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
b.HasKey("AccountId");
b.ToTable("rf_settings", (string)null);
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,32 @@
using System;
namespace PolyTrader.Modules.ResolutionFarming.Models
{
/// <summary>
/// Ein Scanner-Ergebnis (Tabelle rf_candidates) auch abgelehnte Kandidaten werden mit Grund
/// gespeichert, damit die Filterkette später datenbasiert kalibriert werden kann (Phase RF-5).
/// </summary>
public class RfCandidate
{
public long Id { get; set; } // DB-Autoincrement
public DateTime ScannedAt { get; set; }
public int AccountId { get; set; }
public string TokenId { get; set; } = string.Empty;
public string MarketSlug { get; set; } = string.Empty;
public string EventSlug { get; set; } = string.Empty;
public string MarketQuestion { get; set; } = string.Empty;
public string Outcome { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public string ClusterKey { get; set; } = string.Empty;
public decimal Ask { get; set; }
public int FeeBps { get; set; }
public decimal NetEdgePct { get; set; }
public decimal Score { get; set; }
public DateTime? EndDate { get; set; }
public bool Accepted { get; set; }
public string RejectReason { get; set; } = string.Empty;
}
}
@@ -0,0 +1,36 @@
using System;
namespace PolyTrader.Modules.ResolutionFarming.Models
{
/// <summary>
/// Abgeschlossener Farming-Trade (Tabelle rf_closed_trades) inkl. Fees und Redeem-Status.
/// Das Modul schreibt zusätzlich einen generischen Core-Trade-Log-Eintrag (Dashboard).
/// </summary>
public class RfClosedTrade
{
public long Id { get; set; } // DB-Autoincrement
public int AccountId { get; set; }
public bool IsDemo { get; set; }
public string TokenId { get; set; } = string.Empty;
public string MarketSlug { get; set; } = string.Empty;
public string MarketQuestion { get; set; } = string.Empty;
public string Outcome { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public string ClusterKey { get; set; } = string.Empty;
public decimal EntryPrice { get; set; }
public decimal ExitPrice { get; set; }
public decimal Size { get; set; }
public decimal RealizedPnl { get; set; }
public decimal PnlPercent { get; set; }
public decimal TotalFees { get; set; }
public DateTime OpenedAt { get; set; }
public DateTime ClosedAt { get; set; }
public string ExitReason { get; set; } = string.Empty;
/// <summary>"None", "Pending", "Redeemed", "Sold" (PreRedeem-Verkauf statt On-Chain-Redeem).</summary>
public string RedeemStatus { get; set; } = "None";
}
}
@@ -0,0 +1,33 @@
using System;
namespace PolyTrader.Modules.ResolutionFarming.Models
{
/// <summary>
/// Offene Farming-Position (Tabelle rf_positions). Zusammengesetzter Schlüssel (AccountId, TokenId):
/// eine offene Position je Markt/Account. Beim Schließen wird die Zeile gelöscht und ein
/// <see cref="RfClosedTrade"/> geschrieben.
/// </summary>
public class RfPosition
{
public int AccountId { get; set; }
public string TokenId { get; set; } = string.Empty;
public string MarketSlug { get; set; } = string.Empty;
public string MarketQuestion { get; set; } = string.Empty;
public string Outcome { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public string ClusterKey { get; set; } = string.Empty;
public decimal EntryPrice { get; set; }
public decimal Size { get; set; } // Shares
public decimal AmountUsd { get; set; } // Einstiegskosten inkl. Fee
public int EntryFeeBps { get; set; }
public bool IsDemo { get; set; }
public DateTime OpenedAt { get; set; }
public DateTime? EndDate { get; set; }
/// <summary>"Pending" (Order platziert, nicht gefüllt), "Open" (gefüllt), "Resolving".</summary>
public string Status { get; set; } = "Pending";
}
}
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using PolyTrader.Modules.ResolutionFarming.Models;
namespace PolyTrader.Modules.ResolutionFarming.Persistence.Ef
{
public class EfRfSettingsRepository : IRfSettingsRepository
{
private readonly IDbContextFactory<ResolutionFarmingDbContext> _factory;
public EfRfSettingsRepository(IDbContextFactory<ResolutionFarmingDbContext> factory) => _factory = factory;
public List<RfSettings> GetAll()
{
using var ctx = _factory.CreateDbContext();
return ctx.Settings.AsNoTracking().ToList();
}
public RfSettings? Get(int accountId)
{
using var ctx = _factory.CreateDbContext();
return ctx.Settings.AsNoTracking().FirstOrDefault(s => s.AccountId == accountId);
}
public void Upsert(RfSettings settings)
{
using var ctx = _factory.CreateDbContext();
bool exists = ctx.Settings.Any(s => s.AccountId == settings.AccountId);
if (exists) ctx.Settings.Update(settings);
else ctx.Settings.Add(settings);
ctx.SaveChanges();
}
}
public class EfRfCandidateRepository : IRfCandidateRepository
{
private readonly IDbContextFactory<ResolutionFarmingDbContext> _factory;
public EfRfCandidateRepository(IDbContextFactory<ResolutionFarmingDbContext> factory) => _factory = factory;
public void Insert(RfCandidate candidate)
{
using var ctx = _factory.CreateDbContext();
ctx.Candidates.Add(candidate);
ctx.SaveChanges();
}
public List<RfCandidate> GetRecent(int accountId, int limit)
{
using var ctx = _factory.CreateDbContext();
return ctx.Candidates.AsNoTracking()
.Where(c => c.AccountId == accountId)
.OrderByDescending(c => c.ScannedAt)
.Take(limit)
.ToList();
}
}
public class EfRfPositionRepository : IRfPositionRepository
{
private readonly IDbContextFactory<ResolutionFarmingDbContext> _factory;
public EfRfPositionRepository(IDbContextFactory<ResolutionFarmingDbContext> factory) => _factory = factory;
public List<RfPosition> GetOpen(int accountId)
{
using var ctx = _factory.CreateDbContext();
return ctx.Positions.AsNoTracking().Where(p => p.AccountId == accountId).ToList();
}
public List<RfPosition> GetAllOpen()
{
using var ctx = _factory.CreateDbContext();
return ctx.Positions.AsNoTracking().ToList();
}
public RfPosition? Find(int accountId, string tokenId)
{
using var ctx = _factory.CreateDbContext();
return ctx.Positions.AsNoTracking().FirstOrDefault(p => p.AccountId == accountId && p.TokenId == tokenId);
}
public void Upsert(RfPosition position)
{
using var ctx = _factory.CreateDbContext();
bool exists = ctx.Positions.Any(p => p.AccountId == position.AccountId && p.TokenId == position.TokenId);
if (exists) ctx.Positions.Update(position);
else ctx.Positions.Add(position);
ctx.SaveChanges();
}
public void Delete(int accountId, string tokenId)
{
using var ctx = _factory.CreateDbContext();
var row = ctx.Positions.FirstOrDefault(p => p.AccountId == accountId && p.TokenId == tokenId);
if (row != null) { ctx.Positions.Remove(row); ctx.SaveChanges(); }
}
public int CountOpenedSince(int accountId, DateTime since)
{
using var ctx = _factory.CreateDbContext();
return ctx.Positions.AsNoTracking().Count(p => p.AccountId == accountId && p.OpenedAt >= since);
}
}
public class EfRfClosedTradeRepository : IRfClosedTradeRepository
{
private readonly IDbContextFactory<ResolutionFarmingDbContext> _factory;
public EfRfClosedTradeRepository(IDbContextFactory<ResolutionFarmingDbContext> factory) => _factory = factory;
public void Insert(RfClosedTrade trade)
{
using var ctx = _factory.CreateDbContext();
ctx.ClosedTrades.Add(trade);
ctx.SaveChanges();
}
public List<RfClosedTrade> Find(Expression<Func<RfClosedTrade, bool>> predicate)
{
using var ctx = _factory.CreateDbContext();
return ctx.ClosedTrades.AsNoTracking().Where(predicate).ToList();
}
public decimal RealizedPnlSince(int accountId, DateTime since)
{
using var ctx = _factory.CreateDbContext();
return ctx.ClosedTrades.AsNoTracking()
.Where(t => t.AccountId == accountId && t.ClosedAt >= since)
.Select(t => (decimal?)t.RealizedPnl)
.Sum() ?? 0m;
}
}
}
@@ -0,0 +1,98 @@
using Microsoft.EntityFrameworkCore;
using PolyTrader.Modules.ResolutionFarming.Models;
namespace PolyTrader.Modules.ResolutionFarming.Persistence.Ef
{
/// <summary>
/// EF-Core-Kontext des ResolutionFarming-Moduls (gleiche MySQL-DB wie der Core, eigene Tabellen
/// mit Präfix rf_). Autoincrement-PKs von Anfang an (keine code-vergebenen Schlüssel vermeidet
/// die im Copytrading nachträglich aufgefallene TradeId-Kollisionsklasse).
/// </summary>
public class ResolutionFarmingDbContext : DbContext
{
public ResolutionFarmingDbContext(DbContextOptions<ResolutionFarmingDbContext> options) : base(options) { }
public DbSet<RfSettings> Settings => Set<RfSettings>();
public DbSet<RfCandidate> Candidates => Set<RfCandidate>();
public DbSet<RfPosition> Positions => Set<RfPosition>();
public DbSet<RfClosedTrade> ClosedTrades => Set<RfClosedTrade>();
protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<RfSettings>(e =>
{
e.ToTable("rf_settings");
e.HasKey(x => x.AccountId);
e.Property(x => x.AccountId).ValueGeneratedNever();
e.Property(x => x.MinPrice).HasPrecision(18, 6);
e.Property(x => x.MaxPrice).HasPrecision(18, 6);
e.Property(x => x.MinEdgePct).HasPrecision(18, 6);
e.Property(x => x.MaxPerMarketUsd).HasPrecision(18, 6);
e.Property(x => x.MaxPerClusterPct).HasPrecision(18, 6);
e.Property(x => x.MaxTotalExposurePct).HasPrecision(18, 6);
e.Property(x => x.DailyLossKillSwitchUsd).HasPrecision(18, 6);
e.Property(x => x.CategoryWhitelistCsv).HasMaxLength(500);
e.Property(x => x.BlacklistCsv).HasMaxLength(1000);
});
b.Entity<RfCandidate>(e =>
{
e.ToTable("rf_candidates");
e.HasKey(x => x.Id);
e.Property(x => x.Id).ValueGeneratedOnAdd();
e.Property(x => x.TokenId).HasMaxLength(120);
e.Property(x => x.MarketSlug).HasMaxLength(300);
e.Property(x => x.EventSlug).HasMaxLength(300);
e.Property(x => x.MarketQuestion).HasMaxLength(1000);
e.Property(x => x.Outcome).HasMaxLength(200);
e.Property(x => x.Category).HasMaxLength(100);
e.Property(x => x.ClusterKey).HasMaxLength(300);
e.Property(x => x.RejectReason).HasMaxLength(300);
e.Property(x => x.Ask).HasPrecision(18, 6);
e.Property(x => x.NetEdgePct).HasPrecision(18, 6);
e.Property(x => x.Score).HasPrecision(18, 6);
e.HasIndex(x => x.ScannedAt);
e.HasIndex(x => new { x.AccountId, x.Accepted });
});
b.Entity<RfPosition>(e =>
{
e.ToTable("rf_positions");
e.HasKey(x => new { x.AccountId, x.TokenId });
e.Property(x => x.TokenId).HasMaxLength(120);
e.Property(x => x.MarketSlug).HasMaxLength(300);
e.Property(x => x.MarketQuestion).HasMaxLength(1000);
e.Property(x => x.Outcome).HasMaxLength(200);
e.Property(x => x.Category).HasMaxLength(100);
e.Property(x => x.ClusterKey).HasMaxLength(300);
e.Property(x => x.Status).HasMaxLength(20);
e.Property(x => x.EntryPrice).HasPrecision(18, 6);
e.Property(x => x.Size).HasPrecision(18, 6);
e.Property(x => x.AmountUsd).HasPrecision(18, 6);
});
b.Entity<RfClosedTrade>(e =>
{
e.ToTable("rf_closed_trades");
e.HasKey(x => x.Id);
e.Property(x => x.Id).ValueGeneratedOnAdd();
e.Property(x => x.TokenId).HasMaxLength(120);
e.Property(x => x.MarketSlug).HasMaxLength(300);
e.Property(x => x.MarketQuestion).HasMaxLength(1000);
e.Property(x => x.Outcome).HasMaxLength(200);
e.Property(x => x.Category).HasMaxLength(100);
e.Property(x => x.ClusterKey).HasMaxLength(300);
e.Property(x => x.ExitReason).HasMaxLength(200);
e.Property(x => x.RedeemStatus).HasMaxLength(20);
e.Property(x => x.EntryPrice).HasPrecision(18, 6);
e.Property(x => x.ExitPrice).HasPrecision(18, 6);
e.Property(x => x.Size).HasPrecision(18, 6);
e.Property(x => x.RealizedPnl).HasPrecision(18, 6);
e.Property(x => x.PnlPercent).HasPrecision(18, 6);
e.Property(x => x.TotalFees).HasPrecision(18, 6);
e.HasIndex(x => new { x.AccountId, x.TokenId });
e.HasIndex(x => x.ClosedAt);
});
}
}
}
@@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using PolyTrader.Core.Configuration;
namespace PolyTrader.Modules.ResolutionFarming.Persistence.Ef
{
/// <summary>
/// Design-Time-Factory für EF-Tooling. Nutzt die fest gepinnte Server-Version
/// (<see cref="DatabaseServerVersion"/>) statt <c>ServerVersion.AutoDetect</c>, damit
/// Migrations-Scaffolding OHNE DB-Verbindung funktioniert (kein Zugriff auf die produktive DB
/// beim Generieren). Connection-String rein nominell über POLYTRADER_MYSQL; es wird beim
/// bloßen Scaffolding keine Verbindung geöffnet.
/// </summary>
public class ResolutionFarmingDbContextFactory : IDesignTimeDbContextFactory<ResolutionFarmingDbContext>
{
public ResolutionFarmingDbContext CreateDbContext(string[] args)
{
var conn = Environment.GetEnvironmentVariable("POLYTRADER_MYSQL")
?? "Server=localhost;Port=3306;Database=polytrader;User ID=root;Password=;";
var options = new DbContextOptionsBuilder<ResolutionFarmingDbContext>()
.UseMySql(conn, DatabaseServerVersion.Value)
.Options;
return new ResolutionFarmingDbContext(options);
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using PolyTrader.Modules.ResolutionFarming.Models;
namespace PolyTrader.Modules.ResolutionFarming.Persistence
{
public interface IRfSettingsRepository
{
List<RfSettings> GetAll();
RfSettings? Get(int accountId);
void Upsert(RfSettings settings);
}
public interface IRfCandidateRepository
{
void Insert(RfCandidate candidate);
List<RfCandidate> GetRecent(int accountId, int limit);
}
public interface IRfPositionRepository
{
List<RfPosition> GetOpen(int accountId);
List<RfPosition> GetAllOpen();
RfPosition? Find(int accountId, string tokenId);
void Upsert(RfPosition position);
void Delete(int accountId, string tokenId);
/// <summary>Anzahl heute (seit <paramref name="since"/>) neu eröffneter Positionen (Tages-Drossel).</summary>
int CountOpenedSince(int accountId, DateTime since);
}
public interface IRfClosedTradeRepository
{
void Insert(RfClosedTrade trade);
List<RfClosedTrade> Find(Expression<Func<RfClosedTrade, bool>> predicate);
/// <summary>Summe des realisierten PnL seit <paramref name="since"/> (Kill-Switch).</summary>
decimal RealizedPnlSince(int accountId, DateTime since);
}
}
@@ -1,8 +1,12 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Configuration;
using PolyTrader.Core.Modularity;
using PolyTrader.Modules.ResolutionFarming.Persistence;
using PolyTrader.Modules.ResolutionFarming.Persistence.Ef;
namespace PolyTrader.Modules.ResolutionFarming
{
@@ -22,8 +26,16 @@ namespace PolyTrader.Modules.ResolutionFarming
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
{
// Slice 1: reines Logik-Skelett. Persistenz (DbContext/Repos), Scanner-/Monitor-Jobs und
// Execution werden in den folgenden Slices registriert.
// Modul-Persistenz: EF Core / Pomelo / MySQL (thread-safer DbContextFactory), eigene rf_-Tabellen.
var conn = configuration["Database:MySqlConnectionString"] ?? string.Empty;
services.AddDbContextFactory<ResolutionFarmingDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value));
services.AddSingleton<IRfSettingsRepository, EfRfSettingsRepository>();
services.AddSingleton<IRfCandidateRepository, EfRfCandidateRepository>();
services.AddSingleton<IRfPositionRepository, EfRfPositionRepository>();
services.AddSingleton<IRfClosedTradeRepository, EfRfClosedTradeRepository>();
// Scanner-/Monitor-Jobs, Execution und UI folgen in den nächsten Slices.
}
public void RegisterUi(IModuleUiHost host, System.IServiceProvider services)