Files
PolyTraderSharp/src/PolyTrader.Web/Endpoints/ResolutionFarmingEndpoints.cs
T
RichardandClaude Opus 5 4c556be57d
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s
Einstellungen: erst schreiben, dann uebernehmen
Behebt #534.

Die vier schreibenden Endpunkte verschluckten die Ausnahme des Repositories und
meldeten anschliessend unbedingt Erfolg. Das Verschlucken war unschoen; die
Reihenfolge war gefaehrlich: copyState wurde auch dann geaendert, wenn die
Datenbank abgelehnt hatte. Das Modul handelte danach mit Risikolimits, die
nirgends standen, und nach dem naechsten Neustart konnte niemand mehr
rekonstruieren, womit es gehandelt hatte.

Neu geht der Schreibvorgang zuerst, und der Hauptspeicher wird nur nachgezogen,
wenn er gelang. Sonst 500 mit Klartext und unveraenderter Zustand. Fehlt die
Ablage ganz, ist die Aenderung ebenso rein fluechtig — auch das wird abgelehnt,
mit 503.

Betroffen: CopyTrading-Kontoeinstellungen, ResolutionFarming-Kontoeinstellungen,
Master-Trader anlegen/aendern, Master-Trader loeschen.

Dass alle vier denselben Helfer (PersistenceGuard) benutzen, ist der eigentliche
Punkt: Aus je eigenem try/catch je Endpunkt ist die Ungleichheit ueberhaupt erst
entstanden.

Bewusst anders behandelt bleibt das Archivieren eines Supervisor-Berichts: Die
Antwort des Agenten steht dort bereits und hat Geld gekostet: sie wegen eines
fehlgeschlagenen Archiveintrags zu verwerfen, waere schlimmer als der fehlende
Eintrag, und an diesem Schreibvorgang haengt kein Verhalten des Systems.
Stillschweigend verschwindet er aber nicht mehr, sondern wird protokolliert.

In der Oberflaeche zeigten die Schreibwege bisher nur "HTTP 500" an und
verschenkten damit genau die Begruendung, die der Server jetzt mitliefert.
errorText() liest sie aus der Antwort.

Entscheidung von Richard: ablehnen statt fluechtig uebernehmen. Damit sind
waehrend eines Datenbankausfalls keine Aenderungen an den Einstellungen mehr
moeglich; das Handeln selbst laeuft weiter.

Geprueft auf der Linux-Test-VM mit angehaltener MariaDB: alle drei Schreibwege
antworten mit 500 und nennen den Grund, das Limit bleibt bei 11,25 statt auf
99,99 zu springen, der Phantom-Trader entsteht weder im Hauptspeicher noch in
der Datenbank, jeder Fehlschlag steht im Protokoll. Nach dem Wiederanlauf
speichert es wieder. 556 von 556 Tests bestanden (5 neue).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 15:51:16 +02:00

