Phase 5: AccountState-Split (Copytrading-Limits -> Modul)
- Copytrading-Detail-Einstellungen (PerMarketLimit, MaxBuyPrice, PerMasterLimit, Zeit-Limits, PreRedeemLimit, ProfitTarget, MaxPriceDifference) aus dem Core- AccountState in das Modul-Modell CopyTradingAccountSettings verschoben. - CopyTradingState.AccountSettings + GetAccountSettings(accountId) (Default-safe). - Consumer umgestellt: CopyTradingEngine, TraderMonitorService, PolymarketWssClient, frm_main lesen die Limits jetzt aus den Account-Settings. - Neues Modul-Repo ICopyTradingAccountSettingsRepository (Collection ct_account_settings), in CopyTradingModule registriert. - StartupHydration: Settings laden + EINMALIGE Migration der Alt-Limits aus dem Roh-accounts-Dokument (keine konfigurierten Limits gehen verloren). - Build 0 Fehler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
37f9d0fae2
commit
095c4b64aa
+8
-8
@@ -737,34 +737,34 @@ namespace PolyTraderSharp
|
|||||||
decimal SafeMaxUsd(decimal pct, decimal tot) => tot * (pct / 100m);
|
decimal SafeMaxUsd(decimal pct, decimal tot) => tot * (pct / 100m);
|
||||||
decimal SafeUtil(decimal curr, decimal maxUsd) => maxUsd > 0 ? (curr / maxUsd) * 100m : 0m;
|
decimal SafeUtil(decimal curr, decimal maxUsd) => maxUsd > 0 ? (curr / maxUsd) * 100m : 0m;
|
||||||
|
|
||||||
decimal max6 = SafeMaxUsd(acc.perMaxTime6h, acc.TotalBalance);
|
decimal max6 = SafeMaxUsd(_copyState.GetAccountSettings(acc.AccountId).perMaxTime6h, acc.TotalBalance);
|
||||||
dgv_dashboard_detaillaufzeit.Rows.Add(
|
dgv_dashboard_detaillaufzeit.Rows.Add(
|
||||||
"< 6h",
|
"< 6h",
|
||||||
$"{acc.perMaxTime6h:F2}%",
|
$"{_copyState.GetAccountSettings(acc.AccountId).perMaxTime6h:F2}%",
|
||||||
$"${max6:F2}",
|
$"${max6:F2}",
|
||||||
$"{SafeUtil(under6, max6):F2}%",
|
$"{SafeUtil(under6, max6):F2}%",
|
||||||
$"${under6:F2}");
|
$"${under6:F2}");
|
||||||
|
|
||||||
decimal max24 = SafeMaxUsd(acc.perMaxTime24h, acc.TotalBalance);
|
decimal max24 = SafeMaxUsd(_copyState.GetAccountSettings(acc.AccountId).perMaxTime24h, acc.TotalBalance);
|
||||||
dgv_dashboard_detaillaufzeit.Rows.Add(
|
dgv_dashboard_detaillaufzeit.Rows.Add(
|
||||||
"< 24h",
|
"< 24h",
|
||||||
$"{acc.perMaxTime24h:F2}%",
|
$"{_copyState.GetAccountSettings(acc.AccountId).perMaxTime24h:F2}%",
|
||||||
$"${max24:F2}",
|
$"${max24:F2}",
|
||||||
$"{SafeUtil(under24, max24):F2}%",
|
$"{SafeUtil(under24, max24):F2}%",
|
||||||
$"${under24:F2}");
|
$"${under24:F2}");
|
||||||
|
|
||||||
decimal max72 = SafeMaxUsd(acc.perMaxTime72h, acc.TotalBalance);
|
decimal max72 = SafeMaxUsd(_copyState.GetAccountSettings(acc.AccountId).perMaxTime72h, acc.TotalBalance);
|
||||||
dgv_dashboard_detaillaufzeit.Rows.Add(
|
dgv_dashboard_detaillaufzeit.Rows.Add(
|
||||||
"< 72h",
|
"< 72h",
|
||||||
$"{acc.perMaxTime72h:F2}%",
|
$"{_copyState.GetAccountSettings(acc.AccountId).perMaxTime72h:F2}%",
|
||||||
$"${max72:F2}",
|
$"${max72:F2}",
|
||||||
$"{SafeUtil(under72, max72):F2}%",
|
$"{SafeUtil(under72, max72):F2}%",
|
||||||
$"${under72:F2}");
|
$"${under72:F2}");
|
||||||
|
|
||||||
decimal maxOver72 = SafeMaxUsd(acc.perMaxTimeNone, acc.TotalBalance);
|
decimal maxOver72 = SafeMaxUsd(_copyState.GetAccountSettings(acc.AccountId).perMaxTimeNone, acc.TotalBalance);
|
||||||
dgv_dashboard_detaillaufzeit.Rows.Add(
|
dgv_dashboard_detaillaufzeit.Rows.Add(
|
||||||
"> 72h",
|
"> 72h",
|
||||||
$"{acc.perMaxTimeNone:F2}%",
|
$"{_copyState.GetAccountSettings(acc.AccountId).perMaxTimeNone:F2}%",
|
||||||
$"${maxOver72:F2}",
|
$"${maxOver72:F2}",
|
||||||
$"{SafeUtil(over72, maxOver72):F2}%",
|
$"{SafeUtil(over72, maxOver72):F2}%",
|
||||||
$"${over72:F2}");
|
$"${over72:F2}");
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ namespace PolyTraderSharp.Services
|
|||||||
pos.CurrentValueUsd = pos.Size * price;
|
pos.CurrentValueUsd = pos.Size * price;
|
||||||
|
|
||||||
// Execute Auto-Redeem if config conditions are met
|
// Execute Auto-Redeem if config conditions are met
|
||||||
if (acc.PreRedeemLimit > 0 && price >= acc.PreRedeemLimit && acc.IsActive)
|
if (_copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit > 0 && price >= _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit && acc.IsActive)
|
||||||
{
|
{
|
||||||
string redeemKey = $"{acc.AccountId}_{assetId}";
|
string redeemKey = $"{acc.AccountId}_{assetId}";
|
||||||
// Spam protection: max 2 attempts per position, 5 minutes apart
|
// Spam protection: max 2 attempts per position, 5 minutes apart
|
||||||
@@ -195,13 +195,13 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
if (!acc.IsDemo && _state.LiveTradingMode == TradingMode.Active)
|
if (!acc.IsDemo && _state.LiveTradingMode == TradingMode.Active)
|
||||||
{
|
{
|
||||||
_logger.Trade($"🚨 [AUTO REDEEM] {acc.Name} | {pos.MarketQuestion} | Preis >= {acc.PreRedeemLimit}");
|
_logger.Trade($"🚨 [AUTO REDEEM] {acc.Name} | {pos.MarketQuestion} | Preis >= {_copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit}");
|
||||||
// Best effort non-blocking
|
// Best effort non-blocking
|
||||||
_ = Task.Run(async () => await ExecuteAutoRedeemLive(acc, pos, price));
|
_ = Task.Run(async () => await ExecuteAutoRedeemLive(acc, pos, price));
|
||||||
}
|
}
|
||||||
else if (acc.IsDemo && _state.DemoTradingMode == TradingMode.Active)
|
else if (acc.IsDemo && _state.DemoTradingMode == TradingMode.Active)
|
||||||
{
|
{
|
||||||
_logger.Trade($"🚨 [AUTO REDEEM DEMO] {acc.Name} | {pos.MarketQuestion} | Preis >= {acc.PreRedeemLimit}");
|
_logger.Trade($"🚨 [AUTO REDEEM DEMO] {acc.Name} | {pos.MarketQuestion} | Preis >= {_copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit}");
|
||||||
_ = Task.Run(() => ExecuteAutoRedeemDemo(acc, pos, price));
|
_ = Task.Run(() => ExecuteAutoRedeemDemo(acc, pos, price));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +227,7 @@ namespace PolyTraderSharp.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// The user explicitly requested an exact GTC order using the configured PreRedeemLimit, without slippage
|
// The user explicitly requested an exact GTC order using the configured PreRedeemLimit, without slippage
|
||||||
decimal expectedFillPrice = acc.PreRedeemLimit;
|
decimal expectedFillPrice = _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit;
|
||||||
decimal amountUsdc = Math.Max(pos.Size * expectedFillPrice, 0.01m);
|
decimal amountUsdc = Math.Max(pos.Size * expectedFillPrice, 0.01m);
|
||||||
|
|
||||||
// Fire and forget SELL via ClobClient
|
// Fire and forget SELL via ClobClient
|
||||||
@@ -259,7 +259,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
_positionRepo.DeleteDemo(acc.AccountId, pos.TokenId);
|
_positionRepo.DeleteDemo(acc.AccountId, pos.TokenId);
|
||||||
|
|
||||||
decimal exactLimitPrice = acc.PreRedeemLimit;
|
decimal exactLimitPrice = _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit;
|
||||||
decimal exitUsd = pos.Size * exactLimitPrice;
|
decimal exitUsd = pos.Size * exactLimitPrice;
|
||||||
decimal realizedPnl = exitUsd - pos.AmountUsd;
|
decimal realizedPnl = exitUsd - pos.AmountUsd;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using MongoDB.Bson;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using PolyTrader.Core.Persistence;
|
using PolyTrader.Core.Persistence;
|
||||||
|
using PolyTrader.Modules.CopyTrading.Persistence;
|
||||||
using PolyTraderSharp.Models;
|
using PolyTraderSharp.Models;
|
||||||
|
|
||||||
namespace PolyTraderSharp.Services
|
namespace PolyTraderSharp.Services
|
||||||
@@ -21,6 +24,7 @@ namespace PolyTraderSharp.Services
|
|||||||
private readonly CopyTradingState _copyState;
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly IAccountRepository _accountRepo;
|
private readonly IAccountRepository _accountRepo;
|
||||||
private readonly IPositionRepository _positionRepo;
|
private readonly IPositionRepository _positionRepo;
|
||||||
|
private readonly ICopyTradingAccountSettingsRepository _accountSettingsRepo;
|
||||||
private readonly IMongoDatabase _db;
|
private readonly IMongoDatabase _db;
|
||||||
private readonly TerminalLogger _logger;
|
private readonly TerminalLogger _logger;
|
||||||
|
|
||||||
@@ -29,6 +33,7 @@ namespace PolyTraderSharp.Services
|
|||||||
CopyTradingState copyState,
|
CopyTradingState copyState,
|
||||||
IAccountRepository accountRepo,
|
IAccountRepository accountRepo,
|
||||||
IPositionRepository positionRepo,
|
IPositionRepository positionRepo,
|
||||||
|
ICopyTradingAccountSettingsRepository accountSettingsRepo,
|
||||||
IMongoDatabase db,
|
IMongoDatabase db,
|
||||||
TerminalLogger logger)
|
TerminalLogger logger)
|
||||||
{
|
{
|
||||||
@@ -36,6 +41,7 @@ namespace PolyTraderSharp.Services
|
|||||||
_copyState = copyState;
|
_copyState = copyState;
|
||||||
_accountRepo = accountRepo;
|
_accountRepo = accountRepo;
|
||||||
_positionRepo = positionRepo;
|
_positionRepo = positionRepo;
|
||||||
|
_accountSettingsRepo = accountSettingsRepo;
|
||||||
_db = db;
|
_db = db;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -44,7 +50,8 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var acc in _accountRepo.GetAll())
|
var accounts = _accountRepo.GetAll();
|
||||||
|
foreach (var acc in accounts)
|
||||||
{
|
{
|
||||||
if (acc.IsDemo)
|
if (acc.IsDemo)
|
||||||
{
|
{
|
||||||
@@ -56,6 +63,8 @@ namespace PolyTraderSharp.Services
|
|||||||
_state.Accounts[acc.AccountId] = acc;
|
_state.Accounts[acc.AccountId] = acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HydrateAccountSettings(accounts);
|
||||||
|
|
||||||
// Trader (TrackedTrader) sind aktuell noch modulnah; bis zum Trader-Repo in
|
// Trader (TrackedTrader) sind aktuell noch modulnah; bis zum Trader-Repo in
|
||||||
// Phase 5 direkt aus der "trackers"-Collection geladen.
|
// Phase 5 direkt aus der "trackers"-Collection geladen.
|
||||||
var tradersCol = _db.GetCollection<TrackedTrader>("trackers");
|
var tradersCol = _db.GetCollection<TrackedTrader>("trackers");
|
||||||
@@ -74,6 +83,63 @@ namespace PolyTraderSharp.Services
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lädt die Copytrading-Account-Settings in den State. Existiert für einen Account noch
|
||||||
|
/// kein Eintrag, werden die Werte einmalig aus den alten AccountState-Feldern des
|
||||||
|
/// Roh-Dokuments migriert (damit konfigurierte Limits nach dem Modell-Split erhalten bleiben).
|
||||||
|
/// </summary>
|
||||||
|
private void HydrateAccountSettings(List<AccountState> accounts)
|
||||||
|
{
|
||||||
|
var existing = _accountSettingsRepo.GetAll().ToDictionary(s => s.AccountId);
|
||||||
|
var rawAccounts = _db.GetCollection<BsonDocument>("accounts");
|
||||||
|
int migrated = 0;
|
||||||
|
|
||||||
|
foreach (var acc in accounts)
|
||||||
|
{
|
||||||
|
if (existing.TryGetValue(acc.AccountId, out var s))
|
||||||
|
{
|
||||||
|
_copyState.AccountSettings[acc.AccountId] = s;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings = new CopyTradingAccountSettings { AccountId = acc.AccountId };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var raw = rawAccounts.Find(Builders<BsonDocument>.Filter.Eq("_id", acc.AccountId)).FirstOrDefault();
|
||||||
|
if (raw != null)
|
||||||
|
{
|
||||||
|
settings.PerMarketLimit = Dec(raw, "PerMarketLimit", settings.PerMarketLimit);
|
||||||
|
settings.MaxPriceDifference = Dec(raw, "MaxPriceDifference", settings.MaxPriceDifference);
|
||||||
|
settings.MaxBuyPrice = Dec(raw, "MaxBuyPrice", settings.MaxBuyPrice);
|
||||||
|
settings.ProfitTarget = Dec(raw, "ProfitTarget", settings.ProfitTarget);
|
||||||
|
settings.PreRedeemLimit = Dec(raw, "PreRedeemLimit", settings.PreRedeemLimit);
|
||||||
|
settings.PerMasterLimit = Dec(raw, "PerMasterLimit", settings.PerMasterLimit);
|
||||||
|
settings.perMaxTime6h = Dec(raw, "perMaxTime6h", settings.perMaxTime6h);
|
||||||
|
settings.perMaxTime24h = Dec(raw, "perMaxTime24h", settings.perMaxTime24h);
|
||||||
|
settings.perMaxTime72h = Dec(raw, "perMaxTime72h", settings.perMaxTime72h);
|
||||||
|
settings.perMaxTimeNone = Dec(raw, "perMaxTimeNone", settings.perMaxTimeNone);
|
||||||
|
migrated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
_accountSettingsRepo.Upsert(settings);
|
||||||
|
_copyState.AccountSettings[acc.AccountId] = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (migrated > 0)
|
||||||
|
_logger.Info($"Copytrading-Account-Settings: {migrated} Account(s) aus Alt-Feldern migriert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal Dec(BsonDocument doc, string field, decimal fallback)
|
||||||
|
{
|
||||||
|
if (doc.TryGetValue(field, out var v))
|
||||||
|
{
|
||||||
|
try { return v.ToDecimal(); } catch { }
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,36 +51,9 @@ namespace PolyTraderSharp.Models
|
|||||||
public decimal AvailableBalance { get; set; }
|
public decimal AvailableBalance { get; set; }
|
||||||
|
|
||||||
// Risk Settings
|
// Risk Settings
|
||||||
[Category("04. Risk Management")]
|
// Copytrading-Detail-Einstellungen (PerMarketLimit, MaxBuyPrice, Zeit-Limits …)
|
||||||
public decimal PerMarketLimit { get; set; } = 5.0m;
|
// liegen bewusst NICHT mehr hier, sondern im Copytrading-Modul
|
||||||
|
// (CopyTradingAccountSettings), da sie modulspezifisch sind.
|
||||||
[Category("04. Risk Management")]
|
|
||||||
public decimal MaxPriceDifference { get; set; } = 2.0m;
|
|
||||||
|
|
||||||
[Category("04. Risk Management")]
|
|
||||||
public decimal MaxBuyPrice { get; set; } = 0.98m;
|
|
||||||
|
|
||||||
[Category("04. Risk Management")]
|
|
||||||
public decimal ProfitTarget { get; set; } = 50.0m;
|
|
||||||
|
|
||||||
[Category("04. Risk Management")]
|
|
||||||
public decimal PreRedeemLimit { get; set; } = 0.0m;
|
|
||||||
|
|
||||||
[Category("04. Risk Management")]
|
|
||||||
public decimal PerMasterLimit { get; set; } = 10.0m;
|
|
||||||
|
|
||||||
// Time limits
|
|
||||||
[Category("05. Time Limits")]
|
|
||||||
public decimal perMaxTime6h { get; set; } = 20.0m;
|
|
||||||
|
|
||||||
[Category("05. Time Limits")]
|
|
||||||
public decimal perMaxTime24h { get; set; } = 20.0m;
|
|
||||||
|
|
||||||
[Category("05. Time Limits")]
|
|
||||||
public decimal perMaxTime72h { get; set; } = 20.0m;
|
|
||||||
|
|
||||||
[Category("05. Time Limits")]
|
|
||||||
public decimal perMaxTimeNone { get; set; } = 40.0m;
|
|
||||||
|
|
||||||
[Browsable(false)]
|
[Browsable(false)]
|
||||||
public ConcurrentDictionary<string, Position> OpenPositions { get; } = new(StringComparer.OrdinalIgnoreCase);
|
public ConcurrentDictionary<string, Position> OpenPositions { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ namespace PolyTrader.Modules.CopyTrading
|
|||||||
// Modul-eigener Trade-Log
|
// Modul-eigener Trade-Log
|
||||||
services.AddSingleton<ICopyTradeLogRepository, MongoCopyTradeLogRepository>();
|
services.AddSingleton<ICopyTradeLogRepository, MongoCopyTradeLogRepository>();
|
||||||
|
|
||||||
|
// Copytrading-Account-Detail-Einstellungen
|
||||||
|
services.AddSingleton<ICopyTradingAccountSettingsRepository, MongoCopyTradingAccountSettingsRepository>();
|
||||||
|
|
||||||
// Modul-Services (Signalquelle, Ausführung, Analytics)
|
// Modul-Services (Signalquelle, Ausführung, Analytics)
|
||||||
services.AddSingleton<TraderMonitorService>();
|
services.AddSingleton<TraderMonitorService>();
|
||||||
services.AddHostedService(sp => sp.GetRequiredService<TraderMonitorService>());
|
services.AddHostedService(sp => sp.GetRequiredService<TraderMonitorService>());
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ namespace PolyTraderSharp
|
|||||||
// Kopierte Master-Trader (TraderId -> TrackedTrader)
|
// Kopierte Master-Trader (TraderId -> TrackedTrader)
|
||||||
public ConcurrentDictionary<int, TrackedTrader> Traders { get; } = new();
|
public ConcurrentDictionary<int, TrackedTrader> Traders { get; } = new();
|
||||||
|
|
||||||
|
// Copytrading-Detail-Einstellungen je Account (AccountId -> Settings).
|
||||||
|
public ConcurrentDictionary<int, CopyTradingAccountSettings> AccountSettings { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liefert die Copytrading-Einstellungen für einen Account. Legt bei Bedarf einen
|
||||||
|
/// Default-Eintrag an, damit der Hot-Path nie null erhält.
|
||||||
|
/// </summary>
|
||||||
|
public CopyTradingAccountSettings GetAccountSettings(int accountId)
|
||||||
|
{
|
||||||
|
return AccountSettings.GetOrAdd(accountId, id => new CopyTradingAccountSettings { AccountId = id });
|
||||||
|
}
|
||||||
|
|
||||||
private int _totalCopyTrades = 0;
|
private int _totalCopyTrades = 0;
|
||||||
public int TotalCopyTrades
|
public int TotalCopyTrades
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace PolyTraderSharp.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Copytrading-spezifische Detail-Einstellungen je Account (Investment-/Zeit-Limits).
|
||||||
|
/// Bewusst getrennt vom Core-<see cref="AccountState"/> (allgemeine Account-Daten):
|
||||||
|
/// Diese Werte gehören dem Copytrading-Modul und werden im Modul-View bearbeitet.
|
||||||
|
/// Persistiert in der Collection "ct_account_settings" (BsonId = AccountId).
|
||||||
|
/// </summary>
|
||||||
|
public class CopyTradingAccountSettings
|
||||||
|
{
|
||||||
|
[Browsable(false)]
|
||||||
|
[MongoDB.Bson.Serialization.Attributes.BsonId]
|
||||||
|
public int AccountId { get; set; }
|
||||||
|
|
||||||
|
[Category("01. Risk Management")]
|
||||||
|
public decimal PerMarketLimit { get; set; } = 5.0m;
|
||||||
|
|
||||||
|
[Category("01. Risk Management")]
|
||||||
|
public decimal MaxPriceDifference { get; set; } = 2.0m;
|
||||||
|
|
||||||
|
[Category("01. Risk Management")]
|
||||||
|
public decimal MaxBuyPrice { get; set; } = 0.98m;
|
||||||
|
|
||||||
|
[Category("01. Risk Management")]
|
||||||
|
public decimal ProfitTarget { get; set; } = 50.0m;
|
||||||
|
|
||||||
|
[Category("01. Risk Management")]
|
||||||
|
public decimal PreRedeemLimit { get; set; } = 0.0m;
|
||||||
|
|
||||||
|
[Category("01. Risk Management")]
|
||||||
|
public decimal PerMasterLimit { get; set; } = 10.0m;
|
||||||
|
|
||||||
|
[Category("02. Time Limits")]
|
||||||
|
public decimal perMaxTime6h { get; set; } = 20.0m;
|
||||||
|
|
||||||
|
[Category("02. Time Limits")]
|
||||||
|
public decimal perMaxTime24h { get; set; } = 20.0m;
|
||||||
|
|
||||||
|
[Category("02. Time Limits")]
|
||||||
|
public decimal perMaxTime72h { get; set; } = 20.0m;
|
||||||
|
|
||||||
|
[Category("02. Time Limits")]
|
||||||
|
public decimal perMaxTimeNone { get; set; } = 40.0m;
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.CopyTrading.Persistence
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Persistenz der copytrading-spezifischen Account-Detail-Einstellungen
|
||||||
|
/// (Collection "ct_account_settings", Schlüssel = AccountId).
|
||||||
|
/// </summary>
|
||||||
|
public interface ICopyTradingAccountSettingsRepository
|
||||||
|
{
|
||||||
|
List<CopyTradingAccountSettings> GetAll();
|
||||||
|
CopyTradingAccountSettings? Get(int accountId);
|
||||||
|
void Upsert(CopyTradingAccountSettings settings);
|
||||||
|
void Delete(int accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.CopyTrading.Persistence
|
||||||
|
{
|
||||||
|
public class MongoCopyTradingAccountSettingsRepository : ICopyTradingAccountSettingsRepository
|
||||||
|
{
|
||||||
|
private readonly IMongoCollection<CopyTradingAccountSettings> _col;
|
||||||
|
|
||||||
|
public MongoCopyTradingAccountSettingsRepository(IMongoDatabase db)
|
||||||
|
{
|
||||||
|
_col = db.GetCollection<CopyTradingAccountSettings>("ct_account_settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CopyTradingAccountSettings> GetAll() => _col.Find(_ => true).ToList();
|
||||||
|
|
||||||
|
public CopyTradingAccountSettings? Get(int accountId) =>
|
||||||
|
_col.Find(x => x.AccountId == accountId).FirstOrDefault();
|
||||||
|
|
||||||
|
public void Upsert(CopyTradingAccountSettings settings) =>
|
||||||
|
_col.ReplaceOne(x => x.AccountId == settings.AccountId, settings, new ReplaceOptions { IsUpsert = true });
|
||||||
|
|
||||||
|
public void Delete(int accountId) => _col.DeleteOne(x => x.AccountId == accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -210,6 +210,9 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
private async Task ProcessAccountOrderAsync(AccountState account, TrackedTrader? trader, CopySignal signal, bool isNegRisk)
|
private async Task ProcessAccountOrderAsync(AccountState account, TrackedTrader? trader, CopySignal signal, bool isNegRisk)
|
||||||
{
|
{
|
||||||
|
// Copytrading-Detail-Einstellungen (Limits) dieses Accounts.
|
||||||
|
var settings = _copyState.GetAccountSettings(account.AccountId);
|
||||||
|
|
||||||
var mode = account.IsDemo ? _state.DemoTradingMode : _state.LiveTradingMode;
|
var mode = account.IsDemo ? _state.DemoTradingMode : _state.LiveTradingMode;
|
||||||
if (mode == TradingMode.Inactive)
|
if (mode == TradingMode.Inactive)
|
||||||
{
|
{
|
||||||
@@ -270,11 +273,11 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
if (signal.Side == "BUY")
|
if (signal.Side == "BUY")
|
||||||
{
|
{
|
||||||
if (signal.Price > account.MaxBuyPrice)
|
if (signal.Price > settings.MaxBuyPrice)
|
||||||
{
|
{
|
||||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen (Risk Limit):\n" +
|
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen (Risk Limit):\n" +
|
||||||
$" Konto: {account.Name}\n" +
|
$" Konto: {account.Name}\n" +
|
||||||
$" Begründung: Preis (${signal.Price:F3}) übersteigt das MaxBuy Limit (${account.MaxBuyPrice:F3})");
|
$" Begründung: Preis (${signal.Price:F3}) übersteigt das MaxBuy Limit (${settings.MaxBuyPrice:F3})");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +285,7 @@ namespace PolyTraderSharp.Services
|
|||||||
decimal investedInMarket = activePositions.FirstOrDefault(p => p.TokenId == signal.TokenId)?.AmountUsd ?? 0m;
|
decimal investedInMarket = activePositions.FirstOrDefault(p => p.TokenId == signal.TokenId)?.AmountUsd ?? 0m;
|
||||||
|
|
||||||
decimal minTrade = 1.0m;
|
decimal minTrade = 1.0m;
|
||||||
decimal maxAllowed = account.TotalBalance * (account.PerMarketLimit / 100.0m);
|
decimal maxAllowed = account.TotalBalance * (settings.PerMarketLimit / 100.0m);
|
||||||
|
|
||||||
// Low Balance Bypass (Stufen-System) ALWAYS APPLIES
|
// Low Balance Bypass (Stufen-System) ALWAYS APPLIES
|
||||||
if (account.TotalBalance < 150m) maxAllowed = Math.Min(1.20m, Math.Max(account.AvailableBalance, 0m));
|
if (account.TotalBalance < 150m) maxAllowed = Math.Min(1.20m, Math.Max(account.AvailableBalance, 0m));
|
||||||
@@ -298,9 +301,9 @@ namespace PolyTraderSharp.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
desiredLimitForSix = signal.Price * (1.0m + account.MaxPriceDifference / 100.0m);
|
desiredLimitForSix = signal.Price * (1.0m + settings.MaxPriceDifference / 100.0m);
|
||||||
}
|
}
|
||||||
decimal orderPriceForSix = Math.Min(desiredLimitForSix, account.MaxBuyPrice);
|
decimal orderPriceForSix = Math.Min(desiredLimitForSix, settings.MaxBuyPrice);
|
||||||
if (orderPriceForSix > 0.99m) orderPriceForSix = 0.99m;
|
if (orderPriceForSix > 0.99m) orderPriceForSix = 0.99m;
|
||||||
decimal costSix = 6m * orderPriceForSix;
|
decimal costSix = 6m * orderPriceForSix;
|
||||||
|
|
||||||
@@ -314,14 +317,14 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
decimal investedInMaster = trader != null ? activePositions.Where(p => p.SourceTraderId == trader.Id).Sum(p => (decimal)p.AmountUsd) : 0m;
|
decimal investedInMaster = trader != null ? activePositions.Where(p => p.SourceTraderId == trader.Id).Sum(p => (decimal)p.AmountUsd) : 0m;
|
||||||
|
|
||||||
decimal maxAllowedPerMaster = account.TotalBalance * (account.PerMasterLimit / 100.0m);
|
decimal maxAllowedPerMaster = account.TotalBalance * (settings.PerMasterLimit / 100.0m);
|
||||||
|
|
||||||
if (trader != null && (investedInMaster + maxAmountToBuy) > maxAllowedPerMaster)
|
if (trader != null && (investedInMaster + maxAmountToBuy) > maxAllowedPerMaster)
|
||||||
{
|
{
|
||||||
decimal pctInvested = account.TotalBalance > 0 ? (investedInMaster / account.TotalBalance) * 100m : 0m;
|
decimal pctInvested = account.TotalBalance > 0 ? (investedInMaster / account.TotalBalance) * 100m : 0m;
|
||||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
|
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
|
||||||
$" Konto: {account.Name}\n" +
|
$" Konto: {account.Name}\n" +
|
||||||
$" Begründung: PerMasterLimit ({account.PerMasterLimit:F1}%) erreicht. Bisher investiert in '{trader.DisplayName}': ${investedInMaster:F2} ({pctInvested:F1}%).");
|
$" Begründung: PerMasterLimit ({settings.PerMasterLimit:F1}%) erreicht. Bisher investiert in '{trader.DisplayName}': ${investedInMaster:F2} ({pctInvested:F1}%).");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,25 +338,25 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
if (hoursLeft < 6)
|
if (hoursLeft < 6)
|
||||||
{
|
{
|
||||||
applicableTimeLimitPct = account.perMaxTime6h;
|
applicableTimeLimitPct = settings.perMaxTime6h;
|
||||||
timeframeLabel = "< 6h";
|
timeframeLabel = "< 6h";
|
||||||
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 6).Sum(p => (decimal)p.AmountUsd);
|
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 6).Sum(p => (decimal)p.AmountUsd);
|
||||||
}
|
}
|
||||||
else if (hoursLeft < 24)
|
else if (hoursLeft < 24)
|
||||||
{
|
{
|
||||||
applicableTimeLimitPct = account.perMaxTime24h;
|
applicableTimeLimitPct = settings.perMaxTime24h;
|
||||||
timeframeLabel = "< 24h";
|
timeframeLabel = "< 24h";
|
||||||
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 6 && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 24).Sum(p => (decimal)p.AmountUsd);
|
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 6 && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 24).Sum(p => (decimal)p.AmountUsd);
|
||||||
}
|
}
|
||||||
else if (hoursLeft < 72)
|
else if (hoursLeft < 72)
|
||||||
{
|
{
|
||||||
applicableTimeLimitPct = account.perMaxTime72h;
|
applicableTimeLimitPct = settings.perMaxTime72h;
|
||||||
timeframeLabel = "< 72h";
|
timeframeLabel = "< 72h";
|
||||||
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 24 && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 72).Sum(p => (decimal)p.AmountUsd);
|
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 24 && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 72).Sum(p => (decimal)p.AmountUsd);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
applicableTimeLimitPct = account.perMaxTimeNone;
|
applicableTimeLimitPct = settings.perMaxTimeNone;
|
||||||
timeframeLabel = "> 72h";
|
timeframeLabel = "> 72h";
|
||||||
investedInTimeframe = openVals.Where(p => !p.ExpiryDate.HasValue || (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 72).Sum(p => (decimal)p.AmountUsd);
|
investedInTimeframe = openVals.Where(p => !p.ExpiryDate.HasValue || (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 72).Sum(p => (decimal)p.AmountUsd);
|
||||||
}
|
}
|
||||||
@@ -401,10 +404,10 @@ namespace PolyTraderSharp.Services
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Normaler Trader: prozentuales Limit aus Slave-Account Settings
|
// Normaler Trader: prozentuales Limit aus Slave-Account Settings
|
||||||
desiredLimit = signal.Price * (1.0m + account.MaxPriceDifference / 100.0m);
|
desiredLimit = signal.Price * (1.0m + settings.MaxPriceDifference / 100.0m);
|
||||||
}
|
}
|
||||||
|
|
||||||
orderPrice = Math.Min(desiredLimit, account.MaxBuyPrice);
|
orderPrice = Math.Min(desiredLimit, settings.MaxBuyPrice);
|
||||||
if (orderPrice > 0.99m) orderPrice = 0.99m;
|
if (orderPrice > 0.99m) orderPrice = 0.99m;
|
||||||
|
|
||||||
var exact = PolymarketClobClient.CalculateExactOrderAmounts(maxAmountToBuy, orderPrice, orderPrice, "BUY");
|
var exact = PolymarketClobClient.CalculateExactOrderAmounts(maxAmountToBuy, orderPrice, orderPrice, "BUY");
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
decimal expectedFillPrice = acc.PreRedeemLimit;
|
decimal expectedFillPrice = _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit;
|
||||||
decimal amountUsdc = Math.Max(pos.Size * expectedFillPrice, 0.01m);
|
decimal amountUsdc = Math.Max(pos.Size * expectedFillPrice, 0.01m);
|
||||||
var result = await _clob.PlaceOrderAsync(acc, pos.TokenId, "SELL", amountUsdc, expectedFillPrice, "GTC", false, false);
|
var result = await _clob.PlaceOrderAsync(acc, pos.TokenId, "SELL", amountUsdc, expectedFillPrice, "GTC", false, false);
|
||||||
|
|
||||||
@@ -703,7 +703,7 @@ namespace PolyTraderSharp.Services
|
|||||||
try { _positionRepo.UpsertLive(acc.AccountId, existing); } catch { }
|
try { _positionRepo.UpsertLive(acc.AccountId, existing); } catch { }
|
||||||
|
|
||||||
// Auto-Redeem Fallback via REST
|
// Auto-Redeem Fallback via REST
|
||||||
if (acc.PreRedeemLimit > 0 && curPrice >= acc.PreRedeemLimit && acc.IsActive)
|
if (_copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit > 0 && curPrice >= _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit && acc.IsActive)
|
||||||
{
|
{
|
||||||
string redeemKey = $"{acc.AccountId}_{asset}";
|
string redeemKey = $"{acc.AccountId}_{asset}";
|
||||||
bool allowAttempt = true;
|
bool allowAttempt = true;
|
||||||
@@ -718,7 +718,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
if (!acc.IsDemo && _state.LiveTradingMode == TradingMode.Active)
|
if (!acc.IsDemo && _state.LiveTradingMode == TradingMode.Active)
|
||||||
{
|
{
|
||||||
_logger.Trade($"🚨 [REST AUTO REDEEM] {acc.Name} | {existing.MarketQuestion} | Preis >= {acc.PreRedeemLimit}");
|
_logger.Trade($"🚨 [REST AUTO REDEEM] {acc.Name} | {existing.MarketQuestion} | Preis >= {_copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit}");
|
||||||
_ = Task.Run(async () => await ExecuteRestAutoRedeemLive(acc, existing));
|
_ = Task.Run(async () => await ExecuteRestAutoRedeemLive(acc, existing));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user