ResolutionFarming haelt sich an Pause und Handelsmodi
Behebt #569. GlobalTradingPaused, LiveTradingMode und DemoTradingMode wertete bisher allein CopyTrading aus. Die Farming-Ausfuehrung pruefte nur RfSettings.Enabled und eroeffnete weiter Positionen, auch wenn "Trading pausieren" gedrueckt war oder der Demo-Modus auf Inaktiv stand. Der Pause-Knopf hielt damit nur die Haelfte an. Echtes Geld war noch nicht betroffen: RF fuehrt nur Demo-Konten aus, die Live-Ausfuehrung wird als Zielland-Arbeit uebersprungen. Mit der Live-Anbindung haette eine Pause ohne Wirkung vor echten Positionen gestanden. Jetzt gelten dieselben Regeln wie im CopyTrading. Die Pause sperrt, und der Modus des Kontotyps (Demo oder Live) muss auf Aktiv stehen. "Nur Verkauf" sperrt ebenfalls, denn Farming kauft ausschliesslich. Protokolliert wird nur der Wechsel zwischen gesperrt und frei, nicht jeder Fuenf-Minuten-Durchlauf. VERHALTENSAENDERUNG: Demo-Farming eroeffnet nur noch bei Demo-Modus = Aktiv. Bisher lief es unabhaengig vom Modus, und der steht nach jedem Start auf Inaktiv. Bewusst nicht gesperrt ist die Aufloesungsueberwachung. Sie bucht nur Maerkte ab, die bereits aufgeloest sind, und sendet keine Orders. Hielte man sie an, wiche der ausgewiesene PnL von der Wirklichkeit ab. Der Scanner schreibt nur Kandidaten und bleibt ebenfalls aktiv. Der Schleifenrumpf liegt jetzt als RunCycle() vor und ist damit testbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,22 +58,7 @@ namespace PolyTrader.Modules.ResolutionFarming.Services
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var settings in _settingsRepo.GetAll().Where(s => s.Enabled))
|
RunCycle(stoppingToken);
|
||||||
{
|
|
||||||
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 (OperationCanceledException) { break; }
|
||||||
catch (Exception ex) { _logger.Error($"RF-Execution Fehler: {ex.Message}"); }
|
catch (Exception ex) { _logger.Error($"RF-Execution Fehler: {ex.Message}"); }
|
||||||
@@ -82,6 +67,83 @@ namespace PolyTrader.Modules.ResolutionFarming.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ein Durchlauf über alle freigeschalteten Konten - der testbare Kern der Schleife.
|
||||||
|
/// Liefert die Zahl der eröffneten Positionen.
|
||||||
|
/// </summary>
|
||||||
|
internal int RunCycle(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
foreach (var settings in _settingsRepo.GetAll().Where(s => s.Enabled))
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
if (!_state.Accounts.TryGetValue(settings.AccountId, out var account)) continue;
|
||||||
|
|
||||||
|
string? blocked = EntryBlockReason(_state, account.IsDemo);
|
||||||
|
ReportBlockChange(account, blocked);
|
||||||
|
if (blocked != null) continue;
|
||||||
|
|
||||||
|
if (account.IsDemo)
|
||||||
|
{
|
||||||
|
var opened = RunDemoForAccount(settings.AccountId, account.TotalBalance, settings);
|
||||||
|
total += opened.Count;
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Warum dieses Konto gerade keine neuen Positionen eröffnen darf, oder <c>null</c>, wenn es darf (#569).
|
||||||
|
///
|
||||||
|
/// <para>Bis zum 13.09.2026 prüfte die Farming-Ausführung weder die globale Pause noch die
|
||||||
|
/// Handelsmodi - beides wertete allein CopyTrading aus. "Trading pausieren" hielt damit nur die
|
||||||
|
/// Hälfte an. Hier gelten jetzt dieselben Regeln wie dort: Pause sperrt, und der Modus des
|
||||||
|
/// Kontotyps (Demo oder Live) muss auf Aktiv stehen. "Nur Verkauf" sperrt ebenfalls, denn
|
||||||
|
/// Farming kauft ausschließlich.</para>
|
||||||
|
///
|
||||||
|
/// <para>Die Auflösungsüberwachung bleibt davon bewusst unberührt: Sie bucht nur Märkte ab, die
|
||||||
|
/// bereits aufgelöst sind, und sendet keine Orders. Hielte man sie an, wiche der ausgewiesene
|
||||||
|
/// PnL von der Wirklichkeit ab.</para>
|
||||||
|
/// </summary>
|
||||||
|
internal static string? EntryBlockReason(TradingState state, bool isDemo)
|
||||||
|
{
|
||||||
|
if (state.GlobalTradingPaused)
|
||||||
|
return "globale Pause ist aktiv";
|
||||||
|
|
||||||
|
TradingMode mode = isDemo ? state.DemoTradingMode : state.LiveTradingMode;
|
||||||
|
if (mode == TradingMode.Active)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string which = isDemo ? "Demo" : "Live";
|
||||||
|
return mode == TradingMode.SellOnly
|
||||||
|
? $"{which}-Modus steht auf \"Nur Verkauf\" (Farming kauft ausschließlich)"
|
||||||
|
: $"{which}-Modus ist inaktiv";
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<int, string?> _lastBlockReason = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Protokolliert nur den Wechsel zwischen gesperrt und frei. Die Schleife läuft alle fünf
|
||||||
|
/// Minuten; eine Zeile je Durchlauf und Konto würde das Protokoll zuschütten.
|
||||||
|
/// </summary>
|
||||||
|
private void ReportBlockChange(AccountState account, string? reason)
|
||||||
|
{
|
||||||
|
_lastBlockReason.TryGetValue(account.AccountId, out var last);
|
||||||
|
if (last == reason) return;
|
||||||
|
_lastBlockReason[account.AccountId] = reason;
|
||||||
|
|
||||||
|
if (reason != null)
|
||||||
|
_logger.Info($"⏸️ [RF-Execution] Konto {account.Name}: keine neuen Positionen – {reason}.");
|
||||||
|
else
|
||||||
|
_logger.Info($"▶️ [RF-Execution] Konto {account.Name}: Einstiege wieder freigegeben.");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plant und eröffnet Demo-Positionen für einen Account (testbarer Kern). Liefert die eröffneten
|
/// 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
|
/// Positionen. Bankroll = Referenz für die %-Limits; die Demo-Balance wird bewusst NICHT mutiert
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using PolyTrader.Modules.ResolutionFarming.Models;
|
||||||
|
using PolyTrader.Modules.ResolutionFarming.Persistence.Ef;
|
||||||
|
using PolyTrader.Modules.ResolutionFarming.Services;
|
||||||
|
using PolyTrader.Tests.Fakes;
|
||||||
|
using PolyTrader.Tests.TestSupport;
|
||||||
|
using PolyTraderSharp;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
using PolyTraderSharp.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PolyTrader.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Die Farming-Ausführung hält sich an Pause und Handelsmodi (#569). Bis dahin hielt
|
||||||
|
/// "Trading pausieren" nur CopyTrading an; Resolution Farming eröffnete weiter Positionen.
|
||||||
|
/// </summary>
|
||||||
|
public class FarmingExecutionGateTests
|
||||||
|
{
|
||||||
|
private sealed record Rig(FarmingExecutionService Svc, TradingState State, EfRfPositionRepository Positions);
|
||||||
|
|
||||||
|
/// <summary>Ein freigeschaltetes Konto mit zwei angenommenen Kandidaten, die ohne Sperre beide eröffnet würden.</summary>
|
||||||
|
private static Rig Build(bool demoAccount = true)
|
||||||
|
{
|
||||||
|
var factory = new InMemoryContextFactory<ResolutionFarmingDbContext>(o => new ResolutionFarmingDbContext(o));
|
||||||
|
var candRepo = new EfRfCandidateRepository(factory);
|
||||||
|
var posRepo = new EfRfPositionRepository(factory);
|
||||||
|
var closedRepo = new EfRfClosedTradeRepository(factory);
|
||||||
|
var settingsRepo = new EfRfSettingsRepository(factory);
|
||||||
|
|
||||||
|
settingsRepo.Upsert(new RfSettings
|
||||||
|
{
|
||||||
|
AccountId = 1, Enabled = true, MaxPerMarketUsd = 25m, MaxPerClusterPct = 10m,
|
||||||
|
MaxTotalExposurePct = 60m, MaxNewPositionsPerDay = 20, DailyLossKillSwitchUsd = 0m
|
||||||
|
});
|
||||||
|
candRepo.Insert(new RfCandidate { AccountId = 1, Accepted = true, TokenId = "a", ClusterKey = "c1", Ask = 0.95m, Score = 5m });
|
||||||
|
candRepo.Insert(new RfCandidate { AccountId = 1, Accepted = true, TokenId = "b", ClusterKey = "c2", Ask = 0.95m, Score = 3m });
|
||||||
|
|
||||||
|
var state = new TradingState();
|
||||||
|
state.Accounts[1] = new AccountState { AccountId = 1, Name = "Farming", IsDemo = demoAccount, TotalBalance = 1000m };
|
||||||
|
|
||||||
|
var svc = new FarmingExecutionService(state, settingsRepo, candRepo, posRepo, closedRepo, new TerminalLogger(), new FakeDecisionJournal());
|
||||||
|
return new Rig(svc, state, posRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Aktiver_Demo_Modus_eroeffnet_Positionen()
|
||||||
|
{
|
||||||
|
var rig = Build();
|
||||||
|
rig.State.DemoTradingMode = TradingMode.Active;
|
||||||
|
|
||||||
|
Assert.Equal(2, rig.Svc.RunCycle());
|
||||||
|
Assert.Equal(2, rig.Positions.GetOpen(1).Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Globale_Pause_verhindert_Einstiege_trotz_aktivem_Modus()
|
||||||
|
{
|
||||||
|
var rig = Build();
|
||||||
|
rig.State.DemoTradingMode = TradingMode.Active;
|
||||||
|
rig.State.GlobalTradingPaused = true;
|
||||||
|
|
||||||
|
Assert.Equal(0, rig.Svc.RunCycle());
|
||||||
|
Assert.Empty(rig.Positions.GetOpen(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(TradingMode.Inactive)]
|
||||||
|
[InlineData(TradingMode.SellOnly)] // Farming kauft ausschliesslich
|
||||||
|
public void Demo_Modus_ungleich_Aktiv_verhindert_Einstiege(TradingMode mode)
|
||||||
|
{
|
||||||
|
var rig = Build();
|
||||||
|
rig.State.DemoTradingMode = mode;
|
||||||
|
|
||||||
|
Assert.Equal(0, rig.Svc.RunCycle());
|
||||||
|
Assert.Empty(rig.Positions.GetOpen(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Aktiver_Live_Modus_gibt_Demo_Konten_nicht_frei()
|
||||||
|
{
|
||||||
|
var rig = Build();
|
||||||
|
rig.State.LiveTradingMode = TradingMode.Active;
|
||||||
|
rig.State.DemoTradingMode = TradingMode.Inactive;
|
||||||
|
|
||||||
|
Assert.Equal(0, rig.Svc.RunCycle());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Nach_Aufheben_der_Pause_geht_es_weiter()
|
||||||
|
{
|
||||||
|
var rig = Build();
|
||||||
|
rig.State.DemoTradingMode = TradingMode.Active;
|
||||||
|
rig.State.GlobalTradingPaused = true;
|
||||||
|
Assert.Equal(0, rig.Svc.RunCycle());
|
||||||
|
|
||||||
|
rig.State.GlobalTradingPaused = false;
|
||||||
|
|
||||||
|
Assert.Equal(2, rig.Svc.RunCycle());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sperrgrund_nennt_die_Ursache()
|
||||||
|
{
|
||||||
|
var state = new TradingState { DemoTradingMode = TradingMode.Active, LiveTradingMode = TradingMode.SellOnly };
|
||||||
|
|
||||||
|
Assert.Null(FarmingExecutionService.EntryBlockReason(state, isDemo: true));
|
||||||
|
Assert.Contains("Nur Verkauf", FarmingExecutionService.EntryBlockReason(state, isDemo: false));
|
||||||
|
|
||||||
|
state.GlobalTradingPaused = true;
|
||||||
|
Assert.Contains("Pause", FarmingExecutionService.EntryBlockReason(state, isDemo: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user