242 lines
9.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Modules.ResolutionFarming.Models;
using PolyTrader.Modules.ResolutionFarming.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Services;
namespace PolyTrader.Web.Endpoints
{
public static class ResolutionFarmingEndpoints
{
public static IEndpointRouteBuilder MapResolutionFarmingEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/modules/resolutionfarming");
// 1. Kandidaten (Scanner-Ergebnisse)
group.MapGet("/candidates", (IServiceProvider sp, TradingState state, int? accountId) =>
{
int accId = accountId.GetValueOrDefault(-1);
if (accId <= 0)
{
var firstAcc = state.Accounts.Values.OrderBy(a => a.AccountId).FirstOrDefault();
if (firstAcc != null) accId = firstAcc.AccountId;
}
var repo = sp.GetService<IRfCandidateRepository>();
List<RfCandidate> candidates;
try
{
candidates = repo?.GetRecent(accId, 500) ?? new List<RfCandidate>();
}
catch
{
candidates = new List<RfCandidate>();
}
var list = candidates.Select(c => new
{
id = c.Id,
scannedAt = c.ScannedAt.ToString("yyyy-MM-dd HH:mm:ss"),
accountId = c.AccountId,
tokenId = c.TokenId,
marketSlug = c.MarketSlug,
marketQuestion = c.MarketQuestion,
outcome = c.Outcome,
category = c.Category,
clusterKey = c.ClusterKey,
ask = c.Ask,
feeBps = c.FeeBps,
netEdgePct = c.NetEdgePct,
score = c.Score,
endDate = c.EndDate.HasValue ? c.EndDate.Value.ToString("yyyy-MM-dd HH:mm") : "—",
accepted = c.Accepted,
rejectReason = c.RejectReason
}).ToList();
return Results.Ok(new
{
accountId = accId,
count = list.Count,
candidates = list
});
});
// 2. Offene Positionen
group.MapGet("/positions", (IServiceProvider sp, TradingState state, int? accountId) =>
{
int accId = accountId.GetValueOrDefault(-1);
var repo = sp.GetService<IRfPositionRepository>();
List<RfPosition> positions;
try
{
if (accId > 0)
positions = repo?.GetOpen(accId) ?? new List<RfPosition>();
else
positions = repo?.GetAllOpen() ?? new List<RfPosition>();
}
catch
{
positions = new List<RfPosition>();
}
var ordered = positions.OrderByDescending(p => p.OpenedAt).ToList();
decimal totalAmount = ordered.Sum(p => p.AmountUsd);
var list = ordered.Select(p =>
{
string accName = $"#{p.AccountId}";
if (state.Accounts.TryGetValue(p.AccountId, out var acc) && !string.IsNullOrEmpty(acc.Name))
accName = acc.Name + (p.IsDemo ? " (Demo)" : "");
return new
{
accountId = p.AccountId,
account = accName,
isDemo = p.IsDemo,
tokenId = p.TokenId,
marketSlug = p.MarketSlug,
marketQuestion = p.MarketQuestion,
outcome = p.Outcome,
category = p.Category,
clusterKey = p.ClusterKey,
entryPrice = p.EntryPrice,
size = p.Size,
amountUsd = p.AmountUsd,
entryFeeBps = p.EntryFeeBps,
openedAt = p.OpenedAt.ToString("yyyy-MM-dd HH:mm"),
endDate = p.EndDate.HasValue ? p.EndDate.Value.ToString("yyyy-MM-dd HH:mm") : "—",
status = p.Status
};
}).ToList();
return Results.Ok(new
{
accountId = accId,
count = list.Count,
totalAmount,
positions = list
});
});
// 3. Historie (abgeschlossene Trades)
group.MapGet("/history", (IServiceProvider sp, TradingState state, int? accountId, string? search) =>
{
int accId = accountId.GetValueOrDefault(-1);
var repo = sp.GetService<IRfClosedTradeRepository>();
List<RfClosedTrade> allClosed;
try
{
allClosed = repo?.Find(t => accId <= 0 || t.AccountId == accId) ?? new List<RfClosedTrade>();
}
catch
{
allClosed = new List<RfClosedTrade>();
}
var query = allClosed.AsEnumerable();
if (!string.IsNullOrWhiteSpace(search))
{
string q = search.Trim();
query = query.Where(t =>
(t.MarketQuestion != null && t.MarketQuestion.Contains(q, StringComparison.OrdinalIgnoreCase)) ||
(t.Outcome != null && t.Outcome.Contains(q, StringComparison.OrdinalIgnoreCase)) ||
(t.Category != null && t.Category.Contains(q, StringComparison.OrdinalIgnoreCase)));
}
var filtered = query.OrderByDescending(t => t.ClosedAt).ToList();
decimal totalPnl = filtered.Sum(t => t.RealizedPnl);
int wins = filtered.Count(t => t.RealizedPnl > 0);
decimal winrate = filtered.Count > 0 ? 100m * wins / filtered.Count : 0m;
var list = filtered.Select(t =>
{
string accName = $"#{t.AccountId}";
if (state.Accounts.TryGetValue(t.AccountId, out var acc) && !string.IsNullOrEmpty(acc.Name))
accName = acc.Name + (t.IsDemo ? " (Demo)" : "");
return new
{
id = t.Id,
accountId = t.AccountId,
account = accName,
isDemo = t.IsDemo,
tokenId = t.TokenId,
marketSlug = t.MarketSlug,
marketQuestion = t.MarketQuestion,
outcome = t.Outcome,
category = t.Category,
clusterKey = t.ClusterKey,
entryPrice = t.EntryPrice,
exitPrice = t.ExitPrice,
size = t.Size,
realizedPnl = t.RealizedPnl,
pnlPercent = t.PnlPercent,
totalFees = t.TotalFees,
openedAt = t.OpenedAt.ToString("yyyy-MM-dd HH:mm"),
closedAt = t.ClosedAt.ToString("yyyy-MM-dd HH:mm"),
exitReason = t.ExitReason,
redeemStatus = t.RedeemStatus
};
}).ToList();
return Results.Ok(new
{
accountId = accId,
summary = new
{
count = list.Count,
pnl = totalPnl,
wins,
winRate = winrate
},
trades = list
});
});
// 4. Einstellungen abrufen
group.MapGet("/settings/{accountId:int}", (IServiceProvider sp, int accountId) =>
{
var repo = sp.GetService<IRfSettingsRepository>();
RfSettings? settings = null;
try
{
settings = repo?.Get(accountId);
}
catch { }
if (settings == null)
settings = new RfSettings { AccountId = accountId };
return Results.Ok(settings);
});
// 5. Einstellungen speichern
group.MapPost("/settings/{accountId:int}", (IServiceProvider sp, TerminalLogger logger, int accountId, RfSettings input) =>
{
input.AccountId = accountId;
var repo = sp.GetService<IRfSettingsRepository>();
var failed = PersistenceGuard.Write(repo != null, () => repo!.Upsert(input), logger,
$"ResolutionFarming-Einstellungen fuer Konto {accountId}");
if (failed != null) return failed;
return Results.Ok(new
{
success = true,
settings = input
});
});
return routes;
}
}
}