Files
PolyTraderSharp/services/StartupHydrationService.cs
T
RichardandClaude Opus 4.8 782c08a860 Slice 6 (Fable-Fixes): Totcode/Demo-Balance/Settings-Validierung + Plan-Abschluss
- Totcode: services/SnapshotService.cs entfernt (nirgends registriert).
- Demo-Balance/PnL-Reconciliation: Demo-BUY zieht jetzt die Entry-Fee ab, Demo-Close
  schreibt netto (exitUsd - Exit-Fee) gut -> Summe(Balance-Aenderungen) = Summe(PnL)
  statt um die Fees zu driften (Demo als Validierung konsistent).
- Settings-Validierung: SellLogic.IsLadderConfigInverted (Max-Preisabstand >= SELL-Floor
  -> Leiter startet am Floor) + Warnung beim Settings-Laden (StartupHydration).

Bewusst als Follow-up dokumentiert (Live-Verifikation/Risiko): M3-Autoincrement-Migration,
PersistenceService-Dedup-Zeitfenster, Perf (UpsertLive-Dirty-Check, Leiter-Parallelitaet).

Tests: +3 (IsLadderConfigInverted). Build 0 Fehler, 236 gruen, --smoke-ui ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:45:52 +02:00

116 lines
4.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using PolyTrader.Core.Persistence;
using PolyTrader.Modules.CopyTrading.Logic;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp.Models;
namespace PolyTraderSharp.Services
{
/// <summary>
/// Hydriert den TradingState (Accounts, Demo-Positionen, Trader) EINMALIG beim App-Start.
/// Registriert als erster IHostedService, damit die Hydration abgeschlossen ist, bevor die
/// Trading-BackgroundServices (TraderMonitor, CopyTradingEngine, ...) ihre ExecuteAsync
/// starten. Behebt den Startup-Race, bei dem Services gegen einen leeren State anliefen
/// (früher lag diese Logik in frm_main.LoadDatabaseAndState und lief erst NACH AppHost.Start()).
/// </summary>
public class StartupHydrationService : IHostedService
{
private readonly TradingState _state;
private readonly CopyTradingState _copyState;
private readonly IAccountRepository _accountRepo;
private readonly IPositionRepository _positionRepo;
private readonly ICopyTradingAccountSettingsRepository _accountSettingsRepo;
private readonly ITrackedTraderRepository _traderRepo;
private readonly TerminalLogger _logger;
public StartupHydrationService(
TradingState state,
CopyTradingState copyState,
IAccountRepository accountRepo,
IPositionRepository positionRepo,
ICopyTradingAccountSettingsRepository accountSettingsRepo,
ITrackedTraderRepository traderRepo,
TerminalLogger logger)
{
_state = state;
_copyState = copyState;
_accountRepo = accountRepo;
_positionRepo = positionRepo;
_accountSettingsRepo = accountSettingsRepo;
_traderRepo = traderRepo;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
try
{
var accounts = _accountRepo.GetAll();
foreach (var acc in accounts)
{
if (acc.IsDemo)
{
foreach (var pos in _positionRepo.GetDemo(acc.AccountId))
{
acc.OpenPositions.TryAdd(pos.TokenId, pos);
}
}
_state.Accounts[acc.AccountId] = acc;
}
HydrateAccountSettings(accounts);
foreach (var trd in _traderRepo.GetAll())
{
_copyState.Traders[trd.Id] = trd;
}
_logger.Info($"Startup-Hydration abgeschlossen: {_state.Accounts.Count} Accounts, {_copyState.Traders.Count} Trader geladen.");
}
catch (System.Exception ex)
{
_logger.Error($"Startup-Hydration fehlgeschlagen: {ex.Message}");
}
return Task.CompletedTask;
}
/// <summary>
/// Lädt die Copytrading-Account-Settings in den State. Existiert für einen Account noch
/// kein Eintrag, wird ein Default-Eintrag angelegt und persistiert.
/// </summary>
private void HydrateAccountSettings(List<AccountState> accounts)
{
var existing = _accountSettingsRepo.GetAll().ToDictionary(s => s.AccountId);
int created = 0;
foreach (var acc in accounts)
{
if (existing.TryGetValue(acc.AccountId, out var s))
{
_copyState.AccountSettings[acc.AccountId] = s;
// Konfig-Plausibilität: BUY-Preisabstand ≥ SELL-Floor → Leiter startet am Floor.
if (SellLogic.IsLadderConfigInverted(s.MaxPriceDifference, s.SellFloorPct))
_logger.Warning($"⚠️ [Settings] {acc.Name}: Max. Preisabstand ({s.MaxPriceDifference:F1}%) ≥ SELL-Floor " +
$"({s.SellFloorPct:F1}%) die SELL-Leiter startet direkt am Floor (keine echte Eskalation). Floor erhöhen oder Abstand senken.");
continue;
}
var settings = new CopyTradingAccountSettings { AccountId = acc.AccountId };
_accountSettingsRepo.Upsert(settings);
_copyState.AccountSettings[acc.AccountId] = settings;
created++;
}
if (created > 0)
_logger.Info($"Copytrading-Account-Settings: {created} Default-Eintrag/-Einträge angelegt.");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}