using System;
using System.Linq;
using System.Threading.Tasks;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTrader.Modules.ResolutionFarming.Models;
using PolyTrader.Modules.ResolutionFarming.Persistence;
using PolyTraderSharp.Models;
using Xunit;
namespace PolyTrader.Tests.WebUi
{
///
/// CopyTrading- und ResolutionFarming-Endpunkte ueber echte HTTP-Aufrufe (#568).
///
/// Die Datei gleichen Namens enthielt vorher acht Tests, die keine einzige Zeile Produktionscode
/// aufriefen: Sie summierten Listen im Test und verglichen das Ergebnis mit sich selbst. Die
/// Absichten sind dieselben geblieben, geprueft wird jetzt die Antwort der Route.
///
public class CopyTradingAndResolutionFarmingEndpointsTests
{
private static Task StartCopyTradingAsync() =>
WebUiTestHost.StartAsync(WebUiTestHost.AddCopyTradingStore);
private static Task StartResolutionFarmingAsync() =>
WebUiTestHost.StartAsync(WebUiTestHost.AddResolutionFarmingStore);
// ---- CopyTrading -----------------------------------------------------
[Fact]
public async Task CopyTrading_Trader_anlegen_und_wieder_lesen()
{
await using var web = await StartCopyTradingAsync();
var (status, created) = await web.PostJsonAsync("/api/modules/copytrading/traders", new
{
id = 0,
displayName = "Whale Alpha",
walletAddress = " 0x1234567890abcdef1234567890abcdef12345678 ",
category = " ",
description = "Top Polymarket Trader",
reasoning = "Winrate ueber 70 Prozent",
isActive = true,
isHidden = false,
autoPauseEnabled = true,
makerEntry = true,
assignedAccountIds = new[] { 1, 2 }
});
Assert.Equal(200, status);
var trader = created.GetProperty("trader");
Assert.Equal(1, trader.GetProperty("id").GetInt32());
Assert.Equal("NEW_BIG_BET", trader.GetProperty("category").GetString()); // leere Kategorie -> Vorgabe
Assert.Equal("0x1234567890abcdef1234567890abcdef12345678", trader.GetProperty("walletAddress").GetString());
var all = await web.GetJsonAsync("/api/modules/copytrading/traders");
var stored = Assert.Single(all.EnumerateArray());
Assert.Equal("Whale Alpha", stored.GetProperty("displayName").GetString());
Assert.True(stored.GetProperty("makerEntry").GetBoolean());
Assert.Equal(new[] { 1, 2 },
stored.GetProperty("assignedAccountIds").EnumerateArray().Select(e => e.GetInt32()).OrderBy(x => x).ToArray());
}
[Fact]
public async Task CopyTrading_offene_Trades_rechnen_Buchgewinn_je_Position_und_gesamt()
{
await using var web = await StartCopyTradingAsync();
var acc1 = new AccountState { AccountId = 1, Name = "Konto 1" };
acc1.OpenPositions["tok-1"] = new Position
{
AccountId = 1, TokenId = "tok-1", SourceTraderId = 1, SourceTraderName = "Whale 1",
MarketQuestion = "Will Trump win in 2024?", Outcome = "Yes",
EntryPrice = 0.50m, CurrentPrice = 0.70m, Size = 100m, AmountUsd = 50m, CurrentValueUsd = 70m
};
var acc2 = new AccountState { AccountId = 2, Name = "Konto 2" };
acc2.OpenPositions["tok-2"] = new Position
{
AccountId = 2, TokenId = "tok-2", SourceTraderId = 2, SourceTraderName = "Whale 2",
MarketQuestion = "Will Fed cut rates?", Outcome = "Yes",
EntryPrice = 0.80m, CurrentPrice = 0.60m, Size = 50m, AmountUsd = 40m, CurrentValueUsd = 30m
};
web.State.Accounts[1] = acc1;
web.State.Accounts[2] = acc2;
var open = await web.GetJsonAsync("/api/modules/copytrading/open-trades");
Assert.Equal(2, open.GetProperty("count").GetInt32());
Assert.Equal(90m, open.GetProperty("totalAmount").GetDecimal());
Assert.Equal(10m, open.GetProperty("totalUnrealized").GetDecimal()); // (70-50) + (30-40)
var byMarket = open.GetProperty("trades").EnumerateArray()
.ToDictionary(t => t.GetProperty("market").GetString()!);
Assert.Equal(20m, byMarket["Will Trump win in 2024?"].GetProperty("unrealizedPnl").GetDecimal());
Assert.Equal(40m, byMarket["Will Trump win in 2024?"].GetProperty("unrealizedPct").GetDecimal());
Assert.Equal(-10m, byMarket["Will Fed cut rates?"].GetProperty("unrealizedPnl").GetDecimal());
Assert.Equal(-25m, byMarket["Will Fed cut rates?"].GetProperty("unrealizedPct").GetDecimal());
}
[Fact]
public async Task CopyTrading_geschlossene_Trades_filtern_nach_Trader_Ergebnis_und_Suche()
{
await using var web = await StartCopyTradingAsync();
var log = web.Get();
log.Insert(new ClosedTrade { TradeId = 1, AccountId = 1, SourceTraderId = 10, TokenId = "a",
MarketQuestion = "Bitcoin reaches 100k?", Outcome = "Yes", RealizedPnl = 35m, ClosedAt = DateTime.UtcNow.AddHours(-2) });
log.Insert(new ClosedTrade { TradeId = 2, AccountId = 1, SourceTraderId = 10, TokenId = "b",
MarketQuestion = "Solana reaches 300?", Outcome = "No", RealizedPnl = -12m, ClosedAt = DateTime.UtcNow.AddHours(-5) });
log.Insert(new ClosedTrade { TradeId = 3, AccountId = 2, SourceTraderId = 20, TokenId = "c",
MarketQuestion = "Ethereum reaches 5k?", Outcome = "Yes", RealizedPnl = 50m, ClosedAt = DateTime.UtcNow.AddHours(-1) });
var byTrader = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?traderId=10");
Assert.Equal(2, byTrader.GetProperty("totalCount").GetInt32());
var winners = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?result=Gewinner");
Assert.Equal(2, winners.GetProperty("totalCount").GetInt32());
Assert.Equal(85m, winners.GetProperty("summary").GetProperty("totalPnl").GetDecimal());
var search = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?search=bitcoin");
var hit = Assert.Single(search.GetProperty("items").EnumerateArray());
Assert.Equal(1, hit.GetProperty("tradeId").GetInt32());
}
[Fact]
public async Task CopyTrading_Kontoeinstellungen_ohne_Eintrag_liefern_die_Vorgaben()
{
await using var web = await StartCopyTradingAsync();
var settings = await web.GetJsonAsync("/api/modules/copytrading/settings/42");
Assert.Equal(42, settings.GetProperty("accountId").GetInt32());
Assert.Equal(5.0m, settings.GetProperty("perMarketLimit").GetDecimal());
Assert.Equal(10.0m, settings.GetProperty("perMasterLimit").GetDecimal());
Assert.Equal(0.98m, settings.GetProperty("maxBuyPrice").GetDecimal());
Assert.Equal(15.0m, settings.GetProperty("sellFloorPct").GetDecimal());
}
// ---- ResolutionFarming -----------------------------------------------
[Fact]
public async Task ResolutionFarming_Kandidaten_liefern_Annahme_und_Ablehnungsgrund()
{
await using var web = await StartResolutionFarmingAsync();
web.State.Accounts[1] = new AccountState { AccountId = 1, Name = "Demo", IsDemo = true };
var repo = web.Get();
repo.Insert(new RfCandidate { AccountId = 1, TokenId = "t1", ScannedAt = DateTime.UtcNow,
MarketQuestion = "Will candidate A win election?", Outcome = "Yes", Category = "Politics",
Ask = 0.94m, NetEdgePct = 2.5m, Accepted = true, RejectReason = "" });
repo.Insert(new RfCandidate { AccountId = 1, TokenId = "t2", ScannedAt = DateTime.UtcNow,
MarketQuestion = "Will crypto token reach 100?", Outcome = "Yes", Category = "Crypto",
Ask = 0.91m, NetEdgePct = 0.5m, Accepted = false, RejectReason = "Fee too high for edge" });
var result = await web.GetJsonAsync("/api/modules/resolutionfarming/candidates?accountId=1");
Assert.Equal(2, result.GetProperty("count").GetInt32());
var items = result.GetProperty("candidates").EnumerateArray().ToList();
var accepted = Assert.Single(items, c => c.GetProperty("accepted").GetBoolean());
Assert.Equal("Politics", accepted.GetProperty("category").GetString());
var rejected = Assert.Single(items, c => !c.GetProperty("accepted").GetBoolean());
Assert.Equal("Fee too high for edge", rejected.GetProperty("rejectReason").GetString());
}
[Fact]
public async Task ResolutionFarming_Positionen_summieren_den_Einsatz()
{
await using var web = await StartResolutionFarmingAsync();
var repo = web.Get();
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "tok-1", MarketQuestion = "Match 1",
EntryPrice = 0.95m, Size = 100m, AmountUsd = 95m, Status = "Open", OpenedAt = DateTime.UtcNow });
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "tok-2", MarketQuestion = "Match 2",
EntryPrice = 0.92m, Size = 50m, AmountUsd = 46m, Status = "Open", OpenedAt = DateTime.UtcNow });
var result = await web.GetJsonAsync("/api/modules/resolutionfarming/positions?accountId=1");
Assert.Equal(2, result.GetProperty("count").GetInt32());
Assert.Equal(141m, result.GetProperty("totalAmount").GetDecimal());
}
[Fact]
public async Task ResolutionFarming_Historie_rechnet_PnL_und_Trefferquote()
{
await using var web = await StartResolutionFarmingAsync();
var repo = web.Get();
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "a", MarketQuestion = "Match A", RealizedPnl = 4.50m, RedeemStatus = "Redeemed", ClosedAt = DateTime.UtcNow });
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "b", MarketQuestion = "Match B", RealizedPnl = 3.80m, RedeemStatus = "Redeemed", ClosedAt = DateTime.UtcNow });
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "c", MarketQuestion = "Match C", RealizedPnl = -20.00m, RedeemStatus = "None", ClosedAt = DateTime.UtcNow });
var result = await web.GetJsonAsync("/api/modules/resolutionfarming/history?accountId=1");
var summary = result.GetProperty("summary");
Assert.Equal(3, summary.GetProperty("count").GetInt32());
Assert.Equal(-11.70m, summary.GetProperty("pnl").GetDecimal());
Assert.Equal(2, summary.GetProperty("wins").GetInt32());
Assert.Equal(66.7m, Math.Round(summary.GetProperty("winRate").GetDecimal(), 1));
}
[Fact]
public async Task ResolutionFarming_Einstellungen_ohne_Eintrag_liefern_die_Vorgaben()
{
await using var web = await StartResolutionFarmingAsync();
var settings = await web.GetJsonAsync("/api/modules/resolutionfarming/settings/5");
Assert.Equal(5, settings.GetProperty("accountId").GetInt32());
Assert.False(settings.GetProperty("enabled").GetBoolean());
Assert.Equal(0.90m, settings.GetProperty("minPrice").GetDecimal());
Assert.Equal(0.98m, settings.GetProperty("maxPrice").GetDecimal());
Assert.Equal(48, settings.GetProperty("maxHoursToResolution").GetInt32());
Assert.Contains("Sports", settings.GetProperty("categoryWhitelistCsv").GetString());
}
}
}