RF-Slice 5: Demo-Execution + Resolution-Monitor (Phase RF-2)
Schliesst die Demo-Handelsschleife des ResolutionFarming: - FarmingExecutionPlanner (pur): waehlt aus akzeptierten Kandidaten die zu oeffnenden Positionen + Groesse, priorisiert nach Score, unter Markt-/Cluster-/Gesamt-Limits, Kill-Switch und Tages-Drossel; dedupliziert Token, schreibt Exposure im Lauf fort. - FarmingResolution (pur): baut aus Position + Ergebnis den RfClosedTrade (PnL/Fees/Redeem-Status). - FarmingExecutionService: Demo-Einstieg (Maker-Fill 0 Fee via FarmingFillModel) -> rf_positions. Live-Execution bewusst geloggt/uebersprungen (Zielland). Demo-Balance NICHT mutiert (kein Shared-Account-Konflikt; PnL fliesst ueber rf_closed_trades). - FarmingResolutionMonitorService: schliesst aufgeloeste Positionen, bucht GlobalPnl, Dual-Write ins Core-Trade-Log (Dashboard). Auflösungsstatus via IMarketResolutionSource. - NullMarketResolutionSource als Default (nichts loest auf), bis Live-Data-API verdrahtet ist. 15 neue Tests (Planner 8, Resolution 3, Execution-Service 2, Monitor 2). Build 0 Fehler, 311 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
80e6ad9b2d
commit
1c3a364df2
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using PolyTrader.Modules.ResolutionFarming.Logic;
|
||||
using PolyTrader.Modules.ResolutionFarming.Models;
|
||||
using PolyTrader.Modules.ResolutionFarming.Persistence;
|
||||
using PolyTraderSharp;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.Modules.ResolutionFarming.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Eröffnet Farming-Positionen aus akzeptierten Kandidaten (Phase RF-2, Demo). Auswahl/Sizing per
|
||||
/// reinem <see cref="FarmingExecutionPlanner"/> (Score-Priorisierung unter allen Risiko-Limits),
|
||||
/// Demo-Fill per <see cref="FarmingFillModel"/> (Maker-Einstieg, 0 Fee – der Netto-Edge-Check nutzte
|
||||
/// bereits konservativ den Taker-Satz). Positionen landen in rf_positions.
|
||||
///
|
||||
/// Live-Execution (Maker-GTC via CLOB, Taker-Fallback nach Timeout) ist bewusst Zielland-Arbeit und
|
||||
/// hier nur geloggt – die Entscheidungs-/Sizing-Logik ist identisch und getestet.
|
||||
/// </summary>
|
||||
public class FarmingExecutionService : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan Interval = TimeSpan.FromMinutes(5);
|
||||
|
||||
private readonly TradingState _state;
|
||||
private readonly IRfSettingsRepository _settingsRepo;
|
||||
private readonly IRfCandidateRepository _candidateRepo;
|
||||
private readonly IRfPositionRepository _positionRepo;
|
||||
private readonly IRfClosedTradeRepository _closedRepo;
|
||||
private readonly TerminalLogger _logger;
|
||||
|
||||
public FarmingExecutionService(
|
||||
TradingState state, IRfSettingsRepository settingsRepo, IRfCandidateRepository candidateRepo,
|
||||
IRfPositionRepository positionRepo, IRfClosedTradeRepository closedRepo, TerminalLogger logger)
|
||||
{
|
||||
_state = state;
|
||||
_settingsRepo = settingsRepo;
|
||||
_candidateRepo = candidateRepo;
|
||||
_positionRepo = positionRepo;
|
||||
_closedRepo = closedRepo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(45), stoppingToken); // nach dem Scanner anlaufen
|
||||
_logger.Info("ResolutionFarming-Execution gestartet.");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var settings in _settingsRepo.GetAll().Where(s => s.Enabled))
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
if (!_state.Accounts.TryGetValue(settings.AccountId, out var account)) continue;
|
||||
|
||||
if (account.IsDemo)
|
||||
{
|
||||
var opened = RunDemoForAccount(settings.AccountId, account.TotalBalance, settings);
|
||||
if (opened.Count > 0)
|
||||
_logger.Trade($"🌱 [RF-Demo] Konto {account.Name}: {opened.Count} Position(en) eröffnet.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info($"[RF-Execution] Konto {account.Name}: Live-Execution (Maker-GTC via CLOB) ist Zielland-Arbeit – übersprungen.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex) { _logger.Error($"RF-Execution Fehler: {ex.Message}"); }
|
||||
|
||||
await Task.Delay(Interval, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plant und eröffnet Demo-Positionen für einen Account (testbarer Kern). Liefert die eröffneten
|
||||
/// Positionen. Bankroll = Referenz für die %-Limits; die Demo-Balance wird bewusst NICHT mutiert
|
||||
/// (kein Konflikt mit anderen Modulen auf einem geteilten Konto – PnL fließt über rf_closed_trades).
|
||||
/// </summary>
|
||||
internal List<RfPosition> RunDemoForAccount(int accountId, decimal bankrollUsd, RfSettings settings)
|
||||
{
|
||||
var accepted = _candidateRepo.GetRecent(accountId, 200).Where(c => c.Accepted).ToList();
|
||||
var open = _positionRepo.GetOpen(accountId);
|
||||
DateTime today = DateTime.UtcNow.Date;
|
||||
int todayCount = _positionRepo.CountOpenedSince(accountId, today);
|
||||
decimal dailyPnl = _closedRepo.RealizedPnlSince(accountId, today);
|
||||
|
||||
var plan = FarmingExecutionPlanner.Plan(accepted, open, settings, bankrollUsd, todayCount, dailyPnl);
|
||||
|
||||
var opened = new List<RfPosition>();
|
||||
foreach (var (c, sizeUsd) in plan)
|
||||
{
|
||||
decimal shares = FarmingFillModel.SharesForBudget(sizeUsd, c.Ask);
|
||||
if (shares <= 0m) continue;
|
||||
|
||||
const int makerFeeBps = 0; // Maker-Einstieg fee-frei
|
||||
var pos = new RfPosition
|
||||
{
|
||||
AccountId = accountId,
|
||||
TokenId = c.TokenId,
|
||||
MarketSlug = c.MarketSlug,
|
||||
MarketQuestion = c.MarketQuestion,
|
||||
Outcome = c.Outcome,
|
||||
Category = c.Category,
|
||||
ClusterKey = c.ClusterKey,
|
||||
EntryPrice = c.Ask,
|
||||
Size = shares,
|
||||
AmountUsd = FarmingFillModel.EntryCostWithFee(shares, c.Ask, makerFeeBps),
|
||||
EntryFeeBps = makerFeeBps,
|
||||
IsDemo = true,
|
||||
OpenedAt = DateTime.UtcNow,
|
||||
EndDate = c.EndDate,
|
||||
Status = "Open"
|
||||
};
|
||||
_positionRepo.Upsert(pos);
|
||||
opened.Add(pos);
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user