using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Alpaca.Markets;
using DesktopBot.Models;
using DesktopBot.Services;
namespace DesktopBot.Engine
{
///
/// Motore di trading automatizzato multi-strategia.
/// Strategie supportate:
/// 1. EMA Crossover - Golden/Death cross su EMA veloce vs lenta
/// 2. RSI - Ipervenduto/Ipercomprato con Wilder RSI
/// 3. MACD - Crossover MACD line / Signal line
/// 4. Volatility Breakout - Keltner Channel + filtro RVOL (Cap.3 report)
/// 5. Kalman Mean Reversion- Fair-value con filtro di Kalman (Cap.4 report)
///
public class AutomatedBotEngine
{
private readonly ITradingService _tradingService;
/// Lock per serializzare le operazioni di apertura/chiusura ordini.
private readonly SemaphoreSlim _orderExecutionLock = new SemaphoreSlim(1, 1);
/// Asset class per il controllo orario mercato (settato dal viewmodel).
public string AssetClass { get; set; } = "us_equity";
// ?? eventi esposti al viewmodel ???????????????????????????????????????
public event EventHandler LogGenerated;
public event EventHandler SignalGenerated;
public event EventHandler EquityUpdated;
public event EventHandler WaitingForMarketChanged;
// ?? stato runtime ?????????????????????????????????????????????????????
private CancellationTokenSource _cts;
private Task _loopTask;
private bool _isRunning;
private BotConfiguration _config;
private KalmanFilterState _kalman;
private BtcUsdAlgorithm _btcAlgo;
private PositionRiskManager _riskManager;
public bool IsRunning => _isRunning;
public AutomatedBotEngine(ITradingService tradingService)
{
_tradingService = tradingService ?? throw new ArgumentNullException(nameof(tradingService));
}
// ?? avvio / stop ??????????????????????????????????????????????????????
public Task StartAsync(BotConfiguration config)
{
if (_isRunning) return Task.CompletedTask;
_config = config ?? throw new ArgumentNullException(nameof(config));
_kalman = new KalmanFilterState(_config.KalmanDelta, _config.KalmanObservationVariance);
_btcAlgo = new BtcUsdAlgorithm();
_riskManager = new PositionRiskManager(_tradingService, _config, Info, Warn);
_cts = new CancellationTokenSource();
_isRunning = true;
_loopTask = Task.Run(() => BotMainLoopAsync(_cts.Token));
Info($"Bot avviato � strategia: {_config.Strategy}");
return Task.CompletedTask;
}
public void Stop()
{
if (!_isRunning) return;
_cts?.Cancel();
_isRunning = false;
Info("Bot fermato.");
}
// ?? loop principale ???????????????????????????????????????????????????
private async Task BotMainLoopAsync(CancellationToken ct)
{
bool waitingState = false;
bool authErrorDetected = false; // Flag per tracciare errori di autorizzazione
while (!ct.IsCancellationRequested)
{
try
{
bool marketOpen = MarketHoursService.IsMarketOpen(AssetClass);
if (!marketOpen)
{
if (!waitingState) { waitingState = true; WaitingForMarketChanged?.Invoke(this, true); }
await Task.Delay(TimeSpan.FromSeconds(30), ct);
continue;
}
if (waitingState) { waitingState = false; WaitingForMarketChanged?.Invoke(this, false); }
await AnalyzeMarketAsync(ct);
authErrorDetected = false; // Reset il flag se una chiamata ha successo
}
catch (OperationCanceledException) { break; }
catch (Exception ex) when (ex.Message.Contains("401") || ex.Message.Contains("Unauthorized") || ex.Message.Contains("non autorizzato"))
{
if (!authErrorDetected)
{
Warn("⚠ Errore di autorizzazione rilevato. Il bot si fermerà automaticamente. Verifica API Key e Secret.");
authErrorDetected = true;
_isRunning = false;
break; // Ferma il loop
}
}
catch (Exception ex) { Warn($"Errore nel loop: {ex.Message}"); }
await Task.Delay(TimeSpan.FromSeconds(_config.CheckIntervalSeconds), ct);
}
}
// ?? dispatch analisi ??????????????????????????????????????????????????
private async Task AnalyzeMarketAsync(CancellationToken ct)
{
// Determina il timeframe e il numero di barre da richiedere
var timeFrame = _config.AnalysisTimeFrame;
int barCount;
if (_config.HistoricalBarCount > 0)
{
// Usa il valore configurato se specificato
barCount = _config.HistoricalBarCount;
}
else
{
// Calcolo automatico in base al timeframe e alla strategia
barCount = timeFrame == BarTimeFrame.Minute
? Math.Max(200, _config.SlowEmaPeriod * 10) // Per 1min: almeno 200 barre
: Math.Max(60, _config.SlowEmaPeriod * 3); // Per day/hour: almeno 60 barre
}
List bars = await FetchBarsWithRetryAsync(_config.Symbol, timeFrame, barCount, ct);
if (bars == null) return; // retry esauriti, ciclo saltato
try { decimal eq = await _tradingService.GetAvailableEquityAsync(); EquityUpdated?.Invoke(this, eq); }
catch { /* non bloccante */ }
TradingSignal signal;
// BTC/USD: usa l'algoritmo avanzato dedicato
if (_config.Symbol.StartsWith("BTC", StringComparison.OrdinalIgnoreCase))
{
signal = AnalyzeBtcUsd(bars);
}
else
{
switch (_config.Strategy)
{
case TradingStrategy.EMA_CROSSOVER: signal = AnalyzeEmaCrossover(bars); break;
case TradingStrategy.RSI: signal = AnalyzeRsi(bars); break;
case TradingStrategy.MACD: signal = AnalyzeMacd(bars); break;
case TradingStrategy.VOLATILITY_BREAKOUT: signal = AnalyzeVolatilityBreakout(bars); break;
case TradingStrategy.KALMAN_MEAN_REVERSION: signal = AnalyzeKalmanMeanReversion(bars); break;
default: return;
}
}
// ── Gestione proattiva delle posizioni aperte (profit lock / stop loss dinamico) ──
await _riskManager.ManageOpenPositionsAsync();
if (signal == null || signal.Type == SignalType.None || signal.Type == SignalType.Hold) return;
SignalGenerated?.Invoke(this, signal);
Info($"SIGNAL {signal.Type}: {signal.Reason} (confidenza: {signal.Confidence}%)");
// Acquisisci il lock per serializzare le operazioni di ordine
await _orderExecutionLock.WaitAsync();
try
{
bool hasPos = await _tradingService.HasOpenPositionAsync(_config.Symbol);
if (signal.Type == SignalType.Buy && !hasPos)
{
// Verifica risk prima di aprire
bool canOpen = await _riskManager.CanOpenNewPositionAsync();
if (canOpen)
await ExecuteOrderAsync(signal);
}
if (signal.Type == SignalType.Sell && hasPos)
{
Info($"[SIGNAL] Segnale SELL: chiusura posizione {_config.Symbol}.");
await _tradingService.ClosePositionAsync(_config.Symbol);
}
}
finally
{
_orderExecutionLock.Release();
}
}
// -- Fetch con retry lineare -----------------------------------------
private async System.Threading.Tasks.Task> FetchBarsWithRetryAsync(
string symbol, Alpaca.Markets.BarTimeFrame timeFrame, int barCount, System.Threading.CancellationToken ct)
{
const int MaxRetries = 10;
const int RetryStepSec = 10;
for (int attempt = 1; attempt <= MaxRetries; attempt++)
{
if (ct.IsCancellationRequested) return null;
var bars = await _tradingService.GetHistoricalBarsAsync(symbol, timeFrame, barCount);
if (bars != null && bars.Count >= 30)
return bars;
int waitSec = attempt * RetryStepSec;
Warn(string.Format("Dati storici insufficienti ({0} barre). Tentativo {1}/{2} tra {3}s...", bars?.Count ?? 0, attempt, MaxRetries, waitSec));
try { await System.Threading.Tasks.Task.Delay(System.TimeSpan.FromSeconds(waitSec), ct); }
catch (System.OperationCanceledException) { return null; }
}
Warn("Dati storici non disponibili dopo tutti i tentativi. Ciclo saltato.");
return null;
}
// ?? ALGORITMO BTC/USD (dispatcher verso BtcUsdAlgorithm) ??????????????
private TradingSignal AnalyzeBtcUsd(List bars)
{
var result = _btcAlgo.Analyze(bars);
Info($"[BTC] {result.ToLogString()}");
if (result.Type == SignalType.Buy)
return MakeSignal(SignalType.Buy, result.Price, result.Reason,
result.Confidence, result.StopLoss, result.TakeProfit);
if (result.Type == SignalType.Sell)
return MakeSignal(SignalType.Sell, result.Price, result.Reason, result.Confidence);
return Neutral();
}
// ?? STRATEGIA 1: EMA CROSSOVER ?????????????????????????????????????????
// BUY : EMA veloce incrocia al rialzo EMA lenta (golden cross)
// SELL: EMA veloce incrocia al ribasso EMA lenta (death cross)
private TradingSignal AnalyzeEmaCrossover(List bars)
{
var closes = bars.Select(b => b.Close).ToList();
var fastEma = CalculateEma(closes, _config.FastEmaPeriod);
var slowEma = CalculateEma(closes, _config.SlowEmaPeriod);
if (fastEma.Count < 2 || slowEma.Count < 2) return Neutral();
decimal fPrev = fastEma[fastEma.Count - 2], sPrev = slowEma[slowEma.Count - 2];
decimal fCurr = fastEma[fastEma.Count - 1], sCurr = slowEma[slowEma.Count - 1];
decimal price = closes.Last(), atr = CalculateAtr(bars, 14);
if (fPrev <= sPrev && fCurr > sCurr)
return MakeSignal(SignalType.Buy, price, $"Golden cross EMA{_config.FastEmaPeriod}/EMA{_config.SlowEmaPeriod}", 70,
atr > 0 ? price - atr * 1.5m : price * (1 - _config.StopLossPercentage),
atr > 0 ? price + atr * 2.5m : price * (1 + _config.TakeProfitPercentage));
if (fPrev >= sPrev && fCurr < sCurr)
return MakeSignal(SignalType.Sell, price, $"Death cross EMA{_config.FastEmaPeriod}/EMA{_config.SlowEmaPeriod}", 65);
return Neutral();
}
// ?? STRATEGIA 2: RSI ??????????????????????????????????????????????????
// BUY : RSI < soglia oversold
// SELL: RSI > soglia overbought
private TradingSignal AnalyzeRsi(List bars)
{
var closes = bars.Select(b => b.Close).ToList();
decimal rsi = CalculateRsi(closes, _config.RsiPeriod);
decimal price = closes.Last(), atr = CalculateAtr(bars, 14);
if (rsi <= _config.RsiOversoldThreshold)
return MakeSignal(SignalType.Buy, price, $"RSI oversold ({rsi:F1})", 65,
atr > 0 ? price - atr * 1.5m : price * (1 - _config.StopLossPercentage),
atr > 0 ? price + atr * 2.0m : price * (1 + _config.TakeProfitPercentage));
if (rsi >= _config.RsiOverboughtThreshold)
return MakeSignal(SignalType.Sell, price, $"RSI overbought ({rsi:F1})", 65);
return Neutral();
}
// ?? STRATEGIA 3: MACD ?????????????????????????????????????????????????
// BUY : MACD line incrocia al rialzo la signal line
// SELL: MACD line incrocia al ribasso la signal line
private TradingSignal AnalyzeMacd(List bars)
{
var closes = bars.Select(b => b.Close).ToList();
var emaFast = CalculateEma(closes, _config.MacdFastPeriod);
var emaSlow = CalculateEma(closes, _config.MacdSlowPeriod);
int minLen = Math.Min(emaFast.Count, emaSlow.Count);
if (minLen < _config.MacdSignalPeriod + 2) return Neutral();
var macdLine = new List();
for (int i = 0; i < minLen; i++)
macdLine.Add(emaFast[emaFast.Count - minLen + i] - emaSlow[emaSlow.Count - minLen + i]);
var sigLine = CalculateEma(macdLine, _config.MacdSignalPeriod);
if (sigLine.Count < 2) return Neutral();
decimal mPrev = macdLine[macdLine.Count - 2], mCurr = macdLine[macdLine.Count - 1];
decimal sPrev = sigLine[sigLine.Count - 2], sCurr = sigLine[sigLine.Count - 1];
decimal price = closes.Last(), atr = CalculateAtr(bars, 14);
if (mPrev <= sPrev && mCurr > sCurr)
return MakeSignal(SignalType.Buy, price, "MACD crossover bullish", 72,
atr > 0 ? price - atr * 1.5m : price * (1 - _config.StopLossPercentage),
atr > 0 ? price + atr * 2.5m : price * (1 + _config.TakeProfitPercentage));
if (mPrev >= sPrev && mCurr < sCurr)
return MakeSignal(SignalType.Sell, price, "MACD crossover bearish", 68);
return Neutral();
}
// ?? STRATEGIA 4: VOLATILITY BREAKOUT (Keltner + RVOL) ????????????????
// Capitolo 3 del report: breakout banda Keltner + conferma volume relativo
// BUY : close > upper band AND RVOL >= RvolMinThreshold
// SELL: close < lower band
// SL = entry - AtrStopMultiplier*ATR | TP = entry + 2*AtrStopMultiplier*ATR
private TradingSignal AnalyzeVolatilityBreakout(List bars)
{
int period = _config.KeltnerPeriod;
if (bars.Count < period + 5) return Neutral();
var recent = bars.Skip(bars.Count - period - 5).ToList();
var closes = recent.Select(b => b.Close).ToList();
var vols = recent.Select(b => b.Volume).ToList();
decimal atr = CalculateAtr(recent, period);
decimal midEma = CalculateEma(closes, period).Last();
decimal upper = midEma + _config.KeltnerMultiplier * atr;
decimal lower = midEma - _config.KeltnerMultiplier * atr;
decimal price = closes.Last();
decimal avgVol = vols.Take(vols.Count - 1).Average();
decimal rvol = avgVol > 0 ? vols.Last() / (decimal)avgVol : 0m;
Info($"[VBKOUT] Close={price:F2} Upper={upper:F2} Lower={lower:F2} RVOL={rvol:F2}");
if (price > upper && rvol >= _config.RvolMinThreshold)
{
decimal d = _config.AtrStopMultiplier * atr;
return MakeSignal(SignalType.Buy, price, $"Keltner breakout UP - RVOL={rvol:F2}x", 78, price - d, price + d * 2m);
}
if (price < lower)
return MakeSignal(SignalType.Sell, price, "Keltner breakout DOWN", 70);
return Neutral();
}
// ?? STRATEGIA 5: KALMAN MEAN REVERSION ???????????????????????????????
// Capitolo 4 del report: filtro di Kalman per stima del fair value.
// BUY : Z-Score <= -entryThreshold (prezzo molto sotto fair value)
// SELL: Z-Score >= +entryThreshold (prezzo molto sopra fair value)
// EXIT: |Z-Score| <= exitThreshold (convergenza alla media)
private TradingSignal AnalyzeKalmanMeanReversion(List bars)
{
if (bars.Count < 30) return Neutral();
foreach (var bar in bars) _kalman.Update(bar.Close);
decimal price = bars.Last().Close, fair = (decimal)_kalman.StateEstimate;
double z = _kalman.SpreadStdDev > 0 ? (double)(price - fair) / _kalman.SpreadStdDev : 0.0;
Info($"[KALMAN] Price={price:F2} Fair={fair:F2} Z={z:F3}");
decimal atr = CalculateAtr(bars, 14);
if (z <= -_config.KalmanEntryZScore)
return MakeSignal(SignalType.Buy, price, $"Kalman BUY (z={z:F2})", Math.Min(95, (int)(Math.Abs(z) * 30)),
atr > 0 ? price - atr * 2m : price * (1 - _config.StopLossPercentage), fair);
if (z >= _config.KalmanEntryZScore)
return MakeSignal(SignalType.Sell, price, $"Kalman SELL (z={z:F2})", Math.Min(95, (int)(Math.Abs(z) * 30)));
if (Math.Abs(z) <= _config.KalmanExitZScore)
return MakeSignal(SignalType.Sell, price, $"Kalman EXIT convergenza (z={z:F2})", 80);
return Neutral();
}
// ?? esecuzione ordine bracket ?????????????????????????????????????????
private async Task ExecuteOrderAsync(TradingSignal signal)
{
try
{
// ── Verifica FINALE prima di piazzare ──
// Riconta le posizioni in tempo reale da Alpaca
IReadOnlyList positions = null;
try
{
positions = await _tradingService.GetBotPositionsAsync();
}
catch (Exception exPos)
{
Warn($"[VERIFY] Errore nel recupero posizioni: {exPos.Message}. Procedo comunque con caution.");
// Continua lo stesso, ma con più cautela
}
int openCount = positions?.Count ?? 0;
bool hasPosition = positions?.Any(p => string.Equals(p.Symbol, signal.Symbol, StringComparison.OrdinalIgnoreCase)) ?? false;
if (signal.Type == SignalType.Buy && hasPosition)
{
Warn($"[VERIFY] Posizione già aperta su {signal.Symbol}; ordine non piazzato.");
return;
}
// Verifica che non abbia superato il limite globale
if (signal.Type == SignalType.Buy && openCount >= _config.MaxOpenPositions)
{
Warn($"[VERIFY] Limite posizioni raggiunto ({openCount}/{_config.MaxOpenPositions}); ordine non piazzato.");
return;
}
// Verifica per-asset
int assetCount = positions?.Count(p => string.Equals(
p.Symbol, signal.Symbol, StringComparison.OrdinalIgnoreCase)) ?? 0;
if (signal.Type == SignalType.Buy && assetCount >= _config.MaxOpenPositionsPerAsset)
{
Warn($"[VERIFY] Limite posizioni per asset raggiunto ({assetCount}/{_config.MaxOpenPositionsPerAsset}); ordine non piazzato.");
return;
}
decimal equity = 0;
try
{
equity = await _tradingService.GetAvailableEquityAsync();
}
catch (Exception exEq)
{
Warn($"[VERIFY] Errore nel recupero equity: {exEq.Message}. Procedo con cautela.");
}
bool isCryptoOrder2 = !string.IsNullOrEmpty(AssetClass) &&
AssetClass.IndexOf("crypto", StringComparison.OrdinalIgnoreCase) >= 0;
decimal qty;
if (signal.Price > 0)
{
qty = await _riskManager.ComputeQuantityAsync(signal.Price, isCryptoOrder2);
}
else
{
qty = _config.Quantity;
}
if (qty <= 0)
{
Warn($"[VERIFY] Capitale insufficiente (equity: ${equity:F2}) o limite risk raggiunto. Ordine non piazzato.");
return;
}
// Log stato prima di piazzare
Info($"[PRE-ORDER] Posizioni aperte: {openCount}/{_config.MaxOpenPositions} | Equity: ${equity:F2} | Qty da piazzare: {qty}");
bool isCryptoOrder = isCryptoOrder2;
decimal sl = signal.StopLoss > 0 ? signal.StopLoss : signal.Price * (1 - _config.StopLossPercentage);
decimal tp = signal.TakeProfit > 0 ? signal.TakeProfit : signal.Price * (1 + _config.TakeProfitPercentage);
sl = Math.Max(sl, signal.Price * 0.80m);
tp = Math.Min(tp, signal.Price * 1.50m);
if (isCryptoOrder)
{
// Alpaca crypto non supporta bracket/OTOCO: piazza market + stop/limit separati
var entry = await _tradingService.PlaceMarketOrderAsync(signal.Symbol, qty, signal.Side);
Info($"✓ Market {signal.Side} {qty}x {signal.Symbol} @ ~${signal.Price:F2} | #{entry?.OrderId}");
// Stop-loss separato (lato opposto)
var stopSide = signal.Side == OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy;
try
{
// arrotonda stop price a 2 decimali per passare la validazione Alpaca
decimal slRounded = Math.Round(sl, 2);
decimal tpRounded = Math.Round(tp, 2);
// Garantisce che SL < prezzo corrente per Buy (stop sell)
if (signal.Side == OrderSide.Buy && slRounded >= signal.Price)
slRounded = Math.Round(signal.Price * 0.99m, 2);
if (signal.Side == OrderSide.Sell && slRounded <= signal.Price)
slRounded = Math.Round(signal.Price * 1.01m, 2);
var slOrder = await _tradingService.PlaceStopOrderAsync(signal.Symbol, qty, slRounded, stopSide);
Info($"✓ Stop-loss {stopSide} @ ${slRounded:F2} | #{slOrder?.OrderId}");
var tpOrder = await _tradingService.PlaceLimitOrderAsync(signal.Symbol, qty, tpRounded, stopSide);
Info($"✓ Take-profit {stopSide} @ ${tpRounded:F2} | #{tpOrder?.OrderId}");
}
catch (Exception exProt) { Warn($"SL/TP separati non piazzati: {exProt.Message}"); }
}
else
{
var order = await _tradingService.PlaceBracketOrderAsync(
signal.Symbol, qty, signal.Price, tp, sl, signal.Side);
Info($"✓ Bracket {signal.Side} {qty}x {signal.Symbol} @ ${signal.Price:F2} | SL=${sl:F2} TP=${tp:F2} | #{order?.OrderId}");
}
}
catch (Exception ex)
{
if (ex.Message.Contains("401") || ex.Message.Contains("Unauthorized") || ex.Message.Contains("non autorizzato"))
Warn($"⚠ Errore di autorizzazione Alpaca: {ex.Message}. Verifica API Key e Secret.");
else
Warn($"Errore esecuzione ordine: {ex.Message}");
}
}
// ?? indicatori tecnici ????????????????????????????????????????????????
private static List CalculateEma(List v, int period)
{
var r = new List();
if (v.Count < period) return r;
decimal k = 2m / (period + 1), cur = v.Take(period).Average();
r.Add(cur);
for (int i = period; i < v.Count; i++) { cur = v[i] * k + cur * (1 - k); r.Add(cur); }
return r;
}
private static decimal CalculateRsi(List c, int period)
{
if (c.Count < period + 1) return 50m;
decimal ag = 0, al = 0;
for (int i = 1; i <= period; i++) { decimal d = c[i] - c[i - 1]; if (d > 0) ag += d; else al -= d; }
ag /= period; al /= period;
for (int i = period + 1; i < c.Count; i++)
{
decimal d = c[i] - c[i - 1], g = d > 0 ? d : 0, l = d < 0 ? -d : 0;
ag = (ag * (period - 1) + g) / period;
al = (al * (period - 1) + l) / period;
}
if (al == 0) return 100m;
return 100m - (100m / (1 + ag / al));
}
private static decimal CalculateAtr(List bars, int period)
{
if (bars.Count < period + 1) return 0m;
decimal atr = 0;
for (int i = 1; i <= period; i++)
{
decimal hl = bars[i].High - bars[i].Low;
decimal hpc = Math.Abs(bars[i].High - bars[i - 1].Close);
decimal lpc = Math.Abs(bars[i].Low - bars[i - 1].Close);
atr += Math.Max(hl, Math.Max(hpc, lpc));
}
atr /= period;
for (int i = period + 1; i < bars.Count; i++)
{
decimal hl = bars[i].High - bars[i].Low;
decimal hpc = Math.Abs(bars[i].High - bars[i - 1].Close);
decimal lpc = Math.Abs(bars[i].Low - bars[i - 1].Close);
decimal tr = Math.Max(hl, Math.Max(hpc, lpc));
atr = (atr * (period - 1) + tr) / period;
}
return atr;
}
// ?????????????????????????????????????????????????????????????????????
private TradingSignal MakeSignal(SignalType t, decimal price, string reason, int conf,
decimal sl = 0, decimal tp = 0)
=> new TradingSignal
{
Type = t, Symbol = _config.Symbol, Price = price,
Timestamp = DateTime.UtcNow, Reason = reason, Confidence = conf,
StopLoss = sl, TakeProfit = tp
};
private static TradingSignal Neutral() => new TradingSignal { Type = SignalType.None };
private void Info(string msg) => LogGenerated?.Invoke(this, new BotLogEntry(LogLevel.Info, msg));
private void Warn(string msg) => LogGenerated?.Invoke(this, new BotLogEntry(LogLevel.Warning, msg));
}
// ???????????????????????????????????????????????????????????????????????????
// FILTRO DI KALMAN � stato persistente single-asset
// Capitolo 4 del report: stima del fair value a guadagno variabile.
// Predict: P_pred = P + Q (Q = delta/(1-delta))
// Update : K = P_pred/(P_pred+R)
// x = x + K*(z-x) P = (1-K)*P_pred
// ???????????????????????????????????????????????????????????????????????????
internal sealed class KalmanFilterState
{
private readonly double _delta, _r;
private double _x, _p;
private readonly Queue _spreads = new Queue();
private const int SpreadWindow = 20;
public double StateEstimate => _x;
public double SpreadStdDev { get; private set; }
public KalmanFilterState(double delta, double observationVariance)
{ _delta = delta; _r = observationVariance; _x = 0; _p = 1; }
public void Update(decimal observedPrice)
{
double z = (double)observedPrice;
if (_x == 0) { _x = z; _p = 1; return; }
double q = _delta / (1 - _delta), pPred = _p + q, k = pPred / (pPred + _r);
_x = _x + k * (z - _x); _p = (1 - k) * pPred;
_spreads.Enqueue(z - _x);
if (_spreads.Count > SpreadWindow) _spreads.Dequeue();
SpreadStdDev = Std(_spreads);
}
private static double Std(IEnumerable v)
{
var l = v.ToList(); if (l.Count < 2) return 1.0;
double m = l.Average();
return Math.Sqrt(l.Sum(x => (x - m) * (x - m)) / (l.Count - 1));
}
}
}