- Legacy-Form stillgelegt: frm_main(.Designer/.resx) + frm_analytics(.Designer/.resx) geloescht, 'Open Legacy'-Menue aus LauncherForm entfernt. - Program.cs: IMongoDatabase-Registrierung + closed_trades-Cleanup weg; Trade-Nummerierung liest jetzt hoechste TradeId aus ICopyTradeLogRepository. - StartupHydrationService: Mongo-/Bson-Fallback entfernt (Settings kommen aus dem Repo; fehlende Accounts erhalten Default-Settings). - Alle Mongo*Repository (Core 4 + Modul 4) geloescht; DI-Zweige auf EF/MySQL reduziert (AddCorePersistence, CopyTradingModule). - ConfigMigrator: Mongo-Pfad (Run/MigrateSettings) entfernt; nur noch RunFromJson + VerifyMySql (kein Mongo-Treiber mehr). - Modelle: [BsonId] entfernt (EF-Keys via HasKey); ObjectId-Defaults -> Guid. - LiteDB-Shim (MongoDbLiteDBShim) geloescht; MongoDB.Driver- + LiteDB-Pakete aus App/Core entfernt; stale usings + user-sichtbare Strings bereinigt. - DatabaseOptions/appsettings.json: Mongo-Provider/Connection-Felder entfernt. - Build gruen; --verify-mysql bestaetigt 3 Accounts/3 Settings/32 Trader/2344 Markets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
4.9 KiB
C#
117 lines
4.9 KiB
C#
using System.Threading.Channels;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using PolyTrader.Core.Persistence;
|
|
using PolyTrader.Modules.CopyTrading.Persistence;
|
|
using PolyTraderSharp.Models;
|
|
|
|
namespace PolyTraderSharp.Services
|
|
{
|
|
public class PersistenceService : BackgroundService
|
|
{
|
|
private readonly ChannelReader<ClosedTrade> _tradeReader;
|
|
private readonly ICopyTradeLogRepository _tradeLog;
|
|
private readonly ITradeLogRepository _coreTradeLog;
|
|
private readonly TerminalLogger _logger;
|
|
private readonly JobStatusRow _jobStatus;
|
|
|
|
public PersistenceService(ChannelReader<ClosedTrade> tradeReader, ICopyTradeLogRepository tradeLog, ITradeLogRepository coreTradeLog, TerminalLogger logger, JobManager jobManager)
|
|
{
|
|
_tradeReader = tradeReader;
|
|
_tradeLog = tradeLog;
|
|
_coreTradeLog = coreTradeLog;
|
|
_logger = logger;
|
|
|
|
_jobStatus = new JobStatusRow
|
|
{
|
|
JobName = "MySQL Transaction Log",
|
|
Description = "Awaits internal signals to write Closed Trades to the database safely.",
|
|
StatusText = "Pending Initial Delay..."
|
|
};
|
|
|
|
_jobStatus.ManualTriggerAction = async () =>
|
|
{
|
|
_jobStatus.StatusText = "Manual trigger not supported for Channel Reader";
|
|
await Task.Delay(2000);
|
|
_jobStatus.StatusText = "Listening (Channel)...";
|
|
};
|
|
|
|
jobManager.RegisterJob(_jobStatus);
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.Info("PersistenceService started writing background DB logs.");
|
|
_jobStatus.StatusText = "Listening (Channel)...";
|
|
|
|
// One-time index setup (moved out of hot loop)
|
|
_tradeLog.EnsureIndexes();
|
|
_coreTradeLog.EnsureIndexes();
|
|
|
|
// We do a loop waiting for items in the channel
|
|
await foreach(var trade in _tradeReader.ReadAllAsync(stoppingToken))
|
|
{
|
|
if (!_jobStatus.IsEnabled)
|
|
{
|
|
// If paused, we just drop the trade for now or log a warning
|
|
_logger.Warning("PersistenceService is paused, ignoring trade log.");
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
_jobStatus.StatusText = "Writing to DB...";
|
|
|
|
// ===== DEDUPLIZIERUNG: Verhindert das Mehrfach-Einfügen desselben Trades =====
|
|
// Prüft ob für diesen Account + TokenId bereits ein ClosedTrade existiert.
|
|
// Dies verhindert den "Background Sync Duplicate Bug", bei dem geschlossene
|
|
// Trades bei jedem Sync-Zyklus oder nach einem Neustart erneut eingefügt werden.
|
|
if (!string.IsNullOrEmpty(trade.TokenId))
|
|
{
|
|
if (_tradeLog.Exists(trade.AccountId, trade.TokenId))
|
|
{
|
|
_logger.Debug($"Duplikat ignoriert: ClosedTrade für Account {trade.AccountId} + Token {trade.TokenId.Substring(0, Math.Min(10, trade.TokenId.Length))}... existiert bereits.");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
_tradeLog.Insert(trade);
|
|
|
|
// Dual-Write: generischer, modulübergreifender Core-Trade-Log (fürs Dashboard).
|
|
_coreTradeLog.Insert(new TradeRecord
|
|
{
|
|
ModuleName = "CopyTrading",
|
|
AccountId = trade.AccountId,
|
|
IsDemo = trade.IsDemo,
|
|
TokenId = trade.TokenId,
|
|
MarketQuestion = trade.MarketQuestion,
|
|
Outcome = trade.Outcome,
|
|
Side = trade.Side,
|
|
EntryPrice = trade.EntryPrice,
|
|
ExitPrice = trade.ExitPrice,
|
|
Size = trade.Size,
|
|
RealizedPnl = trade.RealizedPnl,
|
|
PnlPercent = trade.PnlPercent,
|
|
OpenedAt = trade.OpenedAt,
|
|
ClosedAt = trade.ClosedAt,
|
|
ExitReason = trade.ExitReason
|
|
});
|
|
|
|
_logger.Debug($"Saved ClosedTrade {trade.TradeId} to MySQL");
|
|
_jobStatus.LastRun = DateTime.Now;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"Failed to persist ClosedTrade (ID: {trade.TradeId}): {ex.Message}");
|
|
_jobStatus.StatusText = "Error!";
|
|
}
|
|
finally
|
|
{
|
|
if (_jobStatus.StatusText != "Error!")
|
|
_jobStatus.StatusText = "Listening (Channel)...";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|