Sviluppo TradingBot
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Alpaca.Markets;
|
||||
using DesktopBot.Models;
|
||||
using DesktopBot.Services;
|
||||
|
||||
namespace DesktopBot.Engine
|
||||
{
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// POSITION RISK MANAGER
|
||||
// Valuta la situazione del portafoglio live PRIMA di aprire nuove posizioni
|
||||
// e gestisce le uscite in profitto / in perdita in modo proattivo.
|
||||
//
|
||||
// Regole applicate:
|
||||
// 1. MaxOpenPositions → blocca nuove aperture se già raggiunte
|
||||
// 2. MaxCapitalAllocated → blocca se troppo capitale è già impegnato
|
||||
// 3. ProfitLockPercent → chiude posizioni con PnL% >= soglia (lock profit)
|
||||
// 4. MaxLossPercent → chiude posizioni con PnL% <= -soglia (stop loss dinamico)
|
||||
// 5. UseBreakEvenStop → quando PnL% >= ProfitLockPercent/2, annota il
|
||||
// break-even e lo riporta al chiamante per aggiornare
|
||||
// l'ordine stop (o come informazione di log)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
internal sealed class PositionRiskManager
|
||||
{
|
||||
private readonly ITradingService _svc;
|
||||
private readonly BotConfiguration _cfg;
|
||||
private readonly Action<string> _logInfo;
|
||||
private readonly Action<string> _logWarn;
|
||||
|
||||
public PositionRiskManager(
|
||||
ITradingService service,
|
||||
BotConfiguration config,
|
||||
Action<string> logInfo,
|
||||
Action<string> logWarn)
|
||||
{
|
||||
_svc = service;
|
||||
_cfg = config;
|
||||
_logInfo = logInfo;
|
||||
_logWarn = logWarn;
|
||||
}
|
||||
|
||||
// ── Verifica se è possibile aprire un nuovo ordine ──────────────────
|
||||
/// <returns>true = ok aprire, false = bloccato dal risk manager</returns>
|
||||
public async Task<bool> CanOpenNewPositionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Conta solo le posizioni aperte dal bot (ClientOrderId BOT_)
|
||||
var positions = await _svc.GetBotPositionsAsync();
|
||||
int openCount = positions?.Count ?? 0;
|
||||
|
||||
// 1. Limite numero posizioni globale
|
||||
if (openCount >= _cfg.MaxOpenPositions)
|
||||
{
|
||||
_logWarn($"[RISK] Apertura bloccata: {openCount} posizioni aperte su {_cfg.MaxOpenPositions} consentite.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Limite posizioni per asset
|
||||
int assetCount = positions?.Count(p => string.Equals(
|
||||
p.Symbol, _cfg.Symbol, StringComparison.OrdinalIgnoreCase)) ?? 0;
|
||||
if (assetCount >= _cfg.MaxOpenPositionsPerAsset)
|
||||
{
|
||||
_logWarn($"[RISK] Apertura bloccata: {assetCount} posizioni aperte su {_cfg.Symbol} (limite per asset: {_cfg.MaxOpenPositionsPerAsset}).");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Limite capitale allocato
|
||||
if (positions != null && positions.Count > 0)
|
||||
{
|
||||
decimal equity = await _svc.GetAvailableEquityAsync();
|
||||
decimal totalEquity = equity + positions.Sum(p => Math.Abs(p.MarketValue ?? 0));
|
||||
decimal allocated = positions.Sum(p => Math.Abs(p.MarketValue ?? 0));
|
||||
decimal allocPct = totalEquity > 0 ? allocated / totalEquity : 0;
|
||||
|
||||
if (allocPct >= _cfg.MaxCapitalAllocatedPercent)
|
||||
{
|
||||
_logWarn($"[RISK] Apertura bloccata: capitale allocato {allocPct:P1} >= limite {_cfg.MaxCapitalAllocatedPercent:P1}.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logWarn($"[RISK] Errore controllo posizioni: {ex.Message} — apertura permessa per sicurezza.");
|
||||
return true; // fail-open: non bloccare per errori di rete
|
||||
}
|
||||
}
|
||||
|
||||
// ── Calcola la dimensione ottimale per la nuova posizione ────────────
|
||||
/// <summary>
|
||||
/// Ritorna la quantità da comprare in base al capitale disponibile e
|
||||
/// alla quota assegnabile a questa specifica posizione.
|
||||
/// </summary>
|
||||
public async Task<decimal> ComputeQuantityAsync(decimal price, bool isCrypto)
|
||||
{
|
||||
if (price <= 0) return 0;
|
||||
|
||||
decimal equity = await _svc.GetAvailableEquityAsync();
|
||||
|
||||
// Considera solo le posizioni aperte dal bot per stimare il capitale già allocato
|
||||
IReadOnlyList<IPosition> positions;
|
||||
try { positions = await _svc.GetBotPositionsAsync(); }
|
||||
catch { positions = new List<IPosition>(); }
|
||||
|
||||
decimal allocated = positions?.Sum(p => Math.Abs(p.MarketValue ?? 0)) ?? 0m;
|
||||
decimal totalEquity = equity + allocated;
|
||||
|
||||
// Budget massimo assoluto per questa posizione
|
||||
decimal maxBudget = totalEquity * _cfg.MaxPositionSizePercent;
|
||||
|
||||
// Non superare la quota di capitale libero disponibile
|
||||
decimal usableBudget = Math.Min(maxBudget, equity * 0.95m); // riserva 5% liquidità
|
||||
|
||||
decimal rawQty = usableBudget / price;
|
||||
|
||||
decimal qty = isCrypto
|
||||
? Math.Round(rawQty, 4, MidpointRounding.ToEven)
|
||||
: Math.Floor(rawQty);
|
||||
|
||||
if (qty <= 0)
|
||||
_logWarn($"[RISK] Quantità calcolata = 0 (budget usabile: {usableBudget:F2}, prezzo: {price:F2}).");
|
||||
|
||||
return qty;
|
||||
}
|
||||
|
||||
// ── Gestione proattiva delle posizioni aperte (profit lock / stop) ───
|
||||
/// <summary>
|
||||
/// Valuta ogni posizione aperta per il simbolo del bot. Se una posizione
|
||||
/// supera la soglia di profitto o di perdita, la chiude emettendo log adeguati.
|
||||
/// Chiamare a ogni ciclo PRIMA della valutazione del segnale.
|
||||
/// </summary>
|
||||
public async Task ManageOpenPositionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var position = await _svc.GetPositionAsync(_cfg.Symbol);
|
||||
if (position == null) return;
|
||||
|
||||
decimal entryPrice = position.AverageEntryPrice;
|
||||
decimal currentPrice = position.AssetCurrentPrice ?? entryPrice;
|
||||
decimal qty = position.Quantity;
|
||||
decimal marketValue = position.MarketValue ?? (currentPrice * qty);
|
||||
decimal costBasis = position.CostBasis != 0 ? position.CostBasis : entryPrice * qty;
|
||||
|
||||
if (costBasis == 0) return;
|
||||
|
||||
decimal unrealizedPnl = marketValue - costBasis;
|
||||
decimal pnlPct = unrealizedPnl / costBasis;
|
||||
|
||||
_logInfo($"[RISK] Posizione {_cfg.Symbol}: ingresso={entryPrice:F2} corrente={currentPrice:F2} PnL={pnlPct:P2} ({unrealizedPnl:+F2;-F2})");
|
||||
|
||||
// ── Profit Lock: chiudi se in profitto sopra soglia ──────────
|
||||
if (_cfg.ProfitLockPercent > 0 && pnlPct >= _cfg.ProfitLockPercent)
|
||||
{
|
||||
_logInfo($"[RISK] PROFIT LOCK: PnL {pnlPct:P2} >= {_cfg.ProfitLockPercent:P2}. Chiudo posizione.");
|
||||
await _svc.ClosePositionAsync(_cfg.Symbol);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Break-even alert (solo log, SL fisico già piazzato) ──────
|
||||
if (_cfg.UseBreakEvenStop && _cfg.ProfitLockPercent > 0)
|
||||
{
|
||||
decimal breakEvenTrigger = _cfg.ProfitLockPercent / 2m;
|
||||
if (pnlPct >= breakEvenTrigger)
|
||||
_logInfo($"[RISK] Break-even zone raggiunta (PnL {pnlPct:P2}). Lo stop-loss dovrebbe essere al prezzo di ingresso ({entryPrice:F2}).");
|
||||
}
|
||||
|
||||
// ── Stop Loss dinamico: chiudi se in perdita oltre soglia ────
|
||||
if (_cfg.MaxLossPercent > 0 && pnlPct <= -_cfg.MaxLossPercent)
|
||||
{
|
||||
_logWarn($"[RISK] STOP LOSS DINAMICO: PnL {pnlPct:P2} <= -{_cfg.MaxLossPercent:P2}. Chiudo posizione per limitare le perdite.");
|
||||
await _svc.ClosePositionAsync(_cfg.Symbol);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// La posizione potrebbe non esistere (già chiusa automaticamente dall'ordine SL/TP)
|
||||
if (!ex.Message.Contains("position does not exist", StringComparison.OrdinalIgnoreCase) &&
|
||||
!ex.Message.Contains("404", StringComparison.OrdinalIgnoreCase))
|
||||
_logWarn($"[RISK] Errore gestione posizione: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Report sintetico della situazione del portafoglio ────────────────
|
||||
public async Task LogPortfolioStatusAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var positions = await _svc.GetAllPositionsAsync();
|
||||
if (positions == null || positions.Count == 0)
|
||||
{
|
||||
_logInfo("[RISK] Nessuna posizione aperta.");
|
||||
return;
|
||||
}
|
||||
|
||||
decimal totalUnrealized = positions.Sum(p => p.UnrealizedProfitLoss.GetValueOrDefault());
|
||||
_logInfo($"[RISK] Portafoglio: {positions.Count} posizioni aperte | PnL totale non realizzato: {totalUnrealized:+F2;-F2}");
|
||||
|
||||
foreach (var p in positions)
|
||||
{
|
||||
decimal cb = p.CostBasis != 0 ? p.CostBasis : 1m;
|
||||
decimal pnlPct = p.UnrealizedProfitLoss.GetValueOrDefault() / cb;
|
||||
_logInfo($" [{p.Symbol}] qty={p.Quantity} entry={p.AverageEntryPrice:F2} mktVal={p.MarketValue:F2} PnL={pnlPct:P2}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logWarn($"[RISK] Errore lettura portafoglio: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user