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
+338
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+192
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+335
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using PolyTrader.Core.Configuration;
|
||||||
using PolyTrader.Core.Modularity;
|
using PolyTrader.Core.Modularity;
|
||||||
|
using PolyTrader.Modules.ResolutionFarming.Persistence;
|
||||||
|
using PolyTrader.Modules.ResolutionFarming.Persistence.Ef;
|
||||||
|
|
||||||
namespace PolyTrader.Modules.ResolutionFarming
|
namespace PolyTrader.Modules.ResolutionFarming
|
||||||
{
|
{
|
||||||
@@ -22,8 +26,16 @@ namespace PolyTrader.Modules.ResolutionFarming
|
|||||||
|
|
||||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
// Slice 1: reines Logik-Skelett. Persistenz (DbContext/Repos), Scanner-/Monitor-Jobs und
|
// Modul-Persistenz: EF Core / Pomelo / MySQL (thread-safer DbContextFactory), eigene rf_-Tabellen.
|
||||||
// Execution werden in den folgenden Slices registriert.
|
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)
|
public void RegisterUi(IModuleUiHost host, System.IServiceProvider services)
|
||||||
|
|||||||
@@ -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