From e3c0bd51b2e474a7732b440d8767987dfcd61452 Mon Sep 17 00:00:00 2001 From: Alberto Balbo Date: Tue, 9 Jun 2026 18:29:41 +0200 Subject: [PATCH] Sviluppo TradingBot --- DesktopBot/App.config | 26 + DesktopBot/App.xaml | 24 + DesktopBot/App.xaml.cs | 17 + DesktopBot/Controls/PriceLineChart.cs | 221 ++ DesktopBot/Converters/Converters.cs | 177 + DesktopBot/Converters/MaxItemsConverter.cs | 36 + DesktopBot/DesktopBot.csproj | 340 ++ ... Strategie Trading Algoritmico Avanzato.md | 806 +++++ .../Alpaca/245-trading-for-trading-api.md | 188 ++ .../5f36ea05-b1b4-5fd1-91b5-ef51db361a4b.md | 15 + DesktopBot/Documents/Alpaca/about-alpaca.md | 25 + .../Documents/Alpaca/about-connect-api.md | 58 + .../Documents/Alpaca/about-market-data-api.md | 117 + .../Documents/Alpaca/account-activities.md | 117 + DesktopBot/Documents/Alpaca/account-plans.md | 545 +++ .../Documents/Alpaca/additional-resources.md | 29 + .../Documents/Alpaca/alpaca-api-platform.md | 52 + .../Alpaca/alpaca-elite-smart-router.md | 448 +++ .../Documents/Alpaca/alpaca-mcp-server.md | 154 + DesktopBot/Documents/Alpaca/authentication.md | 81 + DesktopBot/Documents/Alpaca/crypto-fees.md | 45 + DesktopBot/Documents/Alpaca/crypto-orders.md | 25 + .../Documents/Alpaca/crypto-pricing-data.md | 61 + DesktopBot/Documents/Alpaca/crypto-trading.md | 190 ++ DesktopBot/Documents/Alpaca/fix-messages.md | 2920 +++++++++++++++++ .../Documents/Alpaca/fractional-trading.md | 84 + ...getting-started-with-alpaca-market-data.md | 179 + .../getting-started-with-trading-api.md | 27 + .../Documents/Alpaca/getting-started.md | 18 + DesktopBot/Documents/Alpaca/historical-api.md | 24 + .../Alpaca/historical-crypto-data-1.md | 9 + .../Documents/Alpaca/historical-news-data.md | 15 + .../Alpaca/historical-option-data.md | 16 + .../Alpaca/historical-stock-data-1.md | 71 + .../Alpaca/margin-and-short-selling.md | 86 + .../Documents/Alpaca/market-data-faq.md | 1483 +++++++++ .../non-trade-activities-for-option-events.md | 86 + .../Alpaca/options-level-3-trading.md | 432 +++ DesktopBot/Documents/Alpaca/options-orders.md | 64 + .../Documents/Alpaca/options-trading.md | 227 ++ .../Documents/Alpaca/orders-at-alpaca.md | 1628 +++++++++ DesktopBot/Documents/Alpaca/paper-trading.md | 78 + ...osition-average-entry-price-calculation.md | 139 + .../Alpaca/real-time-crypto-pricing-data.md | 279 ++ .../Documents/Alpaca/real-time-option-data.md | 102 + .../Alpaca/real-time-stock-pricing-data.md | 453 +++ .../Documents/Alpaca/registering-your-app.md | 37 + .../Documents/Alpaca/regulatory-fees.md | 35 + DesktopBot/Documents/Alpaca/sdks-and-tools.md | 34 + .../Documents/Alpaca/streaming-market-data.md | 378 +++ .../Alpaca/streaming-real-time-news.md | 67 + .../Alpaca/the-intraday-margin-rule.md | 63 + DesktopBot/Documents/Alpaca/trading-api.md | 29 + ...intraday-margin-rule-and-the-end-of-pdt.md | 82 + ...erstanding-the-new-intraday-margin-rule.md | 31 + .../Documents/Alpaca/user-protection.md | 220 ++ .../Alpaca/using-oauth2-and-trading-api.md | 155 + .../Documents/Alpaca/websocket-streaming.md | 369 +++ .../Documents/Alpaca/working-with-account.md | 199 ++ .../Documents/Alpaca/working-with-assets.md | 191 ++ .../Documents/Alpaca/working-with-orders.md | 998 ++++++ .../Alpaca/working-with-positions.md | 117 + .../Code/gemini-code-1779811123559.cs | 19 + .../Code/gemini-code-1779811125057.cs | 8 + .../Code/gemini-code-1779811126091.cs | 13 + .../Code/gemini-code-1779811127546.cs | 13 + .../Code/gemini-code-1779811128687.cs | 13 + DesktopBot/Engine/AutomatedBotEngine.cs | 611 ++++ DesktopBot/Engine/BtcUsdAlgorithm.cs | 459 +++ DesktopBot/Engine/PositionRiskManager.cs | 218 ++ DesktopBot/Engine/StrategyAdvisor.cs | 248 ++ DesktopBot/Files/Icon/favicon.ico | Bin 0 -> 162081 bytes DesktopBot/MainWindow.xaml | 12 + DesktopBot/MainWindow.xaml.cs | 28 + DesktopBot/Models/AssetSearchResult.cs | 19 + DesktopBot/Models/BotConfiguration.cs | 236 ++ DesktopBot/Models/BotInstance.cs | 74 + DesktopBot/Models/BotInstanceStore.cs | 57 + DesktopBot/Models/BotLogEntry.cs | 34 + DesktopBot/Models/BotTradeRecord.cs | 47 + DesktopBot/Models/LoggingConfiguration.cs | 56 + DesktopBot/Models/TradingSignal.cs | 40 + DesktopBot/Properties/AssemblyInfo.cs | 52 + DesktopBot/Properties/Resources.Designer.cs | 71 + DesktopBot/Properties/Resources.resx | 117 + DesktopBot/Properties/Settings.Designer.cs | 30 + DesktopBot/Properties/Settings.settings | 7 + DesktopBot/Services/AlpacaPingService.cs | 101 + DesktopBot/Services/AlpacaTradingService.cs | 541 +++ DesktopBot/Services/ApiCallCounterService.cs | 209 ++ DesktopBot/Services/CredentialService.cs | 99 + DesktopBot/Services/ITradingService.cs | 119 + DesktopBot/Services/MarketHoursService.cs | 113 + DesktopBot/Themes/DarkTheme.xaml | 414 +++ DesktopBot/ViewModels/AccountViewModels.cs | 260 ++ DesktopBot/ViewModels/BaseViewModel.cs | 82 + DesktopBot/ViewModels/BotConfigViewModel.cs | 85 + DesktopBot/ViewModels/BotInstanceViewModel.cs | 548 ++++ DesktopBot/ViewModels/BotsManagerViewModel.cs | 173 + DesktopBot/ViewModels/DashboardViewModel.cs | 379 +++ DesktopBot/ViewModels/LiveLogViewModel.cs | 58 + .../ViewModels/LoggingSettingsViewModel.cs | 67 + DesktopBot/ViewModels/MainViewModel.cs | 184 ++ DesktopBot/ViewModels/PingViewModel.cs | 84 + DesktopBot/ViewModels/PriceChartViewModel.cs | 285 ++ DesktopBot/ViewModels/SettingsViewModel.cs | 223 ++ DesktopBot/ViewModels/WalletViewModel.cs | 146 + DesktopBot/Views/BalanceView.xaml | 115 + DesktopBot/Views/BalanceView.xaml.cs | 8 + DesktopBot/Views/BotConfigView.xaml | 245 ++ DesktopBot/Views/BotConfigView.xaml.cs | 12 + DesktopBot/Views/BotsManagerView.xaml | 477 +++ DesktopBot/Views/BotsManagerView.xaml.cs | 12 + DesktopBot/Views/DashboardView.xaml | 438 +++ DesktopBot/Views/DashboardView.xaml.cs | 12 + DesktopBot/Views/LiveLogView.xaml | 144 + DesktopBot/Views/LiveLogView.xaml.cs | 12 + DesktopBot/Views/MainView.xaml | 275 ++ DesktopBot/Views/MainView.xaml.cs | 17 + DesktopBot/Views/OrdersView.xaml | 199 ++ DesktopBot/Views/OrdersView.xaml.cs | 8 + DesktopBot/Views/PositionsView.xaml | 182 + DesktopBot/Views/PositionsView.xaml.cs | 8 + DesktopBot/Views/PriceChartView.xaml | 184 ++ DesktopBot/Views/PriceChartView.xaml.cs | 15 + DesktopBot/Views/SettingsView.xaml | 426 +++ DesktopBot/Views/SettingsView.xaml.cs | 35 + DesktopBot/Views/WalletView.xaml | 176 + DesktopBot/Views/WalletView.xaml.cs | 12 + DesktopBot/favicon.ico | Bin 0 -> 162081 bytes DesktopBot/packages.config | 16 + .../Services/AlpacaTradingService.cs | 0 TradingBot/TradingBot.slnx | 2 +- 133 files changed, 24903 insertions(+), 1 deletion(-) create mode 100644 DesktopBot/App.config create mode 100644 DesktopBot/App.xaml create mode 100644 DesktopBot/App.xaml.cs create mode 100644 DesktopBot/Controls/PriceLineChart.cs create mode 100644 DesktopBot/Converters/Converters.cs create mode 100644 DesktopBot/Converters/MaxItemsConverter.cs create mode 100644 DesktopBot/DesktopBot.csproj create mode 100644 DesktopBot/Documents/Algorithm/Report Strategie Trading Algoritmico Avanzato.md create mode 100644 DesktopBot/Documents/Alpaca/245-trading-for-trading-api.md create mode 100644 DesktopBot/Documents/Alpaca/5f36ea05-b1b4-5fd1-91b5-ef51db361a4b.md create mode 100644 DesktopBot/Documents/Alpaca/about-alpaca.md create mode 100644 DesktopBot/Documents/Alpaca/about-connect-api.md create mode 100644 DesktopBot/Documents/Alpaca/about-market-data-api.md create mode 100644 DesktopBot/Documents/Alpaca/account-activities.md create mode 100644 DesktopBot/Documents/Alpaca/account-plans.md create mode 100644 DesktopBot/Documents/Alpaca/additional-resources.md create mode 100644 DesktopBot/Documents/Alpaca/alpaca-api-platform.md create mode 100644 DesktopBot/Documents/Alpaca/alpaca-elite-smart-router.md create mode 100644 DesktopBot/Documents/Alpaca/alpaca-mcp-server.md create mode 100644 DesktopBot/Documents/Alpaca/authentication.md create mode 100644 DesktopBot/Documents/Alpaca/crypto-fees.md create mode 100644 DesktopBot/Documents/Alpaca/crypto-orders.md create mode 100644 DesktopBot/Documents/Alpaca/crypto-pricing-data.md create mode 100644 DesktopBot/Documents/Alpaca/crypto-trading.md create mode 100644 DesktopBot/Documents/Alpaca/fix-messages.md create mode 100644 DesktopBot/Documents/Alpaca/fractional-trading.md create mode 100644 DesktopBot/Documents/Alpaca/getting-started-with-alpaca-market-data.md create mode 100644 DesktopBot/Documents/Alpaca/getting-started-with-trading-api.md create mode 100644 DesktopBot/Documents/Alpaca/getting-started.md create mode 100644 DesktopBot/Documents/Alpaca/historical-api.md create mode 100644 DesktopBot/Documents/Alpaca/historical-crypto-data-1.md create mode 100644 DesktopBot/Documents/Alpaca/historical-news-data.md create mode 100644 DesktopBot/Documents/Alpaca/historical-option-data.md create mode 100644 DesktopBot/Documents/Alpaca/historical-stock-data-1.md create mode 100644 DesktopBot/Documents/Alpaca/margin-and-short-selling.md create mode 100644 DesktopBot/Documents/Alpaca/market-data-faq.md create mode 100644 DesktopBot/Documents/Alpaca/non-trade-activities-for-option-events.md create mode 100644 DesktopBot/Documents/Alpaca/options-level-3-trading.md create mode 100644 DesktopBot/Documents/Alpaca/options-orders.md create mode 100644 DesktopBot/Documents/Alpaca/options-trading.md create mode 100644 DesktopBot/Documents/Alpaca/orders-at-alpaca.md create mode 100644 DesktopBot/Documents/Alpaca/paper-trading.md create mode 100644 DesktopBot/Documents/Alpaca/position-average-entry-price-calculation.md create mode 100644 DesktopBot/Documents/Alpaca/real-time-crypto-pricing-data.md create mode 100644 DesktopBot/Documents/Alpaca/real-time-option-data.md create mode 100644 DesktopBot/Documents/Alpaca/real-time-stock-pricing-data.md create mode 100644 DesktopBot/Documents/Alpaca/registering-your-app.md create mode 100644 DesktopBot/Documents/Alpaca/regulatory-fees.md create mode 100644 DesktopBot/Documents/Alpaca/sdks-and-tools.md create mode 100644 DesktopBot/Documents/Alpaca/streaming-market-data.md create mode 100644 DesktopBot/Documents/Alpaca/streaming-real-time-news.md create mode 100644 DesktopBot/Documents/Alpaca/the-intraday-margin-rule.md create mode 100644 DesktopBot/Documents/Alpaca/trading-api.md create mode 100644 DesktopBot/Documents/Alpaca/understanding-finras-new-intraday-margin-rule-and-the-end-of-pdt.md create mode 100644 DesktopBot/Documents/Alpaca/understanding-the-new-intraday-margin-rule.md create mode 100644 DesktopBot/Documents/Alpaca/user-protection.md create mode 100644 DesktopBot/Documents/Alpaca/using-oauth2-and-trading-api.md create mode 100644 DesktopBot/Documents/Alpaca/websocket-streaming.md create mode 100644 DesktopBot/Documents/Alpaca/working-with-account.md create mode 100644 DesktopBot/Documents/Alpaca/working-with-assets.md create mode 100644 DesktopBot/Documents/Alpaca/working-with-orders.md create mode 100644 DesktopBot/Documents/Alpaca/working-with-positions.md create mode 100644 DesktopBot/Documents/Code/gemini-code-1779811123559.cs create mode 100644 DesktopBot/Documents/Code/gemini-code-1779811125057.cs create mode 100644 DesktopBot/Documents/Code/gemini-code-1779811126091.cs create mode 100644 DesktopBot/Documents/Code/gemini-code-1779811127546.cs create mode 100644 DesktopBot/Documents/Code/gemini-code-1779811128687.cs create mode 100644 DesktopBot/Engine/AutomatedBotEngine.cs create mode 100644 DesktopBot/Engine/BtcUsdAlgorithm.cs create mode 100644 DesktopBot/Engine/PositionRiskManager.cs create mode 100644 DesktopBot/Engine/StrategyAdvisor.cs create mode 100644 DesktopBot/Files/Icon/favicon.ico create mode 100644 DesktopBot/MainWindow.xaml create mode 100644 DesktopBot/MainWindow.xaml.cs create mode 100644 DesktopBot/Models/AssetSearchResult.cs create mode 100644 DesktopBot/Models/BotConfiguration.cs create mode 100644 DesktopBot/Models/BotInstance.cs create mode 100644 DesktopBot/Models/BotInstanceStore.cs create mode 100644 DesktopBot/Models/BotLogEntry.cs create mode 100644 DesktopBot/Models/BotTradeRecord.cs create mode 100644 DesktopBot/Models/LoggingConfiguration.cs create mode 100644 DesktopBot/Models/TradingSignal.cs create mode 100644 DesktopBot/Properties/AssemblyInfo.cs create mode 100644 DesktopBot/Properties/Resources.Designer.cs create mode 100644 DesktopBot/Properties/Resources.resx create mode 100644 DesktopBot/Properties/Settings.Designer.cs create mode 100644 DesktopBot/Properties/Settings.settings create mode 100644 DesktopBot/Services/AlpacaPingService.cs create mode 100644 DesktopBot/Services/AlpacaTradingService.cs create mode 100644 DesktopBot/Services/ApiCallCounterService.cs create mode 100644 DesktopBot/Services/CredentialService.cs create mode 100644 DesktopBot/Services/ITradingService.cs create mode 100644 DesktopBot/Services/MarketHoursService.cs create mode 100644 DesktopBot/Themes/DarkTheme.xaml create mode 100644 DesktopBot/ViewModels/AccountViewModels.cs create mode 100644 DesktopBot/ViewModels/BaseViewModel.cs create mode 100644 DesktopBot/ViewModels/BotConfigViewModel.cs create mode 100644 DesktopBot/ViewModels/BotInstanceViewModel.cs create mode 100644 DesktopBot/ViewModels/BotsManagerViewModel.cs create mode 100644 DesktopBot/ViewModels/DashboardViewModel.cs create mode 100644 DesktopBot/ViewModels/LiveLogViewModel.cs create mode 100644 DesktopBot/ViewModels/LoggingSettingsViewModel.cs create mode 100644 DesktopBot/ViewModels/MainViewModel.cs create mode 100644 DesktopBot/ViewModels/PingViewModel.cs create mode 100644 DesktopBot/ViewModels/PriceChartViewModel.cs create mode 100644 DesktopBot/ViewModels/SettingsViewModel.cs create mode 100644 DesktopBot/ViewModels/WalletViewModel.cs create mode 100644 DesktopBot/Views/BalanceView.xaml create mode 100644 DesktopBot/Views/BalanceView.xaml.cs create mode 100644 DesktopBot/Views/BotConfigView.xaml create mode 100644 DesktopBot/Views/BotConfigView.xaml.cs create mode 100644 DesktopBot/Views/BotsManagerView.xaml create mode 100644 DesktopBot/Views/BotsManagerView.xaml.cs create mode 100644 DesktopBot/Views/DashboardView.xaml create mode 100644 DesktopBot/Views/DashboardView.xaml.cs create mode 100644 DesktopBot/Views/LiveLogView.xaml create mode 100644 DesktopBot/Views/LiveLogView.xaml.cs create mode 100644 DesktopBot/Views/MainView.xaml create mode 100644 DesktopBot/Views/MainView.xaml.cs create mode 100644 DesktopBot/Views/OrdersView.xaml create mode 100644 DesktopBot/Views/OrdersView.xaml.cs create mode 100644 DesktopBot/Views/PositionsView.xaml create mode 100644 DesktopBot/Views/PositionsView.xaml.cs create mode 100644 DesktopBot/Views/PriceChartView.xaml create mode 100644 DesktopBot/Views/PriceChartView.xaml.cs create mode 100644 DesktopBot/Views/SettingsView.xaml create mode 100644 DesktopBot/Views/SettingsView.xaml.cs create mode 100644 DesktopBot/Views/WalletView.xaml create mode 100644 DesktopBot/Views/WalletView.xaml.cs create mode 100644 DesktopBot/favicon.ico create mode 100644 DesktopBot/packages.config create mode 100644 TradingBot/DesktopBot/Services/AlpacaTradingService.cs diff --git a/DesktopBot/App.config b/DesktopBot/App.config new file mode 100644 index 0000000..7670f9e --- /dev/null +++ b/DesktopBot/App.config @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DesktopBot/App.xaml b/DesktopBot/App.xaml new file mode 100644 index 0000000..3889940 --- /dev/null +++ b/DesktopBot/App.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/DesktopBot/App.xaml.cs b/DesktopBot/App.xaml.cs new file mode 100644 index 0000000..aee6368 --- /dev/null +++ b/DesktopBot/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace DesktopBot +{ + /// + /// Logica di interazione per App.xaml + /// + public partial class App : Application + { + } +} diff --git a/DesktopBot/Controls/PriceLineChart.cs b/DesktopBot/Controls/PriceLineChart.cs new file mode 100644 index 0000000..793fabe --- /dev/null +++ b/DesktopBot/Controls/PriceLineChart.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; +using System.Windows; +using System.Windows.Media; +using DesktopBot.ViewModels; + +namespace DesktopBot.Controls +{ + /// + /// Line chart WPF nativo (nessuna dipendenza esterna). + /// Legge una + /// e ridisegna automaticamente ad ogni variazione. + /// + public class PriceLineChart : FrameworkElement + { + // ── Dependency Properties ──────────────────────────────────────────── + + public static readonly DependencyProperty PriceDataProperty = + DependencyProperty.Register( + nameof(PriceData), + typeof(IEnumerable), + typeof(PriceLineChart), + new FrameworkPropertyMetadata(null, + FrameworkPropertyMetadataOptions.AffectsRender, + OnPriceDataChanged)); + + public IEnumerable PriceData + { + get => (IEnumerable)GetValue(PriceDataProperty); + set => SetValue(PriceDataProperty, value); + } + + public static readonly DependencyProperty LineColorProperty = + DependencyProperty.Register( + nameof(LineColor), typeof(Color), typeof(PriceLineChart), + new FrameworkPropertyMetadata(Colors.LimeGreen, + FrameworkPropertyMetadataOptions.AffectsRender)); + + public Color LineColor + { + get => (Color)GetValue(LineColorProperty); + set => SetValue(LineColorProperty, value); + } + + // ── Osserva notifiche della collection ────────────────────────────── + + private static void OnPriceDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var chart = (PriceLineChart)d; + + if (e.OldValue is INotifyCollectionChanged oldCol) + oldCol.CollectionChanged -= chart.OnCollectionChanged; + + if (e.NewValue is INotifyCollectionChanged newCol) + newCol.CollectionChanged += chart.OnCollectionChanged; + + chart.InvalidateVisual(); + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + => InvalidateVisual(); + + // ── Rendering ─────────────────────────────────────────────────────── + + protected override void OnRender(DrawingContext dc) + { + double w = ActualWidth; + double h = ActualHeight; + if (w < 10 || h < 10) return; + + // Sfondo + dc.DrawRectangle(new SolidColorBrush(Color.FromRgb(0x0E, 0x0E, 0x14)), + null, new Rect(0, 0, w, h)); + + var points = PriceData?.OfType().ToList(); + if (points == null || points.Count < 2) + { + DrawNoData(dc, w, h); + return; + } + + const double padLeft = 72; + const double padRight = 76; // spazio per la label del prezzo corrente + const double padTop = 16; + const double padBottom = 32; + + double chartW = w - padLeft - padRight; + double chartH = h - padTop - padBottom; + + decimal minP = points.Min(p => p.Price); + decimal maxP = points.Max(p => p.Price); + decimal range = maxP - minP; + if (range == 0) range = maxP * 0.01m; + + // Griglia orizzontale (5 linee) + var gridPen = new Pen(new SolidColorBrush(Color.FromArgb(40, 255, 255, 255)), 1); + gridPen.Freeze(); + for (int i = 0; i <= 4; i++) + { + double y = padTop + chartH * i / 4.0; + dc.DrawLine(gridPen, + new Point(padLeft, y), + new Point(padLeft + chartW, y)); + + decimal price = maxP - range * (decimal)(i / 4.0); + DrawLabel(dc, price.ToString("N0"), padLeft - 6, y, + Color.FromRgb(0x88, 0x88, 0x99), 9, TextAlignment.Right); + } + + // Curva prezzi con fill gradiente + var geometry = new StreamGeometry(); + using (var ctx = geometry.Open()) + { + for (int i = 0; i < points.Count; i++) + { + double x = padLeft + chartW * i / (double)(points.Count - 1); + double y = padTop + chartH * (double)((maxP - points[i].Price) / range); + if (i == 0) ctx.BeginFigure(new Point(x, y), isFilled: false, isClosed: false); + else ctx.LineTo(new Point(x, y), isStroked: true, isSmoothJoin: true); + } + } + geometry.Freeze(); + + // Fill sotto la curva + var fillGeometry = new StreamGeometry(); + using (var ctx = fillGeometry.Open()) + { + double x0 = padLeft; + double xN = padLeft + chartW; + double yBase = padTop + chartH; + + ctx.BeginFigure(new Point(x0, yBase), isFilled: true, isClosed: true); + for (int i = 0; i < points.Count; i++) + { + double x = padLeft + chartW * i / (double)(points.Count - 1); + double y = padTop + chartH * (double)((maxP - points[i].Price) / range); + ctx.LineTo(new Point(x, y), isStroked: false, isSmoothJoin: true); + } + ctx.LineTo(new Point(xN, yBase), isStroked: false, isSmoothJoin: false); + } + fillGeometry.Freeze(); + + var fillBrush = new LinearGradientBrush( + Color.FromArgb(80, LineColor.R, LineColor.G, LineColor.B), + Color.FromArgb(0, LineColor.R, LineColor.G, LineColor.B), + new Point(0, 0), new Point(0, 1)); + fillBrush.Freeze(); + dc.DrawGeometry(fillBrush, null, fillGeometry); + + var linePen = new Pen(new SolidColorBrush(LineColor), 1.8); + linePen.Freeze(); + dc.DrawGeometry(null, linePen, geometry); + + // Dot sul prezzo corrente (ultimo punto) + var last = points.Last(); + double lx = padLeft + chartW; + double ly = padTop + chartH * (double)((maxP - last.Price) / range); + var dotBrush = new SolidColorBrush(LineColor); + dotBrush.Freeze(); + dc.DrawEllipse(dotBrush, null, new Point(lx, ly), 4, 4); + + // Label prezzo corrente: disegnata a destra del dot, dentro padRight + // Sfondo scuro per leggibilità sul gradiente + var priceText = last.Price.ToString("N2"); + var priceTf = new FormattedText( + priceText, + System.Globalization.CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface("Consolas"), + 10, + new SolidColorBrush(LineColor), + VisualTreeHelper.GetDpi(new DrawingVisual()).PixelsPerDip); + double labelX = lx + 6; + double labelY = ly - priceTf.Height / 2; + // Sfondo pill + var bgRect = new Rect(labelX - 2, labelY - 1, priceTf.Width + 4, priceTf.Height + 2); + dc.DrawRectangle(new SolidColorBrush(Color.FromRgb(0x0E, 0x0E, 0x14)), null, bgRect); + dc.DrawText(priceTf, new Point(labelX, labelY)); + + // Etichette asse X (prime e ultime) + if (points.Count >= 2) + { + DrawLabel(dc, points.First().Timestamp.ToString("HH:mm"), + padLeft, padTop + chartH + 6, Color.FromRgb(0x66, 0x66, 0x77), 9, TextAlignment.Left); + DrawLabel(dc, points.Last().Timestamp.ToString("HH:mm"), + padLeft + chartW, padTop + chartH + 6, Color.FromRgb(0x66, 0x66, 0x77), 9, TextAlignment.Right); + } + } + + private static void DrawNoData(DrawingContext dc, double w, double h) + { + var tf = new FormattedText( + "In attesa dei dati...", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface("Segoe UI"), + 13, + new SolidColorBrush(Color.FromRgb(0x55, 0x55, 0x66)), + VisualTreeHelper.GetDpi(new DrawingVisual()).PixelsPerDip); + dc.DrawText(tf, new Point((w - tf.Width) / 2, (h - tf.Height) / 2)); + } + + private static void DrawLabel(DrawingContext dc, string text, + double x, double y, Color color, double size, TextAlignment align) + { + var tf = new FormattedText( + text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface("Consolas"), + size, + new SolidColorBrush(color), + VisualTreeHelper.GetDpi(new DrawingVisual()).PixelsPerDip); + tf.TextAlignment = align; + dc.DrawText(tf, new Point(x, y - tf.Height / 2)); + } + } +} diff --git a/DesktopBot/Converters/Converters.cs b/DesktopBot/Converters/Converters.cs new file mode 100644 index 0000000..93790c5 --- /dev/null +++ b/DesktopBot/Converters/Converters.cs @@ -0,0 +1,177 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; + +namespace DesktopBot.Converters +{ + /// + /// Converter che restituisce lo stile NavItemActive se il tab corrisponde, + /// altrimenti NavItem (usato nella barra di navigazione laterale) + /// + public class NavStyleConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + // Non restituiamo lo stile direttamente (non è possibile in WPF in questo modo) + // Restituiamo true se il tab selezionato corrisponde al parametro + return value?.ToString() == parameter?.ToString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + /// + /// Converter per i colori dei log in base al livello + /// + public class LogLevelColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Models.LogLevel level) + { + if (level == Models.LogLevel.Success) return "#FF00E676"; + if (level == Models.LogLevel.Error) return "#FFFF1744"; + if (level == Models.LogLevel.Warning) return "#FFFFC107"; + } + + return "#FF8888A0"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + /// Inverte un valore booleano (usato per il radio button Live/Paper) + public class InverseBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) return !b; + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) return !b; + return value; + } + } + + /// bool → Visibility (true=Visible, false=Collapsed) + public class BoolVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is bool b && b ? Visibility.Visible : Visibility.Collapsed; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + /// bool → Visibility invertito (true=Collapsed, false=Visible) + public class InvertedBoolVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is bool b && b ? Visibility.Collapsed : Visibility.Visible; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + /// null → Visible, not-null → Collapsed (placeholder "nessun bot selezionato") + public class NullToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value == null ? Visibility.Visible : Visibility.Collapsed; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + /// not-null → Visible, null → Collapsed (mostra pannello dettaglio) + public class NotNullToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value != null ? Visibility.Visible : Visibility.Collapsed; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + /// stringa vuota/null → Visible (placeholder search), non-vuota → Collapsed + public class EmptyStringToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => string.IsNullOrEmpty(value as string) ? Visibility.Visible : Visibility.Collapsed; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + /// int 0 → Collapsed, >0 → Visible (usato per nascondere la lista risultati vuota) + public class ZeroToCollapsedConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is int i) return i > 0 ? Visibility.Visible : Visibility.Collapsed; + return Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + /// bool IsProfit → verde o rosso per P&L + public class ProfitColorConverter : IValueConverter + { + private static readonly SolidColorBrush Green = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#00E676")); + private static readonly SolidColorBrush Red = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF1744")); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is bool b && b ? Green : (object)Red; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + /// + /// TradingStrategy → Visibility. + /// ConverterParameter = nome strategia (es. "EMA_CROSSOVER"). + /// Visible se la strategia attiva corrisponde al parametro, Collapsed altrimenti. + /// Usato nei pannelli parametri dinamici per-strategia di BotsManagerView. + /// + public class StrategyToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null || parameter == null) return Visibility.Collapsed; + return value.ToString() == parameter.ToString() ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } + + /// + /// Converter per il colore dell'indicatore di streaming. + /// True (streaming attivo) → Verde, False (fermo) → Grigio + /// + public class BoolToStreamColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isStreaming && isStreaming) + return Colors.LimeGreen; // #00E676 + return Colors.Gray; // #808080 + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } +} diff --git a/DesktopBot/Converters/MaxItemsConverter.cs b/DesktopBot/Converters/MaxItemsConverter.cs new file mode 100644 index 0000000..18bddfb --- /dev/null +++ b/DesktopBot/Converters/MaxItemsConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Windows.Data; + +namespace DesktopBot.Converters +{ + /// + /// Converte una IEnumerable in una collezione limitata ai primi N elementi. + /// Parametro: numero massimo di elementi da visualizzare (default 50). + /// + public class MaxItemsConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (!(value is IEnumerable enumerable)) + return value; + + // Estrai il parametro per il limite + int maxItems = 50; + if (parameter != null && int.TryParse(parameter.ToString(), out int parsedMax)) + maxItems = parsedMax; + + // Converti a lista e prendi gli ultimi N elementi (ordine LIFO) + var list = enumerable.Cast().Take(maxItems).ToList(); + return new ObservableCollection(list); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/DesktopBot/DesktopBot.csproj b/DesktopBot/DesktopBot.csproj new file mode 100644 index 0000000..f30afdb --- /dev/null +++ b/DesktopBot/DesktopBot.csproj @@ -0,0 +1,340 @@ + + + + + Debug + AnyCPU + {4ED6728E-EAC8-4745-A106-476DCBDFB710} + WinExe + DesktopBot + DesktopBot + v4.8.1 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + latest + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + favicon.ico + + + + + + + + + + + + + 4.0 + + + + + + packages\Newtonsoft.Json.13.0.3\lib\netstandard2.0\Newtonsoft.Json.dll + + + packages\Polly.8.5.0\lib\net462\Polly.dll + + + packages\Polly.Core.8.5.0\lib\net462\Polly.Core.dll + + + packages\Portable.System.DateTimeOnly.9.0.0\lib\net462\Portable.System.DateTimeOnly.dll + + + packages\System.IO.Pipelines.9.0.0\lib\net462\System.IO.Pipelines.dll + + + packages\System.Net.Http.WinHttpHandler.9.0.0\lib\net462\System.Net.Http.WinHttpHandler.dll + + + packages\System.Threading.Channels.9.0.0\lib\net462\System.Threading.Channels.dll + + + packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll + + + packages\Alpaca.Markets.7.2.0\lib\net462\Alpaca.Markets.dll + + + packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + + packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + True + + + packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + True + + + packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + True + + + packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + True + + + packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + True + + + packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll + + + + + MSBuild:Compile + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + + + + + + + + + + + + MainWindow.xaml + Code + + + MSBuild:Compile + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + + + + + + + + + + + + + + + + + + + + BotsManagerView.xaml + + + MainView.xaml + + + DashboardView.xaml + + + BotConfigView.xaml + + + LiveLogView.xaml + + + PriceChartView.xaml + + + SettingsView.xaml + + + WalletView.xaml + + + + BalanceView.xaml + + + PositionsView.xaml + + + OrdersView.xaml + + + + + + + + + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + + + + + + xcopy /Y /D "$(ProjectDir)packages\System.Memory.4.5.5\lib\net461\System.Memory.dll" "$(TargetDir)" +xcopy /Y /D "$(ProjectDir)packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll" "$(TargetDir)" +xcopy /Y /D "$(ProjectDir)packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll" "$(TargetDir)" +xcopy /Y /D "$(ProjectDir)packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll" "$(TargetDir)" + + \ No newline at end of file diff --git a/DesktopBot/Documents/Algorithm/Report Strategie Trading Algoritmico Avanzato.md b/DesktopBot/Documents/Algorithm/Report Strategie Trading Algoritmico Avanzato.md new file mode 100644 index 0000000..7ec11e5 --- /dev/null +++ b/DesktopBot/Documents/Algorithm/Report Strategie Trading Algoritmico Avanzato.md @@ -0,0 +1,806 @@ +# **Specifiche Tecniche e Blueprint per lo Sviluppo di un Bot di Trading Algoritmico Proprietario** + +## **1\. Introduzione e identificazione del "bordo" (edge) statistico** + +Nei mercati finanziari contemporanei, caratterizzati da frammentazione strutturale della liquidità e dominanza di algoritmi ad alta frequenza (HFT), la generazione sistematica di Alpha richiede l'identificazione di un vantaggio ("edge") statistico ed esecutivo quantificabile.1 Un vantaggio competitivo sostenibile non si basa su indicatori tecnici ritardati, ma sullo sfruttamento delle inefficienze microstrutturali, della latenza esecutiva, dell'asimmetria informativa e degli squilibri temporanei tra acquirenti e venditori aggressivi.1 +L'estrazione del valore avviene principalmente attraverso due canali: + +1. **Gestione della microstruttura del mercato:** Sfruttamento della dinamica del Limit Order Book (LOB) a livello L2/L3, monitorando la velocità di sbilanciamento del flusso d'ordine (Order Flow Imbalance) e la tossicità del flusso (Flow Toxicity) in tempo reale per anticipare i movimenti dei prezzi a breve termine.1 +2. **Sfruttamento di relazioni di equilibrio stocastico:** Identificazione di anomalie temporanee nei processi di convergenza di asset cointegrati o di mean reversion dinamica attraverso modelli a spazio di stato.9 + +La transizione dal backtesting teorico alla produzione reale rappresenta la principale barriera d'ingresso per i sistemi quantitativi. Per garantire la robustezza statistica ed esecutiva delle strategie, l'infrastruttura di ricerca deve neutralizzare sistematicamente due trappole metodologiche cruciali: + +* **Overfitting e distorsione da ottimizzazione:** L'eccesso di parametri liberi consente a un modello di adattarsi perfettamente al rumore storico, distruggendo la sua capacità predittiva out-of-sample.13 Per mitigare questo rischio, è imperativo utilizzare metodi di validazione rigorosi come la Walk-Forward Analysis (WFA) di tipo rolling, che ottimizza periodicamente i parametri su finestre storiche mobili e li testa su periodi out-of-sample non sovrapposti.13 +* **Survivorship e Lookahead Bias:** L'esclusione di asset delistati o falliti dal database storico sovrastima sistematicamente le performance reali della strategia. Inoltre, l'utilizzo inconscio di informazioni future (lookahead bias)—come l'aggregazione di metriche di sbilanciamento volumetrico calcolate su intervalli temporali che terminano dopo il punto di esecuzione dell'ordine—invalida l'intero processo di backtesting.17 + +## **2\. Analisi dettagliata delle strategie profittevoli** + +La progettazione del bot di trading si focalizza su quattro macro-categorie di strategie quantitative, ciascuna ingegnerizzata per estrarre Alpha da specifiche inefficienze microstrutturali o statistiche dei mercati Crypto, Forex e Azionario. + +### **Categoria 1: Market Making Algoritmico e Scalping Quantitativo** + +La strategia proposta implementa una variante evoluta del modello classico di Avellaneda-Stoikov (2008).4 Invece di assumere parametri microstrutturali statici, l'algoritmo calcola dinamicamente lo sbilanciamento dell'Order Book (Order Book Imbalance \- OBI) 6 e l'intensità del flusso d'ordine (Order Flow Imbalance \- OFI) 8 per modulare asimmetricamente i prezzi di bid e ask.23 Questo consente di catturare lo spread minimizzando il rischio di inventario (inventory risk) e riducendo l'esposizione alla selezione avversa (adverse selection) in presenza di flussi tossici o sbilanciati.4 + +### **Categoria 2: Arbitraggio Avanzato** + +La variante selezionata è l'Arbitraggio Statistico Multidimensionale basato sulla cointegrazione stimata tramite il framework a correzione dell'errore vettoriale (VECM) di Johansen.10 Rispetto all'approccio a due fasi di Engle-Granger (limitato a coppie di asset e suscettibile di distorsioni di endogeneità) 29, il modello di Johansen consente di identificare molteplici vettori di cointegrazione significativi su un paniere di ![][image1] asset altamente correlati.10 Ciò permette di costruire un portafoglio sintetico stazionario (![][image2]) con una capacità di assorbimento della liquidità notevolmente superiore.10 + +### **Categoria 3: Trend Following e Momentum Quantitativo** + +Questa strategia implementa un modello di Volatility Breakout adattivo basato sul canale di Keltner modificato.34 Per discriminare i veri breakout dalle frequenti cacciate di liquidità (fakes), l'ingresso viene subordinato a una validazione volumetrica rigorosa basata sul Volume Relativo (RVOL) 37 e sulla derivata del Cumulative Volume Delta (CVD) ad alta frequenza.37 Questo garantisce che la rottura dinamica del canale sia supportata dall'ingresso coordinato di flussi istituzionali aggressivi (taker).35 + +### **Categoria 4: Mean Reversion Quantitativa** + +La strategia adotta un approccio a spazio di stato guidato dal Filtro di Kalman per calcolare l'ancoraggio dinamico del prezzo (fair value) e l'hedge ratio ottimale tra due o più attività finanziarie in tempo reale.9 Il Filtro di Kalman opera come uno stimatore ricorsivo online, superando le metodologie basate su medie mobili semplici o regressioni rolling OLS, che introducono un ritardo temporale (lag) deleterio e richiedono la sintonizzazione arbitraria della lunghezza della finestra di calcolo.40 L'inseguimento dinamico dei parametri consente di sfruttare la convergenza dello spread anche in condizioni di mercato instabili o in presenza di micro-derive strutturali.9 + +## **3\. Architettura di implementazione per ogni strategia** + +### **Strategia 1: Market Making basato su modello Avellaneda-Stoikov e OBI/OFI** + +#### **Logica Matematica e Concettuale** + +Il prezzo medio dell'asset (mid-price) ![][image3] segue un processo diffusivo aritmetico: +![][image4] +20 +dove ![][image5] è un moto browniano standard e ![][image6] è la volatilità infinitesimale del prezzo.20 Il market maker cerca di massimizzare l'utilità attesa della ricchezza terminale penalizzando la varianza del proprio inventario ![][image7] sotto un'avversione al rischio ![][image8].4 Il prezzo di riserva (reservation price) ![][image9] è definito da: +![][image10] +19 +Nel modello classico a orizzonte finito ![][image11], gli spread ottimali unilaterali di bid (![][image12]) e ask (![][image13]) rispetto al mid-price sono calcolati come: +![][image14] +19 +![][image15] +19 +dove ![][image16] e ![][image17] rappresentano i parametri empirici della microstruttura del book relativi alla probabilità di esecuzione dei limit order al variare della distanza dal mid-price, modellati tramite l'intensità di Poisson ![][image18].43 In questo blueprint, i parametri ![][image16] e ![][image17] vengono aggiornati in tempo reale calcolando l'Order Book Imbalance (OBI) 6: +![][image19] +6 +Se ![][image20], l'intensità stimata lato ask (![][image16]) si riduce, indicando una maggiore probabilità di esecuzione immediata dei sell limit order, mentre l'intensità lato bid (![][image17]) aumenta.23 Questo costringe l'algoritmo a spostare la quotazione verso l'alto per assecondare lo sbilanciamento del flusso d'ordine.23 + +#### **Condizioni di Setup e Trigger (Inizio/Fine)** + +* **Inizializzazione (Warm-up):** Calcolo della volatilità storica ![][image6] su un rolling window di 10 minuti di dati L2 Tick-by-Tick. Calcolo iniziale di ![][image21] e ![][image22] tramite regressione logaritmica della frequenza dei "fill" storici. +* **Trigger di Quoting:** Ad ogni aggiornamento del modulo L2 Order Book o alla ricezione di un trade sul mercato che modifica l'Order Flow Imbalance (![][image23]) oltre una deviazione standard rispetto alla media.17 +* **Trigger di Stop-Loss / Sospensione (Halt):** Interruzione immediata del posizionamento degli ordini qualora la tossicità del flusso (VPIN) calcolata su base "volume-clock" superi la soglia del novantesimo percentile storico (![][image24]), segnalando un'imminente fase di selezione avversa estrema da parte di insider trader.1 + +#### **Blocchi di Logica Condizionale / Pseudocodice** + +Python +\# Pseudocodice: Market Making Avellaneda-Stoikov Adattivo +import numpy as np + +class AvellanedaStoikovEngine: + def \_\_init\_\_(self, gamma: float, max\_inventory: int, base\_kappa: float, sigma\_window: int): + self.gamma \= gamma \# Avversione al rischio + self.max\_q \= max\_inventory \# Inventario massimo consentito (risk limit) + self.k\_base \= base\_kappa \# Parametro di sensibilità del book di base + self.sigma\_window \= sigma\_window \# Finestra per stima volatilità infinitesimale + self.mid\_history \= + self.q \= 0 \# Inventario fisico corrente + + def update\_volatility(self, current\_mid: float) \-\> float: + self.mid\_history.append(current\_mid) + if len(self.mid\_history) \> self.sigma\_window: + self.mid\_history.pop(0) + if len(self.mid\_history) \< 2: + return 0.01 + returns \= np.diff(np.log(self.mid\_history)) + return np.std(returns) + + def calculate\_adaptive\_quotes(self, S: float, best\_bid\_qty: float, best\_ask\_qty: float, T\_t: float \= 1.0): + sigma \= self.update\_volatility(S) + + \# Sbilanciamento statico del book (OBI) + obi \= (best\_bid\_qty \- best\_ask\_qty) / (best\_bid\_qty \+ best\_ask\_qty) + + \# Modulazione asimmetrica di kappa basata su OBI + kappa\_a \= self.k\_base \* (1.0 \- 0.5 \* obi) + kappa\_b \= self.k\_base \* (1.0 \+ 0.5 \* obi) + + \# Calcolo del Prezzo di Riserva (Reservation Price) + reservation\_price \= S \- self.q \* self.gamma \* (sigma \*\* 2) \* T\_t + + \# Calcolo degli spread asimmetrici + spread\_a \= (reservation\_price \- S) / 2.0 \+ (1.0 / self.gamma) \* np.log(1.0 \+ self.gamma / kappa\_a) + spread\_b \= (S \- reservation\_price) / 2.0 \+ (1.0 / self.gamma) \* np.log(1.0 \+ self.gamma / kappa\_b) + + \# Quotazioni assolute nel book + ask\_price \= reservation\_price \+ spread\_a + bid\_price \= reservation\_price \- spread\_b + + \# Limite di sicurezza sull'inventario (Hard Ceiling) + if self.q \>= self.max\_q: + bid\_price \= \-1.0 \# Sospendi invio ordini in acquisto (Bid) + if self.q \<= \-self.max\_q: + ask\_price \= \-1.0 \# Sospendi invio ordini in vendita (Ask) + + return bid\_price, ask\_price + +#### **Gestione del Rischio e Money Management** + +Il rischio di inventario viene mitigato tramite un modello di posizionamento asimmetrico ("skew") che riduce progressivamente la dimensione degli ordini (size) esposti sul lato in direzione dell'inventario accumulato.43 La dimensione dinamica dell'ordine segue la legge esponenziale: +![][image25] +43 +![][image26] +43 +dove ![][image27] rappresenta il volume massimo ordinabile e ![][image28] è il fattore di decadimento dell'esposizione.43 La gestione del rischio prevede uno stop-loss globale (hard liquidation) qualora la svalutazione MTM del portafoglio superi la tolleranza di perdita massima giornaliera. + +#### **Fattori di Rischio Esecutivo e Mitigazione** + +* **Latenza di esecuzione e accodamento (Queue Position):** Gli ordini passivi inviati tardivamente rispetto ai movimenti del mercato subiscono la selezione avversa (vengono "fillati" solo quando il prezzo rompe il livello).4 Mitigazione: Sviluppo dell'infrastruttura di ricezione in C++ con architettura di threading bloccata sulla CPU e connessione WebSocket asincrona a bassa latenza, utilizzando il protocollo ZeroMQ per la messaggistica IPC interna.47 + +#### **Metriche di Performance Attese** + +| Metrica di Performance | Valore Obiettivo | +| :---- | :---- | +| **Sharpe Ratio** | **![][image29]** | +| **Profit Factor** | **![][image30]** | +| **Massimo Drawdown Atteso** | **![][image31]** | + +### **Strategia 2: Arbitraggio Statistico VECM Johansen Multidimensionale** + +#### **Logica Matematica e Concettuale** + +Il modello descrive un sistema a ![][image1] variabili tramite una formulazione di autoregressione vettoriale a correzione dell'errore (VECM) 10: +![][image32] +10 +dove ![][image33] è un vettore di rango ![][image34] contenente i prezzi in scala logaritmica degli asset analizzati, ![][image35] rappresenta la matrice di impatto di lungo periodo con rango ![][image36], e ![][image37] è un vettore di rumore bianco gaussiano multivariato con matrice di covarianza ![][image38].24 Sotto l'ipotesi di cointegrazione, la matrice ![][image35] viene fattorizzata in due matrici ![][image39]: +![][image40] +24 +dove ![][image41] rappresenta la matrice dei vettori di cointegrazione, i quali combinano linearmente le serie storiche non stazionarie ![][image42] trasformandole in uno spread stazionario ![][image2] 10: +![][image43] +10 +La matrice ![][image44] contiene i coefficienti di velocità di regolazione (adjustment speed), i quali determinano la rapidità con cui il sistema corregge gli scostamenti transitori rispetto all'equilibrio di lungo periodo.24 Affinché il sistema converga, gli elementi di ![][image44] devono essere statisticamente significativi e di segno opposto rispetto alla direzione dello spread (retroazione negativa stazionaria).28 + +#### **Condizioni di Setup e Trigger (Inizio/Fine)** + +* **Fase In-Sample (Stima settimanale):** Esecuzione del test di traccia di Johansen su un rolling window di 30 giorni di dati orari.27 Se la traccia rifiuta l'ipotesi nulla di assenza di vettori di cointegrazione (![][image45]) con un livello di confidenza del ![][image46], viene estratto il vettore ![][image47] associato all'autovalore massimo.10 +* **Condizione di Entrata (Z-Score):** Lo spread corrente ![][image3] viene standardizzato in base alla media (![][image48]) e alla deviazione standard (![][image49]) in-sample. + * **Trigger Long Spread:** ![][image50]. Acquisto immediato di ![][image33] proporzionalmente ai coefficienti positivi di ![][image47] e vendita simultanea degli asset con coefficienti negativi.51 + * **Trigger Short Spread:** ![][image51]. Configurazione speculare del paniere a mercato.51 +* **Condizione di Uscita (Take Profit):** Rientro dello Z-Score all'interno della banda centrale neutra ![][image52].42 Lo stop-loss viene innescato se ![][image53] eccede ![][image54] per più di 24 ore, indicando una rottura strutturale permanente della cointegrazione. + +#### **Blocchi di Logica Condizionale / Pseudocodice** + +C++ +// Pseudocodice: Cointegration Strategy Engine (C++) +\#**include** \ +\#**include** \ +\#**include** \ + +class JohansenVECMEngine { +private: + std::vector\ beta\_vector; // Coefficienti del vettore di cointegrazione beta\_1 + double mean\_spread; // Media storica in-sample dello spread + double std\_spread; // Deviazione standard storica + bool in\_position \= false; + int current\_direction \= 0; // 1: Long Spread, \-1: Short Spread, 0: Flat + +public: + JohansenVECMEngine(std::vector\ beta, double mean, double std) + : beta\_vector(beta), mean\_spread(mean), std\_spread(std) {} + + double calculate\_spread(const std::vector\& log\_prices) { + double spread \= 0.0; + for (size\_t i \= 0; i \< log\_prices.size(); \++i) { + spread \+= beta\_vector\[i\] \* log\_prices\[i\]; + } + return spread; + } + + void evaluate\_market\_tick(const std::vector\& raw\_prices) { + std::vector\ log\_prices(raw\_prices.size()); + for (size\_t i \= 0; i \< raw\_prices.size(); \++i) { + log\_prices\[i\] \= std::log(raw\_prices\[i\]); + } + + double spread \= calculate\_spread(log\_prices); + double z\_score \= (spread \- mean\_spread) / std\_spread; + + if (\!in\_position) { + if (z\_score \< \-2.0) { + execute\_basket\_orders(raw\_prices, 1); // Entrata Long Spread + in\_position \= true; + current\_direction \= 1; + } else if (z\_score \> 2.0) { + execute\_basket\_orders(raw\_prices, \-1); // Entrata Short Spread + in\_position \= true; + current\_direction \= \-1; + } + } else { + // Chiusura della posizione su convergenza alla media (reversion) + if ((current\_direction \== 1 && z\_score \>= \-0.25) || + (current\_direction \== \-1 && z\_score \<= 0.25)) { + execute\_basket\_orders(raw\_prices, 0); // Chiusura totale + in\_position \= false; + current\_direction \= 0; + } + } + } + +private: + void execute\_basket\_orders(const std::vector\& prices, int direction) { + // Logica di instradamento simultaneo degli ordini FIX a mercato + // Calcola la quantità esatta per ogni asset moltiplicando la size per il coefficiente beta + } +}; + +#### **Gestione del Rischio e Money Management** + +La dimensione del paniere è controllata applicando un modello di Volatility Targeting basato su previsioni di volatilità condizionale stimate tramite un'equazione GARCH(1,1) a livello di portafoglio 11: +![][image55] +11 +Il capitale allocato allo spread viene scalato dinamicamente ad ogni ribilanciamento in base al fattore di leva 53: +![][image56] +53 +In caso di aumento improvviso della volatilità dello spread (![][image57]), la leva ![][image58] decresce riducendo l'esposizione del portafoglio prima che si verifichino violazioni dello stop-loss.52 + +#### **Fattori di Rischio Esecutivo e Mitigazione** + +* **Rischio di esecuzione asincrona (Execution Leg Risk):** Durante l'invio degli ordini di paniere, una frazione degli asset può subire ritardi di esecuzione stravolgendo la neutralità del portafoglio e introducendo un rischio direzionale indesiderato. Mitigazione: Utilizzo di un gateway di esecuzione interna con routing di tipo Immediate-or-Cancel (IOC) o Fill-or-Kill (FOK). L'eventuale frazione non eseguita viene immediatamente coperta a mercato tramite ordini taker correttivi. + +#### **Metriche di Performance Attese** + +| Metrica di Performance | Valore Obiettivo | +| :---- | :---- | +| **Sharpe Ratio** | **![][image59]** | +| **Profit Factor** | **![][image60]** | +| **Massimo Drawdown Atteso** | **![][image61]** | + +### **Strategia 3: Trend Following e Volatility Breakout con Filtri Volumetrici** + +#### **Logica Matematica e Concettuale** + +La strategia sfrutta la transizione impulsiva del mercato da regimi stazionari a bassa volatilità (accumulazione) a trend direzionali caratterizzati da forte espansione della volatilità e volumi dominanti.34 La compressione iniziale viene quantificata tramite la larghezza dinamica delle bande del canale di Keltner.34 Le bande superiore (![][image62]) e inferiore (![][image63]) sono definite come: +![][image64] +35 +![][image65] +35 +Il breakout viene validato solo se supportato da un eccezionale afflusso volumetrico aggressivo.35 A tale scopo, si analizza il Relative Volume (![][image66]), normalizzato rispetto alla media mobile semplice a 20 periodi del volume 37: +![][image67] +37 +La dinamica del flusso viene integrata analizzando l'accelerazione del Cumulative Volume Delta (CVD) a livello sub-secondario, calcolato filtrando gli ordini eseguiti sul lato "bid" rispetto al lato "ask" 37: +![][image68] +Un incremento impulsivo della derivata del ![][image69] (![][image70]) conferma che gli operatori istituzionali stanno consumando attivamente la liquidità passiva esposta sul book sul lato ask, confermando l'inizio di una traiettoria direzionale robusta alla rottura del canale superiore.35 + +#### **Condizioni di Setup e Trigger (Inizio/Fine)** + +* **Setup Long:** Il prezzo di chiusura della barra supera ![][image71] 35 con un volume ![][image72].37 Il prezzo deve chiudere nel ![][image73] superiore dell'intervallo totale di negoziazione della barra (High \- Low).37 La derivata del CVD deve essere strettamente positiva nei 5 periodi precedenti. +* **Setup Short:** Il prezzo di chiusura scende sotto ![][image74] con ![][image72] 37, chiusura nel ![][image73] inferiore del range complessivo della candela 37, e derivata del CVD decrescente. +* **Trigger di Uscita (Stop-Loss / Take Profit):** Stop-loss fisso posizionato sul minimo della barra di breakout (per posizioni Long), definendo la distanza di rischio ![][image75].37 Il take profit è fissato a un target simmetrico pari a ![][image76].37 Viene implementato un Chandelier trailing stop basato sull'ATR per proteggere i profitti latenti in caso di trend estesi.35 + +#### **Blocchi di Logica Condizionale / Pseudocodice** + +Python +\# Pseudocodice: Volatility Breakout con filtri RVOL e CVD +class VolatilityBreakoutEngine: + def \_\_init\_\_(self, atr\_period: int \= 20, rvol\_limit: float \= 3.0): + self.period \= atr\_period + self.rvol\_limit \= rvol\_limit + self.closes \= + self.highs \= + self.lows \= + self.volumes \= + self.cvd \= + + def process\_candle(self, high: float, low: float, close: float, volume: float, cvd\_value: float) \-\> int: + self.highs.append(high) + self.lows.append(low) + self.closes.append(close) + self.volumes.append(volume) + self.cvd.append(cvd\_value) + + if len(self.closes) \< self.period \+ 1: + return 0 \# Warm-up phase + + \# Calcolo canali di Keltner basati su ATR + tr \= np.maximum( + np.array(self.highs\[-self.period:\]) \- np.array(self.lows\[-self.period:\]), + np.abs(np.array(self.highs\[-self.period:\]) \- np.array(self.closes\[-self.period-1:-1\])) + ) + atr \= np.mean(tr) + ema \= np.mean(self.closes\[-self.period:\]) \# Semplificazione lineare + + upper\_band \= ema \+ 2.0 \* atr + lower\_band \= ema \- 2.0 \* atr + + \# Calcolo filtri volumetrici avanzati + avg\_volume \= np.mean(self.volumes\[-self.period:\]) + rvol \= volume / avg\_volume if avg\_volume \> 0 else 1.0 + + \# Derivata del CVD (accelerazione del flusso) + cvd\_slope \= self.cvd\[-1\] \- self.cvd\[-3\] if len(self.cvd) \>= 3 else 0.0 + + \# Controllo della qualità della chiusura della candela (Close position ratio) + candle\_range \= high \- low + close\_ratio \= (close \- low) / candle\_range if candle\_range \> 0 else 0.5 + + \# Generazione Segnale Direzionale + if close \> upper\_band and rvol \>= self.rvol\_limit and cvd\_slope \> 0 and close\_ratio \>= 0.8: + return 1 \# Trigger segnale d'acquisto (Long) + elif close \< lower\_band and rvol \>= self.rvol\_limit and cvd\_slope \< 0 and close\_ratio \<= 0.2: + return \-1 \# Trigger segnale di vendita (Short) + + return 0 + +#### **Gestione del Rischio e Money Management** + +La dimensione della posizione viene calcolata dinamicamente in base alla distanza monetaria tra l'ingresso e lo stop-loss (pari a ![][image75]).37 L'algoritmo limita il rischio all'1.5% del patrimonio totale per singola operazione (Fixed Fractional Risk) 37: +![][image77] +Questo approccio normalizza le perdite attese in periodi di alta o bassa volatilità, garantendo la stabilità del capitale operativo.56 + +#### **Fattori di Rischio Esecutivo e Mitigazione** + +* **Slippage su rottura impulsiva (Taker Execution Slippage):** L'ingresso simultaneo a mercato di molteplici algoritmi di breakout sui medesimi livelli tecnici genera sbalzi violenti del prezzo nel millisecondo successivo alla rottura, riducendo drasticamente il profitto atteso.35 Mitigazione: Utilizzo di ordini Stop-Limit anziché Stop-Market, con una tolleranza massima di prezzo configurata a ![][image78] rispetto al punto teorico di breakout. + +#### **Metriche di Performance Attese** + +| Metrica di Performance | Valore Obiettivo | +| :---- | :---- | +| **Sharpe Ratio** | **![][image60]** | +| **Profit Factor** | **![][image79]** | +| **Massimo Drawdown Atteso** | **![][image80]** | + +### **Strategia 4: Mean Reversion Dinamica basata su Filtro di Kalman** + +#### **Logica Matematica e Concettuale** + +La strategia modella la relazione di spread tra due attività finanziarie cointegrate ![][image81] (osservabile) e ![][image82] (regressore) utilizzando una formulazione ricorsiva a spazio di stato.9 Lo stato latente non osservabile è rappresentato dai coefficienti dinamici del modello ![][image83], indicanti rispettivamente l'intercetta e l'hedge ratio in tempo reale 9: + +* **Equazione di Transizione di Stato:** + ![][image84] + 39 +* **Equazione di Osservazione:** + ![][image85] + 39 + +La matrice di transizione dello stato è posta pari all'identità ![][image86], definendo una traiettoria a cammino casuale per i parametri stocastici di cointegrazione.39 La matrice di osservazione è definita dal vettore riga ![][image87].9 La covarianza del rumore di transizione ![][image5] descrive l'instabilità del sistema nel tempo e viene parametrizzata tramite il fattore di decadimento ![][image88] (tipicamente ![][image89]) 9: +![][image90] +39 +La fase ricorsiva adatta le stime dei parametri ad ogni nuovo tick ricevuto tramite il calcolo del residuo di previsione (forecast error) ![][image91] e della sua varianza dinamica ![][image92] 9: +![][image93] +9 +![][image94] +9 +dove ![][image95] rappresenta la proiezione a priori della matrice di covarianza dell'errore di stato.9 Lo scostamento standardizzato è definito come: +![][image96] +9 +Questo Z-Score non soffre del ritardo introdotto dai calcoli rolling storici e rappresenta il saggio di deviazione istantaneo rispetto al fair value implicito.40 + +#### **Condizioni di Setup e Trigger (Inizio/Fine)** + +* **Entrata Long:** ![][image97] (Sottovalutazione estrema dell'asset ![][image81] rispetto alla relazione stocastica).41 Acquisto di 1 unità di ![][image81] e vendita contemporanea di ![][image98] unità di ![][image82].41 +* **Entrata Short:** ![][image99].41 Configurazione speculare del paniere a mercato.41 +* **Trigger di Uscita (Target):** Liquidazione totale dell'esposizione quando lo spread converge nuovamente alla media, identificato dal superamento dello zero da parte del residuo (![][image100]).41 Lo stop-loss viene innescato se lo Z-Score supera la banda estrema ![][image101] per più di 60 cicli di esecuzione consecutivi. + +#### **Blocchi di Logica Condizionale / Pseudocodice** + +Python +\# Pseudocodice: Filtro di Kalman per Mean Reversion +import numpy as np + +class AdaptiveKalmanFilter: + def \_\_init\_\_(self, delta: float \= 1e-5, observation\_cov: float \= 1.0): + self.delta \= delta + self.V \= observation\_cov \# Varianza rumore osservazione + self.W \= (delta / (1.0 \- delta)) \* np.eye(2) \# Covarianza rumore transizione + + self.theta \= np.zeros((2, 1)) \# Stato iniziale^T + self.P \= np.zeros((2, 2)) \# Incertezza iniziale dello stato + self.R \= None + + def execute\_filter\_step(self, y: float, x: float): + \# Definisco la matrice di misura F\_t + F \= np.array(\[\[1.0, x\]\]) + + \# 1\. Fase di Predizione della covarianza dello stato + if self.R is not None: + self.R \= self.P \+ self.W + else: + self.R \= np.eye(2) \* 1.0 + + \# 2\. Calcolo della stima a priori dell'errore (Forecast Error) + y\_hat \= np.dot(F, self.theta) + e\_t \= y \- y\_hat + + \# 3\. Calcolo della covarianza del residuo Q\_t + Q\_t \= np.dot(F, np.dot(self.R, F.T)) \+ self.V + sqrt\_Q\_t \= np.sqrt(Q\_t) + + \# 4\. Calcolo del Guadagno di Kalman K\_t + K\_t \= np.dot(self.R, F.T) / Q\_t + + \# 5\. Fase di Aggiornamento dello stato e della covarianza a posteriori + self.theta \= self.theta \+ K\_t \* e\_t + self.P \= self.R \- np.dot(K\_t, np.dot(F, self.R)) + + \# Calcolo lo Z-Score dinamico per l'esecuzione del segnale + z\_score \= e\_t / sqrt\_Q\_t + current\_beta \= self.theta + + return z\_score, current\_beta + +#### **Gestione del Rischio e Money Management** + +Il dimensionamento dell'esposizione viene definito in modo frazionario applicando la variante del Criterio di Kelly Frazionario (![][image102]) per prevenire la rovina statistica in caso di deviazioni eccezionali dello spread.56 La frazione ottima ![][image103] del portafoglio da allocare a ciascuna transazione è definita da 57: +![][image104] +40 +dove ![][image105] e ![][image106] indicano rispettivamente la media esponenziale e la varianza a breve termine dell'errore di previsione residuo ![][image91]. + +#### **Fattori di Rischio Esecutivo e Mitigazione** + +* **Rottura strutturale della cointegrazione (Drift Risk):** Un mutamento radicale dei fondamentali economici di uno dei due asset può generare una svalutazione infinita senza ritorno alla media dello spread.12 Mitigazione: Disaccoppiamento istantaneo ed esclusione dell'asset dal sistema qualora il residuo standardizzato rimanga all'esterno della banda limite (![][image107]) per un intervallo temporale superiore a tre volte la "half-life" teorica del processo di ritorno alla media.11 + +#### **Metriche di Performance Attese** + +| Metrica di Performance | Valore Obiettivo | +| :---- | :---- | +| **Sharpe Ratio** | **![][image108]** | +| **Profit Factor** | **![][image109]** | +| **Massimo Drawdown Atteso** | **![][image110]** | + +## **4\. Tabella comparativa delle strategie** + +La tabella sottostante sintetizza e confronta i requisiti strutturali e operativi delle quattro varianti strategiche analizzate per orientare l'allocazione ottimale delle risorse del server e del capitale: + +| Strategia | Complessità di Sviluppo | Capitale Minimo Richiesto | Sensibilità alla Latenza | Condizioni di Mercato Ideali | Capacità di Scalabilità del Capitale | +| :---- | :---- | :---- | :---- | :---- | :---- | +| **Market Making Avellaneda-Stoikov \+ OBI/OFI** | Alta | $150,000 | Alta (HFT, esecuzione sub-millisecondo) 4 | Range trading, altissima liquidità sul book, assenza di trend impulsivi | Bassa (limitata dallo spessore del book dell'asset selezionato) | +| **Arbitraggio Statistico VECM Johansen** | Alta | $100,000 | Media (Low-latency per stabilità d'esecuzione) | Correlazioni settoriali stabili, anomalie di spread a breve termine 10 | Alta (applicabile a panieri complessi diversificati) | +| **Trend Following RVOL/CVD Breakout** | Media | $15,000 | Bassa (Esecuzione su base bar close) | Espansioni di volatilità, forte pressione volumetrica unilaterale 34 | Altissima (scalabile su mercati globali profondi) | +| **Mean Reversion Filtro di Kalman** | Media | $30,000 | Media (Uscita e monitoraggio ricorsivo intra-day) | Mercati volatili in assenza di trend macro persistenti 12 | Media | + +## **5\. Linee guida per il backtesting e lo sviluppo del bot** + +### **Linee Guida per il Backtesting Rigoroso** + +Per eliminare l'overfitting e garantire la stabilità dell'Alpha durante l'esecuzione live del bot di trading, l'architettura di simulazione deve implementare rigorosamente la Walk-Forward Analysis (WFA).13 + +1. **Segmentazione dei Dati (In-Sample / Out-of-Sample):** L'intero intervallo storico dei dati storici viene suddiviso in blocchi di ottimizzazione temporali rotanti.13 I parametri ottimali staccati dal modello nella fase "In-Sample" (es. lookback, moltiplicatori delle deviazioni standard) devono essere validati sulla finestra adiacente "Out-of-Sample" senza alcuna sovrapposizione.13 + + Finestra 1: + Finestra 2: + Finestra 3: + +2. **Integrazione Realistica dei Costi di Transazione:** Molte simulazioni teoriche mostrano equity curve esponenziali a causa della mancata o errata rendicontazione dell'impatto microstrutturale dell'esecuzione.46 Ogni esecuzione simulata deve incorporare: + * La struttura asimmetrica delle fee dell'exchange (struttura Maker ridotta o negativa rispetto a fee Taker elevate).19 + * Un modello di slippage dinamico basato sul consumo del volume esposto nel book a livello di profondità storica al tick esatto dell'operazione. + +### **Linee Guida di Architettura Software per Sistemi ad Alta Affidabilità** + +Lo sviluppo del motore esecutivo deve essere progettato secondo i criteri della programmazione concorrente e a tolleranza d'errore (Fault Tolerance). + +#### **Stack Tecnologico Consigliato** + +* **Linguaggio di Core Execution:** Rust o C++ per garantire il controllo deterministico della memoria e l'assenza di pause dovute alla Garbage Collection, critiche nei sistemi di trading ad alta frequenza.45 +* **Linguaggio di Data Ingestion e Analisi:** Python per l'elaborazione dei flussi, interfacciato tramite PyO3 con il core esecutivo in Rust. + +#### **Gestione Asincrona dei Flussi tramite WebSocket e ZeroMQ** + +L'architettura software del bot di trading deve separare nettamente l'attività di ingestion dei dati di mercato, il modulo logico di calcolo strategico e l'esecuzione degli ordini tramite un design a microservizi asincroni, come illustrato nel seguente diagramma di flusso dei dati: + +\+---------------------------------------------------------------------------------------------------+ +| INGESTION ADAPTER (Rust) | +| \- Connessione WebSocket persistente (SSL/TLS) con l'Exchange | +| \- Parsing asincrono dei frame JSON/Binary ad alta frequenza | +\+-------------------------------------------------+-------------------------------------------------+ + | + | ZeroMQ IPC (PUB-SUB Pattern / Zero-Copy) + v +\+---------------------------------------------------------------------------------------------------+ +| MESSAGE BUS (ZeroMQ) | +\+------------------------+--------------------------------------------------+-----------------------+ + | | + | IPC / TCP (sub-millisecond lat.) | IPC / TCP + v v +\+---------------------------------------------------+ \+------------------------------------------+ +| QUANT STRATEGY ENGINE A | | QUANT STRATEGY ENGINE B | +| \- Thread di calcolo logico separato (Isolamento) | | \- Thread di calcolo (es. Kalman Filter) | +| \- Generazione dei segnali ed ordini di trading | | \- Gestione del portafoglio e coperture | +\+------------------------+--------------------------+ \+------------------+-----------------------+ + | | + | ZeroMQ (PUSH-PULL Pattern / DEALER-ROUTER) \[61, 62, 63\] + \+------------------------+-------------------------+ + | + v +\+---------------------------------------------------------------------------------------------------+ +| ORDER EXECUTION ROUTER | +| \- Gestione prioritaria delle code di messaggi (asyncio.Queue) | +| \- Calcolo del controllo dell'inventario e Risk Management globale | +| \- Spedizione via HTTP REST (Rate-Limit friendly) o WebSocket privata | +\+---------------------------------------------------------------------------------------------------+ + +* **ZeroMQ PUB-SUB per i Dati di Mercato:** L'Ingestion Adapter si collega tramite socket WebSocket ai server dell'exchange, normalizza i frame strutturati L2/L3 e li pubblica istantaneamente su un canale ZeroMQ locale (ipc://market\_data.ipc) utilizzando la modalità Zero-Copy per eliminare il sovraccarico di allocazione in memoria.49 +* **Isolamento dei Thread di Calcolo:** Ciascuna strategia di trading opera in un processo o thread separato della CPU, in modalità di ascolto (Subscriber) del canale di mercato.49 Questo impedisce che un errore o un ritardo nel calcolo di una strategia possa compromettere o rallentare il funzionamento globale del sistema.49 +* **ZeroMQ PUSH-PULL / DEALER-ROUTER per l'Invio degli Ordini:** I segnali calcolati vengono indirizzati a un microservizio di gestione degli ordini centralizzato tramite socket ZeroMQ di tipo PUSH.61 Il gestore degli ordini (PULL o ROUTER) valida i parametri di rischio del portafoglio prima dell'invio esecutivo effettivo, ottimizzando le code in base alla priorità del segnale generato.61 + +#### **Meccanismi di Protezione del Software (Crash Prevention)** + +* **High-Water Mark (HWM) di ZeroMQ:** Per evitare un accumulo incontrollato di messaggi in memoria durante fasi di estrema attività del mercato (es. flash crash), le connessioni socket devono impostare un limite di ritenzione massimo (ZMQ\_RCVHWM / ZMQ\_SNDHWM), superato il quale l'infrastruttura inizia a scartare i frame di mercato non essenziali in modo deterministico anziché mandare in crash l'intero sistema operativo per mancanza di RAM.62 +* **Code Asincrone Limitate (Bounded asyncio.Queue):** Il modulo Order Execution Router deve utilizzare code asincrone a dimensione fissa.64 Qualora la coda ordini si saturi per blocchi nell'invio di rete (rate limit dell'exchange), l'algoritmo sospende l'invio di nuovi segnali per salvaguardare lo stato di coerenza del bot di trading.64 + +#### **Bibliografia** + +1. theopenstreet/VPIN\_HFT \- GitHub, accesso eseguito il giorno maggio 27, 2026, [https://github.com/theopenstreet/VPIN\_HFT](https://github.com/theopenstreet/VPIN_HFT) +2. Volume-Synchronized Probability of Informed Trading (VPIN) \- VisualHFT, accesso eseguito il giorno maggio 27, 2026, [https://visualhft.com/blog/volume-synchronized-probability-of-informed-trading-vpin/](https://visualhft.com/blog/volume-synchronized-probability-of-informed-trading-vpin/) +3. VPIN 1 The Volume Synchronized Probability of INformed Trading, commonly known as VPIN, is a mathematical model used in financia \- QuantResearch.org, accesso eseguito il giorno maggio 27, 2026, [https://www.quantresearch.org/VPIN.pdf](https://www.quantresearch.org/VPIN.pdf) +4. Ultra Low Latency High Frequency Market Making: A Comprehensive Analysis of the Avellaneda-Stoikov Framework with Order Flow Imbalance Enhancement \- QuantLabsNet.com, accesso eseguito il giorno maggio 27, 2026, [https://www.quantlabsnet.com/post/ultra-low-latency-high-frequency-market-making-a-comprehensive-analysis-of-the-avellaneda-stoikov-f](https://www.quantlabsnet.com/post/ultra-low-latency-high-frequency-market-making-a-comprehensive-analysis-of-the-avellaneda-stoikov-f) +5. Bulk Volume Trade Classification and Informed Trading\*, accesso eseguito il giorno maggio 27, 2026, [http://faculty.bus.olemiss.edu/rvanness/Speakers/Presentations%202019-2020/AlCarrion\_BVC\_info\_Jan2020.pdf](http://faculty.bus.olemiss.edu/rvanness/Speakers/Presentations%202019-2020/AlCarrion_BVC_info_Jan2020.pdf) +6. Order Book Imbalance | QuestDB, accesso eseguito il giorno maggio 27, 2026, [https://questdb.com/glossary/order-book-imbalance/](https://questdb.com/glossary/order-book-imbalance/) +7. Order Book Imbalance in High-Frequency Markets \- Emergent Mind, accesso eseguito il giorno maggio 27, 2026, [https://www.emergentmind.com/topics/order-book-imbalance-obi](https://www.emergentmind.com/topics/order-book-imbalance-obi) +8. Order Flow Imbalance Models \- QuestDB, accesso eseguito il giorno maggio 27, 2026, [https://questdb.com/glossary/order-flow-imbalance-models/](https://questdb.com/glossary/order-flow-imbalance-models/) +9. 04 Kalman Filters and Pairs Trading.ipynb \- CoCalc, accesso eseguito il giorno maggio 27, 2026, [https://cocalc.com/github/QuantConnect/Research/blob/master/Research2Production/Python/04%20Kalman%20Filters%20and%20Pairs%20Trading.ipynb](https://cocalc.com/github/QuantConnect/Research/blob/master/Research2Production/Python/04%20Kalman%20Filters%20and%20Pairs%20Trading.ipynb) +10. Johansen Test for Cointegrating Time Series Analysis in R \- QuantStart, accesso eseguito il giorno maggio 27, 2026, [https://www.quantstart.com/articles/Johansen-Test-for-Cointegrating-Time-Series-Analysis-in-R/](https://www.quantstart.com/articles/Johansen-Test-for-Cointegrating-Time-Series-Analysis-in-R/) +11. Machine Learning for Algorithmic Trading — Part 9 Time Series Models for Volatility Forecasting & Statistical Arbitrage | by Connie Zhou | Medium, accesso eseguito il giorno maggio 27, 2026, [https://medium.com/@conniezhou678/machine-learning-for-algorithmic-trading-part-9-time-series-models-for-volatility-forecasting-c8f1afb4abd5](https://medium.com/@conniezhou678/machine-learning-for-algorithmic-trading-part-9-time-series-models-for-volatility-forecasting-c8f1afb4abd5) +12. Cointegrated Time Series Analysis for Mean Reversion Trading with R \- QuantStart, accesso eseguito il giorno maggio 27, 2026, [https://www.quantstart.com/articles/Cointegrated-Time-Series-Analysis-for-Mean-Reversion-Trading-with-R/](https://www.quantstart.com/articles/Cointegrated-Time-Series-Analysis-for-Mean-Reversion-Trading-with-R/) +13. Walk-Forward Analysis: Enhancing Your Backtesting Results \- BlueChip Algos, accesso eseguito il giorno maggio 27, 2026, [https://bluechipalgos.com/blog/walk-forward-analysis-enhancing-your-backtesting-results/](https://bluechipalgos.com/blog/walk-forward-analysis-enhancing-your-backtesting-results/) +14. The Future of Backtesting: A Deep Dive into Walk Forward Analysis \- Interactive Brokers, accesso eseguito il giorno maggio 27, 2026, [https://www.interactivebrokers.com/campus/ibkr-quant-news/the-future-of-backtesting-a-deep-dive-into-walk-forward-analysis/](https://www.interactivebrokers.com/campus/ibkr-quant-news/the-future-of-backtesting-a-deep-dive-into-walk-forward-analysis/) +15. Walk-Forward Optimization \- StrategyQuant, accesso eseguito il giorno maggio 27, 2026, [https://strategyquant.com/doc/strategyquant/walk-forward-optimization/](https://strategyquant.com/doc/strategyquant/walk-forward-optimization/) +16. Walk forward optimization \- Wikipedia, accesso eseguito il giorno maggio 27, 2026, [https://en.wikipedia.org/wiki/Walk\_forward\_optimization](https://en.wikipedia.org/wiki/Walk_forward_optimization) +17. Order Flow Imbalance \- A High Frequency Trading Signal | Dean Markwick, accesso eseguito il giorno maggio 27, 2026, [https://dm13450.github.io/2022/02/02/Order-Flow-Imbalance.html](https://dm13450.github.io/2022/02/02/Order-Flow-Imbalance.html) +18. Avellaneda-Stoikov HFT market making algorithm implementation \- GitHub, accesso eseguito il giorno maggio 27, 2026, [https://github.com/fedecaccia/avellaneda-stoikov](https://github.com/fedecaccia/avellaneda-stoikov) +19. A reinforcement learning approach to improve the performance of the Avellaneda-Stoikov market-making algorithm \- PMC, accesso eseguito il giorno maggio 27, 2026, [https://pmc.ncbi.nlm.nih.gov/articles/PMC9767337/](https://pmc.ncbi.nlm.nih.gov/articles/PMC9767337/) +20. High-frequency trading in a limit order book, accesso eseguito il giorno maggio 27, 2026, [https://people.orie.cornell.edu/sfs33/LimitOrderBook.pdf](https://people.orie.cornell.edu/sfs33/LimitOrderBook.pdf) +21. Basic Statistics and Order Book Imbalance: The Brazilian BMF\&Bovespa market |, accesso eseguito il giorno maggio 27, 2026, [https://davidsevangelista.github.io/post/basic\_statistics\_order\_imbalance/](https://davidsevangelista.github.io/post/basic_statistics_order_imbalance/) +22. Order Flow Imbalance (OFI) Prediction \- Emergent Mind, accesso eseguito il giorno maggio 27, 2026, [https://www.emergentmind.com/topics/order-flow-imbalance-ofi-prediction](https://www.emergentmind.com/topics/order-flow-imbalance-ofi-prediction) +23. Market Making with Alpha \- Order Book Imbalance \- HftBacktest, accesso eseguito il giorno maggio 27, 2026, [https://hftbacktest.readthedocs.io/en/latest/tutorials/Market%20Making%20with%20Alpha%20-%20Order%20Book%20Imbalance.html](https://hftbacktest.readthedocs.io/en/latest/tutorials/Market%20Making%20with%20Alpha%20-%20Order%20Book%20Imbalance.html) +24. jcitest \- Johansen cointegration test \- MATLAB \- MathWorks, accesso eseguito il giorno maggio 27, 2026, [https://www.mathworks.com/help/econ/jcitest.html](https://www.mathworks.com/help/econ/jcitest.html) +25. Cointegration: an overview Søren Johansen Department of Applied Mathematics and Statistics University of Copenhagen November 20, accesso eseguito il giorno maggio 27, 2026, [https://web.math.ku.dk/\~susanne/Klimamode/OverviewCointegration.pdf](https://web.math.ku.dk/~susanne/Klimamode/OverviewCointegration.pdf) +26. A Guide to Conducting Cointegration Tests \- Aptech, accesso eseguito il giorno maggio 27, 2026, [https://www.aptech.com/blog/a-guide-to-conducting-cointegration-tests/](https://www.aptech.com/blog/a-guide-to-conducting-cointegration-tests/) +27. Multivariate models and cointegration: the Johansen's procedure, accesso eseguito il giorno maggio 27, 2026, [https://warwick.ac.uk/fac/soc/economics/staff/gboero/personal/hand3\_vecm.pdf](https://warwick.ac.uk/fac/soc/economics/staff/gboero/personal/hand3_vecm.pdf) +28. VAR Models and Cointegration, accesso eseguito il giorno maggio 27, 2026, [https://faculty.washington.edu/ezivot/econ584/notes/cointegrationslides2.pdf](https://faculty.washington.edu/ezivot/econ584/notes/cointegrationslides2.pdf) +29. Johansen test \- Wikipedia, accesso eseguito il giorno maggio 27, 2026, [https://en.wikipedia.org/wiki/Johansen\_test](https://en.wikipedia.org/wiki/Johansen_test) +30. Identifying Single Cointegrating Relations \- MATLAB & Simulink \- MathWorks, accesso eseguito il giorno maggio 27, 2026, [https://www.mathworks.com/help/econ/identifying-single-cointegrating-relations.html](https://www.mathworks.com/help/econ/identifying-single-cointegrating-relations.html) +31. Johansen's Test: Simple Definition \- Statistics How To, accesso eseguito il giorno maggio 27, 2026, [https://www.statisticshowto.com/johansens-test/](https://www.statisticshowto.com/johansens-test/) +32. Johansen cointegration and its beauty\! | by Angelina \- Medium, accesso eseguito il giorno maggio 27, 2026, [https://medium.com/@AngelinaRule/johansen-cointegration-and-its-beauty-2c3ec0006f58](https://medium.com/@AngelinaRule/johansen-cointegration-and-its-beauty-2c3ec0006f58) +33. Cointegration: The Engle and Granger approach \- University of Warwick, accesso eseguito il giorno maggio 27, 2026, [https://warwick.ac.uk/fac/soc/economics/staff/gboero/personal/hand2\_cointeg.pdf](https://warwick.ac.uk/fac/soc/economics/staff/gboero/personal/hand2_cointeg.pdf) +34. Volatility Breakout Strategies: Examples and How to Use \- Gotrade, accesso eseguito il giorno maggio 27, 2026, [https://www.heygotrade.com/en/blog/volatility-breakout-strategies/](https://www.heygotrade.com/en/blog/volatility-breakout-strategies/) +35. What Is a Volatility Breakout Strategy? \- STP TRADING, accesso eseguito il giorno maggio 27, 2026, [https://www.stptrading.io/blog/what-is-a-volatility-breakout-strategy/](https://www.stptrading.io/blog/what-is-a-volatility-breakout-strategy/) +36. Volatility Breakout Trading Strategy \- Traders Mastermind, accesso eseguito il giorno maggio 27, 2026, [https://tradersmastermind.com/volatility-breakout-trading-strategy/](https://tradersmastermind.com/volatility-breakout-trading-strategy/) +37. I analyzed 2,877 breakouts and built a volume filter that grades them A through D. Here's what the data showed. : r/Daytrading \- Reddit, accesso eseguito il giorno maggio 27, 2026, [https://www.reddit.com/r/Daytrading/comments/1r19lax/i\_analyzed\_2877\_breakouts\_and\_built\_a\_volume/](https://www.reddit.com/r/Daytrading/comments/1r19lax/i_analyzed_2877_breakouts_and_built_a_volume/) +38. Volume at Breakout Strategy: Maximizing Trading Success \- Trade with the Pros, accesso eseguito il giorno maggio 27, 2026, [https://tradewiththepros.com/volume-at-breakout-strategy/](https://tradewiththepros.com/volume-at-breakout-strategy/) +39. Dynamic Hedge Ratio Between ETF Pairs Using the Kalman Filter ..., accesso eseguito il giorno maggio 27, 2026, [https://www.quantstart.com/articles/Dynamic-Hedge-Ratio-Between-ETF-Pairs-Using-the-Kalman-Filter/](https://www.quantstart.com/articles/Dynamic-Hedge-Ratio-Between-ETF-Pairs-Using-the-Kalman-Filter/) +40. Kelly Criterion Optimization. : r/quant \- Reddit, accesso eseguito il giorno maggio 27, 2026, [https://www.reddit.com/r/quant/comments/1qvvtg9/kelly\_criterion\_optimization/](https://www.reddit.com/r/quant/comments/1qvvtg9/kelly_criterion_optimization/) +41. Kalman Filter-Based Pairs Trading Strategy In QSTrader \- QuantStart, accesso eseguito il giorno maggio 27, 2026, [https://www.quantstart.com/articles/kalman-filter-based-pairs-trading-strategy-in-qstrader/](https://www.quantstart.com/articles/kalman-filter-based-pairs-trading-strategy-in-qstrader/) +42. Implementing a Kalman Filter-Based Trading Strategy | by Serdar İlarslan \- Medium, accesso eseguito il giorno maggio 27, 2026, [https://medium.com/@serdarilarslan/implementing-a-kalman-filter-based-trading-strategy-8dec764d738e](https://medium.com/@serdarilarslan/implementing-a-kalman-filter-based-trading-strategy-8dec764d738e) +43. Optimal High-Frequency Market Making \- Stanford University, accesso eseguito il giorno maggio 27, 2026, [https://stanford.edu/class/msande448/2018/Final/Reports/gr5.pdf](https://stanford.edu/class/msande448/2018/Final/Reports/gr5.pdf) +44. High Frequency Trading in a Limit Order Book \- ResearchGate, accesso eseguito il giorno maggio 27, 2026, [https://www.researchgate.net/publication/24086205\_High\_Frequency\_Trading\_in\_a\_Limit\_Order\_Book](https://www.researchgate.net/publication/24086205_High_Frequency_Trading_in_a_Limit_Order_Book) +45. What messaging system can handle sub millisecond latency for trading signals? \- Reddit, accesso eseguito il giorno maggio 27, 2026, [https://www.reddit.com/r/golang/comments/1q5230j/what\_messaging\_system\_can\_handle\_sub\_millisecond/](https://www.reddit.com/r/golang/comments/1q5230j/what_messaging_system_can_handle_sub_millisecond/) +46. Quant Radio: Fast Trend Following with Kalman Filters \- YouTube, accesso eseguito il giorno maggio 27, 2026, [https://www.youtube.com/watch?v=Q2y2ORL-PiY](https://www.youtube.com/watch?v=Q2y2ORL-PiY) +47. What is ZMQ? Competitors, Complementary Techs & Usage | Sumble, accesso eseguito il giorno maggio 27, 2026, [https://sumble.com/tech/zmq](https://sumble.com/tech/zmq) +48. Get started \- ZeroMQ, accesso eseguito il giorno maggio 27, 2026, [https://zeromq.org/get-started/](https://zeromq.org/get-started/) +49. How OpenAlgo WebSocket Works, accesso eseguito il giorno maggio 27, 2026, [https://blog.openalgo.in/how-openalgo-websocket-works-8c5e61b71d06](https://blog.openalgo.in/how-openalgo-websocket-works-8c5e61b71d06) +50. PowerPoint Presentation, accesso eseguito il giorno maggio 27, 2026, [https://www.cambridge.org/us/download\_file/193163](https://www.cambridge.org/us/download_file/193163) +51. Machine-Learning-For-Finance/Regression Based Machine Learning for Algorithmic Trading/Pairs Trading with Kalman Filters.py at master \- GitHub, accesso eseguito il giorno maggio 27, 2026, [https://github.com/anthonyng2/Machine-Learning-For-Finance/blob/master/Regression%20Based%20Machine%20Learning%20for%20Algorithmic%20Trading/Pairs%20Trading%20with%20Kalman%20Filters.py](https://github.com/anthonyng2/Machine-Learning-For-Finance/blob/master/Regression%20Based%20Machine%20Learning%20for%20Algorithmic%20Trading/Pairs%20Trading%20with%20Kalman%20Filters.py) +52. GARCH Volatility Documentation \- V-Lab, accesso eseguito il giorno maggio 27, 2026, [https://vlab.stern.nyu.edu/docs/volatility/GARCH](https://vlab.stern.nyu.edu/docs/volatility/GARCH) +53. Volatility target \- Read the Docs, accesso eseguito il giorno maggio 27, 2026, [https://kundan-reads.readthedocs.io/en/latest/finance/risk\_management/volatility\_target/](https://kundan-reads.readthedocs.io/en/latest/finance/risk_management/volatility_target/) +54. Volatility targeting using delayed diffusions \- LMU München, accesso eseguito il giorno maggio 27, 2026, [https://cms-cdn.lmu.de/media/16-finmath/publikation/assessing\_tvs\_sdde.pdf](https://cms-cdn.lmu.de/media/16-finmath/publikation/assessing_tvs_sdde.pdf) +55. Quantitative Finance Investment instruments with volatility target mechanism \- SciSpace, accesso eseguito il giorno maggio 27, 2026, [https://scispace.com/pdf/investment-instruments-with-volatility-target-mechanism-v46zqewu9h.pdf](https://scispace.com/pdf/investment-instruments-with-volatility-target-mechanism-v46zqewu9h.pdf) +56. Kelly Criterion vs Fixed Fractional: Which Risk Model Maximizes Long‑Term Growth?, accesso eseguito il giorno maggio 27, 2026, [https://medium.com/@tmapendembe\_28659/kelly-criterion-vs-fixed-fractional-which-risk-model-maximizes-long-term-growth-972ecb606e6c](https://medium.com/@tmapendembe_28659/kelly-criterion-vs-fixed-fractional-which-risk-model-maximizes-long-term-growth-972ecb606e6c) +57. Kelly criterion \- Wikipedia, accesso eseguito il giorno maggio 27, 2026, [https://en.wikipedia.org/wiki/Kelly\_criterion](https://en.wikipedia.org/wiki/Kelly_criterion) +58. The Kelly Criterion \- Quantitative Trading \- Nick Yoder, accesso eseguito il giorno maggio 27, 2026, [https://nickyoder.com/kelly-criterion/](https://nickyoder.com/kelly-criterion/) +59. Kelly Criterion: From a Simple Random Walk to Lévy Processes \- USC Dornsife, accesso eseguito il giorno maggio 27, 2026, [https://dornsife.usc.edu/sergey-lototsky/wp-content/uploads/sites/211/2023/11/Kelly-Fin-SIFIN-Final.pdf](https://dornsife.usc.edu/sergey-lototsky/wp-content/uploads/sites/211/2023/11/Kelly-Fin-SIFIN-Final.pdf) +60. Mean reversion in international markets: evidence from G.A.R.C.H. and half-life volatility models \- IDEAS/RePEc, accesso eseguito il giorno maggio 27, 2026, [https://ideas.repec.org/a/taf/reroxx/v31y2018i1p1198-1217.html](https://ideas.repec.org/a/taf/reroxx/v31y2018i1p1198-1217.html) +61. Python Multiprocessing with ZeroMQ \- Tao te Tek \- WordPress.com, accesso eseguito il giorno maggio 27, 2026, [https://taotetek.wordpress.com/2011/02/02/python-multiprocessing-with-zeromq/](https://taotetek.wordpress.com/2011/02/02/python-multiprocessing-with-zeromq/) +62. 2\. Sockets and Patterns | ØMQ \- The ZeroMQ Guide, accesso eseguito il giorno maggio 27, 2026, [https://zguide.zeromq.org/docs/chapter2/](https://zguide.zeromq.org/docs/chapter2/) +63. ZeroMQ async multithreading with ROUTER and DEALER \- Stack Overflow, accesso eseguito il giorno maggio 27, 2026, [https://stackoverflow.com/questions/49329294/zeromq-async-multithreading-with-router-and-dealer](https://stackoverflow.com/questions/49329294/zeromq-async-multithreading-with-router-and-dealer) +64. Mastering Asynchronous Queues in Python: Concurrency Made Easy with asyncio | by Basant C. | Medium, accesso eseguito il giorno maggio 27, 2026, [https://medium.com/@caring\_smitten\_gerbil\_914/mastering-asynchronous-queues-in-python-concurrency-made-easy-with-asyncio-878566ef9d7d](https://medium.com/@caring_smitten_gerbil_914/mastering-asynchronous-queues-in-python-concurrency-made-easy-with-asyncio-878566ef9d7d) +65. 3\. Advanced Request-Reply Patterns | ØMQ \- ZeroMQ Guide, accesso eseguito il giorno maggio 27, 2026, [https://zguide.zeromq.org/docs/chapter3/](https://zguide.zeromq.org/docs/chapter3/) + +[image1]: + +[image2]: + +[image3]: + +[image4]: + +[image5]: + +[image6]: + +[image7]: + +[image8]: + +[image9]: + +[image10]: + +[image11]: + +[image12]: + +[image13]: + +[image14]: + +[image15]: + +[image16]: + +[image17]: + +[image18]: + +[image19]: + +[image20]: + +[image21]: + +[image22]: + +[image23]: + +[image24]: + +[image25]: + +[image26]: + +[image27]: + +[image28]: + +[image29]: + +[image30]: + +[image31]: + +[image32]: + +[image33]: + +[image34]: + +[image35]: + +[image36]: + +[image37]: + +[image38]: + +[image39]: + +[image40]: + +[image41]: + +[image42]: + +[image43]: + +[image44]: + +[image45]: + +[image46]: + +[image47]: + +[image48]: + +[image49]: + +[image50]: + +[image51]: + +[image52]: + +[image53]: + +[image54]: + +[image55]: + +[image56]: + +[image57]: + +[image58]: + +[image59]: + +[image60]: + +[image61]: + +[image62]: + +[image63]: + +[image64]: + +[image65]: + +[image66]: + +[image67]: + +[image68]: + +[image69]: + +[image70]: + +[image71]: + +[image72]: + +[image73]: + +[image74]: + +[image75]: + +[image76]: + +[image77]: + +[image78]: + +[image79]: + +[image80]: + +[image81]: + +[image82]: + +[image83]: + +[image84]: + +[image85]: + +[image86]: + +[image87]: + +[image88]: + +[image89]: + +[image90]: + +[image91]: + +[image92]: + +[image93]: + +[image94]: + +[image95]: + +[image96]: + +[image97]: + +[image98]: + +[image99]: + +[image100]: + +[image101]: + +[image102]: + +[image103]: + +[image104]: + +[image105]: + +[image106]: + +[image107]: + +[image108]: + +[image109]: + +[image110]: \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/245-trading-for-trading-api.md b/DesktopBot/Documents/Alpaca/245-trading-for-trading-api.md new file mode 100644 index 0000000..f091181 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/245-trading-for-trading-api.md @@ -0,0 +1,188 @@ +# 24/5 Trading + +### What is 24/5 Trading? + +Overnight Trading extends market hours to provide a 24-hour, 5-day-a-week trading experience for all NMS securities. The overnight session operates from 8:00 PM ET Sunday to 4:00 AM ET Friday, bridging the gap between one day's post-market session and the next day's pre-market session. + +*** + +### How does the overnight trading session work? + +Overnight trade executions and market data are facilitated by the Blue Ocean Alternative Trading System (BOATS). As an Alternative Trading System (ATS), BOATS operates an independent overnight trading session outside of traditional stock exchanges. + +*** + +### What are the hours for the 24/5 trading sessions? + +The trading sessions are structured as follows, from Sunday evening to Friday evening: + +* **Overnight Session:** 8:00 PM - 4:00 AM ET (technically occurs on the evening before the trade date) +* **Pre-Market Session:** 4:00 AM – 9:30 AM ET +* **Regular Market Session:** 9:30 AM - 4:00 PM ET +* **After-Hours Session:** 4:00 PM - 8:00 PM ET + +The overnight session follows the NYSE holiday calendar. For example, if there is a market holiday on a Friday, the overnight session will not run on Thursday evening. However, the overnight session runs for a full 8 hours on market half-days. + +*** + +### How can I enable overnight trading? + +Trading API accounts are enabled for 24/5 trading by default. If needed, you can disable trading in the overnight session. + +*** + +### How can I access market data for the overnight session? + +Alpaca’s 24/5 trading includes overnight market access, which is powered by two data sources: the Blue Ocean Alternative Trading System (BOATS) and Overnight. Which feed you use depends on your [market data plan](https://alpaca.markets/data). + +Overnight market data is available between 8:00 PM and 4:00 AM ET and supports the following data types: Bars, Quotes, Trades, and Snapshots. + +**Market data plans and feeds** + +Algo Trader Plus Plan + +* Use feed=`boats` for: + * [Latest Quotes](https://docs.alpaca.markets/reference/stocklatestquotes-1) and [Latest Trades](https://docs.alpaca.markets/reference/stocklatesttrades-1), [Snapshots](https://docs.alpaca.markets/reference/stocksnapshotsingle) + * [Historical Bars](https://docs.alpaca.markets/reference/stockbarsingle-1), [Quotes](https://docs.alpaca.markets/reference/stockquotes-1), and [Trades](https://docs.alpaca.markets/reference/stocktrades-1) + +Free Plan + +* Use feed=`overnight` for: + * [Latest Bars](https://docs.alpaca.markets/reference/stocklatestbars-1), + * [Latest Quotes](https://docs.alpaca.markets/reference/stocklatestquotes-1) (real-time indicative), + * [Latest Trades](https://docs.alpaca.markets/reference/stocklatesttrades-1) (15-minute delayed), + * [Snapshots](https://docs.alpaca.markets/reference/stocksnapshots-1) +* Use feed=`boats` for + * [Historical Bars](https://docs.alpaca.markets/reference/stockbarsingle-1), [Quotes](https://docs.alpaca.markets/reference/stockquotes-1), and [Trades](https://docs.alpaca.markets/reference/stocktrades-1) (all 15-minute delayed BOATS data) + +You can select the feed by setting the feed parameter on the Bars, Quotes, Trades, and Snapshots endpoints. + +See the [API documentation for “DataFeed”](https://alpaca.markets/sdks/python/api_reference/data/enums.html#alpaca.data.enums.DataFeed) for details. + +*** + +### What securities are available for overnight trading? + +All National Market System (NMS) securities are available for trading during the overnight session. Assets tradable in the overnight session can be identified via the `overnight_tradable` attribute in the Assets API. The list may be limited due to compliance or risk procedures, and the best way to validate the available securities is by using our Assets API as outlined above. OTC securities are not part of the NMS and are therefore not available. + +*** + +### What order types and time-in-force (TIF) options are supported? + +* **Order Type:** Only `limit` orders are supported during the overnight session. +* **Time-in-Force (TIF):** The only TIF currently supported is `day` and `gtc`. + * `day` orders placed during the overnight session will remain active through the regular and after-hours sessions of the upcoming trading day. If unfilled, the order is canceled at 8:00 PM ET. + * `gtc` (Good-Til-Canceled) orders will expire 90 days after the order is placed. In the case of corporate actions events, the `gtc` order will be cancelled prior to the corporate action event. + +*** + +### Is fractional share trading supported? + +Yes, fractional share trading is supported during the overnight session and functions the same way as it does during other extended-hours sessions. + +*** + +### Are there restrictions on margin or buying power during the overnight session? + +Yes. Day Trading Buying Power (DTBP) does not apply to the overnight session. This means 2x multiplier is the max margin buying power for overnight session trades. If an account uses DTBP for an order during the post market session, the order might be rejected (at 8 PM ET) due to insufficient buying power (if reg\_t or non-DT buying power is insufficient). + +*** + +### How does overnight trading impact Pattern Day Trader (PDT) rules? + +A day trade is defined as buying and selling the same security on the same calendar day. Trades executed during the overnight session are assigned a trade date based on when they occur: + +* Trades between **8:00 PM and 11:59 PM ET** are assigned the next day's trade date (T+1). +* Trades between **12:00 AM and 4:00 AM ET** are assigned the current day's trade date (T). + +Therefore, a position opened during the overnight session and closed during the regular hours of the same assigned trade date would count as a day trade. + +*** + +### What happens to an order that is not filled during the overnight session? + +If your `day` order is not filled during the overnight session, it will automatically carry over into the pre-market, regular, and after-hours sessions for that trade date. It remains an active order until it is executed or the market closes at 8:00 PM ET. + +*** + +### How does trade settlement work for overnight trades? + +The overnight session marks the beginning of a new trading day. Trades are assigned a date based on their execution time and settle on a T+1 basis from that date. + +* **Example:** A trade executed at 9:00 PM ET on a Monday is assigned a trade date of Tuesday and will settle on Wednesday (T+1). + +*** + +### How do corporate actions affect overnight trading? + +A security may be halted from trading during the overnight session while a pending corporate action is being processed. Furthermore, due to the way trade dates are assigned, purchasing a stock during the overnight session on its ex-dividend date will not entitle you to that stock's dividend. + +*** + +### What happens when a security is halted during the overnight session? + +If an asset is halted overnight (e.g., due to a corporate action or pending news), the halt typically persists for the entire session. Orders submitted for a halted security will be accepted by the system with a status of `accepted` but will be held in a pending state. This ensures your order can be executed as soon as the halt is lifted and trading resumes in the next session. + +*** + +### What API changes were made? + +Two new boolean attributes have been added to the Assets API to support 24/5 trading: + +When retrieving assets from the Assets API, you’ll see the following new attributes: + +* `overnight_tradable`: Indicates whether an asset is eligible for overnight trading. +* `overnight_halted`: Indicates whether an otherwise eligible asset is temporarily halted from overnight trading, such as during a corporate action. + +In addition, `boats` and `overnight` data feeds have been introduced in the Market Data API. + +*** + +### How can I identify which assets are tradable overnight via the API? + +You can identify eligible assets by calling the `v1/assets` endpoint and filtering for assets where the `overnight_tradable` attribute is `true`. + +*** + +### When is the best time to sync the list of overnight-tradable assets? + +We begin syncing our list of tradable assets at 7:05 PM ET and run updates every 10 minutes until 7:45 PM ET. For the most up-to-date list, we recommend syncing your asset list between 7:45 PM and 8:00 PM ET, with 7:55 PM ET being the ideal time. + +*** + +### Can I access delayed historical overnight requests with an overnight subscription? + +Yes, you can access delayed historical overnight requests with an overnight subscription, provided that the end parameter in your request is at least 15 minutes old. + +**To access delayed historical overnight data, make sure to include the parameter `feed=boats` in your request, `feed=overnight` will give error.** + +Example: +If you have an overnight subscription and want to request overnight/BOATS data ending at 10:00 PM EST , you can do so any time after 10:15 PM by specifying`feed=boats` in your request. + +*** + +#### + +#### Disclosures + +The content of this article is for general information only and is believed to be accurate as of the posting date, but may be subject to change. Alpaca does not provide investment, tax, or legal advice. Please consult your own independent advisor as to any investment, tax, or legal statements made herein. + +Orders placed outside regular trading hours (9:30 a.m. – 4:00 p.m. ET) may experience price fluctuations, partial executions, or delays due to lower liquidity and higher volatility.  + +Orders not designated for extended hours execution will be queued for the next trading session. + +Additionally, fractional trading may be limited during extended hours. For more details, please review [Alpaca Extended Hours & Overnight Trading Risk Disclosure](https://files.alpaca.markets/disclosures/library/ExtHrsOvernightRisk.pdf). + +Fractional share trading allows a customer to buy and sell fractional share quantities and dollar amounts of certain securities. Fractional share trading presents unique risks and is subject to particular limitations that you should be aware of before engaging in such activity. See Alpaca Customer Agreement at [Alpaca - Disclosures and Agreements](https://alpaca.markets/disclosures) for more details. + +Margin trading involves significant risk and is not suitable for all investors. Before considering a margin loan, it is crucial that you carefully consider how borrowing fits with your investment objectives and risk tolerance. + +When trading on margin, you assume higher market risk, and potential losses can exceed the collateral value in your account. Alpaca may sell any securities in your account, without prior notice, to satisfy a margin call. Alpaca may also change its “house” maintenance margin requirements at any time without advance written notice. You are not entitled to an extension of time on a margin call. Please review the Firm’s [Margin Disclosure Statement](https://files.alpaca.markets/disclosures/library/MarginDiscStmt.pdf) before investing. + +All investments involve risk, and the past performance of a security, or financial product does not guarantee future results or returns. There is no guarantee that any investment strategy will achieve its objectives. Please note that diversification does not ensure a profit, or protect against loss. There is always the potential of losing money when you invest in securities, or other financial products. Investors should consider their investment objectives and risks carefully before investing. + +Securities brokerage services are provided by Alpaca Securities LLC ("Alpaca Securities"), member [FINRA](https://www.finra.org/)/[SIPC](https://www.sipc.org/), a wholly-owned subsidiary of AlpacaDB, Inc. Technology and services are offered by AlpacaDB, Inc. + +Cryptocurrency services are made available by Alpaca Crypto LLC ("Alpaca Crypto"), a FinCEN registered money services business (NMLS # 2160858), and a wholly-owned subsidiary of AlpacaDB, Inc. Alpaca Crypto is not a member of SIPC or FINRA. Cryptocurrencies are not stocks and your cryptocurrency investments are not protected by either FDIC or SIPC. Please see the [Disclosure Library](https://alpaca.markets/disclosures) for more information. + +This is not an offer, solicitation of an offer, or advice to buy or sell securities or cryptocurrencies or open a brokerage account or cryptocurrency account in any jurisdiction where Alpaca Securities or Alpaca Crypto, respectively, are not registered or licensed, as applicable. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/5f36ea05-b1b4-5fd1-91b5-ef51db361a4b.md b/DesktopBot/Documents/Alpaca/5f36ea05-b1b4-5fd1-91b5-ef51db361a4b.md new file mode 100644 index 0000000..321a4b4 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/5f36ea05-b1b4-5fd1-91b5-ef51db361a4b.md @@ -0,0 +1,15 @@ +# About FIX API + +Alpaca FIX (Financial Information eXchange) API + +# Introduction + +Welcome to the Alpaca FIX (Financial Information eXchange) API! The purpose of this document is to show how the FIX messaging protocol can be used to place orders, receive order updates and trades, replace orders and cancel orders. + +## Overview + +Our API is based on FIX 4.2 specification. Please reach out to our support for FIX credentials. + +## Hours of Operation + +FIX connectivity is available between \<3:30a-4a> and \<8p-8:15p> New York time, Monday through Friday for equities trading. Any changes or maintenance activities will be notified via email. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/about-alpaca.md b/DesktopBot/Documents/Alpaca/about-alpaca.md new file mode 100644 index 0000000..8b46b0b --- /dev/null +++ b/DesktopBot/Documents/Alpaca/about-alpaca.md @@ -0,0 +1,25 @@ +# About Alpaca + +# About Alpaca + +## History & Founders + +Alpaca is a technology company headquartered in Silicon Valley that builds a simple and modern API for stock and crypto trading. Our Brokerage services are provided by Alpaca Securities LLC, a member of [FINRA](https://www.finra.org/#/)/[SIPC](https://www.sipc.org/). We are a team of diverse background individuals with deep financial and technology expertise, backed by some of the top investors in the industry globally. We are proud to be supported by the love of enthusiastic community members on various platforms. + +Alpaca’s globally distributed team consists of developers, traders, and brokerage business specialists, who collectively have decades of financial services and technology industry experience at organizations such as FINRA, Apple, Wealthfront, Robinhood, EMC, Cloudera, JP Morgan, and Lehman Brothers. Alpaca is co-founded and led by [Yoshi Yokokawa](https://www.linkedin.com/in/yoshiyokokawa/) (CEO) and [Hitoshi Harada](https://www.linkedin.com/in/hitoshi-harada-02b01425/) (CPO). + +Our investors include a group of well-capitalized investors including Portage Ventures, Spark Capital, Tribe Capital, Social Leverage, Horizons Ventures, Elderidge, and Y Combinator as well as highly experienced industry angel investors such as Joshua S. Levine (former CTO/COO of ETRADE), Nate Rodland (former COO of Robinhood & GP of Elefund), Patrick O’Shaughnessy (“Invest Like the Best” podcast host & Partner of Positive Sum), Jacqueline Reses (former Executive Chairman of Square Financial Services), Asiff Hirjii (former President/COO of Coinbase), Aaron Levie (CEO of Box), and founders of leading Fintech companies including Plaid and Wealthsimple. + +## Vision + +> ## Allow 8+ billion people on the planet to access financial markets. + +We are committed to providing a secure, reliable and compliant platform for anyone who wants to build their own trading strategies, asset management automation, trading and robo-advisor apps, new brokerage services, investment advisory services and investment products. We value having a variety of developers who create exciting and innovative projects with our API. + +## What Services Does Alpaca Provide? + +Alpaca provides trading and clearing services for US equities under its subsidiary Alpaca Securities LLC. We currently support stocks, ETFs listed in the US public exchanges (NMS stocks), Options trading, and cryptocurrencies. Support for other asset classes, such as futures, FX, private equities, and international equities are on our roadmap. + +We work with retail traders and institutional investors directly, and also work with app developers, broker-dealers globally, investment advisors and fintech companies that offer US stock investing features to their end customers, both in and out of the United States. + +To serve our customers, we provide Web API, Web dashboards, market data, paper trading simulation, API sandbox environment, and community platforms. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/about-connect-api.md b/DesktopBot/Documents/Alpaca/about-connect-api.md new file mode 100644 index 0000000..563c1b0 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/about-connect-api.md @@ -0,0 +1,58 @@ +# About Connect API + +Develop applications on Alpaca’s platform using OAuth2. Let 10M+ users with an Alpaca brokerage account connect to your app. + +Alpaca’s OAuth allows you to seamlessly integrate financial markets into your application and expand your audience to millions of brokerage accounts on Alpaca’s platform + +Read Register Your App to learn how you can register your app. In addition, you can visit OAuth Integration Guide to learn more about using OAuth to connect your applications with Alpaca. + +# Broker Partners + +Broker partners are able to create their own OAuth service. Allow your end users to use OAuth apps like TradingView through your Broker API application. Learn more about OAuth with Broker API in the [Broker API reference](https://docs.alpaca.markets/docs/about-broker-api) + +
+ +# Non-Registered and Fintech Partners + +Alpaca Connect allows non-registered firms, developers and fintech companies to build trading apps on Alpaca’s platform. Using OAuth, your application can connect to Alpaca brokerage accounts . Apps ranging from algorithmic trading and charting to crypto and more are published on the Alpaca Connect Marketplace once they are approved by Alpaca Compliance. + +
+ +# Terms of Access and Use + +* You must read the terms and register in order to connect and use Alpaca’s APIs +* All API clients must authenticate with OAuth 2.0 +* You may not imply that the app was developed by Alpaca. +* If you are building a commercial application that makes money (including ads, in-app purchases, etc), you must disclose it in the registration form and receive written approval. +* To allow live trading for other users, the app needs to be approved by Alpaca. Please create your application via the Alpaca Dashboard under the [Alpaca Connect](https://app.alpaca.markets/connect) tab. For further details, please review [Registering Your App](https://docs.alpaca.markets/update/docs/registering-your-app) +* For any additional questions, please reach out to [Support@alpaca.markets](mailto:Support@alpaca.markets) + +
+ +> ❗️ This is not an offer, solicitation of an offer, or advice to open a brokerage account. +> +> Disclosure can be found [here](https://alpaca.markets/#disclosures) + +# FAQs + +### Q: What can an OAuth app do? + +A: OAuth allows you to manage your end-user’s Alpaca brokerage account on their behalf. This means you can create many types of financial services including automated investing, portfolio analytics and much more. + +### Q: Should I use OAuth or Broker API? + +A: OAuth allows you to expand your audience to users with Alpaca brokerage accounts. On the other hand, Broker API allows you to build an application fully within your environment. Users sign up for a brokerage account under your application. If you want to create your own brokerage, automated investment app, or any app where you want to own your users, use the Broker API. If you want to build your trading service on Alpaca’s platform, use OAuth. + +### Q: How secure is OAuth? + +A: OAuth2 itself is very secure. However you must make sure to follow good practices in how you handle tokens. Make sure to never publicly expose your client secret and access tokens. + +### Q: How to get OAuth App live? + +A: You will need to register your app in the OAuth apps section of the dashboard. Learn more about [Registering Your App](https://docs.alpaca.markets/update/docs/registering-your-app) + +### Q: I’m developing an app/service targeting non-US users. Can we integrate with Alpaca’s OAuth API? + +A: Alpaca’s platform supports brokerage accounts for international users. When you build an app on OAuth, all users on Alpaca’s platform will be able to use your service, including international users. + +
\ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/about-market-data-api.md b/DesktopBot/Documents/Alpaca/about-market-data-api.md new file mode 100644 index 0000000..232a700 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/about-market-data-api.md @@ -0,0 +1,117 @@ +# About Market Data API + +Gain seamless access to a wealth of data with Alpaca Market Data API, offering real-time and historical information for equities, options, crypto and more. + +# Overview + +The Market Data API offers seamless access to market data through both HTTP and WebSocket protocols. With a focus on historical and real-time data, developers can efficiently integrate these APIs into their applications. + +To simplify the integration process, we provide user-friendly SDKs in [Python](https://github.com/alpacahq/alpaca-py), [Go](https://github.com/alpacahq/alpaca-trade-api-go), [NodeJS](https://github.com/alpacahq/alpaca-trade-api-js), and [C#](https://github.com/alpacahq/alpaca-trade-api-csharp). These SDKs offer comprehensive functionalities, making it easier for developers to work with the Market Data APIs & Web Sockets. + +To experiment with the APIs, developers can try them with [Postman](https://www.postman.com/): either through the [public workspace on Postman](https://www.postman.com/alpacamarkets) or directly from our [GitHub repository](https://github.com/alpacahq/alpaca-postman). + +By leveraging Alpaca Market Data API and its associated SDKs, developers can seamlessly incorporate historical and real-time market data into their applications, enabling them to build powerful and data-driven financial products. + +# Subscription Plans + +Alpaca offers two distinct subscription models depending on how you access our platform: + +* **Trading API:** For individual traders using Alpaca's trading platform directly +* **Broker API:** For broker partners building their own trading platforms on top of Alpaca's infrastructure + + + Which one applies to me? + + If you signed up for an Alpaca account to trade or build a personal trading app, you're using the Trading API. If you're a business integrating Alpaca as your backend brokerage provider, you're using the Broker API. + + +## Trading API Subscriptions + +For individual traders and developers using Alpaca's Trading API, we offer two subscription plans: **Basic** and **Algo Trader Plus**. + +The **Basic** plan serves as the default option for both Paper and Live trading accounts, ensuring all users can access essential data with zero cost. However, this plan only includes limited real-time data: for equities only the IEX exchange, for options only the indicative feed. For advanced traders we recommend subscribing to **Algo Trader Plus** which includes complete market coverage for stocks and options as well. + +### Equities + +| | Basic | Algo Trader Plus | +| :--------------------------- | :---------------- | :--------------------- | +| Pricing | Free | $99 / month | +| Securities coverage | US Stocks & ETFs | US Stocks & ETFs | +| Real-time market coverage | IEX | All US Stock Exchanges | +| Websocket subscriptions | 30 symbols | Unlimited | +| Historical data timeframe | Since 2016 | Since 2016 | +| Historical data limitation\* | latest 15 minutes | no restriction | +| Historical API calls | 200 / min | 10,000 / min | + +Our data sources are directly fed by the CTA (Consolidated Tape Association), which is administered by NYSE (New York Stock Exchange), and the UTP (Unlisted Trading Privileges) stream, which is administered by Nasdaq. The synergy of these two sources ensures comprehensive market coverage, encompassing 100% of market volume. + +### Options + +| | Basic | Algo Trader Plus | +| :--------------------------- | :---------------------- | :-------------------- | +| Securities coverage | US Options Securities | US Options Securities | +| Real-time market coverage | Indicative Pricing Feed | OPRA Feed | +| Websocket subscriptions | 200 quotes | 1000 quotes | +| Historical data limitation\* | latest 15 minutes | no restriction | +| Historical API calls | 200 / min | 10,000 / min | + +Our options data sources are directly fed by OPRA (Options Price Reporting Authority). + +## Broker API Subscriptions + +For broker partners integrating Alpaca's Broker API, we offer tiered subscription plans designed for higher-volume, multi-user platforms. + +For equities, the below subscription plans are available. + +| Subscription Name | RPM (Request Per Minute) | Stream Connection Limit | Stream Symbol Limit | Price (per month) | Options Indicative Feed | +| :---------------- | :----------------------- | :---------------------- | :------------------ | :---------------- | :-------------------------- | +| Standard | 1,000 | 5 | unlimited | included | additional $1,000 per month | +| StandardPlus3000 | 3,000 | 5 | unlimited | $500 | additional $1,000 per month | +| StandardPlus5000 | 5,000 | 5 | unlimited | $1,000 | included | +| StandardPlus10000 | 10,000 | 10 | unlimited | $2,000 | included | + +Note: Standard subscription plans will only be active when integration starts. Prior to that, the account will be on the Basic plan listed above. Additionally, similar to the free plan all the standard plans are real time IEX or 15 mins delayed SIP. + +For partners on the Standard and StandardPlus3000 plans, an additional subscription fee of $1,000 / month enables access to the same equities plan for options. For StandardPlus5000 and StandardPlus10000 plans, options are included. + + + We offer custom pricing and tailored solutions for Broker API partners seeking to leverage our comprehensive market data. Our goal is to meet the specific needs and requirements of our valued partners, ensuring they have access to the data and tools necessary to enhance their services and provide exceptional value to their customers. If none of the subscription plans listed above are believed to be suitable, kindly reach out to our [sales team](https://alpaca.markets/contact). + + +# Authentication + +With the exception of historical crypto data, all market data endpoints require authentication. Authentication differs between the Trading & Broker API. API keys can be acquired in the web UI (under the API keys on the right sidebar). + +### Trading API + +You should authenticate by passing the key / secret pair in the HTTP request headers named: + +* `APCA-API-KEY-ID` +* `APCA-API-SECRET-KEY` + +### Broker API + +Broker API uses the [Client Credentials](https://docs.alpaca.markets/docs/authentication#client-credentials) authentication flow. You must first exchange your credentials for a short-lived access token, then use that token to authenticate API requests. + +* Step 1: Request an access token + +```curl +curl -X POST "https://authx.alpaca.markets/v1/oauth2/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id={YOUR_CLIENT_ID}" \ + -d "client_secret={YOUR_CLIENT_SECRET}" +``` + +* Step 2: Use the token to authenticate + +```curl +curl -X GET "https://data.alpaca.markets/v2/..." \ + -H "Authorization: Bearer {TOKEN}" +``` + + + Note: Access tokens are valid for 15 minutes. Do not request a new token for each API call. + + +For the WebSocket stream authentication, kindly refer to the [WebSocket Stream documentation](https://docs.alpaca.markets/docs/streaming-market-data#authentication). \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/account-activities.md b/DesktopBot/Documents/Alpaca/account-activities.md new file mode 100644 index 0000000..3e77eab --- /dev/null +++ b/DesktopBot/Documents/Alpaca/account-activities.md @@ -0,0 +1,117 @@ +# Account Activities + +The account activities API provides access to a historical record of transaction activities that have impacted your account. + +# The TradeActivity Object + +## Sample TradeActivity + +```json +{ + "activity_type": "FILL", + "cum_qty": "1", + "id": "20190524113406977::8efc7b9a-8b2b-4000-9955-d36e7db0df74", + "leaves_qty": "0", + "price": "1.63", + "qty": "1", + "side": "buy", + "symbol": "LPCN", + "transaction_time": "2019-05-24T15:34:06.977Z", + "order_id": "904837e3-3b76-47ec-b432-046db621571b", + "type": "fill" +} +``` + +## Properties + +| Attribute | Type | Description | +| :----------------- | :----------------- | :-------------------------------------------------------------------------------------------------------------------------- | +| `activity_type` | string | For trade activities, this will always be `FILL` | +| `cum_qty` | string\ | The cumulative quantity of shares involved in the execution. | +| `id` | string | An ID for the activity. Always in `::` format. Can be sent as `page_token` in requests to facilitate the paging of results. | +| `leaves_qty` | string\ | For `partially_filled` orders, the quantity of shares that are left to be filled. | +| `price` | string\ | The per-share price that the trade was executed at. | +| `qty` | string\ | The number of shares involved in the trade execution. | +| `side` | string | `buy` or `sell` | +| `symbol` | string | The symbol of the security being traded. | +| `transaction_time` | string\ | The time at which the execution occurred. | +| `order_id` | string\ | The id for the order that filled. | +| `type` | string | `fill` or `partial_fill` | + +# The NonTradeActivity (NTA) Object + +## Sample NTA + +```json +{ + "activity_type": "DIV", + "id": "20190801011955195::5f596936-6f23-4cef-bdf1-3806aae57dbf", + "date": "2019-08-01", + "net_amount": "1.02", + "symbol": "T", + "cusip": "C00206R102", + "qty": "2", + "per_share_amount": "0.51" +} +``` + +## Properties + +| Attribute | Type | Description | +| :----------------- | :----------------- | :-------------------------------------------------------------------------------------------------------------------------- | +| `activity_type` | string | See below for a list of possible values. | +| `id` | string | An ID for the activity. Always in `::` format. Can be sent as `page_token` in requests to facilitate the paging of results. | +| `date` | string\ | The date on which the activity occurred or on which the transaction associated with the activity settled. | +| `net_amount` | string\ | The net amount of money (positive or negative) associated with the activity. | +| `symbol` | string | The symbol of the security involved with the activity. Not present for all activity types. | +| `cusip` | string | The CUSIP of the security involved with the activity. Not present for all activity types. | +| `qty` | string\ | For dividend activities, the number of shares that contributed to the payment. Not present for other activity types. | +| `per_share_amount` | string\ | For dividend activities, the average amount paid per share. Not present for other activity types. | + +# Pagination of Results + +Pagination is handled using the `page_token` and `page_size` parameters. + +`page_token` represents the ID of the end of your current page of results. If specified with a direction of desc, for example, the results will end before the activity with the specified ID. If specified with a direction of `asc`, results will begin with the activity immediately after the one specified. `page_size` is the maximum number of entries to return in the response. If `date` is not specified, the default and maximum value is 100. If `date` is specified, the default behavior is to return all results, and there is no maximum page size. + +# Activity Types + +| `activity_type` | Description | +| :-------------- | :------------------------------------------------------------------------------------------ | +| `FILL` | Order fills (both partial and full fills) | +| `TRANS` | Cash transactions (both CSD and CSW) | +| `MISC` | Miscellaneous or rarely used activity types (All types except those in TRANS, DIV, or FILL) | +| `ACATC` | ACATS IN/OUT (Cash) | +| `ACATS` | ACATS IN/OUT (Securities) | +| `CFEE` | Crypto fee | +| `CGD` | Capital Gain Distribution | +| `CSD` | Cash deposit(+) | +| `CSW` | Cash withdrawal(-) | +| `DIV` | Dividends | +| `DIVCGL` | Dividend (capital gain long term) | +| `DIVCGS` | Dividend (capital gain short term) | +| `DIVFEE` | Dividend fee | +| `DIVFT` | Dividend adjusted (Foreign Tax Withheld) | +| `DIVNRA` | Dividend adjusted (NRA Withheld) | +| `DIVROC` | Dividend return of capital | +| `DIVTW` | Dividend adjusted (Tefra Withheld) | +| `DIVTXEX` | Dividend (tax exempt) | +| `FEE` | Fee denominated in USD | +| `FOPT` | Free of Payment Transfer | +| `INT` | Interest (credit/margin) | +| `INTNRA` | Interest adjusted (NRA Withheld) | +| `INTTW` | Interest adjusted (Tefra Withheld) | +| `JNL` | Journal entry | +| `JNLC` | Journal entry (cash) | +| `JNLS` | Journal entry (stock) | +| `MA` | Merger/Acquisition | +| `NC` | Name change | +| `OPASN` | Option assignment | +| `OPEXP` | Option expiration | +| `OPXRC` | Option exercise | +| `PTC` | Pass Thru Charge | +| `PTR` | Pass Thru Rebate | +| `REORG` | Reorg CA | +| `SC` | Symbol change | +| `SSO` | Stock spinoff | +| `SSP` | Stock split | \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/account-plans.md b/DesktopBot/Documents/Alpaca/account-plans.md new file mode 100644 index 0000000..c9bccca --- /dev/null +++ b/DesktopBot/Documents/Alpaca/account-plans.md @@ -0,0 +1,545 @@ +# Trading Account + +# Alpaca Brokerage Account (Live Trading) + +After creating an Alpaca Paper Only Account, you can enable live trading by becoming an Alpaca Brokerage Account holder. This requires you to go through an account on-boarding process with Alpaca Securities LLC, a FINRA member and SEC registered broker-dealer. We now support brokerage accounts for individuals and business entities from around the world. + +With a brokerage account, you will be able to fully utilize Alpaca for your automated trading and investing needs. Using the Alpaca API, you’ll be able to buy and sell stocks in your brokerage account, and you’ll receive real-time consolidated market data. In addition, you will continue to be able to test your strategies and simulate your trades in our paper trading environment. And with the Alpaca web dashboard, it’s easy to monitor both your paper trading and your real money brokerage account. All accounts are opened as margin accounts. Accounts with $2,000 or more equity will have access to margin trading and short selling. + +## Individuals + +Alpaca Securities LLC supports individual taxable brokerage accounts. At this time, we do not support retirement accounts. + +## Businesses/Incorporated Entities + +You can open a business trading account to use Alpaca for trading purposes, but not for building apps/services. + + + ### Alpaca currently accepts entities that are *Corporations*, *LLCs* and *Partnerships* in the U.S., and around the world. There is a $30,000 minimum deposit required for opening a business account at Alpaca. + + +# Markets Supported + +Currently, Alpaca only supports trading of listed U.S. stocks and select cryptocurrencies. + +# The Account Object + +The account API serves important information related to an account, including account status, funds available for trade, funds available for withdrawal, and various flags relevant to an account’s ability to trade. + +An account maybe be blocked for just for trades (`trading_blocked` flag) or for both trades and transfers (`account_blocked` flag) if Alpaca identifies the account to be engaging in any suspicious activity. Also, in accordance with FINRA’s pattern day trading rule, an account may be flagged for pattern day trading (`pattern_day_trader` flag), which would inhibit an account from placing any further day-trades. + +Please note that cryptocurrencies are not eligible assets to be used as collateral for margin accounts and will require the asset be traded using cash only. + +## Sample Object + +```json +{ + "account_blocked": false, + "account_number": "010203ABCD", + "buying_power": "262113.632", + "cash": "-23140.2", + "created_at": "2019-06-12T22:47:07.99658Z", + "currency": "USD", + "crypto_status": "ACTIVE", + "non_marginable_buying_power": "7386.56", + "accrued_fees": "0", + "pending_transfer_in": "0", + "pending_transfer_out": "0", + "daytrade_count": "0", + "daytrading_buying_power": "262113.632", + "equity": "103820.56", + "id": "e6fe16f3-64a4-4921-8928-cadf02f92f98", + "initial_margin": "63480.38", + "last_equity": "103529.24", + "last_maintenance_margin": "38000.832", + "long_market_value": "126960.76", + "maintenance_margin": "38088.228", + "multiplier": "4", + "pattern_day_trader": false, + "portfolio_value": "103820.56", + "regt_buying_power": "80680.36", + "short_market_value": "0", + "shorting_enabled": true, + "sma": "0", + "status": "ACTIVE", + "trade_suspended_by_user": false, + "trading_blocked": false, + "transfers_blocked": false +} +``` + +## Account Properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Attribute + + Type + + Description +
+ `id` + + string`` + + Account ID. +
+ `account_number` + + string + + Account number. +
+ `status` + + string\ + + See detailed account statuses below +
+ `crypto_status` + + string\ + + The current status of the crypto enablement. See detailed crypto statuses below. +
+ `currency` + + string + + "USD" +
+ `cash` + + string`` + + Cash balance +
+ `portfolio_value` + + string`` + + * *lpaca Broker*\* Total value of cash + holding positions (Equivalent to the equity field) +
+ `non_marginable_buying_power` + + string`` + + Current available non-margin dollar buying power +
+ `accrued_fees` + + string`` + + The fees collected. +
+ `pending_transfer_in` + + string`` + + Cash pending transfer in. +
+ `pending_transfer_out` + + string`` + + Cash pending transfer out +
+ `pattern_day_trader` + + boolean + + Whether or not the account has been flagged as a pattern day trader +
+ `trade_suspended_by_user` + + boolean + + User setting. If `true`, the account is not allowed to place orders. +
+ `trading_blocked` + + boolean + + If `true`, the account is not allowed to place orders. +
+ `transfers_blocked` + + boolean + + If `true`, the account is not allowed to request money transfers. +
+ `account_blocked` + + boolean + + If `true`, the account activity by user is prohibited. +
+ `created_at` + + string`` + + Timestamp this account was created at +
+ `shorting_enabled` + + boolean + + Flag to denote whether or not the account is permitted to short +
+ `long_market_value` + + string`` + + Real-time MtM value of all long positions held in the account +
+ `short_market_value` + + string`` + + Real-time MtM value of all short positions held in the account +
+ `equity` + + string`` + + `cash` + `long_market_value` + `short_market_value` +
+ `last_equity` + + string`` + + Equity as of previous trading day at 16:00:00 ET +
+ `multiplier` + + string`` + + Buying power (BP) multiplier that represents account margin classification + + Valid values: + + * **1** (standard limited margin account with 1x BP), + * **2** (reg T margin account with 2x intraday and overnight BP; this is the default for all non-PDT accounts with $2,000 or more equity), + * **4** (PDT account with 4x intraday BP and 2x reg T overnight BP) +
+ `buying_power` + + string`` + + Current available $ buying power; If multiplier = 4, this is your daytrade buying power which is calculated as (last\_equity - (last) maintenance\_margin) *4; If multiplier = 2, buying*power = max(equity – initial\_margin,0) *2; If multiplier = 1, buying*power = cash +
+ `initial_margin` + + string`` + + Reg T initial margin requirement (continuously updated value) +
+ `maintenance_margin` + + string`` + + Maintenance margin requirement (continuously updated value) +
+ `sma` + + string`` + + Value of special memorandum account (will be used at a later date to provide additional buying\_power) +
+ `daytrade_count` + + int + + The current number of daytrades that have been made in the last 5 trading days (inclusive of today) +
+ `last_maintenance_margin` + + string`` + + Your maintenance margin requirement on the previous trading day +
+ `daytrading_buying_power` + + string`` + + Your buying power for day trades (continuously updated value) +
+ `regt_buying_power` + + string`` + + Your buying power under Regulation T (your excess equity - equity minus margin value - times your margin multiplier) +
+ +# Account Status ENUMS + +The following are the possible account status values. Most likely, the account status is `ACTIVE` unless there is an issue. The account status may get to `ACCOUNT_UPDATED` when personal information is being updated from the dashboard, in which case you may not be allowed trading for a short period of time until the change is approved. + +| status | description | +| :------------------ | :--------------------------------------------------------- | +| `ONBOARDING` | The account is onboarding. | +| `SUBMISSION_FAILED` | The account application submission failed for some reason. | +| `SUBMITTED` | The account application has been submitted for review. | +| `ACCOUNT_UPDATED` | The account information is being updated. | +| `APPROVAL_PENDING` | The final account approval is pending. | +| `ACTIVE` | The account is active for trading. | +| `REJECTED` | The account application has been rejected. | \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/additional-resources.md b/DesktopBot/Documents/Alpaca/additional-resources.md new file mode 100644 index 0000000..bc5215b --- /dev/null +++ b/DesktopBot/Documents/Alpaca/additional-resources.md @@ -0,0 +1,29 @@ +# Additional Resources + +# Alpaca Learn + +We post regular content on our Alpaca Learn resource site where you can find the latest market updates, development tools and tips and much more to help you along your journey developing with Alpaca. For more click [here](https://alpaca.markets/learn/). + +# Blog + +Dont miss a beat and find the latest updates from Alpaca in our blog. For more click [here](https://alpaca.markets/blog/). + +# Slack Community + +We have an active community on Slack with developers and traders from all over the world. For Broker API we have a dedicated channel #broker-api for you to discuss with the rest of the community building new products using Broker API. You will be automatically added to `#announcements`, `#community`, `#feedback`, and `#q-and-a`. To join click [here](https://join.slack.com/t/alpaca-community/shared_invite/zt-1mwc1kpf8-ssNEGH6IyKgAAtFaSC_GMg). + +# Forum + +We have set up a community forum to discuss topics ranging from integration, programming, API questions, market data, etc. The forum is also a place to find up-to-date announcements about Alpaca and its features. The forum is the recommended place for discussion, since it has more advanced indexing and search functionality compared to our other community channels. For more click [here](https://forum.alpaca.markets/). + +# Support + +Have questions? Need help? Check out our support page for FAQs and to get in contact with our team. For more click [here](https://alpaca.markets/support). + +# Systems Status + +Stay up to date with Alpaca services status and any updates on outages. For more click [here](https://status.alpaca.markets/). + +# Disclosures + +To view our disclosures library click [here](https://alpaca.markets/disclosures). \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/alpaca-api-platform.md b/DesktopBot/Documents/Alpaca/alpaca-api-platform.md new file mode 100644 index 0000000..e73b0b7 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/alpaca-api-platform.md @@ -0,0 +1,52 @@ +# Alpaca API Platform + +# Why API? + +Alpaca’s features to access financial markets are provided primarily via API. We believe API is the means to interact with services such as ours and innovate your business. Our API is designed to fit your needs and we continue to build what you need. + +# REST, SSE and Websockets + +Our API is primarily built in the REST style. It is a simple and powerful way to integrate with our services. + +In addition to the REST API which replies via synchronous communication, our API includes an asynchronous event API which is based on WebSocket and SSE, or [Server-Sent Events](https://html.spec.whatwg.org/multipage/server-sent-events.html). As many types of events occur in the financial markets (orders fill based on the market movement, cash settles after some time, etc), this event-based API helps you get updates instantly and provide the best user experiences to your customers. + +# Architecture + +Alpaca’s platform consists of APIs, Web dashboards, trade simulator, sandbox environment, authentication services, order management system, trading routing, back office accounting and clearing system, and all of these components are built in-house from the ground up with modern architecture. + +The Alpaca platform is currently hosted on the Google Cloud Platform in the us-east4 region. The site is connected with dedicated fiber lines to a data center in Secaucus, NJ, to cross-connect with various market venues. + +Under the hood, Alpaca works with various third parties. As we are self clearing for equities trade clearing and settlement on DTCC, we are also self clearing for options trade clearing and settlement with the OCC. Cash transfers and custody are primarily provided by BMO Harris, We use Currency Cloud and Airwallex for funding wallets and international transfers. Citadel Securities, Virtu America, Jane Street, Ion Group, and other execution providers provide execution services for our customer orders. We integrate with multiple data service providers, with ICE Data Services being our primary vendor for various kinds of market data. + +Alpaca Crypto executes customer trades on our internal central limit order book, self-clears all trades and does not custody customer cash but has banking relationships with Customers Bank, Cross River Bank, Choice Financial and FVBank. To provide live market data, Alpaca Crypto works with Coinbase, Kraken, FalconX and Stillman Digital. + +# API Updates & Upgrades + +In an effort to continuously provide value to our developers, Alpaca frequently performs updates and upgrades to our API. + +We’ve added the following sections to our docs in order to help make sure that as a developer you know what to expect, when to expect, and how to properly handle certain scenarios . + +## Backwards Compatible Changes + +You should expect any of the following kind of changes that we make to our API to be considered a backwards compatible change: + +* Adding new or similarly named APIs +* Adding new fields to already defined models and objects such as API return objects, nested objects, etc. (Example: adding a new code field to error payloads) +* Adding new items to defined sets or enumerations such as statuses, supported assets, etc. (Example: adding new account status to a set of all ) +* Enhancing ordering on how certain lists get returned +* Supporting new HTTP versions (HTTP2, QUIC) +* Adding new HTTP method(s) for an existing endpoint +* Expecting new HTTP request headers (eg. new authentication) +* Sending new HTTP headers (eg. HTTP caching headers, gzip encoding, etc.) +* Increasing HTTP limits (eg. Nginx large-client-header-buffers) +* Increasing rate limits +* Supporting additional SSL/TLS versions + +Generally, as a rule of thumb, any append or addition operation is considered a backwards compatible update and does not need an upfront communication. These updates should be backwards compatible with existing interfaces and not cause any disruption to any clients calling our APIs. + +## Breaking Changes + +When and if Alpaca decides to perform breaking changes to our APIs the following should be expected: + +* Upfront communication with sufficient time to allow developers to be able to react to new upcoming changes +* Our APIs are versioned, if breaking changes are intended we will generally bump the API version. For example, a route might go from being `/v1/accounts/{account_id}` to `/v2/accounts/{account_id}` if we had to make a breaking change to either the parameters it can take or its return structure \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/alpaca-elite-smart-router.md b/DesktopBot/Documents/Alpaca/alpaca-elite-smart-router.md new file mode 100644 index 0000000..56f486b --- /dev/null +++ b/DesktopBot/Documents/Alpaca/alpaca-elite-smart-router.md @@ -0,0 +1,448 @@ +# DMA Gateway / Advanced Order Types + +Take Control of Your Trades with Direct Market Access Gateway and Advanced Order Types + +# Elite Smart Router + +Elite Smart Router is designed to meet the needs of institutional clients and experienced algorithmic traders. A wide array of advanced investing and trading strategies are supported with higher API limits and cost-effective pricing. + +One year after launching Alpaca Elite, we are expanding the feature set of the Elite Smart Router. The two key additions are Direct Market Access (DMA) Gateway*\** and Advanced Order Types. Direct Market Access Gateway\* (DMA Gateway) gives you direct control of where your orders are sent. This, along with advanced order types like Volume-Weighted Average Price (VWAP) and Time-Weighted Average Price (TWAP), enables you to efficiently manage large orders, control execution costs, help minimize market impact, and meet your specific trading objectives. DMA Gateway, VWAP, and TWAP can only be accessed if users are on the Elite Smart Router as part of the Alpaca Elite Program. + +\*Direct Market Access Gateway is provided solely by DASH Financial Technologies ("DASH"), a member of the listed exchanges. Alpaca enables customers to route orders to the selected exchange through DASH’s DMA capabilities. + +## DMA Gateway + +DMA Gateway is a tool that gives you customization over where your trades are sent. + +Benefits: + +* Efficiently manage large orders +* Execution customization +* Help minimize market impact +* Meet your specific trading objectives + +### Implementation + +DMA orders are configured using `advanced_instructions` in your order request payload: + +```curl Submit +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "side": "buy", + "symbol": "AAPL", + "type": "limit", + "qty": "100", + "time_in_force": "day", + "limit_price": "212", + "order_class": "simple", + "advanced_instructions": { + "algorithm": "DMA", + "destination": "NYSE", + "display_qty": "100" + } +}' | jq -r +``` + +```curl Cancel +curl --request DELETE \ + --url $APIDOMAIN/v2/orders/ \ + --header 'accept: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + +``` + +#### Parameters + +| Parameter | Required | Description | Values | +| ------------- | ------------- | -------------------------------------------------------------- | -------------------------------------- | +| `algorithm` | **mandatory** | Must be set to "DMA" for Direct Market Access routing | `"DMA"` | +| `destination` | **mandatory** | Target exchange for order execution | `"NYSE"`, `"NASDAQ"`, `"ARCA"` | +| `display_qty` | optional | Maximum shares/contracts displayed on the exchange at any time | Must be in round lot increments (100s) | + +Notes: + +* Parameter replacement is not supported for DMA orders + +#### Available Destinations + +* **NYSE** - New York Stock Exchange +* **NASDAQ** - NASDAQ Stock Market +* **ARCA** - NYSE Arca + +We’re starting with the three destinations listed above, with plans to expand to 10+ additional destinations—including BATS, IEX, AMEX, and more—in the coming months. + +### Extended Hours Trading + +DMA orders support extended hours trading for the following destinations: + +* **NASDAQ** - Pre-market and after-hours sessions +* **ARCA** - Pre-market and after-hours sessions + +## VWAP: Volume-Weighted Average Price Orders + +A VWAP order is designed to execute a trade at or near the volume-weighted average price of a security over a specified time period. It is calculated by dividing the total dollar value traded for the security (price × volume) by the total volume traded during that period. + +VWAP automatically weights each trade price by its corresponding trade volume, ensuring the average reflects both price and trading activity. This makes VWAP a valuable reference for assessing execution quality and market trends. + +Benefits: + +* Market Impact Management: VWAP orders are designed to execute in proportion to market volume, which may help reduce the likelihood of large trades significantly influencing the market price. +* Benchmark Alignment: VWAP can be used as a benchmark strategy, aiming to achieve execution prices close to the volume-weighted average price over a specified time period. This approach may help align fills with average market pricing trends. + +### Implementation + +VWAP orders are configured using `advanced_instructions` in your order request payload: + +```curl Submit +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "side": "buy", + "symbol": "AAPL", + "type": "limit", + "qty": "100", + "time_in_force": "day", + "limit_price": "212", + "order_class": "simple", + "advanced_instructions": { + "algorithm": "VWAP", + "start_time": "2025-07-21T09:30:00-04:00", + "end_time": "2025-07-21T15:30:00-04:00", + "max_percentage": "0.123" + } +}' | jq -r +``` + +```curl Replace +curl --request PATCH \ + --url $APIDOMAIN/v2/orders/ \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "qty": "200", + "advanced_instructions": { + "algorithm": "VWAP", + "start_time": "2025-07-21T09:40:00-04:00", + "end_time": "2025-07-21T15:20:00-04:00", + "max_percentage": "0.321" + } +}' | jq -r +``` + +```curl Cancel +curl --request DELETE \ + --url $APIDOMAIN/v2/orders/ \ + --header 'accept: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + +``` + +Notes: + +* If `advanced_instructions` is not included in the replace payload then it will remain the same +* If `advanced_instructions` is included in the replace payload then it will replace the original one. So if the client wants to update only the `end_time` and keep the rest parameters as is, then the whole `advanced_instructions` payload needs to be sent in the replace request, including the unchanged parameters. + +#### Parameters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Parameter + + Required + + Description + + Values +
+ `algorithm` + + **mandatory** + + Must be set to "VWAP" for Volume-Weighted Average Price Orders + + `"VWAP"` +
+ `start_time` + + optional + + When the algorithm is to start executing + + `RFC3339` Timestamp, must be within current market trading hours. Defaults to start immediately or at start of the regular market hours (whichever is later). VWAP orders do NOT participate in Open Auction. +
+ `end_time` + + optional + + When the algorithm is to be done executing + + `RFC3339` Timestamp, must be within current market trading hours and after `start_time`. Defaults to end of regular market hours. VWAP orders do NOT participate in Close Auction. +
+ `max_percentage` + + optional + + Maximum percentage of the ticker's period volume this\ + order might participate in + + Decimal number, must be 0 \< `max_percentage` \< 1, with up to 3 decimal points precision. +
+ +## TWAP: Time-Weighted Average Price Orders + +A TWAP order is designed to execute a trade evenly over a specified time period, regardless of market volume. The order is divided into equal-sized trades that are placed at regular, pre-defined intervals until the order is complete. + +Benefits: + +* Reduces Market Impact: By spreading the order evenly across time, TWAP can help minimize the risk of significant price swings caused by large trades. +* Execution Predictability: Unlike VWAP, which adjusts based on market volume, TWAP may offer more consistent, evenly paced execution, which can be helpful in managing certain trading strategies. +* Effective in Low-Liquidity Environments: When volume patterns are unpredictable, TWAP can help prevent trades from disrupting the market and can help maintain price stability. + +### Implementation + +TWAP orders are configured using `advanced_instructions` in your order request payload: + +```curl Submit +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "side": "buy", + "symbol": "AAPL", + "type": "limit", + "qty": "100", + "time_in_force": "day", + "limit_price": "212", + "order_class": "simple", + "advanced_instructions": { + "algorithm": "TWAP", + "start_time": "2025-07-21T09:30:00-04:00", + "end_time": "2025-07-21T15:30:00-04:00", + "max_percentage": "0.123" + } +}' | jq -r +``` + +```curl Replace +curl --request PATCH \ + --url $APIDOMAIN/v2/orders/ \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "qty": "200", + "advanced_instructions": { + "algorithm": "TWAP", + "start_time": "2025-07-21T09:40:00-04:00", + "end_time": "2025-07-21T15:20:00-04:00", + "max_percentage": "0.321" + } +}' | jq -r +``` + +```curl Cancel +curl --request DELETE \ + --url $APIDOMAIN/v2/orders/ \ + --header 'accept: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + +``` + +Notes: + +* If `advanced_instructions` is not included in the replace payload then it will remain the same +* If `advanced_instructions` is included in the replace payload then it will replace the original one. So if the client wants to update only the `end_time` and keep the rest parameters as is, then the whole `advanced_instructions` payload needs to be sent in the replace request, including the unchanged parameters. + +#### Parameters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Parameter + + Required + + Description + + Values +
+ `algorithm` + + **mandatory** + + Must be set to "TWAP" for Time-Weighted Average Price Orders + + `"TWAP"` +
+ `start_time` + + optional + + When the algorithm is to start executing + + `RFC3339` Timestamp, must be within current market trading hours. Defaults to start immediately or at start of the regular market hours (whichever is later). TWAP orders do NOT participate in Open Auction. +
+ `end_time` + + optional + + When the algorithm is to be done executing + + `RFC3339` Timestamp, must be within current market trading hours and after `start_time`. Defaults to end of regular market hours. TWAP orders do NOT participate in Close Auction. +
+ `max_percentage` + + optional + + Maximum percentage of the ticker's period volume this\ + order might participate in + + Decimal number, must be 0 \< `max_percentage` \< 1, with up to 3 decimal points precision. +
+ +## Key Considerations: + +* `advanced_instructions` will be accepted for paper trading; however, the order will not be simulated in the paper environment. +* DMA gateway only supports market and limit orders and Time in Force (TIF) = day. If you wish to use MOO/MOC, gtc, or stop orders, you cannot specify advanced\_instructions + +*** + +*Direct Market Access Gateway is provided solely by DASH Financial Technologies ("DASH"), a member of the listed exchanges. Alpaca enables customers to route orders to the selected exchange through DASH’s DMA capabilities..* + +*Please note that this is currently only available to users who are on the[Elite Smart Router](https://alpaca.markets/elite). For more information on Alpaca Elite please see the [term and conditions](https://files.alpaca.markets/disclosures/library/Alpaca+Elite+Agreement.pdf).* + +*The content of this article is for general informational purposes only. All examples are for illustrative purposes only.* + +*All investments involve risk, and the past performance of a security, or financial product does not guarantee future results or returns. There is no guarantee that any investment strategy will achieve its objectives. Please note that diversification does not ensure a profit, or protect against loss. There is always the potential of losing money when you invest in securities, or other financial products. Investors should consider their investment objectives and risks carefully before investing.* + +*Securities brokerage services are provided by Alpaca Securities LLC ("Alpaca Securities"), member[FINRA/SIPC](https://www.finra.org/), a wholly-owned subsidiary of AlpacaDB, Inc. Technology and services are offered by AlpacaDB, Inc.* + +*This is not an offer, solicitation of an offer, or advice to buy or sell securities or open a brokerage account in any jurisdiction where Alpaca Securities are not registered or licensed, as applicable.* \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/alpaca-mcp-server.md b/DesktopBot/Documents/Alpaca/alpaca-mcp-server.md new file mode 100644 index 0000000..0c3581b --- /dev/null +++ b/DesktopBot/Documents/Alpaca/alpaca-mcp-server.md @@ -0,0 +1,154 @@ +# Alpaca's MCP Server + +Turn your words into action with Alpaca’s MCP Server + +Alpaca’s MCP Server allows traders to research markets, analyze data, and place orders using natural language across AI chat applications, coding tools, and Command Line Interfaces. + +Alpaca’s MCP Server GitHub URL: [https://github.com/alpacahq/alpaca-mcp-server](https://github.com/alpacahq/alpaca-mcp-server) + +For more information, visit [Alpaca's MCP Server Homepage](https://alpaca.markets/mcp-server). + +*All data and instructions are current as of November 20, 2025.* + +## MCP Server Overview + +An MCP server is a component of the [Model Context Protocol](https://docs.anthropic.com/en/docs/agents-and-tools/mcp) (MCP) architecture developed by Anthropic. MCP is an open standard that provides a consistent way for AI applications, code editors, or Command Line Interface to interact with external tools, data sources, and services through a structured protocol. + +As AI interfaces improve, connecting them to trading workflows often requires multiple APIs, custom integrations, and authentication steps. MCP architecture streamlines this by providing a standard method for accessing data and performing actions. + +An MCP server itself acts as a bridge between the MCP client (AI interfaces) and the capabilities. It presents these capabilities in a predictable and secure format so an AI model can request market data, retrieve information, or carry out defined operations without additional SDKs or complex setup. + +## Alpaca’s MCP Server Overview + +Alpaca’s MCP Server brings this same bridge-like concept to trading by exposing capabilities powered by Alpaca’s Trading API, including: + +* Market data, both historical and live +* Order actions such as entry, change, and cancel +* Portfolio details like positions, buying power, and unrealized P/L +* Optional automation like alerts or risk checks + +This helps users accelerate their research, streamline decision making, and support efficient trade execution, helping users capitalize on potential market opportunities more efficiently. +For more information about the available functions, please see the Available Endpoints section below. + +## Main Benefits + +### Reinforced Decisions, Transparent Execution + +Alpaca’s MCP Server gives your AI model structured access to real time market data, news context, portfolio details, and order actions powered by Alpaca’s Trading API. Instead of acting on its own, the AI assistants help surface relevant insights, organize information, and prepare the actions you ask for. + +### One Interface for Many Markets + +Alpaca’s MCP Server brings equities, ETFs, crypto, and multi-leg options into one workflow and interface. This allows an AI agent to research, analyze, and help execute trading ideas without switching between platforms or juggling APIs. + +### Code-Optional, Extensible by Design + +Alpaca’s MCP Server lets traders begin with natural language prompts and move into vibe coding or full code whenever they want to optimize strategies. + +
+ +## Supported MCP Clients and Connection Types + +Alpaca’s MCP Server can be configured on the following MCP clients. Each client has its own setup requirements. For more details, visit [Alpaca’s MCP Server GitHub](https://github.com/alpacahq/alpaca-mcp-server) . The connection type indicates how you can set up Alpaca’s MCP Server. + +Note: Remote hosting for Alpaca’s MCP Server is not yet available. Traders who wish to use it remotely will need to self host for now. We may consider additional options for remote MCP Server use over time, depending on feasibility and demand. + +For instructions on self hosting Alpaca’s remote MCP Server, visit our learn article “[How to Deploy Alpaca’s MCP Server Remotely on Claude Mobile App](https://alpaca.markets/learn/how-to-deploy-alpaca-mcp-server-remotely-on-claude-mobile-app) ”. + +| MCP client name | Connection type | +| :----------------- | :-------------- | +| Cloud Desktop | Local or Remote | +| Claude Web | Remote only | +| Claude Mobile App | Remote only | +| ChatGPT Web | Remote only | +| ChatGPT Desktop | Remote only | +| ChatGPT Mobile App | Remote only | +| VS Code | Local or Remote | +| Cursor | Local or Remote | +| PyCharm | Local or Remote | +| Claude Code (CLI) | Local or Remote | +| Gemini CLI | Local or Remote | + +## Prerequisites for Connections + +You will need the following prerequisites to configure and run Alpaca’s MCP Server. The requirements may vary depending on which MCP client you connect it with remotely or locally. + +* Terminal (macOS/Linux) | Command Prompt or PowerShell (Windows) +* Python 3.10+ (Check the [official installation guide](https://www.python.org/downloads/) and confirm the version by typing the following command: `python3 --version` in Terminal) +* uv (Install using the [official installation guide](https://docs.astral.sh/uv/getting-started/installation/)) for local setup + * Tip: uv can be installed either through a package manager (like Homebrew) or directly using `curl | sh`. +* Alpaca Trading API keys (free paper trading account available) + * To find your Alpaca API keys, please check our “[How to Get Alpaca’s Trading API Key and Start Connect](https://alpaca.markets/learn/connect-to-alpaca-api) ” or “[How to Start Paper Trading with Alpaca](https://alpaca.markets/learn/start-paper-trading) ” +* MCP client (Claude Desktop, Cursor, VS Code, etc.) + * Some MCP clients may require a paid subscription if you use the MCP server frequently + +## Available Endpoints + +Alpaca’s MCP Server offers 43 functions (endpoints) of Trading API. We are optimizing and expanding the capabilities of our MCP server. + +| Category | Function name | Description | +| :-------------------- | :------------------------------------- | :------------------------------------------------------------------------------------------------------------------- | +| Account | get\_account\_info | Retrieves current account information including balances, buying power, and account status. | +| Positions | get\_all\_positions | Retrieves all current positions in the portfolio with details like quantity, market value, and P\&L. | +| Positions | get\_open\_position | Retrieves detailed information for a specific open position. | +| Portfolio History | get\_portfolio\_history | Retrieves account portfolio history with equity and P\&L over a requested time window. | +| Assets | get\_asset | Retrieves detailed information about a specific asset including trading status and attributes. | +| Assets | get\_all\_assets | Retrieves all available assets with optional filtering by status, class, and exchange. | +| Watchlist | create\_watchlist | Creates a new watchlist with specified symbols for tracking assets. | +| Watchlist | get\_watchlists | Retrieves all watchlists for the account with their symbols. | +| Watchlist | update\_watchlist\_by\_id | Updates an existing watchlist by modifying names or symbols. | +| Watchlist | get\_watchlist\_by\_id | Get a specific watchlist by its ID. | +| Watchlist | add\_asset\_to\_watchlist\_by\_id | Add an asset by symbol to a specific watchlist by ID. | +| Watchlist | remove\_asset\_from\_watchlist\_by\_id | Remove an asset by symbol from a specific watchlist by ID. | +| Watchlist | delete\_watchlist\_by\_id | Delete a specific watchlist by its ID. | +| Corporate Actions | get\_corporate\_actions | Retrieves corporate action announcements like earnings, dividends, and stock splits. | +| Calendar | get\_calendar | Retrieves market calendar for specified date range showing trading days and holidays. | +| Clock | get\_market\_clock | Retrieves current market status and next open or close times. | +| Market Data (Stock) | get\_stock\_bars | Retrieves historical stock price bars with OHLCV data using flexible timeframes. | +| Market Data (Stock) | get\_stock\_quote | Retrieves the historical quote for a stock including bid or ask prices and volumes. | +| Market Data (Stock) | get\_stock\_trades | Retrieves historical trade data for a stock with individual trade details. | +| Market Data (Stock) | get\_stock\_latest\_bar | Retrieves the most recent minute bar for a stock. | +| Market Data (Stock) | get\_stock\_latest\_quote | Retrieves the latest quote for a stock including bid or ask prices and volumes. | +| Market Data (Stock) | get\_stock\_latest\_trade | Retrieves the latest trade information for a stock. | +| Market Data (Stock) | get\_stock\_snapshot | Retrieves comprehensive snapshot with latest quote, trade, minute bar, daily bar, and previous daily bar. | +| Market Data (Crypto) | get\_crypto\_bars | Retrieves historical cryptocurrency price bars with OHLCV data. | +| Market Data (Crypto) | get\_crypto\_quotes | Retrieves historical cryptocurrency quote data with bid or ask information. | +| Market Data (Crypto) | get\_crypto\_trades | Retrieves historical trade prints for one or more cryptocurrencies. | +| Market Data (Crypto) | get\_crypto\_latest\_quote | Returns the latest quote for one or more crypto symbols. | +| Market Data (Crypto) | get\_crypto\_latest\_bar | Returns the latest minute bar for one or more crypto symbols. | +| Market Data (Crypto) | get\_crypto\_latest\_trade | Returns the latest trade for one or more crypto symbols. | +| Market Data (Crypto) | get\_crypto\_snapshot | Returns snapshots including latest trade, quote, minute bar, daily and previous daily bars for crypto symbols. | +| Market Data (Crypto) | get\_crypto\_latest\_orderbook | Returns the latest orderbook for one or more crypto symbols. | +| Market Data (Options) | get\_option\_contracts | Searches for option contracts with flexible filtering by expiration, strike price, and type. | +| Market Data (Options) | get\_option\_latest\_quote | Retrieves the latest quote for an option contract with bid or ask prices and Greeks. | +| Market Data (Options) | get\_option\_snapshot | Retrieves comprehensive snapshots of option contracts including latest trade, quote, implied volatility, and Greeks. | +| Trading (Orders) | get\_orders | Retrieves order history with filtering options by status, date range, and limits. | +| Trading (Orders) | place\_stock\_order | Places a stock order with support for market, limit, stop, stop limit, and trailing stop orders. | +| Trading (Orders) | place\_crypto\_order | Places a cryptocurrency order with support for market, limit, and stop limit orders. | +| Trading (Orders) | place\_option\_market\_order | Places option orders including single leg and multi leg strategies like spreads and straddles. | +| Trading (Orders) | cancel\_all\_orders | Cancels all open orders and returns the status of each cancellation. | +| Trading (Orders) | cancel\_order\_by\_id | Cancels a specific order by its ID. | +| Trading (Positions) | close\_position | Closes a specific position for a single symbol, either partially or completely. | +| Trading (Positions) | close\_all\_positions | Closes all open positions in the portfolio. | +| Trading (Positions) | exercise\_options\_position | Exercises a held option contract, converting it into the underlying asset. | + +## Important Considerations When Trading with Alpaca’s MCP Server + +Using Alpaca’s MCP Server introduces a few important considerations: + +1. Make sure your Alpaca API keys are linked to the correct account type such as live or paper. +2. Some AI tools (MCP clients) may require a paid subscription if you use the MCP Server frequently. +3. Review and confirm orders directly on your Alpaca dashboard. You can do this in real time to ensure accuracy before or after submitting trades. + +## Security Considerations for Remote MCP Server Deployment + +Running an MCP server remotely introduces a few important security considerations. Many early stage examples such as FastMCP are designed for local testing and may not include authentication or encrypted communication by default. + +When a server is publicly accessible, it is possible for external requests to reach it. If the server handles sensitive information such as Alpaca Trading API keys, this can create a risk of unauthorized access or unintended tool execution. + +To reduce these risks, a secure deployment should include HTTPS or TLS for encrypted communication and a reliable token based authentication method. Taking these steps helps protect your credentials and ensures that only trusted clients can interact with your MCP server. + +
+ +## Disclosure + +*Insights generated by our MCP server and connected AI agents are for educational and informational purposes only and should not be taken as investment advice. Past performance from models does not guarantee future results. Please conduct your own due diligence before making any decisions..* \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/authentication.md b/DesktopBot/Documents/Alpaca/authentication.md new file mode 100644 index 0000000..c267b10 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/authentication.md @@ -0,0 +1,81 @@ +# Authentication + +# How to call our API + +Alpaca's APIs are available under different domain names, and you first need to make sure that you are calling the right one. This page describes the machine-to-machine authentication types available in the following scenarios: + +* If you have a live account, you can call: + * Trading API endpoints on `api.alpaca.markets` + * Market Data API endpoints on `data.alpaca.markets` +* If you have a paper account, you can call: + * Trading API endpoints on `paper-api.alpaca.markets` + * Market Data API endpoints on `data.alpaca.markets` +* If you are a live broker partner, you can call: + * Broker API endpoints on `broker-api.alpaca.markets` + * Market Data API endpoints on `data.alpaca.markets` + * Authentication endpoints on `authx.alpaca.markets` +* If you are a sandbox broker partner, you can call: + * Broker API endpoints on `broker-api.sandbox.alpaca.markets` + * Market Data API endpoints on `data.sandbox.alpaca.markets` + * Authentication endpoints on `authx.sandbox.alpaca.markets` + +If you have more than one account (or in case of broker partners, more than one correspondent), each of those have separate credentials. As an example, you cannot use your live account's credentials with the paper API, or vice versa. + +# Authentication flows + +## Client credentials + + + The Client Credentials authentication flow is not yet available for Trading API. + + +When using this flow, you first need to exchange your credentials for a short-lived access token, then use that token to authenticate with our API. Do not request a new access token for each API call. Access tokens issued by our token endpoint are valid for 15 minutes. + +We offer two types of credentials you can use with this flow: + +* Use a client ID and a client secret (`client_secret`) - this is easier, as you can simply pass the secret that was generated when you created your credentials to our token endpoint. Note that we only support passing the client secret in the request body (`client_secret_post`), not in the `Authorization` header (`client_secret_basic`). +* Use a client ID and a signed client assertion (`private_key_jwt`) - this ensures that the private key used to sign client assertions never leaves your custody, but it requires you to construct and sign a JWT token with a private key before each call to the token endpoint. See [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523.html#section-2.2) for more information on how to do so. + +As an example, here is how a Broker API user would request an access token from our [token endpoint](../reference/issuetokens) using the first method: + +```curl +curl -X POST "https://authx.alpaca.markets/v1/oauth2/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id={YOUR_CLIENT_ID}" \ + -d "client_secret={YOUR_CLIENT_SECRET}" +``` + +The response will contain an access token: + +```json +{ + "access_token": "{TOKEN}", + "expires_in": 899, + "token_type": "Bearer" +} +``` + +The returned token can be used to authenticate with Broker API: + +```curl +curl -X GET "https://broker-api.alpaca.markets/v1/accounts" \ + -H "Authorization: Bearer {TOKEN}" +``` + +## Legacy + +Our older authentication flow lets you authenticate with your key ID and secret key directly. You have two options to authenticate your requests: + +* Use HTTP Basic Authentication, send your key ID as the username, and your secret key as the password. +* Use the `APCA-API-KEY-ID` and `APCA-API-SECRET-KEY` headers to send your key ID and secret key. + +As an example, here is how a Trading API user would authenticate with our API using the second method: + +```curl +curl -X GET "https://api.alpaca.markets/v2/account" \ + -H "APCA-API-KEY-ID: {YOUR_API_KEY_ID}" \ + -H "APCA-API-SECRET-KEY: {YOUR_API_SECRET_KEY}" +``` + +
\ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/crypto-fees.md b/DesktopBot/Documents/Alpaca/crypto-fees.md new file mode 100644 index 0000000..11af538 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/crypto-fees.md @@ -0,0 +1,45 @@ +# Crypto Spot Trading Fees + +While Alpaca stock trading remains commission-free, crypto trading includes a small fee per trade dependent on your executed volume and order type. Any market or exchange consists of two parties, buyers and sellers. When you place an order to buy crypto on the Alpaca Exchange, there is someone else on the other side of the trade selling what you want to buy. The seller's posted order on the order book is providing liquidity to the exchange and allows for the trade to take place. Note, that both buyers and sellers can be makers or takers depending on the order entered and current quote of the coin. **A maker is someone who adds liquidity, and the order gets placed on the order book. A Taker on the other hand removes the liquidity by placing a market or marketable limit order which executes against posted orders.** + +See the below table with volume-tiered fee pricing: + +| Tier | 30D Crypto Volume (USD) | Maker | Take | +| :--- | :----------------------- | :---- | :---- | +| 1 | 0 - 100,000 | 0.15% | 0.25% | +| 2 | 100,000 - 500,000 | 0.12% | 0.22% | +| 3 | 500,000 - 1,000,000 | 0.10% | 0.20% | +| 4 | 1,000,000 - 10,000,000 | 0.08% | 0.18% | +| 5 | 10,000,000- 25,000,000 | 0.05% | 0.15% | +| 6 | 25,000,000 - 50,000,000 | 0.02% | 0.13% | +| 7 | 50,000,000 - 100,000,000 | 0.02% | 0.12% | +| 8 | 100,000,000+ | 0.00% | 0.10% | + +The crypto fee will be charged on the credited crypto asset/fiat (what you receive) per trade. Some examples: + +* Buy `ETH/BTC`, you receive `ETH`, the fee is denominated in `ETH` +* Sell `ETH/BTC`, you receive `BTC`, the fee is denominated in `BTC` +* Buy `ETH/USD`, you receive `ETH`, the fee is denominated in `ETH` +* Sell `ETH/USD`, you receive `USD`, the fee is denominated in `USD` + +To get the fees incurred from crypto trading you can use Activities API to query `activity_type` by `CFEE` or `FEE`. + +See below example of CFEE object: + +```json +{ + "id": "20220812000000000::53be51ba-46f9-43de-b81f-576f241dc680", + "activity_type": "CFEE", + "date": "2022-08-12", + "net_amount": "0", + "description": "Coin Pair Transaction Fee (Non USD)", + "symbol": "ETHUSD", + "qty": "-0.000195", + "price": "1884.5", + "status": "executed" +} +``` + +Fees are currently calculated and posted end of day. If you query on same day of trade you might not get results. We will be providing an update for fee posting to be real-time in the near future. + +> 📘 Check out our Crypto Trading FAQ [here](https://alpaca.markets/support/alpaca-crypto-coin-pair-faq) \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/crypto-orders.md b/DesktopBot/Documents/Alpaca/crypto-orders.md new file mode 100644 index 0000000..c1d5f40 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/crypto-orders.md @@ -0,0 +1,25 @@ +# Crypto Orders + +You can submit crypto orders through the traditional orders API endpoints (`POST/orders`). + +* The following order types are supported: `market`, `limit` and `stop_limit` +* The following `time_in_force` values are supported: `gtc`, and `ioc` +* We accept fractional orders as well with either `notional` or `qty` provided + +You can submit crypto orders for a any supported crypto pair via API, see the below cURL POST request. + +```curl +curl --request POST 'https://paper-api.alpaca.markets/v2/orders' \ +--header 'Apca-Api-Key-Id: ' \ +--header 'Apca-Api-Secret-Key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "symbol": "BTC/USD", + "qty": "0.0001", + "side": "buy", + "type": "market", + "time_in_force": "gtc" +}' +``` + +The above request submits a market order via API to buy 0.0001 BTC with USD (BTC/USD pair) that is good till end of day. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/crypto-pricing-data.md b/DesktopBot/Documents/Alpaca/crypto-pricing-data.md new file mode 100644 index 0000000..aa1d655 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/crypto-pricing-data.md @@ -0,0 +1,61 @@ +# Crypto Pricing Data + +Alpaca provides free limited crypto data and a more advanced unlimited paid plan. + +To request trading pairs data via REST API, see [Crypto Pricing Data REST API](https://docs.alpaca.markets/reference/v1beta2cryptobars) Reference. + +The example below requests the latest order book data (bid and asks) for the following three crypto trading pairs: BTC/USD, ETH/BTC and ETH/USD. + +```curl +curl --request GET 'https://data.alpaca.markets/v1beta3/crypto/us/latest/orderbooks?symbols=BTC/USD,ETH/BTC,ETH/USD,SOL/USDT' \ +--header 'Apca-Api-Key-Id: ' \ +--header 'Apca-Api-Secret-Key: ' +``` + +``` +{ + "orderbooks": { + "BTC/USD": { + "a": [ + { + "p": 27539.494, + "s": 0.2632414 + }, + ... + ], + "b": [ + { + "p": 27511.78083, + "s": 0.26265668 + }, + ... + ], + "t": "2023-03-18T13:31:44.932988033Z" + }, + "ETH/USD": { ... }, + "ETH/BTC": { ... }, + "SOL/USDT": { ... } + } +} +``` + +# Real-Time Crypto Market Data + +Additionally, you can subscribe to real-time crypto data via Websockets. Example below leverages wscat to subscribe to: + +* BTC/USD trades. +* ETH/USDT and ETH/USD quotes. +* BTC/USD order books + +``` + $ wscat -c wss://stream.data.alpaca.markets/v1beta3/crypto/us +Connected (press CTRL+C to quit) +< [{"T":"success","msg":"connected"}] +> {"action": "auth", "key": "", "secret": ""} +< [{"T":"success","msg":"authenticated"}] +> {"action":"subscribe", "trades":["BTC/USD"], "quotes":["ETH/USDT","ETH/USD"], "orderbooks":["BTC/USD"]} +< [{"T":"subscription","trades":["BTC/USD"],"quotes":["ETH/USDT","ETH/USD"],"orderbooks":["BTC/USD"],"updatedBars":[],"dailyBars":[]}] +< [{"T":"o","S":"BTC/USD","t":"2023-03-18T13:51:29.754747009Z","b":[{"p":27485.3445,"s":0.25893365},{"p":27466.92727,"s":0.52351568},...],"a":[{"p":27512.92,"s":0.26137249},{"p":27547.9425,"s":0.52011956},...],"r":true}] +< [{"T":"q","S":"ETH/USDT","bp":1815.55510989,"bs":8.24941727,"ap":1818.4,"as":4.15121428,"t":"2023-03-18T13:51:33.256826818Z"}] +< ... +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/crypto-trading.md b/DesktopBot/Documents/Alpaca/crypto-trading.md new file mode 100644 index 0000000..33a8043 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/crypto-trading.md @@ -0,0 +1,190 @@ +# Crypto Spot Trading + +Trade crypto through our API and the Alpaca web dashboard! Trade all day, seven days a week, as frequently as you’d like. + +> 🚧 As of November 18, 2022, cryptocurrency trading is open to select international jurisdictions and some U.S. jurisdictions. +> +> To view the supported US regions for crypto trading, click [here](https://alpaca.markets/support/what-regions-support-cryptocurrency-trading). + +# Supported Coins + +Alpaca supports over 20+ unique crypto assets across 56 trading pairs. Current trading pairs are based on BTC, USD, USDT and USDC) with more assets and trading pairs coming soon. + +To query all available crypto assets and pairs you can you use the following API call: + +```curl +curl --request GET 'https://api.alpaca.markets/v2/assets?asset_class=crypto' \ +--header 'Apca-Api-Key-Id: ' \ +--header 'Apca-Api-Secret-Key: ' +``` + +Below is a sample trading pair object composed of two assets, BTC and USD. + +```json JSON +{ + "id": "276e2673-764b-4ab6-a611-caf665ca6340", + "class": "crypto", + "exchange": "ALPACA", + "symbol": "BTC/USD", + "name": "BTC/USD pair", + "status": "active", + "tradable": true, + "marginable": false, + "shortable": false, + "easy_to_borrow": false, + "fractionable": true, + "min_order_size": "0.0001", + "min_trade_increment": "0.0001", + "price_increment": "1" +} +``` + +Note that symbology for trading pairs has changed from our previous format, where `BTC/USD` was previously referred to as `BTCUSD`. Our API has made proper changes to support the legacy convention as well for backwards compatibility. + +For further reference see Assets API. **add link** + +# Supported Orders + +When submitting crypto orders through the Orders API and the Alpaca web dashboard, Market, Limit and Stop Limit orders are supported while the supported `time_in_force` values are `gtc`, and `ioc`. We accept fractional orders as well with either `notional` or `qty` provided. + +You can submit crypto orders for any supported crypto pair via API, see the below cURL POST request. + +```curl +curl --request POST 'https://paper-api.alpaca.markets/v2/orders' \ +--header 'Apca-Api-Key-Id: ' \ +--header 'Apca-Api-Secret-Key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "symbol": "BTC/USD", + "qty": "0.0001", + "side": "buy", + "type": "market", + "time_in_force": "gtc" +}' +``` + +The above request submits a market order via API to buy 0.0001 BTC with USD (BTC/USD pair) that is good till end of day. + +To learn more see **orders** and **fractional trading**. + +All cryptocurrency assets are fractionable but the supported decimal points vary depending on the cryptocurrency. See **Assets entity** for information on fractional precisions per asset. + +Note these values could change in the future. + +# Crypto Market Data + +You can check out the [documentation](https://docs.alpaca.markets/docs/historical-crypto-data-1) and the [API reference](https://docs.alpaca.markets/reference/cryptobars-1) of our crypto market data. + +Here we provide an example to request the latest order book data (bids and asks) for the following three coin pairs: BTC/USD, ETH/BTC, and ETH/USD. + +```curl +curl 'https://data.alpaca.markets/v1beta3/crypto/us/latest/orderbooks?symbols=BTC/USD,ETH/BTC,ETH/USD' +``` + +```json +{ + "orderbooks": { + "BTC/USD": { + "a": [ + { + "p": 66051.621, + "s": 0.275033 + }, + ... + ], + "b": [ + { + "p": 65986.962, + "s": 0.27813 + }, + ... + ], + "t": "2024-07-24T07:31:01.373709495Z" + }, + "ETH/USD": { ... } + }, + "ETH/BTC": { ... } + } +} +``` + +Additionally, you can subscribe to real-time crypto data via Websockets. Example below leverages wscat to subscribe to BTC/USD order book. + +```json +$ wscat -c wss://stream.data.alpaca.markets/v1beta3/crypto/us +Connected (press CTRL+C to quit) +< [{"T":"success","msg":"connected"}] +> {"action":"auth","key":"","secret":""} +< [{"T":"success","msg":"authenticated"}] +> {"action":"subscribe","quotes":["ETH/USD"]} +< [{"T":"subscription","trades":[],"quotes":["ETH/USD"],"orderbooks":[],"bars":[],"updatedBars":[],"dailyBars":[]}] +< [{"T":"q","S":"ETH/USD","bp":3445.34,"bs":4.339,"ap":3450.2,"as":4.3803,"t":"2024-07-24T07:38:06.88490478Z"}] +< [{"T":"q","S":"ETH/USD","bp":3445.34,"bs":4.339,"ap":3451.1,"as":8.73823,"t":"2024-07-24T07:38:06.88493591Z"}] +< [{"T":"q","S":"ETH/USD","bp":3445.34,"bs":4.339,"ap":3447.03,"as":4.36424,"t":"2024-07-24T07:38:06.88511154Z"}] +< [{"T":"q","S":"ETH/USD","bp":3444.644,"bs":8.797,"ap":3447.03,"as":4.36424,"t":"2024-07-24T07:38:06.88512141Z"}] +< [{"T":"q","S":"ETH/USD","bp":3444.51,"bs":4.33,"ap":3447.03,"as":4.36424,"t":"2024-07-24T07:38:06.88516355Z"}] +``` + +For further reference of real-time crypto pricing data see [its documentation](https://docs.alpaca.markets/docs/real-time-crypto-pricing-data). + +# Transferring Crypto + +Alpaca now offers native on-chain crypto transfers with wallets! If you have crypto trading enabled and reside in an eligible US state or international jurisdiction you can access wallets on the web dashboard via the Crypto Transfers tab. + +Alpaca wallets currently support transfers for Bitcoin, Ethereum, and all Ethereum (ERC20) based tokens. To learn more on transferring crypto with Alpaca, see **[Crypto Wallets FAQs](https://alpaca.markets/support/crypto-wallet-faq)** + +# Crypto Spot Trading Fees + +While Alpaca stock trading remains commission-free, crypto trading includes a small fee per trade dependent on your executed volume and order type. Any market or exchange consists of two parties, buyers and sellers. When you place an order to buy crypto on the Alpaca Exchange, there is someone else on the other side of the trade selling what you want to buy. The seller's posted order on the order book is providing liquidity to the exchange and allows for the trade to take place. Note, that both buyers and sellers can be makers or takers depending on the order entered and current quote of the coin. **A maker is someone who adds liquidity, and the order gets placed on the order book. A Taker on the other hand removes the liquidity by placing a market or marketable limit order which executes against posted orders.** + +See the below table with volume-tiered fee pricing: + +| Tier | 30D Trading Volume (USD) | Maker | Taker | +| :--- | :----------------------- | :----- | :----- | +| 1 | 0 - 100,000 | 15 bps | 25 bps | +| 2 | 100,000 - 500,000 | 12 bps | 22 bps | +| 3 | 500,000 - 1,000,000 | 10 bps | 20 bps | +| 4 | 1,000,000 - 10,000,000 | 8 bps | 18 bps | +| 5 | 10,000,000 - 25,000,000 | 5 bps | 15 bps | +| 6 | 25,000,000 - 50,000,000 | 2 bps | 13 bps | +| 7 | 50,000,000 - 100,000,000 | 2 bps | 12 bps | +| 8 | 100,000,000+ | 0 bps | 10 bps | + +The crypto fee will be charged on the credited crypto asset/fiat (what you receive) per trade. Some examples, + +* Buy `ETH/BTC`, you receive `ETH`, the fee is denominated in `ETH` +* Sell `ETH/BTC`, you receive `BTC`, the fee is denominated in `BTC` +* Buy `ETH/USD`, you receive `ETH`, the fee is denominated in `ETH` +* Sell `ETH/USD`, you receive `USD`, the fee is denominated in `USD` + +To get the fees incurred from crypto trading you can use Activities API to query `activity_type` by `CFEE` or `FEE`. See below example of CFEE object: + +```json +{ + "id": "20220812000000000::53be51ba-46f9-43de-b81f-576f241dc680", + "activity_type": "CFEE", + "date": "2022-08-12", + "net_amount": "0", + "description": "Coin Pair Transaction Fee (Non USD)", + "symbol": "ETHUSD", + "qty": "-0.000195", + "price": "1884.5", + "status": "executed" +} +``` + +Fees are currently calculated and posted end of day. If you query on same day of trade you might not get results. We will be providing an update for fee posting to be real-time in the near future. + +# Margin and Short Selling + +Cryptocurrencies cannot be bought on margin. This means that you cannot use leverage to buy them and orders are evaluated against `non_marginable_buying_power`. + +Cryptocurrencies can not be sold short. + +# Trading Hours + +Crypto trading is offered for 24 hours everyday and your orders will be executed throughout the day. + +# Trading Limits + +Currently, an order (buy or sell) must not exceed $200k in notional. This is per an order. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/fix-messages.md b/DesktopBot/Documents/Alpaca/fix-messages.md new file mode 100644 index 0000000..75c17e2 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/fix-messages.md @@ -0,0 +1,2920 @@ +# FIX Specification + +This document describes the implementation of the FIX 4.2 protocol used by Alpaca to enable order entry via FIX. + +Version 1.1.0 + +## Supported Message Types + +| Message | MsgType | Description | +| --------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------- | +| [Logon](#logon-a) | A | Sent by FIX client to authenticate and establish the FIX session | +| [Heartbeat](#heartbeat-0) | 0 | Sent by either client or server at preset interval within the working FIX session | +| [Test Request](#test-request-1) | 1 | Sent to force a heartbeat from the opposing application | +| [Resend Request](#resend-request-2) | 2 | To request retransmission of messages which were missed | +| [Reject](#reject-3) | 3 | Response to a message that could not be processed | +| [Sequence Reset](#sequence-reset-4) | 4 | To reset the FIX session sequence number | +| [Logout](#logout-5) | 5 | To safely disconnect the connected FIX session | +| [New Order - Single](#new-order---single-d) | D | To submit a new single order (equity or single-leg option) | +| [New Order - Multileg](#new-order---multileg-ab) | AB | To submit a new multi-leg order (e.g. option spreads, covered calls) | +| [Execution Report](#execution-report-8) | 8 | Sent whenever the state of the order changes | +| [Order Cancel Request](#order-cancel-request-f) | F | To request cancellation of an order (single-leg or multileg) | +| [Order Cancel/Replace Request](#order-cancelreplace-request-g) | G | To request modification of a single-leg order | +| [Multileg Order Cancel/Replace Request](#multileg-order-cancelreplace-request-ac) | AC | To request modification of a multi-leg order | +| [Order Cancel Reject](#order-cancel-reject-9) | 9 | Sent if a cancel or cancel/replace request (F, G, or AC) could not be executed | + +## Message Header + +All messages should contain the following tags in the header: + +| Tag | Field | Mandatory | Description | +| --- | ------------ | --------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| 8 | BeginString | Y | FIX.4.2 | +| 9 | BodyLength | Y | Size of the message body in bytes | +| 34 | MsgSeqNum | Y | Message sequence number | +| 35 | MsgType | Y | Message type, should be one of the types supported in this doc | +| 49 | SenderCompID | Y | Provided by Alpaca, must be present in all FIX messages. This will be returned as TargetCompID (56) in all FIX messages to clients | +| 52 | SendingTime | Y | UTC timestamp of message transmission. Precision supported: Millis, Micros, Nanos | +| 56 | TargetCompID | Y | Provided by Alpaca, must be present in all FIX messages. This will be returned as SenderCompID (49) in all FIX messages to clients | + +## Message Trailer + +All messages should contain the following tags in the trailer: + +| Tag | Field | Mandatory | Description | +| --- | -------- | --------- | ------------------------------------------------------------------------------------------------------ | +| 10 | CheckSum | Y | Last field in the messages with trailing ``. Value calculated by the FIX engine from message data | + +## Logon (A) + +| Tag | Field | Mandatory | Description | +| :-- | :-------------- | :-------- | :----------------------------------------------------- | +| 35 | MsgType | Y | A - For Logon | +| 98 | EncryptMethod | Y | 0 - None, only accepted value | +| 108 | HeartBtInt | Y | Must be set to `30` for 30 seconds | +| 141 | ResetSeqNumFlag | N | Y - Resets both sender and target sequence number to 1 | + +**Example FIX Message** + +```text Logon (A) +|8=FIX.4.2|9=73|35=A|34=1|49=SENDER|52=20240524-16:02:42.003|56=ALPACA|98=0|108=30|141=Y|10=131| +``` + +## Heartbeat (0) + +Sent by either client or server if no message has been received since the last heartbeat interval. This is also sent in response to a Test Request (1). + +| Tag | Field | Mandatory | Description | +| --- | --------- | --------- | ---------------------------------------------------------------------- | +| 35 | MsgType | Y | 0 - For Heartbeat | +| 112 | TestReqID | N | Required only when Heartbeat is sent in response to a Test Request (1) | + +**Example FIX Message** + +```text Heartbeat +|8=FIX.4.2|9=91|35=0|49=SENDER|56=ALPACA|34=2|52=20230330-16:52:321.029|10=049| +``` + +## Test Request (1) + +May be sent by either client or server, to force a Heartbeat (0) from the other party. + +| Tag | Field | Mandatory | Description | +| --- | --------- | --------- | -------------------------------------------------------- | +| 35 | MsgType | Y | 1 - For Test Request | +| 112 | TestReqID | Y | Identifier to be returned in the resulting Heartbeat (0) | + +**Example FIX Message** + +```text Test Request +|8=FIX.4.2|9=91|35=1|49=SENDER|56=ALPACA|34=2|52=20230330-16:52:321.029|112=TEST|10=049| +``` + +## Resend Request (2) + +Sent to request retransmission of messages which were missed. + +| Tag | Field | Mandatory | Description | +| --- | ---------- | --------- | ----------------------------------------------- | +| 35 | MsgType | Y | 2 - For Resend Request | +| 7 | BeginSeqNo | Y | Beginning sequence number of requested messages | +| 16 | EndSeqNo | Y | Ending sequence number of requested messages | + +**Example FIX Message** + +```text Resend Request +|8=FIX.4.2|9=66|7=9|16=36|34=12|35=2|49=SENDER|52=20230627-14:10:21.769|56=ALPACA|10=126| +``` + +## Reject (3) + +Sent when a message is rejected or cannot be processed by the FIX server. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tag + + Field + + Mandatory + + Description +
+ 35 + + MsgType + + Y + + 3 - For Reject +
+ 45 + + RefSeqNum + + Y + + Sequence number of the rejected message +
+ 58 + + Text + + N + + Reason for rejection +
+ 371 + + RefTagID + + N + + The tag number of the FIX field being referenced +
+ 372 + + RefMsgType + + N + + The MsgType (35) of the FIX message being referenced +
+ 373 + + SessionRejectReason + + N + + Reject reason codes.\ + 0 - Invalid tag number\ + 1 - Required tag missing\ + 2 - Tag not defined for this message type\ + 3 - Unsupported Message Type\ + 4 - Tag specified without a value\ + 5 - Value is incorrect (out of range) for this tag\ + 6 - Incorrect data format for value\ + 9 - CompID problem\ + 10 - SendingTime accuracy problem\ + 11 - Invalid MsgType +
+ +**Example FIX Message** + +```text Reject +|8=FIX.4.2|9=0134|35=3|34=44196|49=ALPACA|56=TARGET|52=20230330-20:18:38.039|58=0005 Tag specified without a value|45=44196|371=11|372=8|373=4|10=092| +``` + +## Sequence Reset (4) + +Sent to reset the incoming sequence number on the other side. The sequence reset should be used only to increase the sequence number. Any request to decrease the sequence number will result in a Reject (3) message. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tag + + Field + + Mandatory + + Description +
+ 35 + + MsgType + + Y + + 4 - For Sequence Reset +
+ 36 + + NewSeqNo + + Y + + New sequence number +
+ 123 + + GapFillFlag + + N + + N - Sequence reset to recover from an out-of-sequence condition, MsgSeqNum(34) is ignored\ + Y - Gap fill message, MsgSeqNum(34) field must be valid +
+ +**Example FIX Message** + +```text Sequence Reset +|8=FIX.4.2|9=61|34=12|35=4|36=9|49=SENDER|52=20230627-14:14:51.732|56=ALPACA|10=156| +``` + +## Logout (5) + +May be sent by either client or server, to terminate the session. The other party would respond with a confirming Logout message as an acknowledgement. + +| Tag | Field | Mandatory | Description | +| --- | ------- | --------- | -------------- | +| 35 | MsgType | Y | 5 - For Logout | + +**Example FIX Message** + +```text Logout +|8=FIX.4.2|9=56|34=12|35=5|49=SENDER|52=20230627-14:16:50.690|56=ALPACA|10=197| +``` + +## New Order - Single (D) + +Sent by the client to submit a new single order. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tag + + Field + + Mandatory + + Description +
+ 35 + + MsgType + + Y + + D - For New Order - Single +
+ 1 + + Account + + Y + + Account number +
+ 11 + + ClOrdID + + Y + + Unique identifier of the order assigned by the client (must be no longer than 48 characters) +
+ 12 + + Commission + + N + + Commission to collect from the account holder +
+ 13 + + CommType + + N + + 1 - per share\ + 2 - percentage, 5% should be represented as .05\ + 3 - absolute +
+ 18 + + ExecInst + + N + + 1 - Not held\ + 5 - Held +
+ 21 + + HandlInst + + Y + + 1 - Automated execution with no broker intervention, only accepted value +
+ 38 + + OrderQty + + N + + Number of shares to trade, required if CashOrderQty (152) is not set +
+ 40 + + OrdType + + Y + + 1 - Market\ + 2 - Limit\ + 3 - Stop\ + 4 - Stop limit\ + 5 - Market on close\ + B - Limit on close +
+ 44 + + Price + + N + + Required for Limit `40=2` and Stop Limit `40=4` orders +
+ 54 + + Side + + Y + + 1 - Buy\ + 2 - Sell +
+ 55 + + Symbol + + Y + + Ticker symbol +
+ 59 + + TimeInForce + + Y + + 0 - Day (day)\ + 1 - Good Till Cancel (gtc)\ + 2 - At the Opening (opg)\ + 3 - Immediate or Cancel (ioc)\ + 4 - Fill or Kill (fok)\ + 7 - At the Close (cls) +
+ 60 + + TransactTime + + Y + + UTC timestamp of order creation by client +
+ 77 + + OpenClose + + N + + Indicates whether the resulting position from the trade would be an opening or closing position.\ + O - Open\ + C - Close +
+ 99 + + StopPx + + N + + Required for Stop `40=3` and Stop Limit `40=4` orders +
+ 109 + + ClientID + + N + + Sub-account tag for omnibus accounts +
+ 152 + + CashOrderQty + + N + + Notional value to trade, required if OrderQty (38) is not set +
+ 336 + + TradingSessionID + + N + + 8 - Extended Hours\ + Required for extended hours orders only +
+ 167 + + SecurityType + + N + + CS - Common Stock (default when absent)\ + OPT - Option\ + Required to be `OPT` to submit a single-leg option order +
+ 200 + + MaturityMonthYear + + N + + Expiration month and year in `YYYYMM` format.\ + Required when `167=OPT` +
+ 201 + + PutOrCall + + N + + 0 - Put\ + 1 - Call\ + Required when `167=OPT` +
+ 202 + + StrikePrice + + N + + Strike price of the option contract (up to 3 decimal places).\ + Required when `167=OPT` +
+ 205 + + MaturityDay + + N + + Expiration day of month (1-31).\ + Required when `167=OPT` +
+ +**Notes on single-leg option orders** + +* When `SecurityType (167) = OPT`, the option contract is identified by the combination of `Symbol (55)` (underlying root, e.g. `AAPL`), `MaturityMonthYear (200)`, `MaturityDay (205)`, `PutOrCall (201)` and `StrikePrice (202)`. +* `OpenClose (77)` should be supplied to disambiguate the position effect (open vs. close). When omitted, position intent is inferred from `Side (54)`: `Buy` → buy-to-open, `Sell` → sell-to-close, `Sell Short (5)` → sell-to-open. +* Option execution reports echo back `SecurityType`, `MaturityMonthYear`, `MaturityDay`, `PutOrCall` and `StrikePrice` along with the standard fields. + +**Example FIX Messages** + +```text Market order (quantity based) +|8=FIX.4.2|9=139|1=TEST_ACCOUNT|11=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|34=12|35=D|38=10|40=1|49=SENDER|52=20230613-14:01:37.330|54=1|55=SPY|56=ALPACA|59=1|10=030| +``` + +```text Market order (notional based) +|8=FIX.4.2|9=141|1=TEST_ACCOUNT|11=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|34=12|35=D|40=1|49=SENDER|52=20230613-14:43:47.572|54=1|55=SPY|56=ALPACA|59=0|152=100|10=130| +``` + +```text Limit order +|8=FIX.4.2|9=149|1=TEST_ACCOUNT|11=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|34=12|35=D|38=10|40=2|44=350.78|49=SENDER|52=20230613-14:45:58.303|54=1|55=SPY|56=ALPACA|59=0|10=005| +``` + +```text Stop order +|8=FIX.4.2|9=149|1=TEST_ACCOUNT|11=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|34=12|35=D|38=10|40=3|49=SENDER|52=20230613-14:50:51.223|54=1|55=SPY|56=ALPACA|59=0|99=350.78|10=006| +``` + +```text Stop limit order +|8=FIX.4.2|9=159|1=TEST_ACCOUNT|11=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|34=12|35=D|38=10|40=4|44=350.78|49=SENDER|52=20230613-15:26:35.992|54=1|55=SPY|56=ALPACA|59=0|99=350.78|10=246| +``` + +```text Extended Hours Order +|8=FIX.4.2|9=149|1=TEST_ACCOUNT|11=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|34=12|35=D|38=10|40=2|44=350.78|49=SENDER|52=20230613-14:45:58.303|54=1|55=SPY|56=ALPACA|59=5|10=005| +``` + +```text Option order (Buy-to-Open 2 META Jan-17-2025 100 Call @ limit 2.15) +|8=FIX.4.2|9=185|1=TEST_ACCOUNT|11=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|34=12|35=D|38=2|40=2|44=2.15|49=SENDER|52=20240910-14:45:58.303|54=1|55=META|56=ALPACA|59=0|60=20240910-14:45:58.303|77=O|167=OPT|200=202501|201=1|202=100|205=17|10=011| +``` + +## Execution Report (8) + +Sent by the server whenever an order receives an update. Each execution report contains field OrdStatus (39) which is used to convey the current status of the order as understood by Alpaca, as well as fields ExecType (150) and ExecTransType (20) which describe the purpose of the message. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tag + + Field + + Mandatory + + Description +
+ 35 + + MsgType + + Y + + 8 - For Execution Report +
+ 1 + + Account + + Y + + Account number +
+ 6 + + AvgPx + + Y + + Average price of all fills on this order +
+ 11 + + ClOrdID + + Y + + Unique identifier of the order as assigned by the client +
+ 12 + + Commission + + N + + Monetary commission value charged on a fill/partial fill +
+ 14 + + CumQty + + Y + + Filled quantity on the order +
+ 15 + + Currency + + N + + Account base currency, default USD +
+ 17 + + ExecID + + Y + + Unique identifier of the execution assigned by Alpaca +
+ 19 + + ExecRefID + + N + + ExecID (17) of the original execution being canceled or corrected when `20=1` or `20=2` +
+ 20 + + ExecTransType + + Y + + Transaction type.\ + 0 - New\ + 1 - Cancel\ + 2 - Correct\ + 3 - Status, for Restated (D) message upon stop trigger +
+ 30 + + LastMkt + + N + + Market of execution for this fill +
+ 31 + + LastPx + + N + + Price of this fill +
+ 32 + + LastShares + + N + + Quantity traded on this fill +
+ 37 + + OrderID + + Y + + Unique identifier of the order assigned by Alpaca +
+ 38 + + OrderQty + + N + + Either CashOrderQty (152) or OrderQty (38) is provided +
+ 39 + + OrdStatus + + Y + + Identifies current status of order.\ + 0 - New\ + 1 - Partially filled\ + 2 - Filled\ + 3 - Done for day\ + 4 - Canceled\ + 5 - Replaced\ + 6 - Pending Cancel\ + 8 - Rejected\ + A - Pending New\ + C - Expired\ + E - Pending Replace +
+ 40 + + OrdType + + N + + Same as specified on the order.\ + 1 - Market\ + 2 - Limit\ + 3 - Stop\ + 4 - Stop limit\ + 5 - Market on close\ + B - Limit on close +
+ 41 + + OrigClOrdID + + N + + ClOrdID (11) of the original order in cancel and cancel/replace requests +
+ 44 + + Price + + N + + Sent when specified on the order +
+ 54 + + Side + + Y + + 1 - Buy\ + 2 - Sell +
+ 55 + + Symbol + + Y + + Ticker symbol +
+ 58 + + Text + + N + + Contains reject reason when `150=8` +
+ 59 + + TimeInForce + + N + + Same as specified on the order.\ + 0 - Day (day)\ + 1 - Good Till Cancel (gtc)\ + 2 - At the Opening (opg)\ + 3 - Immediate or Cancel (ioc)\ + 4 - Fill or Kill (fok)\ + 7 - At the Close (cls) +
+ 60 + + TransactTime + + N + + Server time in UTC when execution occurred, nanoseconds precision +
+ 99 + + StopPx + + N + + Sent when specified on the order +
+ 150 + + ExecType + + Y + + Describes the type of execution report.\ + 0 - New\ + 1 - Partial fill\ + 2 - Fill\ + 3 - Done for day\ + 4 - Canceled\ + 5 - Replaced\ + 6 - Pending Cancel, ack for Order Cancel Request (F)\ + 8 - Rejected\ + A - Pending New\ + C - Expired\ + D - Restated, for stop price triggers\ + E - Pending Replace, ack for Order Cancel/Replace Request (G) +
+ 151 + + LeavesQty + + Y + + Unfilled quantity on the order. When order is closed (filled, done for day, canceled, replaced, rejected, expired) value could be 0 +
+ 152 + + CashOrderQty + + N + + Either CashOrderQty (152) or OrderQty (38) is provided. Specifies the notional amount conveyed on the order +
+ 378 + + ExecRestatementReason + + N + + Populated when ExecType (150) = Restated (D).\ + 100 - Stop triggered, for stop and stop limit orders +
+ 77 + + OpenClose + + N + + Position effect of the execution.\ + O - Open\ + C - Close\ + Always populated on leg execution reports for multileg orders +
+ 167 + + SecurityType + + N + + CS - Common Stock\ + OPT - Option\ + MLEG - Multileg Instrument (parent execution reports of multileg orders) +
+ 200 + + MaturityMonthYear + + N + + Populated for option executions (`167=OPT`) +
+ 201 + + PutOrCall + + N + + Populated for option executions (`167=OPT`).\ + 0 - Put\ + 1 - Call +
+ 202 + + StrikePrice + + N + + Populated for option executions (`167=OPT`) +
+ 205 + + MaturityDay + + N + + Populated for option executions (`167=OPT`) +
+ 442 + + MultiLegReportingType + + N + + Set only on executions emitted from multileg parent orders.\ + 2 - Individual leg of a multi-leg security (one execution report per leg)\ + 3 - Multi-leg security (parent-level execution report) +
+ 654 + + LegRefID + + N + + Identifier of the leg this execution report corresponds to.\ + Populated on leg execution reports (`442=2`); matches the LegRefID supplied on the parent order +
+ 555 + + NoLegs + + N + + Number of legs in the NoLegs repeating group.\ + Populated on parent execution reports (`442=3`). See [New Order - Multileg (AB)](#new-order---multileg-ab) for repeating group fields +
+ +**Notes on execution reports for multileg orders** + +Each state transition for a multileg parent order produces: + +* One execution report per leg with `MultiLegReportingType (442) = 2` (Individual leg). Leg execution reports carry the leg's `LegRefID (654)`, `Side (54)`, `OrderQty (38) = parent_qty * LegRatioQty`, option tags (when the leg is an option) and `OpenClose (77)` for the leg's position effect. +* Optionally, one parent-level execution report with `MultiLegReportingType (442) = 3` and `SecurityType (167) = MLEG`. Parent execution reports include the `NoLegs (555)` repeating group describing all legs and use the parent's `ClOrdID (11)` and `OrderID (37)`. Whether parent-level execution reports are sent is agreed at session onboarding. + +For fills, leg-level `LastPx (31)` and `LastShares (32)` reflect the per-leg execution, while the parent execution report's `LastPx (31)` reflects the net spread price (sum of buy-leg prices minus sum of sell-leg prices, scaled by leg ratios) and `LastShares (32)` reflects the number of completed spread units. + +**Example FIX Messages** + +```text Pending New +|8=FIX.4.2|9=216|1=TEST_ACCOUNT|17=694bc450-3ca6-461e-8566-f977dcec9e2d|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=A|40=1|49=ALPACA|52=20230615-18:14:29.702|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:14:29.702|150=A|151=10|10=088| +``` + +```text New +|8=FIX.4.2|9=216|1=TEST_ACCOUNT|17=694bc450-3ca6-461e-8566-f977dcec9e2d|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=0|40=1|49=ALPACA|52=20230615-18:14:45.263|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:14:45.263|150=0|151=10|10=054| +``` + +```text Partial Fill +|8=FIX.4.2|9=251|1=TEST_ACCOUNT|6=350.78|14=5|15=USD|17=694bc450-3ca6-461e-8566-f977dcec9e2d|31=350.78|32=5|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=1|40=1|49=ALPACA|52=20230615-18:15:00.622|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:15:00.622|150=1|151=5|10=185| +``` + +```text Fill +|8=FIX.4.2|9=253|1=TEST_ACCOUNT|6=350.78|14=10|15=USD|17=694bc450-3ca6-461e-8566-f977dcec9e2d|31=350.78|32=10|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=2|40=1|49=ALPACA|52=20230615-18:15:21.920|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:15:21.920|150=2|151=0|10=024| +``` + +```text Pending Replace +|8=FIX.4.2|9=216|1=TEST_ACCOUNT|17=694bc450-3ca6-461e-8566-f977dcec9e2d|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=E|40=1|49=ALPACA|52=20230615-18:26:53.971|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:26:53.971|150=E|151=10|10=112| +``` + +```text Replaced +|8=FIX.4.2|9=256|1=TEST_ACCOUNT|17=694bc450-3ca6-461e-8566-f977dcec9e2d|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=5|40=1|41=56fcd203-7a97-430d-b14c-b0d9a7f59f2f|49=ALPACA|52=20230615-18:15:38.108|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:15:38.108|150=5|151=10|10=144| +``` + +```text Pending Cancel +|8=FIX.4.2|9=216|1=TEST_ACCOUNT|17=694bc450-3ca6-461e-8566-f977dcec9e2d|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=6|40=1|49=ALPACA|52=20230615-18:24:50.191|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:24:50.191|150=6|151=10|10=060| +``` + +```text Canceled +|8=FIX.4.2|9=216|1=TEST_ACCOUNT|17=694bc450-3ca6-461e-8566-f977dcec9e2d|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=4|40=1|49=ALPACA|52=20230615-18:17:45.813|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:17:45.813|150=4|151=10|10=070| +``` + +```text Rejected +|8=FIX.4.2|9=227|1=TEST_ACCOUNT|17=694bc450-3ca6-461e-8566-f977dcec9e2d|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=8|40=1|49=ALPACA|52=20230615-18:15:51.825|54=1|55=SPY|56=SENDER|59=0|60=20230615-18:15:51.825|103=Price too low|150=8|10=191| +``` + +```text Option Fill (META Jan-17-2025 100 Call) +|8=FIX.4.2|9=296|1=TEST_ACCOUNT|6=2.15|14=2|15=USD|17=694bc450-3ca6-461e-8566-f977dcec9e2d|31=2.15|32=2|34=12|35=8|37=c5bfc5f6-163d-450e-bb4a-fb25188cde8e|39=2|40=2|44=2.15|49=ALPACA|52=20240910-14:46:01.220|54=1|55=META|56=SENDER|59=0|60=20240910-14:46:01.220|77=O|150=2|151=0|167=OPT|200=202501|201=1|202=100|205=17|10=189| +``` + +```text Multileg Leg Execution Report (call leg, partial fill) +|8=FIX.4.2|9=320|1=TEST_ACCOUNT|6=79.25|11=PARENT_CLORDID|14=1|15=USD|17=4d6a8e2d-b86a-4ab9-9d3b-3a4dc6f5fa1c|31=79.25|32=1|34=18|35=8|37=ORDER_ID|39=1|40=2|44=10.5|49=ALPACA|52=20240910-15:00:00.001|54=1|55=AAPL|56=SENDER|59=0|60=20240910-15:00:00.001|77=O|150=1|151=1|167=OPT|200=202501|201=1|202=100|205=17|442=2|654=0|10=180| +``` + +```text Multileg Parent Execution Report (filled spread) +|8=FIX.4.2|9=412|1=TEST_ACCOUNT|6=77.75|11=PARENT_CLORDID|14=2|15=USD|17=2bbf38f6-89f1-4f25-bb70-9f1e3ad7c1aa|31=77.75|32=2|34=22|35=8|37=ORDER_ID|39=2|40=2|44=10.5|49=ALPACA|52=20240910-15:00:05.500|56=SENDER|59=0|60=20240910-15:00:05.500|150=2|151=0|167=MLEG|442=3|555=2|600=AAPL|608=OC|611=20250117|612=100|624=1|623=1|564=O|654=0|600=AAPL|608=OP|611=20250117|612=100|624=2|623=1|564=O|654=1|10=224| +``` + +## Order Cancel Request (F) + +Sent by the client to request cancellation of an order. The same message type is used to cancel both single-leg (equity/option) and multi-leg orders. For multileg orders, `SecurityType (167)` should be set to `MLEG` and `Symbol (55)` should be the underlying root symbol of the parent order. + +| Tag | Field | Mandatory | Description | +| --- | ------------ | --------- | ------------------------------------------------------------------------------ | +| 35 | MsgType | Y | F - For Order Cancel Request | +| 1 | Account | Y | Account number | +| 11 | ClOrdID | Y | Unique identifier of cancel request assigned by the client | +| 41 | OrigClOrdID | Y | ClOrdID (11) of the order to be canceled as assigned by the client | +| 54 | Side | Y | As specified on the order to be canceled | +| 55 | Symbol | Y | As specified on the order to be canceled (underlying root symbol for multileg) | +| 60 | TransactTime | Y | UTC timestamp when cancel request was initiated | +| 167 | SecurityType | N | `OPT` for option orders, `MLEG` for multileg orders | + +To acknowledge this message, an Execution Report (8) with `150=6` is sent immediately followed by an Execution Report (8) with `150=4` or an Order Cancel Reject (9) message. Sometimes an Order Cancel Reject (9) message might be sent directly without sending an Execution Report (8) with `150=6`. For multileg orders, the acknowledgements are generated for each leg (`442=2`) and, when enabled, at the parent level (`442=3`). + +**Example FIX Messages** + +```text Cancel Request +|8=FIX.4.2|9=190|35=F|34=5|49=SENDER|52=20240524-16:02:44.709|56=ALPACA|1=account1|11=b165965d-0c9d-467e-a174-ee30f3fe6dbe|41=b5db0b8e-bbc1-4906-aff8-c58d18ba3398|54=1|55=AAPL|60=20240524-16:02:44.709206406|10=226| +``` + +## Order Cancel/Replace Request (G) + +Sent by the client to request modification of a single-leg (equity or single-leg option) order. To modify a multi-leg order, use [Multileg Order Cancel/Replace Request (AC)](#multileg-order-cancelreplace-request-ac) instead. + +| Tag | Field | Mandatory | Description | +| --- | ------------ | --------- | -------------------------------------------------------------------------------------------------------- | +| 35 | MsgType | Y | G - For Order Cancel/Replace Request | +| 1 | Account | Y | Account number | +| 11 | ClOrdID | Y | Unique identifier of the replacement order assigned by the client (must be no longer than 48 characters) | +| 21 | HandlInst | Y | 1 - Automated execution with no broker intervention, only accepted value | +| 38 | OrderQty | N | Modified quantity for the order | +| 40 | OrdType | Y | As specified on the original order | +| 41 | OrigClOrdID | Y | ClOrdID (11) of the order to be modified as assigned by the client | +| 44 | Price | N | Modified price for the order | +| 54 | Side | Y | As specified on the original order | +| 55 | Symbol | Y | As specified on the original order | +| 59 | TimeInForce | N | Modified time in force for the order | +| 60 | TransactTime | Y | UTC timestamp when cancel/replace request was initiated | +| 99 | StopPx | N | Modified stop price for the order | + +**Example FIX Messages** + +```text Cancel/Replace Request +|8=FIX.4.2|9=212|35=G|34=21|49=SENDER|52=20240524-16:16:58.956|56=ALPACA|1=account1|11=45169819-088a-4089-9758-f28e830e95f0|21=3|40=2|41=f001f209-2c3e-42e3-9ab1-10e74ee39fe5|44=1.50000|54=1|55=AAPL|60=20240524-16:16:58.956849295|10=173| +``` + +## Order Cancel Reject (9) + +Sent by the server if the Order Cancel Request (F) or Order Cancel/Replace Request (G) message could not be honored. Some common reject scenarios include: + +* when order is already filled or closed +* when a previous Order Cancel Request (F) or Order Cancel/Replace Request (G) is pending for this order + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tag + + Field + + Mandatory + + Description +
+ 35 + + MsgType + + Y + + 9 - For Order Cancel Reject +
+ 1 + + Account + + Y + + Account number +
+ 11 + + ClOrdID + + Y + + Unique identifier of cancel or cancel/replace request as assigned by the client +
+ 37 + + OrderID + + Y + + Unique identifier of the order as assigned by Alpaca for which the cancel or cancel/replace request was rejected +
+ 39 + + OrdStatus + + Y + + Order status after this cancel reject is applied +
+ 41 + + OrigClOrdID + + Y + + ClOrdID (11) of the order for which the cancel or cancel/replace request was rejected +
+ 58 + + Text + + N + + Reject reason +
+ 60 + + TransactTime + + N + + Server time in UTC when cancel or replace reject occurred +
+ 102 + + CxlRejReason + + N + + Code to identify reason for cancel rejection.\ + 0 - Too late to cancel\ + 1 - Unknown order\ + 2 - Broker Option\ + 3 - Order already in Pending Cancel or Pending Replace status +
+ 434 + + CxlRejResponseTo + + Y + + 1 - Response to Order Cancel Request (F)\ + 2 - Response to Order Cancel/Replace Request (G) +
+ +**Example FIX Messages** + +```text Cancel Reject +|8=FIX.4.2|9=220|35=9|34=18|49=ALPACA|52=20240524-16:02:46.215|56=SENDER|1=account1|11=a7860828-4dc5-4f8f-bfb1-8fbca8855c88|37=f50af678-bba4-44ea-9b23-0fc452ed4921|39=6|41=2c017b79-a843-4146-a2b7-3bf83af89482|58=TOO_LATE_TO_CANCEL|434=1|10=116| +``` + +```text Cancel/Replace Reject +|8=FIX.4.2|9=198|35=9|34=45|49=ALPACA|52=20240524-16:16:59.085|56=SENDER|1=account1|11=5cdf9082-067b-4497-a90c-f5e8c666409b|37=UNKNOWN|39=8|41=c7feaf5a-54d2-458d-8ab2-9b2f337a28ec|58=replace pending for order|434=2|10=179| +``` + +## New Order - Multileg (AB) + +Sent by the client to submit a new multi-leg order such as an option spread, straddle, strangle, or covered call. Although the FIX session operates on `BeginString=FIX.4.2`, the `NewOrderMultileg` message (`35=AB`) and its `NoLegs` repeating group follow the FIX 4.4 specification. + +Each leg may be either an option contract or an equity (for strategies that combine equity with options such as covered calls). The order is submitted as a single multileg request, and acknowledgements are returned at both the leg level and (optionally) the parent level — see [Execution Report (8)](#execution-report-8) for details. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tag + + Field + + Mandatory + + Description +
+ 35 + + MsgType + + Y + + AB - For New Order Multileg +
+ 1 + + Account + + Y + + Account number +
+ 11 + + ClOrdID + + Y + + Unique identifier of the parent order assigned by the client (must be no longer than 64 characters) +
+ 18 + + ExecInst + + N + + 1 - Not held\ + 5 - Held +
+ 21 + + HandlInst + + Y + + 1 - Automated execution with no broker intervention, only accepted value +
+ 38 + + OrderQty + + Y + + Number of spread units to trade. Per-leg quantity is `OrderQty * LegRatioQty (623)` +
+ 40 + + OrdType + + Y + + 1 - Market\ + 2 - Limit\ + 3 - Stop\ + 4 - Stop limit\ + 5 - Market on close\ + B - Limit on close +
+ 44 + + Price + + N + + Net debit/credit price per spread unit. Required for Limit (`40=2`) and Stop Limit (`40=4`) orders +
+ 55 + + Symbol + + Y + + Underlying root symbol common to all legs (e.g. `AAPL`) +
+ 59 + + TimeInForce + + Y + + 0 - Day (day)\ + 1 - Good Till Cancel (gtc)\ + 2 - At the Opening (opg)\ + 3 - Immediate or Cancel (ioc)\ + 4 - Fill or Kill (fok)\ + 7 - At the Close (cls) +
+ 60 + + TransactTime + + Y + + UTC timestamp of order creation by client +
+ 99 + + StopPx + + N + + Required for Stop (`40=3`) and Stop Limit (`40=4`) orders +
+ 152 + + CashOrderQty + + N + + Notional value to trade +
+ 167 + + SecurityType + + N + + MLEG - Multileg Instrument (recommended) +
+ 555 + + NoLegs + + Y + + Number of legs in the order. Must be **at least 2**. The repeating group fields below are repeated `NoLegs` times. Side (54) must NOT be supplied at the parent level for multileg orders +
+ →600 + + LegSymbol + + Y + + Per-leg symbol. For option legs, the underlying root (e.g. `AAPL`); for equity legs, the equity ticker +
+ →608 + + LegCFICode + + Y + + OC - Option Call\ + OP - Option Put\ + ES - Equity (for strategies like covered calls) +
+ →611 + + LegMaturityDate + + N + + Expiration date in `YYYYMMDD` format.\ + Required for option legs (`LegCFICode = OC` or `OP`) +
+ →612 + + LegStrikePrice + + N + + Strike price of the option leg.\ + Required for option legs (`LegCFICode = OC` or `OP`) +
+ →623 + + LegRatioQty + + Y + + Ratio of this leg relative to the parent OrderQty. Must be positive. Per-leg quantity = `OrderQty * LegRatioQty` +
+ →624 + + LegSide + + Y + + 1 - Buy\ + 2 - Sell +
+ →654 + + LegRefID + + Y + + Client-assigned per-leg identifier, unique within the order (max 32 characters). Echoed back on every leg execution report +
+ →564 + + LegPositionEffect + + N + + O - Open\ + C - Close\ + Combined with `LegSide` to derive the per-leg position intent. When omitted, intent is inferred from `LegSide` (Buy → open, Sell → close) +
+ +**Notes** + +* Multileg orders that include `Side (54)` at the parent level, have fewer than 2 legs, or are missing any required leg field are rejected with a Business Message Reject (code 9001). +* Each leg's execution report echoes the `LegRefID (654)` supplied on the order, so leg-level acknowledgements can be correlated with the originating leg. +* MOC (`40=5`) and LOC (`40=B`) order types are equivalent to submitting `OrdType` Market / Limit with `TimeInForce (59) = 7` (At the Close). + +**Example FIX Message** + +```text Multileg Limit (long call vertical: buy 100C @ ask, sell 105C @ bid, net debit limit 10.50) +|8=FIX.4.2|9=350|35=AB|34=42|49=SENDER|52=20240910-15:00:00.000|56=ALPACA|1=account1|11=spread-001|21=1|38=2|40=2|44=10.5|55=AAPL|59=0|60=20240910-15:00:00.000|167=MLEG|555=2|600=AAPL|654=0|608=OC|611=20250117|612=100|624=1|623=1|564=O|600=AAPL|654=1|608=OC|611=20250117|612=105|624=2|623=1|564=O|10=147| +``` + +## Multileg Order Cancel/Replace Request (AC) + +Sent by the client to request modification of a multi-leg order. Like [New Order - Multileg (AB)](#new-order---multileg-ab), the message body follows the FIX 4.4 specification while the session remains on `BeginString=FIX.4.2`. + +The replacement message must re-supply the complete `NoLegs` repeating group. Replacing a multileg order will produce a `Pending Replace` (`150=E`) and then a `Replaced` (`150=5`) execution report for each leg, and (when enabled) at the parent level. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Tag + + Field + + Mandatory + + Description +
+ 35 + + MsgType + + Y + + AC - For Multileg Order Cancel/Replace Request +
+ 1 + + Account + + Y + + Account number +
+ 11 + + ClOrdID + + Y + + Unique identifier of the replacement order assigned by the client (must be no longer than 64 characters) +
+ 21 + + HandlInst + + Y + + 1 - Automated execution with no broker intervention, only accepted value +
+ 38 + + OrderQty + + N + + Modified spread quantity for the order +
+ 40 + + OrdType + + Y + + As specified on the original order +
+ 41 + + OrigClOrdID + + Y + + ClOrdID (11) of the parent order being modified +
+ 44 + + Price + + N + + Modified net price per spread unit +
+ 55 + + Symbol + + Y + + Underlying root symbol (as on the original order) +
+ 59 + + TimeInForce + + N + + Modified time in force for the order +
+ 60 + + TransactTime + + Y + + UTC timestamp when cancel/replace request was initiated +
+ 99 + + StopPx + + N + + Modified stop price for the order +
+ 167 + + SecurityType + + N + + MLEG - Multileg Instrument (recommended) +
+ 555 + + NoLegs + + Y + + Re-supply the full leg list (≥ 2 legs). Side (54) must NOT be supplied at the parent level. See [New Order - Multileg (AB)](#new-order---multileg-ab) for the repeating group fields (`600`, `608`, `611`, `612`, `623`, `624`, `654`, `564`) +
+ +**Notes** + +* Submitting a Multileg Cancel/Replace while a previous replace is still pending for the same `OrigClOrdID` is rejected with an [Order Cancel Reject (9)](#order-cancel-reject-9) (`text=replace pending for order`). +* The `NoLegs` group is validated with the same rules as the new-order message; rejections are returned via Business Message Reject (code 9001). + +**Example FIX Message** + +```text Multileg Cancel/Replace (raise net limit from 10.50 to 11.00) +|8=FIX.4.2|9=370|35=AC|34=58|49=SENDER|52=20240910-15:05:00.000|56=ALPACA|1=account1|11=spread-001-rep|21=1|38=2|40=2|41=spread-001|44=11.0|55=AAPL|60=20240910-15:05:00.000|167=MLEG|555=2|600=AAPL|654=0|608=OC|611=20250117|612=100|624=1|623=1|564=O|600=AAPL|654=1|608=OC|611=20250117|612=105|624=2|623=1|564=O|10=189| +``` + +## Version History + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Version + + Date + + Change +
+ 0.1.0 + + 08/05/2024 + + Document Creation +
+ 0.1.1 + + 24/05/2024 + + Changed FIX version to FIX.4.2\ + Removed ApplVerID (1128) from Message Header\ + Added ClOrdID (11) and OrigClOrdID (41) as required in Order Cancel Request (F) and Order Cancel/Replace Request (G)\ + Removed OrderID (37) from Order Cancel Request (F) and Order Cancel/Replace Request (G)\ + Added OrderQty (38) to Order Cancel/Replace Request (G)\ + Updated FIX examples +
+ 0.1.2 + + 12/06/2024 + + Removed Account (1) and RawData (96) from Logon (A) message\ + Added RefTagID (371), RefMsgType (372) and SessionRejectReason (373) to Reject (3) message\ + Added HandlInst (21), TransactTime (60) and OpenClose (77) to New Order - Single (D)\ + Added 1 (per share) as an accepted value for CommType (13) in New Order - Single (D)\ + Updated AvgPx (6) to mandatory in Execution Report (8)\ + Added Commission (12), ExecRefID (19), ExecTransType (20), LastMkt (30), OrderQty (38) and CashOrderQty (152) to Execution Report (8)\ + Removed OrdRejReason (103) from Execution Report (8)\ + Updated possible values for ExecType (150) in Execution Report (8) +
+ 1.0.0 + + 13/06/2024 + + Added Side (54), Symbol (55) and TransactTime (60) to Order Cancel Request (F)\ + Added OrderID (37) to Order Cancel Reject (9)\ + Added HandlInst (21), OrdType (40), Side (54), Symbol (55) and TransactTime (60) to Order Cancel/Replace Request (G) +
+ 1.0.1 + + 04/09/2024 + + Added Market on close (5) and Limit on close (B) as values for OrdType (40) on New Order - Single (D)\ + Added At the Close (7) as a value for TimeInForce (59) on New Order - Single (D) +
+ 1.0.2 + + 08/10/2024 + + Added Restated (D) as value for ExecType (150) on Execution Report (8)\ + Added ExecRestatementReason (378) to Execution Report (8)\ + Added Status (3) as value for ExecTransType (20) on Execution Report (8)\ + Added TransactTime (60) to Order Cancel Reject (9) +
+ 1.0.3 + + 14/01/2025 + + Added Good Till Crossing (5) as a value for TimeInForce (59) on New Order - Single (D)\ + Added ClientID (109) to New Order - Single (D) +
+ 1.0.4 + + 03/04/2025 + + Removed Good Till Crossing (5) as a valid value for TimeInForce (59) on New Order - Single (D)\ + Added TradingSessionID (336) to New Order - Single (D) +
+ 1.1.0 + + 11/05/2026 + + Added Options L3 support:\ + Added SecurityType (167), MaturityMonthYear (200), PutOrCall (201), StrikePrice (202) and MaturityDay (205) to New Order - Single (D) for single-leg option orders\ + Added SecurityType (167), MaturityMonthYear (200), PutOrCall (201), StrikePrice (202), MaturityDay (205), OpenClose (77), MultiLegReportingType (442), LegRefID (654) and NoLegs (555) to Execution Report (8)\ + Added SecurityType (167) to Order Cancel Request (F) to support cancellation of multileg and option orders\ + Added New Order - Multileg (AB) message with the NoLegs (555) repeating group (LegSymbol 600, LegCFICode 608, LegMaturityDate 611, LegStrikePrice 612, LegRatioQty 623, LegSide 624, LegRefID 654, LegPositionEffect 564)\ + Added Multileg Order Cancel/Replace Request (AC) message +
\ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/fractional-trading.md b/DesktopBot/Documents/Alpaca/fractional-trading.md new file mode 100644 index 0000000..32ac811 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/fractional-trading.md @@ -0,0 +1,84 @@ +# Fractional Trading + +Fractional shares are fractions of a whole share, meaning that you don’t need to buy a whole share to own a portion of a company. You can now buy as little as $1 worth of shares for over 2,000 US equities. + +By default all Alpaca accounts are allowed to trade fractional shares in both live and paper environments. Please make sure you reset your paper account if you run into any issues dealing with fractional shares. + +# Supported Order Types + +Alpaca currently supports fractional trading for market, limit, stop & stop limit orders with a time in force=Day, accommodating both fractional quantities and notional values. You can pass either a fractional amount (qty), or a notional value (notional) in any POST/v2/orders request. Note that entering a value for either parameters, will automatically nullify the other. If both qty and notional are entered the request will be rejected with an error status 400. + +Both notional and qty fields can take up to 9 decimal point values. + +Moreover, we support fractional shares trading not only during standard market hours, but extending into pre-market (4:00 a.m. - 9:30 a.m. ET), post-market (4:00 p.m. - 8:00 p.m. ET) and overnight (8:00 p.m. - 4:00 a.m.) hours, offering global investors the ability to trade during the full extended hours session. + +# Eligible Securities + +Only exchange-listed securities are eligible to trade in the extended hours. Additionally, the asset must be enabled as a fractional asset on Alpaca’s side. If there is an asset you want to trade in the extended hours and it is not eligible, please contact our [support](mailto:support@alpaca.markets) team. + +# Sample Requests + +## Notional Request + +```json +{ + "symbol": "AAPL", + "notional": 500.75, + "side": "buy", + "type": "market", + "time_in_force": "day" +} +``` + +## Fractional Request + +```json +{ + "symbol": "AAPL", + "qty": 3.654, + "side": "buy", + "type": "market", + "time_in_force": "day" +} +``` + +# Supported Assets + +Not all assets are fractionable yet so please make sure you query assets details to check for the parameter `fractionable = true`. + +Supported fractionable assets would return a response that looks like this + +```json +{ + "id": "b0b6dd9d-8b9b-48a9-ba46-b9d54906e415", + "class": "us_equity", + "exchange": "NASDAQ", + "symbol": "AAPL", + "name": "Apple Inc. Common Stock", + "status": "active", + "tradable": true, + "marginable": true, + "shortable": true, + "easy_to_borrow": true, + "fractionable": true +} +``` + +If you request a fractional share order for a stock that is not yet fractionable, the order will get rejected with an error message that reads `requested asset is not fractionable`. + +# Dividends + +Dividend payments occur the same way in fractional shares as with whole shares, respecting the proportional value of the share that you own. + +For example if the dividend amount is $0.10 per share and you own 0.5 shares of that stock then you will receive $0.05 as dividend. As a general rule of thumb all dividends are rounded to the nearest penny. + +# Notes on Fractional Trading + +* We do not support short sales in fractional orders. All fractional sell orders are marked long. +* The expected price of fill is the NBBO quote at the time the order was submitted. If you submit an order for a whole and fraction, the price for the whole share fill will be used to price the fractional portion of the order. +* Day trading fractional shares counts towards your day trade count. +* You can cancel a fractional share order that is pending, the same way as whole share orders. +* Limit orders are supported for both fractional and notional orders. Extended hours are also supported with limit orders (same as whole share orders). +* Fees for fractional trading work the same way as with whole shares. + +Alpaca does not make recommendations with regard to fractional share trading, whether to use fractional shares at all, or whether to invest in any specific security. A security’s eligibility on the list of fractional shares available for trading is not an endorsement of any of the securities, nor is it intended to convey that such stocks have low risk. Fractional share transactions are executed either on a principal or riskless principal basis, and can only be bought or sold with market orders during normal market hours. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/getting-started-with-alpaca-market-data.md b/DesktopBot/Documents/Alpaca/getting-started-with-alpaca-market-data.md new file mode 100644 index 0000000..76da671 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/getting-started-with-alpaca-market-data.md @@ -0,0 +1,179 @@ +# Getting Started with Market Data API + +This is a quick guide on how to start consuming market data via APIs. Starting from beginning to end, this section outlines how to install Alpaca’s software development kit (SDK), create a free alpaca account, locate your API keys, and how to request both historical and real-time data. + +# Installing Alpaca’s Client SDK + +In this guide, we’ll be making use of the SDKs provided by Alpaca. Alpaca maintains SDKs in four languages: Python, JavaScript, C#, and Go. Follow the steps in the installation guide below to install the SDK of your choice before proceeding to the next section. + +```python +pip install alpaca-py +``` + +```go +go get -u github.com/alpacahq/alpaca-trade-api-go/v3/alpaca +``` + +```javascript +npm install --save @alpacahq/alpaca-trade-api +``` + +```csharp +dotnet add package Alpaca.Markets +``` + +# Generate API Keys + +Go to the [Alpaca dashboard](https://app.alpaca.markets/brokerage/dashboard/overview) and find the **API Keys** section on the right sidebar. Click on Generate New Keys and save the generated API credentials. If you have previously generated keys there and you lost the secret, you can also regenerate them here. + +# How to Request Market Data Through the SDK + +With the SDK installed and our API keys ready, you can start requesting market data. Alpaca offers many options for both historical and real-time data, so to keep this guide succint, these examples are on obtaining historical and real-time bar data. Information on what other data is available can be found in the Market Data API reference. + +To start using the SDK for historical data, import the SDK and instantiate the crypto historical data client. It’s not required for this client to pass in API keys or a paper URL. + +```python +from alpaca.data.historical import CryptoHistoricalDataClient + +# No keys required for crypto data +client = CryptoHistoricalDataClient() +``` + +```go +package main + +import "github.com/alpacahq/alpaca-trade-api-go/v3/marketdata" + +func main() { + // No keys required for crypto data + client := marketdata.NewClient(marketdata.ClientOpts{}) +} +``` + +```javascript JavaScript +import Alpaca from "@alpacahq/alpaca-trade-api"; + +// Alpaca() requires the API key and sectret to be set, even for crypto +const alpaca = new Alpaca({ + keyId: "YOUR_API_KEY", + secretKey: "YOUR_API_SECRET", +}); +``` + +Next we’ll define the parameters for our request. Import the request class for crypto bars, CryptoBarsRequest and TimeFrame class to access time frame units more easily. This example queries for historical daily bar data of Bitcoin in the first week of September 2022. + +```python +from alpaca.data.requests import CryptoBarsRequest +from alpaca.data.timeframe import TimeFrame + +# Creating request object +request_params = CryptoBarsRequest( + symbol_or_symbols=["BTC/USD"], + timeframe=TimeFrame.Day, + start=datetime(2022, 9, 1), + end=datetime(2022, 9, 7) +) +``` + +```go +request := marketdata.GetCryptoBarsRequest{ + TimeFrame: marketdata.OneDay, + Start: time.Date(2022, 9, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2022, 9, 7, 0, 0, 0, 0, time.UTC), +} +``` + +```javascript JavaScript +let options = { + start: "2022-09-01", + end: "2022-09-07", + timeframe: alpaca.newTimeframe(1, alpaca.timeframeUnit.DAY), +}; +``` + +Finally, send the request using the client’s built-in method, get\_crypto\_bars. Additionally, we’ll access the .df property which returns a pandas DataFrame of the response. + +```python +# Retrieve daily bars for Bitcoin in a DataFrame and printing it +btc_bars = client.get_crypto_bars(request_params) + +# Convert to dataframe +btc_bars.df +``` + +```go + bars, err := client.GetCryptoBars("BTC/USD", request) + if err != nil { + panic(err) + } + for _, bar := range bars { + fmt.Printf("%+v\n", bar) + } +``` + +```javascript JavaScript +(async () => { + const bars = await alpaca.getCryptoBars(["BTC/USD"], options); + + console.table(bars.get("BTC/USD")); +})(); +``` + +Returns + +```text Python + open high low close volume trade_count vwap +symbol timestamp +BTC/USD 2022-09-01 05:00:00+00:00 20055.79 20292.00 19564.86 20156.76 7141.975485 110122.0 19934.167845 + 2022-09-02 05:00:00+00:00 20156.76 20444.00 19757.72 19919.47 7165.911879 96231.0 20075.200868 + 2022-09-03 05:00:00+00:00 19924.83 19968.20 19658.04 19806.11 2677.652012 51551.0 19800.185480 + 2022-09-04 05:00:00+00:00 19805.39 20058.00 19587.86 19888.67 4325.678790 62082.0 19834.451414 + 2022-09-05 05:00:00+00:00 19888.67 20180.50 19635.96 19760.56 6274.552824 84784.0 19812.095982 + 2022-09-06 05:00:00+00:00 19761.39 20026.91 18534.06 18724.59 11217.789784 128106.0 19266.835520 +``` + +```go Go +{Timestamp:2022-09-01 05:00:00 +0000 UTC Open:20055.79 High:20292 Low:19564.86 Close:20156.76 Volume:7141.975485 TradeCount:110122 VWAP:19934.1678446199} +{Timestamp:2022-09-02 05:00:00 +0000 UTC Open:20156.76 High:20444 Low:19757.72 Close:19919.47 Volume:7165.911879 TradeCount:96231 VWAP:20075.2008677126} +{Timestamp:2022-09-03 05:00:00 +0000 UTC Open:19924.83 High:19968.2 Low:19658.04 Close:19806.11 Volume:2677.652012 TradeCount:51551 VWAP:19800.1854803241} +{Timestamp:2022-09-04 05:00:00 +0000 UTC Open:19805.39 High:20058 Low:19587.86 Close:19888.67 Volume:4325.67879 TradeCount:62082 VWAP:19834.4514137038} +{Timestamp:2022-09-05 05:00:00 +0000 UTC Open:19888.67 High:20180.5 Low:19635.96 Close:19760.56 Volume:6274.552824 TradeCount:84784 VWAP:19812.0959815687} +{Timestamp:2022-09-06 05:00:00 +0000 UTC Open:19761.39 High:20026.91 Low:18534.06 Close:18724.59 Volume:11217.789784 TradeCount:128106 VWAP:19266.8355201911} +``` + +```javascript JavaScript +┌─────────┬──────────┬──────────┬──────────┬────────────┬──────────┬────────────────────────┬──────────────┬──────────────────┐ +│ (index) │ Close │ High │ Low │ TradeCount │ Open │ Timestamp │ Volume │ VWAP │ +├─────────┼──────────┼──────────┼──────────┼────────────┼──────────┼────────────────────────┼──────────────┼──────────────────┤ +│ 0 │ 20156.76 │ 20292 │ 19564.86 │ 110122 │ 20055.79 │ '2022-09-01T05:00:00Z' │ 7141.975485 │ 19934.1678446199 │ +│ 1 │ 19919.47 │ 20444 │ 19757.72 │ 96231 │ 20156.76 │ '2022-09-02T05:00:00Z' │ 7165.911879 │ 20075.2008677126 │ +│ 2 │ 19806.11 │ 19968.2 │ 19658.04 │ 51551 │ 19924.83 │ '2022-09-03T05:00:00Z' │ 2677.652012 │ 19800.1854803241 │ +│ 3 │ 19888.67 │ 20058 │ 19587.86 │ 62082 │ 19805.39 │ '2022-09-04T05:00:00Z' │ 4325.67879 │ 19834.4514137038 │ +│ 4 │ 19760.56 │ 20180.5 │ 19635.96 │ 84784 │ 19888.67 │ '2022-09-05T05:00:00Z' │ 6274.552824 │ 19812.0959815687 │ +│ 5 │ 18724.59 │ 20026.91 │ 18534.06 │ 128106 │ 19761.39 │ '2022-09-06T05:00:00Z' │ 11217.789784 │ 19266.8355201911 │ +└─────────┴──────────┴──────────┴──────────┴────────────┴──────────┴────────────────────────┴──────────────┴──────────────────┘ +``` + +# Request ID + +All market data API endpoint provides a unique identifier of the API call in the response header with `X-Request-ID` key, the Request ID helps us to identify the call chain in our system. + +Make sure you provide the Request ID in all support requests that you created, it could help us to solve the issue as soon as possible. Request ID can't be queried in other endpoints, that is why we suggest to persist the recent Request IDs. + +```shell +$ curl -v https://data.alpaca.markets/v2/stocks/bars +... +> GET /v2/stocks/bars HTTP/1.1 +> Host: data.alpaca.markets +> User-Agent: curl/7.88.1 +> Accept: */* +> +< HTTP/1.1 403 Forbidden +< Date: Fri, 25 Aug 2023 09:37:03 GMT +< Content-Type: application/json +< Content-Length: 26 +< Connection: keep-alive +< X-Request-ID: 0d29ba8d9a51ee0eb4e7bbaa9acff223 +< +... +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/getting-started-with-trading-api.md b/DesktopBot/Documents/Alpaca/getting-started-with-trading-api.md new file mode 100644 index 0000000..0e0312f --- /dev/null +++ b/DesktopBot/Documents/Alpaca/getting-started-with-trading-api.md @@ -0,0 +1,27 @@ +# Getting Started with Trading API + +This section outlines how to install Alpaca’s SDKs, create a free alpaca account, locate your API keys, and how to submit orders applicable for both stocks and crypto. + +# Request ID + +All trading API endpoint provides a unique identifier of the API call in the response header with `X-Request-ID` key, the Request ID helps us to identify the call chain in our system. + +Make sure you provide the Request ID in all support requests that you created, it could help us to solve the issue as soon as possible. Request ID can't be queried in other endpoints, that is why we suggest to persist the recent Request IDs. + +```shell +$ curl -v https://paper-api.alpaca.markets/v2/account +... +> GET /v2/account HTTP/1.1 +> Host: paper-api.alpaca.markets +> User-Agent: curl/7.88.1 +> Accept: */* +> +< HTTP/1.1 403 Forbidden +< Date: Fri, 25 Aug 2023 09:34:40 GMT +< Content-Type: application/json +< Content-Length: 26 +< Connection: keep-alive +< X-Request-ID: 649c5a79da1ab9cb20742ffdada0a7bb +< +... +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/getting-started.md b/DesktopBot/Documents/Alpaca/getting-started.md new file mode 100644 index 0000000..a778f4e --- /dev/null +++ b/DesktopBot/Documents/Alpaca/getting-started.md @@ -0,0 +1,18 @@ +# Welcome + +This page will help you get started with Alpaca Docs. You'll be up and running in a jiffy! + +# Welcome to Alpaca 🦙 + +Alpaca offers simple, modern API-first solutions to enable anyone, either individuals or businesses, to connect applications and build algorithms to buy and sell stocks or crypto. + +Whether you are launching an app to access the US equities market or deploy algorithmic trading-strategies with stocks and crypto, Alpaca has an API for you. + +* Build trading apps and brokerage services for your end users. Tailored for businesses such as trading apps, challenger banks, etc. - **Start [here](/docs/getting-started-with-broker-api)** +* Stock trading for individuals and business accounts. Built for retail, algorithmic and proprietary traders. - **Start [here](https://docs.alpaca.markets/docs/getting-started-with-trading-api)** +* Access real-time market pricing data and up to 6+ years worth of historical data for stocks and crypto. - **Start [here](https://docs.alpaca.markets/docs/getting-started-with-alpaca-market-data)** +* Develop applications on Alpaca’s platform using OAuth2. Let any user with an Alpaca brokerage account connect to your app. - **Start [here](https://docs.alpaca.markets/docs/about-connect-api)** + +If you are not a developer and an API is not for you, we also enable users to trade on our responsive web dashboard. + +Stay tuned for our API updates as we have on roadmap plans for futures, FX, and much more! \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/historical-api.md b/DesktopBot/Documents/Alpaca/historical-api.md new file mode 100644 index 0000000..3dd6075 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/historical-api.md @@ -0,0 +1,24 @@ +# Historical API + +This RESTful API provides historical market data through the HTTP protocol. This allows you to query historical market information, which can be used for charting, backtesting and to power your trading strategies. + +Historical market data is available for the following types: + +* [Stocks](https://docs.alpaca.markets/docs/historical-stock-data-1) +* [Crypto](https://docs.alpaca.markets/docs/historical-crypto-data-1) +* [Options](https://docs.alpaca.markets/docs/historical-option-data) +* [News](https://docs.alpaca.markets/docs/historical-news-data) + +# Base URL + +The Base URL for the historical endpoints is + +``` +https://data.alpaca.markets/{version} +``` + +Sandbox URL (for broker partners): + +``` +https://data.sandbox.alpaca.markets/{version} +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/historical-crypto-data-1.md b/DesktopBot/Documents/Alpaca/historical-crypto-data-1.md new file mode 100644 index 0000000..f6d1a06 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/historical-crypto-data-1.md @@ -0,0 +1,9 @@ +# Historical Crypto Data + +This API provides historical market data for crypto. Check the [API Reference](https://docs.alpaca.markets/reference/cryptobars-1) for the detailed descriptions of all the endpoints. + +Since Alpaca now executes all crypto orders in its own exchange, the v1beta3 crypto market data endpoints no longer distribute data from other providers, but from Alpaca itself. + +> 📘 Crypto bars contain quote mid-prices +> +> Due to the volatility of some currencies, including lack of trade volume at any given time, we include the quote midpoint prices in the bars to offer a better data experience. If in a bar no trade happens, the volume will be 0, but the prices will be determined by the quote prices. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/historical-news-data.md b/DesktopBot/Documents/Alpaca/historical-news-data.md new file mode 100644 index 0000000..9d09e3f --- /dev/null +++ b/DesktopBot/Documents/Alpaca/historical-news-data.md @@ -0,0 +1,15 @@ +# Historical News Data + +This API provides historical news data dating back to 2015. You can expect to receive an average of 130+ news articles per day. All news data is currently provided directly by [Benzinga](https://www.benzinga.com/). With a single endpoint, you can request news for both stocks and cryptocurrency tickers. Check the [API Reference](https://docs.alpaca.markets/reference/news-3) for the detailed descriptions the endpoint. + +# Use Cases + +News API is a versatile tool that can be used to support a variety of use cases, such as building an app with the Broker API or Algorithmic Trading using Sentiment Analysis on News with the Trading API. + +1. **News Widgets** + + News API can be used to create visual news widgets for web and mobile apps. These widgets can be used to display the latest news for any stock or crypto symbol, and they include different sized images to give your app a visual appeal. +2. **News Sentiment Analysis**\ + News API can be used to train models that can determine the sentiment of a given headline or news content. This can be done by using historical data from News API to train the model on a variety of different sentiment labels. +3. **Realtime Trading on News**\ + [Real-time news over WebSockets](https://docs.alpaca.markets/edit/streaming-real-time-news) can be used to enable your trading algorithms to react to the latest news across any stock or cryptocurrency. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/historical-option-data.md b/DesktopBot/Documents/Alpaca/historical-option-data.md new file mode 100644 index 0000000..e7a695c --- /dev/null +++ b/DesktopBot/Documents/Alpaca/historical-option-data.md @@ -0,0 +1,16 @@ +# Historical Option Data + +This API provides historical market data for options. Check the [API Reference](https://docs.alpaca.markets/reference/optionbars) for the detailed descriptions of all the endpoints. + +> 🚧 Data availability +> +> Currently we only offer historical option data since February 2024. + +# Data sources + +Similarly to stocks, Alpaca offers two different data sources for options: + +| Source | Description | +| :------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Indicative** | Indicative Pricing Feed is a free derivative of the original OPRA feed: the quotes are not actual OPRA quotes, they’re just indicative derivatives. The trades are also derivatives and they’re delayed by 15 minutes. | +| **OPRA (Options Price Reporting Authority)** | OPRA is the consolidated BBO feed of OPRA. [OPRA Plan](https://www.opraplan.com/document-library) defines the BBO as the highest bid and lowest offer for a series of options available in one or more of the options markets maintained by the parties. OPRA feed is only available to subscribed users. | \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/historical-stock-data-1.md b/DesktopBot/Documents/Alpaca/historical-stock-data-1.md new file mode 100644 index 0000000..a73ed13 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/historical-stock-data-1.md @@ -0,0 +1,71 @@ +# Historical Stock Data + +This API provides historical market data for equities. Check the [API Reference](https://docs.alpaca.markets/reference/stockbars) for detailed descriptions of all REST endpoints. + +# Data Sources + +Alpaca offers market data from various data sources described below. You can use the `feed` parameter on all the stock endpoints to switch between them. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Source + + Description +
+ **iex** + + IEX ([The Investors Exchange](https://www.iexexchange.io/)) is ideal for initial app testing and situations where precise pricing may not be the primary focus. It's a single US exchange that accounts for approximately \~2.5% of the market volume. + + ℹ️ This is the only feed that can be used without a subscription. +
+ **sip** + + This feed covers all US exchanges, originating directly from exchanges and is consolidated by the Securities Information Processors: [UTP](https://utpplan.com/) (Nasdaq) and [CTA](https://www.nyse.com/data/cta) (NYSE). These SIPs play a crucial role in connecting various U.S. markets, processing and consolidating all bid/ask quotes and trades from multiple trading venues into a single, easily accessible data feed. + + Our data delivery ensures ultra-low latency and high reliability, as the information is transmitted directly to Alpaca's bare metal servers located in New Jersey, situated alongside many market participants. + + SIP data is particularly advantageous for developing your trading app, where precise and up-to-date price information is essential for traders and internal operations. It accounts for 100% of the market volume, providing comprehensive coverage for your trading needs. +
+ **boats** + + [Blue Ocean ATS](https://blueocean-tech.io/) is the first alternative trading system to expand market hours, filling the gap to trade equities continuously throughout US evening hours. +
+ **overnight** + + Our "overnight" feed is Alpaca's derived feed from the original BOATS source. It offers a cheaper, but slightly less accurate alternative for overnight US market data. The trades are 15 minutes delayed and adjusted to fit the bid-ask spread. +
+ +
\ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/margin-and-short-selling.md b/DesktopBot/Documents/Alpaca/margin-and-short-selling.md new file mode 100644 index 0000000..f70edb5 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/margin-and-short-selling.md @@ -0,0 +1,86 @@ +# Margin and Short Selling + +> In order to trade on margin or sell short, you must have $2,000 or more account equity. Accounts with less than $2,000 will not have access to these features and will be restricted to 1x buying power. +> +> This is only for Equities Trading. Margin Trading for Crypto is not applicable. In addition, PDT checks do not count towards crypto orders or fills. + +# How Margin Works + +Trading on margin allows you to trade and hold securities with a value that exceeds your account equity. This is made possible by funds loaned to you by your broker, who uses your account’s cash and securities as collateral. For example, a Reg T Margin Account holding $10,000 cash may purchase and hold up to $20,000 in marginable securities overnight (Note: some securities may have a higher maintenance margin requirement making the full 2x overnight buying power effectively unavailable). In addition to the 2x buying power afforded to margin accounts, a Reg T Margin Account flagged as a Pattern Day Trader(PDT) with $25,000 or greater equity will further be allowed to use up to 4x intraday buying power. As an example, a PDT account holding $50,000 cash may purchase and hold up to $200,000 in securities intraday; however, to avoid receiving a margin call the next morning, the securities held would need to be reduced to $100,000 or less depending on the maintenance margin requirement by the end of the day. + +## Initial Margin + +Initial margin denotes the percentage of the trade price of a security or basket of securities that an account holder must pay for with available cash in the margin account, additions to cash in the margin account or other marginable securities. + +Alpaca applies a minimum initial margin requirement of 50% for marginable securities and 100% for non-marginable securities per Regulation T of the Federal Reserve Board. + +## Maintenance Margin + +Maintenance margin is the amount of cash or marginable securities required to continue holding an open position. FINRA has set the minimum maintenance requirement to at least 25% of the total market value of the securities, but brokers are free to set higher requirements as part of their risk management. + +Alpaca uses the following table to calculate the overnight maintenance margin applied to each security held in an account: + +| Position Side | Condition | Margin Requirement | +| :------------ | :-------------------------------- | :----------------------------- | +| LONG | share price \< $2.50 | 100% of EOD market value | +| LONG | share price between $2.50 & $6.00 | 50% of EOD market value | +| LONG | share price > $6.00 | 30% of EOD market value | +| LONG | 2x Leveraged ETF | 50% of EOD market value | +| LONG | 3x Leveraged ETF | 75% of EOD market value | +| SHORT | share price \< $5.00 | Greater of $2.50/share or 100% | +| SHORT | share price >= $5.00 | Greater of $5.00/share or 30% | + +## Margin Calls + +If your account does not satisfy its initial and maintenance margin requirements at the end of the day, you will receive a margin call the following morning. We will contact you and advise you of the call amount that you will need to satisfy either by depositing new funds or liquidating some or all of your positions to reduce your margin requirement sufficiently. + +We may contact you prior to the end of the day and ask you to liquidate your positions immediately in the event that your account equity is materially below your maintenance requirement. Furthermore, although we will make every effort to contact you so that you can determine how to best resolve your margin call, we reserve the right to liquidate your holdings in the event we cannot get ahold of you and your account equity is in danger of turning negative. + +Calculating and tracking your margin requirement at all times is helpful to avoid receiving a margin call. We strongly recommend doing so if you plan to aggressively use overnight leverage. Please use a 50% initial requirement and refer to the maintenance margin table above. In the future, we will provide real-time estimated initial and maintenance margin values as part of the Account API to help users better manage their risk. + +# Margin Interest Rate + +We are pleased to offer a competitive and low annual margin interest rate of 4.75% for elite users and 6.25% for non-elite users (check “Alpaca Securities Brokerage Fee Schedule” on **Important Disclosures** for the latest rate). + +The rate is charged only on your account’s end of day (overnight) debit balance using the following calculation: + +`daily_margin_interest_charge = (settlement_date_debit_balance * rate[non-elite: 0.0625 | elite: 0.0475])) / 360` + +Interest will accrue daily and post to your account at the end of each month. Note that if you have a settlement date debit balance as of the end of day Friday, you will incur interest charges for 3 days (Fri, Sat, Sun). + +As an example, if you are a regular trader and deposited $10,000 into your account and bought $15,000 worth of securities that you held at the end of the day, you would be borrowing $5,000 overnight and would incur a daily interest expense of ($5000 \* 0.0625) / 360 = $0.87. + +On the other hand, if you deposited $10,000 and bought $15,000 worth of stock that you liquidated the same day, you would not incur any interest expense. In other words, this allows you to make use of the additional buying power for intraday trading without any cost. + +# Stock Borrow Rates + +Alpaca currently only supports opening short positions in easy to borrow (“ETB”) securities. Any open short order in a stock that changes from ETB to hard to borrow (“HTB”) overnight will be automatically cancelled prior to market open. + +**Note: Support for HTB securities is not yet available, but we are actively working towards supporting HTB in the future.** + +**In addition, Alpaca has introduced $0 borrow fees on all ETB (easy-to-borrow) shares for Trading API users.** + +Please note that stock borrow availability changes daily, and we update our assets table each morning, so please use our API to check each stock’s borrow status daily. It is infrequent but names can go from ETB → HTB and vice versa. +While we do not currently support opening short positions in HTB securities, we will not force you to close out a position in a stock that has gone from ETB to HTB unless the lender has called the stock. If a stock you hold short has gone from ETB to HTB, you will incur a daily stock borrow fee for that stock. We do not currently provide HTB rates via our API, so please contact us in these cases. + +If you hold an HTB short at any time during the day, you will incur a daily stock borrow fee: + +`Daily stock borrow fee = Daily HTB stock borrow fee` + +Where, + +`Daily HTB stock borrow fee = Σ((each stock’s HTB short $ market value _ that stock’s HTB rate) / 360)` + +Please note that if you hold HTB short positions as of a Friday settlement date, you will incur stock borrow fees for 3 days (Fri, Sat, Sun). HTB stock borrow fees are charged in the nearest round lot (100 shares), regardless of the actual number of shares shorted. This is because stocks are borrowed in round lots. + +# Concentrated Margin Requirements + +Accounts concentrated into a single position will see an increased maintenance margin rate on the symbol in which the account is concentrated. + +1. Concentration is defined as a single security accounting for 70% of the market value of equities and the account is carrying a margin balance of $100,000 or more. +2. The Maintenance Margin Rate on the concentrated position will increase to 50%. + +*** + +**Margin trading involves significant risk and is not suitable for all investors.** Before considering a margin loan, it is crucial that you carefully consider how borrowing fits with your investment objectives and risk tolerance. +When trading on margin, you assume higher market risk, and potential losses can exceed the collateral value in your account. Alpaca may sell any securities in your account, without prior notice, to satisfy a margin call. Alpaca may also change its “house” maintenance margin requirements at any time without advance written notice. You are not entitled to an extension of time on a margin call. Please review the Firm’s Margin Disclosure Statement before investing. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/market-data-faq.md b/DesktopBot/Documents/Alpaca/market-data-faq.md new file mode 100644 index 0000000..670d783 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/market-data-faq.md @@ -0,0 +1,1483 @@ +# Market Data FAQ + +Frequently Asked Questions + +# General + +## Why am I getting HTTP 403 (Forbidden)? + +The market data endpoints return HTTP 403 if any of the following conditions are true: + +* the request was not authenticated +* the provided credentials were incorrect +* the authenticated user has insufficient permissions + +To fix these issues, there are two checklists, one for regular users and one for broker partners. If you're unsure which refers to you, check this [FAQ](https://docs.alpaca.markets/docs/broker-api-faq#what-is-the-difference-between-trading-api-and-broker-api). If you're still unsure, then check your access key. If it starts with the letter `C`, then you're a broker partner, otherwise you're a regular user. If you don't have an access key yet, generate it on the right-hand side of the Alpaca [dashboard](https://app.alpaca.markets/brokerage/dashboard/overview). + +### Checklist for regular users + +* make sure you provide your credentials in the following HTTP headers: + * `APCA-API-KEY-ID` + * `APCA-API-SECRET-KEY` +* make sure your credentials are valid: + * check the key on the[ web UI](https://app.alpaca.markets/brokerage/dashboard/overview) + * when you reset your paper account, you need to regenerate your credentials +* make sure the host is `data.alpaca.markets` [for historical](https://docs.alpaca.markets/docs/historical-api#base-url) or `stream.data.alpaca.markets` [for live](https://docs.alpaca.markets/docs/streaming-market-data#connection) +* if you get a message like `subscription does not permit querying recent SIP data` in the HTTP response body, make sure you have the proper [subscription](https://docs.alpaca.markets/docs/about-market-data-api#subscription-plans) + * for example to query any SIP trades or quotes in the last 15 minutes, you need the Algo Trader Plus subscription + +### Checklist for broker partners + +* make sure you provide your credentials in [HTTP basic authentication](https://docs.alpaca.markets/docs/about-market-data-api#subscription-plans) +* make sure your credentials are valid +* make sure you're using the right host based on your environment: + * the production host is `data.alpaca.markets` [for historical](https://docs.alpaca.markets/docs/historical-api#base-url) and `stream.data.alpaca.markets` [for live](https://docs.alpaca.markets/docs/streaming-market-data#connection) + * the sandbox host (for testing) is `data.sandbox.alpaca.markets` [for historical](https://docs.alpaca.markets/docs/historical-api#base-url) and `stream.data.sandbox.alpaca.markets` [for live](https://docs.alpaca.markets/docs/streaming-market-data#connection) +* if you get a message like `subscription does not permit querying recent SIP data` in the HTTP response body, make sure you have the proper [subscription](https://docs.alpaca.markets/docs/about-market-data-api#broker-partners) + * for example, to query any SIP trades or quotes in the last 15 minutes, you need a special subscription + +## How do I subscribe to AlgoTrader Plus? + +You can subscribe to AlgoTrader Plus on the [Alpaca UI](https://app.alpaca.markets/account/plans-and-features): on the left sidebar of the main page click on "Plans & Features" and on that page click on "Upgrade to AlgoTrader Plus" inside the Market Data box. + +# Stocks + +## What's the difference between IEX and SIP data? + +SIP is short for [Securities Information Processor](https://en.wikipedia.org/wiki/Securities_information_processor). All US exchanges are mandated by the regulators to report their activities (trades and quotes) to the consolidated tape. This is what we call SIP data. + +IEX ([Investors Exchange](https://en.wikipedia.org/wiki/IEX)) is a single stock exchange. + +#### [Websocket stream](https://docs.alpaca.markets/us/docs/real-time-stock-pricing-data) + +Our free market data offering includes live data only from the IEX exchange: + +``` +wss://stream.data.alpaca.markets/v2/iex +``` + +The Algo Trader Plus subscription on the other hand offers SIP data: + +``` +wss://stream.data.alpaca.markets/v2/sip +``` + +#### [Historical data](https://docs.alpaca.markets/reference/stockbars) + +On the historical endpoints, use the `feed` parameter to switch between the two data feeds: + +```json +$ curl -s -H "APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H "APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" \ + "https://data.alpaca.markets/v2/stocks/AAPL/bars?feed=sip&timeframe=1Day&start=2023-09-29&limit=1" | jq . +{ + "bars": [ + { + "t": "2023-09-29T04:00:00Z", + "o": 172.02, + "h": 173.07, + "l": 170.341, + "c": 171.21, + "v": 51861083, + "n": 535134, + "vw": 171.599691 + } + ], + "symbol": "AAPL", + "next_page_token": "QUFQTHxEfDIwMjMtMDktMjlUMDQ6MDA6MDAuMDAwMDAwMDAwWg==" +} +$ curl -s -H "APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H "APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" \ + "https://data.alpaca.markets/v2/stocks/AAPL/bars?feed=iex&timeframe=1Day&start=2023-09-29&limit=1" | jq . +{ + "bars": [ + { + "t": "2023-09-29T04:00:00Z", + "o": 172.015, + "h": 173.06, + "l": 170.36, + "c": 171.29, + "v": 923134, + "n": 12630, + "vw": 171.716432 + } + ], + "symbol": "AAPL", + "next_page_token": null +} +``` + +In this example (2023-09-29 Apple daily bar), you can clearly see the difference between the two feeds: there were **12 630** eligible trades on the IEX exchange that day and more than **535 136** trades in total across all exchanges (naturally including IEX). Similar differences can be seen between their volumes. + +All the latest endpoints (including the [snapshot](https://docs.alpaca.markets/reference/stocksnapshotsingle) endpoint), require a subscription to be used with the SIP feed. For historical queries, the `end` parameter must be at least 15 minutes old to query SIP data without a subscription. The default value for `feed` is always the "best" available feed based on the user's subscription. + +```json +$ curl -s -H "APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H "APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" \ + "https://data.alpaca.markets/v2/stocks/AAPL/trades/latest" | jq . +{ + "symbol": "AAPL", + "trade": { + "t": "2023-09-29T19:59:59.246196362Z", + "x": "V", // << IEX exchange code + "p": 171.29, + "s": 172, + "c": [ + "@" + ], + "i": 12727, + "z": "C" + } +} +$ curl -H "APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H "APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" \ + "https://data.alpaca.markets/v2/stocks/AAPL/trades/latest?feed=sip" +{"code":42210000,"message":"subscription does not permit querying recent SIP data"} +``` + +In this example, we're querying the latest AAPL trade without a subscription. The default `feed` in this case is `iex`. If we were to try to query the SIP feed, we would get an error. To fix that error, we need to subscribe to [Algo Trader Plus](https://docs.alpaca.markets/docs/about-market-data-api#subscription-plans). + +## Why can't I find market data for a particular symbol (e.g. CGRNQ)? + +### OTC + +Make sure the symbol is not traded in OTC using the [assets endpoint](https://docs.alpaca.markets/reference/get-v2-assets-symbol_or_asset_id). `https://api.alpaca.markets/v2/assets/CGRNQ` returns + +```json +{ + "id": "dc2d8be9-33b5-4a32-8f57-5b7d209d2c82", + "class": "us_equity", + "exchange": "OTC", // << This symbol is traded in OTC + "symbol": "CGRNQ", + "name": "CAPSTONE GREEN ENERGY CORP COM PAR $.001", + "status": "active", + "tradable": false, + "marginable": false, + "maintenance_margin_requirement": 100, + "shortable": true, + "easy_to_borrow": true, + "fractionable": true, + "attributes": [] +} +``` + +Market data for OTC symbols can only be queried with a special subscription currently available only for broker partners. If you do have the subscription, you can use `feed=otc` to query the data. + +### Inactive + +Make sure the asset is active. Check the `status` field of the [same endpoint](https://docs.alpaca.markets/reference/get-v2-assets-symbol_or_asset_id). + +### Halt + +Make sure the symbol isn't or wasn't halted at the time you're querying. You can check the [current halts](https://www.nasdaqtrader.com/trader.aspx?id=TradeHalts) or the [historical halts](https://www.nasdaqtrader.com/Trader.aspx?id=TradingHaltSearch) on the Nasdaq website. For example, the symbol SVA has been halted since 2019-02-22. + +## What happens when a ticker symbol of a company changes? + +Perhaps the most famous example for this was when Facebook decided to [rename itself ](https://about.fb.com/news/2021/10/facebook-company-is-now-meta/)to Meta and to change its ticker symbol from FB to META. This transition happened on 2022-06-09. + +### Latest endpoints + +On the latest endpoints ([latest trades](https://docs.alpaca.markets/reference/stocklatesttrades-1), [latest quotes](https://docs.alpaca.markets/reference/stocklatestquotes-1), [latest bars](https://docs.alpaca.markets/reference/stocklatestbars-1) and [snapshots](https://docs.alpaca.markets/reference/stocksnapshots-1)), the data is never manipulated in any way. These endpoints always return the data as it was received at the time (this is also why there is no `adjustment` parameter on the [latest bars](https://docs.alpaca.markets/reference/stocklatestbars-1)). So, in this case, the latest FB trade returns the last trade when the company was still called FB: + +```json +$ curl -s -H "APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H "APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" "${APCA_API_DATA_URL}/v2/stocks/trades/latest?symbols=FB" | jq . +{ + "trades": { + "FB": { + "c": [ + "@", + "T" + ], + "i": 31118, + "p": 196.29, + "s": 121, + "t": "2022-06-08T23:59:55.103033856Z", + "x": "P", + "z": "C" + } + } +} +``` + +Note the timestamp in the response is 2022-06-08, the night before the name change. + +### Stream endpoints + +The symbols always reflect the ones used by the companies at the time of the transmission on the [streaming endpoints](https://docs.alpaca.markets/edit/real-time-stock-pricing-data) as well. In practice this means that a stream client must resubscribe to the new symbol after a name change to continue receiving data. The resubscribe requires no reconnection, in the Facebook example you could simply send a [subscribe message](https://docs.alpaca.markets/docs/streaming-market-data#subscription) to META. + +### Historical endpoints + +On the historical endpoints we introduced the `asof` parameter to link together the data before and after the rename. By default, this parameter is "enabled", so even if you don't specify it, you will get the data for both the old and new symbol when querying the new symbol after the rename. + +For the example of the FB - META rename, we can simply query the daily bars for META for the whole week (the rename happened on Thursday), yet we see the bars for Monday, Tuesday and Wednesday as well, even though on those days, the company was still called FB. + +```shell +$ curl -s -H "APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H "APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" \ + "${APCA_API_DATA_URL}/v2/stocks/bars?timeframe=1Day&symbols=META&start=2022-06-06&end=2022-06-11" | \ + jq -r '.bars.META[] | [.t, .o, .h, .l, .c] | @tsv' +2022-06-06T04:00:00Z 193.99 196.92 188.4 194.25 +2022-06-07T04:00:00Z 191.93 196.53 191.49 195.65 +2022-06-08T04:00:00Z 194.67 202.03 194.41 196.64 +2022-06-09T04:00:00Z 194.28 199.45 183.68 184 +2022-06-10T04:00:00Z 183.04 183.1 175.02 175.57 +``` + +If you disable the `asof` parameter, you won't get the FB bars: + +```shell +$ curl -s -H "APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H "APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" \ + "${APCA_API_DATA_URL}/v2/stocks/bars?timeframe=1Day&symbols=META&start=2022-06-06&end=2022-06-11&asof=-" | \ + jq -r '.bars.META[] | [.t, .o, .h, .l, .c] | @tsv' +2022-06-09T04:00:00Z 194.28 199.45 183.68 184 +2022-06-10T04:00:00Z 183.04 183.1 175.02 175.57 +``` + +If you set `asof` to a date before the rename, you can query by the old ticker: + +```shell +$ curl -s -H "APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H "APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" \ + "${APCA_API_DATA_URL}/v2/stocks/bars?timeframe=1Day&symbols=FB&start=2022-06-06&end=2022-06-11&asof=2022-06-06" | \ + jq -r '.bars.FB[] | [.t, .o, .h, .l, .c] | @tsv' +2022-06-06T04:00:00Z 193.99 196.92 188.4 194.25 +2022-06-07T04:00:00Z 191.93 196.53 191.49 195.65 +2022-06-08T04:00:00Z 194.67 202.03 194.41 196.64 +2022-06-09T04:00:00Z 194.28 199.45 183.68 184 +2022-06-10T04:00:00Z 183.04 183.1 175.02 175.57 +``` + +Unfortunately, the `asof` mapping is only available on our historical endpoints the day after the rename. In the FB-META example, it was available since 2022-06-10, so running the same queries on the day of the rename (2022-06-09) didn't return the FB bars. This is because of a limitation of one of our data sources. We're actively working on improving this and doing the mapping before the market opens with the new symbol. + +## How are bars aggregated? + +Minute and daily bars are aggregated from trades. The (SIP) timestamp of the trade is truncated to the minute for minute bars and to the day (in New York) for daily bars. For example, a trade at 14:52:28 belongs to the 14:52:00 minute bar, which contains all the trades between 14:52:00 (inclusive) and 14:53:00 (exclusive). The timestamp of the bar is the left side of the interval (14:52:00 in this example). + +There are three parts of the bar that a trade can potentially update: + +* open / close price +* high / low price +* volume + +The rules of these updates depend on + +* the tape of the trade (`A`, `B`: NYSE, `C`: Nasdaq, `O`: OTC) +* the conditions of the trade +* the type of the bar (`M`: minute, `D`: daily) + * Some rules are different for minute and daily bars. For example `P` (Prior Reference Price) relates to an obligation to trade at an earlier point in the trading day. This will update the high / low price of a daily bar but will not update the high / low price of a minute bar, because that price possibly happened in another minute. + +The rules are based on the guidelines of the SIPs: + +* the [CTS specification](https://www.ctaplan.com/publicdocs/ctaplan/CTS_Pillar_Output_Specification.pdf) for NYSE (tape `A` and `B`) +* the [UTP specification](https://utpplan.com/DOC/UtpBinaryOutputSpec.pdf) for Nasdaq (tape `C`) +* the [TDDS specification](https://www.finra.org/sites/default/files/2022-05/TDDS-2.1-MOLD.pdf) for OTC (tape `O`) + +The following table contains all the updating rules: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Condition code + + Condition description + + Tape + + Bar type + + Update open / close + + Update high / low + + Update volume +
+ ` ` + + Regular Sale + + AB + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `@` + + Regular Sale + + CO + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `A` + + Acquisition + + C + + MD + + 🟢 + + 🟢 + + 🟢 +
+ `B` + + Average Price Trade + + AB + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `B` + + Bunched Trade + + C + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `C` + + Cash Sale + + ABCO + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `D` + + Distribution + + C + + MD + + 🟢 + + 🟢 + + 🟢 +
+ `E` + + Automatic Execution + + AB + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `F` + + Intermarket Sweep + + ABC + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `G` + + Bunched Sold Trade + + C + + M + + :red\_circle: + + 🔴 + + :green\_circle: +
+ `G` + + Bunched Sold Trade + + C + + D + + :yellow\_circle: + + 🟢 + + 🟢 +
+ `H` + + Price Variation Trade + + ABC + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `I` + + Odd Lot Trade + + ABCO + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `K` + + Rule 127 or Rule 155 + + ABC + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `L` + + Sold Last + + ABC + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `M` + + Market Center Official Close + + ABC + + MD + + :red\_circle: + + :red\_circle: + + :red\_circle: +
+ `N` + + Next Day + + ABCO + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `O` + + Market Center Opening Trade + + ABC + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `P` + + Prior Reference Price + + ABCO + + M + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `P` + + Prior Reference Price + + ABCO + + D + + :yellow\_circle: + + :green\_circle: + + :green\_circle: +
+ `Q` + + Market Center Official Open + + ABC + + MD + + :red\_circle: + + :red\_circle: + + :red\_circle: +
+ `R` + + Seller + + ABCO + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `T` + + Extended Hours Trade + + ABCO + + M + + 🟢 + + 🟢 + + :green\_circle: +
+ `T` + + Extended Hours Trade + + ABCO + + D + + :red\_circle: + + :red\_circle: + + 🟢 +
+ `U` + + Extended Trading Hours + + ABCO + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `V` + + Contingent Trade + + ABC + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `W` + + Average Price Trade + + CO + + MD + + 🔴 + + 🔴 + + 🟢 +
+ `X` + + Cross Trade + + ABC + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `Y` + + Yellow Flag Regular Trade + + C + + MD + + 🟢 + + 🟢 + + 🟢 +
+ `Z` + + Sold Out Of Sequence + + ABC + + M + + 🔴 + + 🔴 + + :green\_circle: +
+ `Z` + + Sold Out Of Sequence + + ABC + + D + + :yellow\_circle: + + :green\_circle: + + :green\_circle: +
+ `4` + + Derivatively Priced + + ABC + + M + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `4` + + Derivatively Priced + + ABC + + D + + :yellow\_circle: + + 🟢 + + 🟢 +
+ `5` + + Market Center Reopening Trade + + ABC + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `6` + + Market Center Closing Trade + + ABC + + MD + + :green\_circle: + + :green\_circle: + + :green\_circle: +
+ `7` + + Qualified Contingent Trade + + ABC + + MD + + :red\_circle: + + :red\_circle: + + :green\_circle: +
+ `9` + + Corrected Consolidated Close + + ABC + + M + + :red\_circle: + + :red\_circle: + + :red\_circle: +
+ `9` + + Corrected Consolidated Close + + ABC + + D + + :green\_circle: + + :green\_circle: + + :red\_circle: +
+ +* :green\_circle: means that the given condition updates the value +* :yellow\_circle: means that the given condition updates the open / close price only if the trade is the first trade of the bar +* :red\_circle: means that the given condition does not update the value + +In the original [CTS](https://www.ctaplan.com/publicdocs/ctaplan/CTS_Pillar_Output_Specification.pdf) / [UTP](https://utpplan.com/DOC/UtpBinaryOutputSpec.pdf) specifications, there are some more complicated update rules (see the footnotes in the linked specifications), but we don't use any of these. In most of the cases, we simply use :yellow\_circle: (or :green\_circle:) instead. + +A trade can have more than one condition. In this case the strictest rule is applied. For example, if a Nasdaq trade has these conditions: `@`, `4`, `I` and a daily bar is being generated, none of the prices of the bar are going to be updated (both open / close and high / low is :red\_circle: because it's an odd lot trade), only the volume :green\_circle: is going to be updated. If the trade had no `I` condition (`@` and `4` only), then the open / close price would only be updated if this is the first trade of the bar :yellow\_circle: , and both the high / low price :green\_circle: and the volume :green\_circle: would be updated. If it was a regular trade (`@`), then all of its values would be updated. + +Once the combined updating rule of the trade has been calculated, the bar's fields are updated: + +* Open is set if it hasn't been set yet and the update open / close rule is :green\_circle: or :yellow\_circle: +* High is set if it hasn't been set yet or if the price of the trade is higher than the previous high and the update high / low is :green\_circle: +* Low is set if it hasn't been set yet or if the price of the trade is lower than the previous low and the update high / low rule is :green\_circle: +* Close is set if the update open / close rule is :green\_circle: or if it's :yellow\_circle: and the close price hasn't been set yet +* Volume is increased by the size of the trade if the update volume rule is :green\_circle: +* The trade count is increased by one if the update volume rule is :green\_circle: +* [VWAP](https://en.wikipedia.org/wiki/Volume-weighted_average_price) is the ratio of these two internal variables: + * The volume-weighted total price is increased by the product of the trade's price and volume if both the update high / low rule and the update volume rule is :green\_circle: + * The total volume is increased by the size of the trade if both the update high / low rule and the update volume rule is :green\_circle:. This means that this volume can be different from the "normal" volume field of the bar if there are trades in the bar that update the volume but not the high / low price (for example condition `R`) + +Finally, the bar is only emitted if none of its fields (open, high, low, close, volume) are 0. So if there are no trades in the bar's interval, or if there's only a single `I` trade (which only updates the volume, but none of the prices), then no bar is generated. + +All the other non-minute and non-daily bars are aggregated from the minute and daily bars. For example, an hour (`1Hour`) bar is aggregated from all the minute bars in the given hour and a weekly bar (`1Week`) is aggregated from all the daily bars in the given week. This aggregation no longer considers the actual trades that happened in the given interval, but instead the bars are aggregated using these rules: + +* Open is the open of the first bar +* High is the maximum of the bars' high prices +* Low is the minimum of the bars' low prices +* Close is the close of the last bar +* Volume is the sum of the bars' volumes +* Trade count is the sum of the bars' trade counts +* VWAP is the volume-weighted average of the bars' VWAPs + +# Crypto + +## Why are there crypto bars with 0 volume / trade count? + +Our crypto market data reflects trades and quotes from our own Alpaca exchange. Due to the volatility of some cryptocurrencies, including lack of trade volume at any given time, we include the quote midpoint prices to offer a better data experience. If within a bar no trade happens, the volume will be 0, but the prices will be determined by the quote prices. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/non-trade-activities-for-option-events.md b/DesktopBot/Documents/Alpaca/non-trade-activities-for-option-events.md new file mode 100644 index 0000000..eaa3739 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/non-trade-activities-for-option-events.md @@ -0,0 +1,86 @@ +# Non-Trade Activities for Option Events + +This page provides an overview of new NTAs for options-specific events + +# Option Exercise + +``` +[ + { + "id": "20190801011955195::5f596936-6f23-4cef-bdf1-3806aae57dbf", + "activity_type": "OPEXC", + "date": "2023-07-21", + "net_amount": "0", + "description": "Option Exercise", + "symbol": "AAPL230721C00150000", + "qty": "-2", + "status": "executed" + }, + { + "id": "20190801011955195::5f596936-6f23-4cef-bdf1-3806aae57dbf", + "activity_type": "OPTRD", + "date": "2023-07-21", + "net_amount": "-30000", + "description": "Option Trade", + "symbol": "AAPL", + "qty": "200", + "price": "90", + "status": "executed" + } +] +``` + +The exercise event (OPEXC) is applicable to 2 contracts, and the corresponding trade (OPTRD) represents 200 of the underlying shares being purchased at a per-share amount of $150 (strike price). + +# Option Assignment + +``` +[ + { + "id": "20190801011955195::5f596936-6f23-4cef-bdf1-3806aae57dbf", + "activity_type": "OPASN", + "date": "2023-07-01", + "net_amount": "0", + "description": "Option Assignment", + "symbol": "AAPL230721C00150000", + "qty": "2", + "status": "executed" + }, + { + "activity_type": "OPTRD", + "id": "20190801011955195::5f596936-6f23-4cef-bdf1-3806aae57dbf", + "date": "2023-07-01", + "net_amount": "30000", + "description": "Option Trade", + "symbol": "AAPL", + "qty": "-200", + "price": "150", + "status": "executed" + } +] +``` + +The assignment event (OPASN) is applicable to 2 contracts, and the corresponding trade (OPTRD) represents 200 of the underlying shares being sold at a per-share amount of $150 (strike price). + +# ITM Option Expiry + +In the event of an in-the-money (ITM) option reaching expiration without being designated as "Do Not Exercise" (DNE), the Alpaca system will automatically initiate the exercise process on behalf of the user. This process mirrors the Exercise event described earlier. In cases where there is insufficient buying power or underlying positions to facilitate the exercise, the system will generate an automated order for the liquidation of the position. + +# OTM Option Expiry + +``` +[ + { + "id": "20190801011955195::5f596936-6f23-4cef-bdf1-3806aae57dbf", + "activity_type": "OPEXP", + "date": "2023-07-21", + "net_amount": "0", + "description": "Option Expiry", + "symbol": "AAPL230721C00150000", + "qty": "-2", + "status": "executed" + } +] +``` + +When a contract expires OTM, the Alpaca system will flatten the position and no further action is taken. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/options-level-3-trading.md b/DesktopBot/Documents/Alpaca/options-level-3-trading.md new file mode 100644 index 0000000..2d7ab48 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/options-level-3-trading.md @@ -0,0 +1,432 @@ +# Options Level 3 Trading + +We're excited to support Multi-leg options trading! Use this section to read up on Alpaca's Multi-leg options trading capabilities. + + + ### Multi-leg options trading is now available for live trading! + + +## What are Multi-leg Orders? + +A multi-leg (MLeg) order is a single, combined order that includes multiple option contracts – calls, puts, or even shares—on the same underlying security. By bundling all legs together, the trade is executed as a single unit and each leg is associated with its own strike price, expiration date, or position type (long or short). MLeg orders are often used when traders need to set up complex strategies with several moving parts. A common example is the call spread, where the trader buys a call option at one strike price while simultaneously selling another call option at a higher strike, both for the same underlying asset. + +## Why are Multi-leg Orders useful? + +MLeg orders are particularly useful because they allow traders to execute complex options or stock combinations in one streamlined process, avoiding the delay or slippage risk of placing each transaction separately. By handling multiple legs at once, traders gain better control over their target price, reduce the chance of partial fills that could distort the intended strategy, and simplify trade management. The potential to minimize transaction costs—whether through tighter spreads, combined commissions, or efficient margin usage, also adds to their appeal. + +A trader anticipates a stock will remain in a narrow price range.\ +They set up an iron condor, which involves four legs: + +* Buying one out-of-the-money (OTM) call. +* Selling a call at a closer strike. +* Buying an OTM put. +* Selling another put. + +Placing these four legs as a single MLeg order ensures they fill together or not at all.\ +This reduces the risk of partial fills, which could otherwise leave the trader with unwanted market exposure or unbalanced positions. + +## How to submit a Multi-leg Order? + +To submit a multi-leg (MLeg) order, set your “order\_class” to “mleg” and list each component of the strategy in a “legs” array, specifying details like the symbol, side (buy or sell), ratio quantity, and position intent (e.g., buy\_to\_open). Include any additional parameters (limit price, time in force, etc.) as part of the order’s settings. Below is a cURL example showing how to place a POST request to the Alpaca API, passing in the necessary headers and JSON payload: + +```curl +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "order_class": "mleg", + "qty": "1", + "type": "limit", + "limit_price": "0.6", + "time_in_force": "day", + "legs": [ + {"symbol": "AAPL250117P00200000", "ratio_qty": "1", "side": "buy", "position_intent": "buy_to_open"}, + {"symbol": "AAPL250117C00250000", "ratio_qty": "1", "side": "buy", "position_intent": "buy_to_open"} + ] +}' | jq -r +``` + +## Some examples + +### Long Call Spread + + + +Buy a lower-strike (190) call and sell a higher-strike (210) call on the same underlying: + +```curl +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "order_class": "mleg", + "qty": "1", + "type": "limit", + "limit_price": "1.00", + "time_in_force": "day", + "legs": [ + { + "symbol": "AAPL250117C00190000", + "ratio_qty": "1", + "side": "buy", + "position_intent": "buy_to_open" + }, + { + "symbol": "AAPL250117C00210000", + "ratio_qty": "1", + "side": "sell", + "position_intent": "sell_to_open" + } + ] +}' | jq -r +``` + +### Long Put Spread + + + +Buy a higher-strike (210) put and sell a lower-strike (190) put on the same underlying: + +```curl +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "order_class": "mleg", + "qty": "1", + "type": "limit", + "limit_price": "1.25", + "time_in_force": "day", + "legs": [ + { + "symbol": "AAPL250117P00210000", + "ratio_qty": "1", + "side": "buy", + "position_intent": "buy_to_open" + }, + { + "symbol": "AAPL250117P00190000", + "ratio_qty": "1", + "side": "sell", + "position_intent": "sell_to_open" + } + ] +}' | jq -r +``` + +### Iron Condor + + + +Combine two spreads (a put spread and a call spread) to bet on limited movement: + +```curl +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "order_class": "mleg", + "qty": "1", + "type": "limit", + "limit_price": "1.80", + "time_in_force": "day", + "legs": [ + { + "symbol": "AAPL250117P00190000", + "ratio_qty": "1", + "side": "buy", + "position_intent": "buy_to_open" + }, + { + "symbol": "AAPL250117P00195000", + "ratio_qty": "1", + "side": "sell", + "position_intent": "sell_to_open" + }, + { + "symbol": "AAPL250117C00205000", + "ratio_qty": "1", + "side": "sell", + "position_intent": "sell_to_open" + }, + { + "symbol": "AAPL250117C00210000", + "ratio_qty": "1", + "side": "buy", + "position_intent": "buy_to_open" + } + ] +}' | jq -r +``` + +Learn about the [differences between an iron condor and iron butterfly](https://alpaca.markets/learn/iron-condor-vs-iron-butterfly). + +### Roll a Call Spread (strike price) + +Close an existing short call spread and open a new one at different strikes in a single transaction: + +```curl +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "order_class": "mleg", + "qty": "1", + "type": "limit", + "limit_price": "2.05", + "time_in_force": "day", + "legs": [ + { + "symbol": "AAPL250117C00200000", + "ratio_qty": "1", + "side": "buy", + "position_intent": "buy_to_close" + }, + { + "symbol": "AAPL250117C00205000", + "ratio_qty": "1", + "side": "sell", + "position_intent": "sell_to_close" + }, + { + "symbol": "AAPL250117C00210000", + "ratio_qty": "1", + "side": "sell", + "position_intent": "sell_to_open" + }, + { + "symbol": "AAPL250117C00215000", + "ratio_qty": "1", + "side": "buy", + "position_intent": "buy_to_open" + } + ] +}' | jq -r +``` + +### Roll a Call Spread (expiration date) + +Below is an example of rolling a short call spread from one expiration date to another in a single multi-leg (MLeg) order. The first two legs (with symbols ending in `250117`) are closed (`buy_to_close` and `sell_to_close`), and the new positions are opened at later-dated strikes (`250224`): + +```curl +curl --request POST \ + --url $APIDOMAIN/v2/orders \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header "Apca-Api-Key-Id: $APIKEY" \ + --header "Apca-Api-Secret-Key: $SECRET" \ + --data ' +{ + "order_class": "mleg", + "qty": "1", + "type": "limit", + "limit_price": "2.05", + "time_in_force": "day", + "legs": [ + { + "symbol": "AAPL250117C00200000", + "ratio_qty": "1", + "side": "buy", + "position_intent": "buy_to_close" + }, + { + "symbol": "AAPL250117C00205000", + "ratio_qty": "1", + "side": "sell", + "position_intent": "sell_to_close" + }, + { + "symbol": "AAPL250124C00200000", + "ratio_qty": "1", + "side": "sell", + "position_intent": "sell_to_open" + }, + { + "symbol": "AAPL250124C00205000", + "ratio_qty": "1", + "side": "buy", + "position_intent": "buy_to_open" + } + ] +}' | jq -r +``` + +## Some deeper explanations + +### How do we calculate maintenance margin requirements? + +1. **Ignore Premiums**\ + When calculating maintenance margin, do not factor in the premiums paid or received. Instead, focus on the intrinsic (exercise) payoffs. +2. **Model Each Option’s Payoff**\ + Each option is represented by a piecewise linear payoff function (PnL) based on the underlying price (p). + + +3. **Combine Positions**\ + To determine total payoff, sum the piecewise functions for all open positions: + + +4. **Find Theoretical Maximum Loss**\ + Maintenance margin is based on the worst-case scenario for the portfolio: + + + +In other words, you determine the underlying price p that yields the lowest (most negative) net payoff. The absolute value of this lowest point is the margin requirement. + +5. **Different Expirations**\ + For option positions with multiple expiration dates, calculate this theoretical maximum-loss approach separately for each expiration date, then use the largest resulting requirement across all expirations. + +Lets see an example in order to understand why this way of calculating the maintenance margin is benefiting the customers. + +Lets assume that a customer has the following positions + +* Long Call for AAPL with Strike Price = 100 +* Short Call for AAPL with Strike Price = 110 +* Long Call for AAPL with Strike Price = 200 +* Short Call for AAPL with Strike Price = 190 + +Using the traditional way of calculating maintenance margin we would form 2 spreads + +Spread 1 (Call Credit Spread): + +* Long Call for AAPL with Strike Price = 200 +* Short Call for AAPL with Strike Price = 190 + + + +With maintenance margin = 1000 since the difference between strike prices is 10 and the option’s multiplier is 100 so the `maintenance_margin = strike_price_diff * multiplier` + +Spread 2 (Call Debit Spread): + +* Long Call for AAPL with Strike Price = 100 +* Short Call for AAPL with Strike Price = 110 + + + +With maintenance margin = 0 since the difference between strike prices is 10 but the long is higher than the short. + +So the **Total Maintenance Margin (Traditional) = 1000 + 0 = $1000** + +**Universal Spread Rule Calculation** + +When combining all four positions and evaluating the theoretical maximum combined loss, the payoff analysis shows that losses from one spread offset gains or losses in the other, resulting in a net theoretical maximum loss of zero. Hence: + +* **Total Maintenance Margin (Universal Spread) = $0** + + + +This “universal spread rule” or piecewise-payoff approach better reflects the true risk when these positions are considered together. By recognizing how the different calls offset one another’s exposures, the required margin is lower—benefiting the customer by aligning margin requirements with the actual worst-case scenario of the entire portfolio rather than assigning sums of individual spreads. + +*** + +References:\ +[https://cdn.cboe.com/resources/membership/Margin\_Manual.pdf](https://cdn.cboe.com/resources/membership/Margin_Manual.pdf) + +*First, compute the intrinsic value of the options at price points for the underlying security or instrument that are set to correspond to every exercise price present in the spread. Then, net the intrinsic values at each price point. The maximum potential loss is the greatest loss, if any, from among the results.* + +[https://cdn.cboe.com/resources/regulation/rule\_filings/margin\_requirements/SR-CBOE-2012-043.pdf](https://cdn.cboe.com/resources/regulation/rule_filings/margin_requirements/SR-CBOE-2012-043.pdf) + +*(A) For spreads as defined in subparagraph (a)(5) of this Rule, the long options\ +must be paid for in full. In addition, margin is required equal to the lesser of the\ +amount required for the short option(s) by subparagraph (c)(5)(A) or (B),\ +whichever is applicable, or the spread’s maximum potential loss, if any. To\ +determine the spread’s maximum potential loss, first compute the intrinsic value\ +of the options at price points for the underlying security or instrument that are set\ +to correspond to every exercise price present in the spread. Then, net the intrinsic\ +values at each price point. The maximum potential loss is the greatest loss, if any,\ +from among the results. The proceeds for establishing the short options may be\ +applied toward the cost of the long options and/or any margin requirement.* + +*** + +### How do we calculate order cost basis? + +**Definition:** + +The **cost basis** of a multi-leg (MLeg) order is the **sum of**: + +1. The **maintenance margin** required for the combined positions (as determined by the universal spread rule), and +2. The **net price** (debit/credit) from buying or selling the option contracts. + +**Example:** + +Consider a call credit spread on AAPL: + +* Long Call (buy) for AAPL with Strike Price = 200 +* Short Call (sell) for AAPL with Strike Price = 190 + +**Maintenance Margin:** Universal spread rule requires a margin of $1,000 for this spread. + +**Net Option Price:** + +* The **long call** premium to be paid is $10 (cost to the buyer). +* The **short call** premium to be received is $15 (credit to the seller). +* **Net Price** = (15−10)=(15 - 10) =$5 credit +* Because each option contract covers 100 shares, multiply by 100:\ + Net Price (per contract) x 100 = 5 x 100 = $500 +* However, for cost-basis purposes, a credit (positive $5) effectively reduces the overall cost, so it becomes **-$5** in the order’s net debit/credit calculation. + +So, Total Cost Basis:\ +Cost Basis = (Maintenance Margin) + (Net Price×Option Multiplier) = 1000 + (-5 x 100) = 1000 - 500 = $500 + +Hence, the cost basis—and the amount charged to the customer—for this multi-leg order is $500. + +### Some Edge Scenarios + +#### GCD `ratio_qty` requirement + +When submitting an MLeg order, each leg’s `leg_ratio` must be in its simplest form. In other words, the greatest common divisor (GCD) among the `leg_ratio` values for the legs must be 1. + +**Example** (wrong) + +* Leg 1: `leg_ratio = 4` +* Leg 2: `leg_ratio = 2` + +Because both ratios share a common divisor of 2, the system will reject this order. If a ratio must be 2:4, for instance, the user should enter it as 1:2 instead (dividing both sides by the GCD of 2). + +**Reason for Enforcing Simplified Ratios** + +By requiring that leg ratios be in their simplest form (prime to each other), the system can: + +**Avoid Redundant Parent Quantities**: The ratio is intended to show the relative proportions of each leg; if the ratio isn’t simplified, you’re effectively duplicating the same information already available through the parent order quantity. + +This approach ensures clarity in trade definitions and prevents potential confusion or errors in calculating fill quantities and margin requirements. + +#### Restrictions on Combo Order(equity leg + contract leg) + +MLeg orders that include an equity leg are not supported at this time. This means that combining an equity position with an options contract in a single order is not currently available for any trading strategy. + +#### MLeg restrictions regarding uncovered legs + +Starting on day zero of Options Level 3 trading, an MLeg order is accepted only if all its legs are covered within the same MLeg order. For example, an MLeg order containing two short call legs would be rejected, though submitting those short calls separately as single-leg orders is allowed. This restriction also impacts certain strategies, including rolling a short contract or rolling a calendar spread, since they would involve uncovered short legs within the same multi-leg order. + +*** + +*The content of this article is for general informational purposes only. All examples are for illustrative purposes only.* + +*Options trading is not suitable for all investors due to its inherent high risk, which can potentially result in significant losses. Please read[Characteristics and Risks of Standardized Options](https://www.theocc.com/company-information/documents-and-archives/options-disclosure-document) before investing in options.* + +*All investments involve risk, and the past performance of a security, or financial product does not guarantee future results or returns. There is no guarantee that any investment strategy will achieve its objectives. Please note that diversification does not ensure a profit, or protect against loss. There is always the potential of losing money when you invest in securities, or other financial products. Investors should consider their investment objectives and risks carefully before investing.* + +*Securities brokerage services are provided by Alpaca Securities LLC ("Alpaca Securities"), member[FINRA/SIPC](https://www.finra.org/), a wholly-owned subsidiary of AlpacaDB, Inc. Technology and services are offered by AlpacaDB, Inc.* + +*This is not an offer, solicitation of an offer, or advice to buy or sell securities or open a brokerage account in any jurisdiction where Alpaca Securities are not registered or licensed, as applicable.* \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/options-orders.md b/DesktopBot/Documents/Alpaca/options-orders.md new file mode 100644 index 0000000..88874a6 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/options-orders.md @@ -0,0 +1,64 @@ +# Options Orders + +This page provides examples of valid order payloads + +# Tier 1 Orders + +## Sell a Covered Call + +``` +{ + "symbol": "AAPL231201C00195000", + "qty": "2", + "side": "sell", + "type": "limit", + "limit_price": "1.05", + "time_in_force": "day" +} +``` + +Note, the account must have an existing position of 200 shares of AAPL as the order is for 2 contracts, and each option contract is for 100 shares of underlying. + +## Sell a Cash-Secured Put + +``` +{ + "symbol": "AAPL231201P00175000", + "qty": "1", + "side": "sell", + "type": "market", + "time_in_force": "day" +} +``` + +Note, the account must have sufficient buying power. The account must have ($175 strike) x (100 shares) x (1 contract) = $17,500 USD buying power available. + +# Tier 2 Orders + +## Buy a Call + +``` +{ + "symbol": "AAPL240119C00190000", + "qty": "1", + "side": "buy", + "type": "market", + "time_in_force": "day" +} +``` + +The account must have sufficient buying power to afford the call option. If the market order is executed at $5.10, the account must have ($5.10 execution price) x (100 shares) x (1 contract) = $510.00 USD buying power. + +## Buy a Put + +``` +{ + "symbol": "AAPL240119P00170000", + "qty": "1", + "side": "buy", + "type": "market", + "time_in_force": "day" +} +``` + +The account must have sufficient buying power to afford purchasing put option. If the market order is executed at $1.04, the account must have ($1.04 execution price) x (100 shares) x (1 contract) = $104.00 USD buying power. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/options-trading.md b/DesktopBot/Documents/Alpaca/options-trading.md new file mode 100644 index 0000000..64ac378 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/options-trading.md @@ -0,0 +1,227 @@ +# Options Trading + +We're excited to support options trading! Use this section to read up on Alpaca's options trading capabilities. + + + ### Options trading is now available on Live! + + Share your feedback on [Options API for Trading API here](https://docs.google.com/forms/d/e/1FAIpQLScIYvKDJnKjXWESs6qxzpgk7pbvkt0IF1_nhv46t4o31-YOng/viewform) + + +# OpenAPI Spec + +You can find our Open API docs here: [\](https://docs.alpaca.markets/reference). + +# Enablement + +In the Paper environment, options trading capability will be enabled by default - there's nothing you need to do! + +*Note, in production there will be a more robust experience to request options trading.* + +For those who do not wish to have options trading enabled, you can disable options trading by navigating to your Trading Dashboard; Account > Configure. + +The [Trading Account](https://docs.alpaca.markets/reference/getaccount-1) model was updated to reflect the account's options approved and trading levels. + +The [Account Configuration](https://docs.alpaca.markets/reference/getaccountconfig-1) model was updated to show the maximum options trading level, and allow a user to downgrade to a lower level. Note, a different API will be provided for requesting a level upgrade for live account. + +## Trading Levels + +Alpaca supports the below options trading levels. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Level + + Supported Trades + + Validation +
+ 0 + + * Options trading is disabled + + * NA +
+ 1 + + * Sell a covered call + * Sell cash-secured put + + * User must own sufficient underlying shares + * User must have sufficient options buying power +
+ 2 + + * Level 1 + * Buy a call + * Buy a put + + * User must have sufficient options buying power +
+ 3 + + * Level 1,2 + * Buy a call spread + * Buy a put spread + + * User must have sufficient options buying power +
+ +# Option Contracts + +## Assets API Update + +The [Assets](https://docs.alpaca.markets/reference/get-v2-assets-1) endpoint has an existing `attributes` query parameter. Symbols which have option contracts will have an attribute called `options_enabled`. + +Querying for symbols with the `options_enabled` attribute allows users to identify the universe of symbols with corresponding option contracts. + +## Fetch Contracts + +To obtain contract details, a new endpoint was introduced: `/v2/options/contracts?underlying_symbols=`. The endpoint supports fetching a single contract such as `/v2/options/contracts/{symbol_or_id}` + +The default params are: + +* expiration\_date\_lte: Next weekend +* limit: 100 + +Example: if `/v2/options/contracts` is called on Thursday, the response will include Thursday and Friday data. If called on a Saturday, the response will include Saturday, Sunday, Monday, Tuesday, Wednesday, Thursday, and Friday. + +Here is an example of a response object: + +```json +{ + "option_contracts": [ + { + "id": "6e58f870-fe73-4583-81e4-b9a37892c36f", + "symbol": "AAPL240119C00100000", + "name": "AAPL Jan 19 2024 100 Call", + "status": "active", + "tradable": true, + "expiration_date": "2024-01-19", + "root_symbol": "AAPL", + "underlying_symbol": "AAPL", + "underlying_asset_id": "b0b6dd9d-8b9b-48a9-ba46-b9d54906e415", + "type": "call", + "style": "american", + "strike_price": "100", + "size": "100", + "open_interest": "6168", + "open_interest_date": "2024-01-12", + "close_price": "85.81", + "close_price_date": "2024-01-12" + }, +... + ], + "page_token": "MTAw", + "limit": 100 +} +``` + +More detailed information regarding this endpoint can be found on the OpenAPI spec [here](https://docs.alpaca.markets/reference/get-options-contracts). + +# Market Data + +Alpaca offers both [real-time](https://docs.alpaca.markets/docs/real-time-option-data) and [historical](https://docs.alpaca.markets/docs/historical-option-data) option data. + +# Placing an Order + +Placing an order for an option contract uses the same [Orders API](https://docs.alpaca.markets/reference/postorder) that is used for equities and crypto asset classes. + +Given the same Orders API endpoint is being used, Alpaca has implemented a series of validations to ensure the options order does not include attributes relevant to other asset classes. Some of these validations include: + +* Ensuring `qty` is a whole number +* `Notional` must not be populated +* `time_in_force` must be `day` or `gtc` +* `extended_hours` must be `false` or not populated +* `type` must be `market`,`limit`,`stop` or ,`stop_limit` (`stop` and `stop_limit` are only available for single-leg orders) + +Examples of valid order payloads can be found as a child page [here](https://docs.alpaca.markets/docs/options-orders). + +# Options Positions + +Good news - the existing [Positions API](https://docs.alpaca.markets/reference/getallopenpositions) model will work with options contracts. There is not expected to be a change to this model. + +As an additive feature, we aim to support fetching positions of a certain asset class; whether that be options, equities, or crypto. + +# Account Activities + +The existing schema for trade activities (TAs) are applicable for the options asset class. For example, the `FILL` activity is applicable to options and maintains it's current shape for options. + +There are new non-trade activities (NTAs) entry types for options, however the schema stays the same. These NTAs reflect exercise, assignment, and expiry. More details can be found on a child page [here](https://docs.alpaca.markets/docs/non-trade-activities-for-option-events) and on the OpenAPI spec [here](https://docs.alpaca.markets/reference/getaccountactivities-2). + +> 🚧 On PAPER NTAs are synced at the start of the following day. While your balance and positions are updated instantly, NTAs on PAPER will be visible in the Activities endpoint only the next day + +# Exercise and DNE Instructions + +## Exercise + +Contract holders may submit exercise instructions to Alpaca. Alpaca will process instructions and work with our clearing partner accordingly. + +All available held shares of this option contract will be exercised. By default, Alpaca will automatically exercise in-the-money (ITM) contracts at expiry. + +Exercise requests will be processed immediately once received. Exercise requests submitted between market close and midnight will be rejected to avoid any confusion about when the exercise will settle. + +Endpoint: `POST /v2/positions/{symbol_or_contract_id}/exercise`(no body) + +More details in our OpenAPI Spec [here](https://docs.alpaca.markets/reference/optionexercise). + +## Do Not Exercise + +To submit a Do-not-exercise (DNE) instruction, please contact our support team. + +# Expiration + +* In the event no instruction is provided on an ITM contract, the Alpaca system will exercise the contract as long as it is ITM by at least $0.01 USD. +* Alpaca Operations has tooling and processes in place to identify accounts which pose a buying power risk with ITM contracts. +* In the event the account does not have sufficient buying power to exercise an ITM position, Alpaca will sell-out the position within 1 hour before expiry. + +# Assignment + +Options assignments are not delivered through websocket events. To check for assignment activity (non-trade activity, or NTA events), you’ll need to poll the REST API endpoints. Websocket support for NTAs is not currently available. + +# FAQ + +Please see our full FAQs here: [https://alpaca.markets/support/tag/options](https://alpaca.markets/support/tag/options) \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/orders-at-alpaca.md b/DesktopBot/Documents/Alpaca/orders-at-alpaca.md new file mode 100644 index 0000000..0f906fc --- /dev/null +++ b/DesktopBot/Documents/Alpaca/orders-at-alpaca.md @@ -0,0 +1,1628 @@ +# Placing Orders + +Using Alpaca's Trading API, users can monitor, place, and cancel orders. Each order has a unique identifier provided by the client. If the client does not provide one, the system will automatically generate a client-side unique order ID. This ID is returned as part of the order object, along with the other fields described below. Once an order is placed, it can be queried using either the client-provided order ID or the system-assigned unique ID to check its status. Updates on open orders are also delivered through the streaming interface, which is the recommended method for maintaining order state. + +# Buying Power + +To accept orders that open new positions or add to existing ones, your account must have sufficient buying power. Alpaca applies a buying power check to both long buys and short sells. + +The calculated value of an opening short sell order is MAX(order’s limit price, 3% above the current ask price) *order quantity. For market short orders, the value is simply (3% above the current ask price)* order quantity. + +The order’s calculated value is then compared against your available buying power to determine whether it can be accepted. Note that your available buying power is reduced by existing open buy long and sell short orders. Your sell long and buy to cover orders do not replenish your available buying power until they are executed. + +For example, if your buying power is $10,000 and you submit a limit buy order with a value of $3,000, the order will be accepted and your remaining available buying power will be $7,000. Even if the order remains unfilled, it will continue to count against your available buying power until it is either executed or canceled. If you then submit another order with a value of $8,000, it will be rejected. + +**Initial buying power checks on orders:** + +When the core session is open: Far side of the NBBO (using Bid & Ask Price) + +If the order is entered while the extended hours session is open: Midpoint of the inside market + +If the order is entered when the core session and extended hours session are closed (pre-market hours): the latest trade from market cache + +# Equities Trading + +The following section deals with equity trading + +## Orders Submitted Outside of Eligible Trading Hours + +Orders not eligible for extended hours submitted after 4:00pm ET will be queued up for release the next trading day. + +Orders eligible for extended hours submitted outside of 4:00am - 8:00pm ET are handled as described in the section below. + +## Extended Hours Trading + +Using API v2, you can submit and fill orders during pre-market and after-hours. Extended hours trading has specific risks due to the less liquidity. Please read through [Alpaca’s Extended Hours Trading Risk Disclosure](https://files.alpaca.markets/disclosures/library/ExtHrsRisk.pdf) for more details. + +Currently, we support full extended hours: + +* Overnight: 8:00pm - 4:00am ET, Sunday to Friday +* Pre-market: 4:00am - 9:30am ET, Monday to Friday +* After-hours: 4:00pm - 8:00pm ET, Monday to Friday + +### Submitting an Extended Hours Eligible Order + +To indicate an order is eligible for extended hours trading, you need to supply a boolean parameter named `extended_hours` to your order request. By setting this parameter to `true`, the order is will be eligible to execute in the pre-market or after-hours. + +Only limit orders with `time_in_force` set to `day` or `gtc` orders are accepted as extended hours eligible. All other order types and TIF values will be rejected with an error. You must adhere to these settings to participate in extended hours: + +1. The order type must be set to `limit` (with a limit price). Any other type of orders will be rejected with an error. +2. Time-in-force must be set to either `day` or `gtc`. Any other `time_in_force` will be rejected with an error. + +For Fractional Orders, starting on October 27, 2021, customers will not be able to send the following sequence of orders outside of market hours for the same security. This is so customers will not have a net short position. Examples of the order sequences that will be rejected are shown below. + +| Summary | Order 1 | Order 2 | Second Order Handling Accept/Reject | +| :-------------------------------- | :------------ | :------------ | :---------------------------------- | +| 2x Sell | Notional Sell | Quantity Sell | Reject | +| 2x Sell | Notional Sell | Notional Sell | Reject | +| 2x Sell | Quantity Sell | Notional Sell | Reject | +| 2 x Sell \[with Correct Quantity] | Quantity Sell | Quantity Sell | Accept | +| 2 x Sell \[with Correct Quantity] | Quantity Sell | Quantity Sell | Reject | + +For more information - please see our [Blog Post](https://alpaca.markets/blog/shorting-prevention-breaking-change-notification/) on this topic. + +If this is done you will see the following error message: `"unable to open new notional orders while having open closing position orders"`. + +All symbols supported during regular market hours are also supported during extended hours. Short selling is also treated the same. Assets tradable in the overnight session can be identified via the assets endpoint, please see our [24/5 FAQ](https://docs.alpaca.markets/docs/245-trading) for more information + +## IPO Symbols + +Alpaca supports trading symbols which have recently begun trading publicly on major exchanges such as the NYSE and NASDAQ. This process is known as a company going public, or an IPO. + +As a registered broker/dealer, Alpaca must follow FINRA [regulations](https://www.finra.org/rules-guidance/guidance/faqs/finra-rule-5131-frequently-asked-questions) regarding order types for IPO symbols: + +* Alpaca is only able to accept `limit` orders prior to the security’s first trade on the exchange +* Once an IPO begins trading on an exchange, `market` orders are accepted + +To assist our customers, Alpaca will expose an `attribute` called `ipo` on the [Assets](https://docs.alpaca.markets/reference/getassets) model. The `ipo` attribute will flag to customers that this particular symbol has an IPO coming up, or is just about to begin trading on an exchange, and therefore a `limit` order must be used. + +# Order Types + +When you submit an order, you can choose one of supported order types. Currently, Alpaca supports four different types of orders. + +## Market Order + +A market order is a request to buy or sell a security at the currently available market price. It provides the most likely method of filling an order. Market orders fill nearly instantaneously. + +As a trade-off, your fill price may slip depending on the available liquidity at each price level as well as any price moves that may occur while your order is being routed to its execution venue. There is also the risk with market orders that they may get filled at unexpected prices due to short-term price spikes. + +## Limit Order + +A limit order is an order to buy or sell at a specified price or better. A buy limit order (a limit order to buy) is executed at the specified limit price or lower (i.e., better). Conversely, a sell limit order (a limit order to sell) is executed at the specified limit price or higher (better). Unlike a market order, you have to specify the limit price parameter when submitting your order. + +While a limit order can prevent slippage, it may not be filled for a quite a bit of time, if at all. For a buy limit order, if the market price is within your specified limit price, you can expect the order to be filled. If the market price is equivalent to your limit price, your order may or may not be filled; if the order cannot immediately execute against resting liquidity, then it is deemed non-marketable and will only be filled once a marketable order interacts with it. You could miss a trading opportunity if price moves away from the limit price before your order can be filled. + +### Sub-penny increments for limit orders + +The minimum price variance exists for limit orders. Orders received in excess of the minimum price variance will be rejected. + +* Limit price >=$1.00: Max Decimals= 2 +* Limit price \<$1.00: Max Decimals = 4 + +``` +{ + "code": 42210000, + "message": "invalid limit_price 290.123. sub-penny increment does not fulfill minimum pricing criteria" +} +``` + +## Stop Orders + +A stop (market) order is an order to buy or sell a security when its price moves past a particular point, ensuring a higher probability of achieving a predetermined entry or exit price. Once the order is elected, the stop order becomes a market order. Sell stop orders are not converted into stop limit orders. A stop order does not guarantee the order will be filled at a certain price after it is converted to a market order. + +In order to submit a stop order, you will need to specify the stop price parameter in the API. + +**Example:** + +* Let's say you want to buy 100 shares of TSLA only if it goes up to $210 (assuming current trading price at $200). +* You place a **buy stop order at $210.** +* If TSLA reaches **$210**, the order is activated and turns into a **market order** + +### Sub-penny increments for stop orders + +The minimum price variance exists for stop orders. Orders received in excess of the minimum price variance will be rejected. + +* Stop price >=$1.00: Max Decimals= 2 +* Stop price \<$1.00: Max Decimals = 4 + +``` +{ + "code": 42210000, + "message": "invalid stop_price 290.123. sub-penny increment does not fulfill minimum pricing criteria" +} +``` + +## Stop Limit Order + +A stop-limit order is a conditional trade over a set time frame that combines the features of a stop order with those of a limit order and is used to mitigate risk. The stop-limit order will be executed at a specified limit price, or better, after a given stop price has been reached. Once the stop price is reached, the stop-limit order becomes a limit order to buy or sell at the limit price or better. In the case of a gap down in the market that causes the election of your order, but not the execution, your order will remain active as a limit order until it is executable or cancelled. + +In order to submit a stop limit order, you will need to specify both the limit and stop price parameters in the API. + +**Example:** + +* You want to buy 50 shares of TSLA, currently trading at $200. +* You want to buy only if it breaks above **$210**, but not higher than **$215.** +* You place a buy stop-limit order with: + * **Stop price: $210** → The order is activated when TSLA reaches this price. + * **Limit price: $215** → The order will only execute at $215 or better. +* If TSLA moves to **$210**, the order is triggered and converted into a **limit order at $215.** + +## Opening and Closing Auction Orders + +Market on open and limit on open orders are only eligible to execute in the opening auction. Market on close and limit on close orders are only eligible to execute in the closing auction. Please see the [Time in Force](/docs/orders-at-alpaca#time-in-force) section for more details. + +## Bracket Orders + +A bracket order is a chain of three orders that can be used to manage your position entry and exit. It is a common use case of an OTOCO (One Triggers OCO \{One Cancels Other}) order. + +The first order is used to enter a new long or short position, and once it is completely filled, two conditional exit orders are activated. One of the two closing orders is called a take-profit order, which is a limit order, and the other is called a stop-loss order, which is either a stop or stop-limit order. Importantly, only one of the two exit orders can be executed. Once one of the exit orders is filled, the other is canceled. Please note, however, that in extremely volatile and fast market conditions, both orders may fill before the cancellation occurs. + +Without a bracket order, you would not be able to submit both entry and exit orders simultaneously since Alpaca’s system only accepts exit orders for existing positions. Additionally, even if you had an open position, you would not be able to submit two conditional closing orders since Alpaca’s system would view one of the two orders as exceeding the available position quantity. Bracket orders address both of these issues, as Alpaca’s system recognizes the entry and exit orders as a group and queues them for execution appropriately. + +In order to submit a bracket order, you need to supply additional parameters to the API. First, add a parameter `order_class` as “bracket”. Second, give two additional fields `take_profit` and stop\_loss both of which are nested JSON objects. The `take_profit` object needs `limit_price` as a field value that specifies limit price of the take-profit order, and the `stop_loss` object needs a mandatory `stop_price` field and optional `limit_price` field. If `limit_price` is specified in `stop_loss`, the stop-loss order is queued as a stop-limit order, but otherwise it is queued as a stop order. + +An example JSON body parameter to submit a bracket order is as follows. + +```json +{ + "side": "buy", + "symbol": "SPY", + "type": "market", + "qty": "100", + "time_in_force": "gtc", + "order_class": "bracket", + "take_profit": { + "limit_price": "301" + }, + "stop_loss": { + "stop_price": "299", + "limit_price": "298.5" + } +} +``` + +This creates three orders. + +* A buy market order for 100 SPY with GTC +* A sell limit order for the same 100 SPY, with limit price = 301 +* A sell stop-limit order, with stop price = 299 and limit price = 298.5 + +The second and third orders won’t be active until the first order is completely filled. Additional bracket order details include: + +* If any one of the orders is canceled, any remaining open order in the group is canceled. +* `take_profit.limit_price` must be higher than `stop_loss.stop_price` for a buy bracket order, and vice versa for a sell. +* Both `take_profit.limit_price` and `stop_loss.stop_price` must be present. +* Extended hours are not supported. `extended_hours` must be “false” or omitted. +* `time_in_force` must be `day` or `gtc`. +* Each order in the group is always sent with a DNR/DNC (Do Not Reduce/Do Not Cancel) instruction. Therefore, the order price will not be adjusted and the order will not be canceled in the event of a dividend or other corporate action. +* If the take-profit order is partially filled, the stop-loss order will be adjusted to the remaining quantity. +* Order replacement (`PATCH /v2/orders`) is supported to update `limit_price` and `stop_price`. + +Each order of the group is reported as an independent order in GET /v2/orders endpoint. But if you specify additional parameter nested=true, the order response will nest the result to include child orders under the parent order with an array field legs in the order entity. + +## OCO Orders + +OCO (One-Cancels-Other) is another type of advanced order type. This is a set of two orders with the same side (buy/buy or sell/sell) and currently only exit order is supported. In other words, this is the second part of the bracket orders where the entry order is already filled, and you can submit the take-profit and stop-loss in one order submission. + +With OCO orders, you can add take-profit and stop-loss after you open the position, without thinking about those two legs upfront. + +In order to submit an OCO order, specify “oco” for the order\_class parameter. + +```json SELL +{ + "side": "sell", + "symbol": "SPY", + "type": "limit", + "qty": "100", + "time_in_force": "gtc", + "order_class": "oco", + "take_profit": { + "limit_price": "301" + }, + "stop_loss": { + "stop_price": "299", + "limit_price": "298.5" + } +} +``` + +```json BUY +{ + "side": "buy", + "symbol": "SPY", + "type": "limit", + "qty": "100", + "time_in_force": "gtc", + "order_class": "oco", + "take_profit":{ + "limit_price": "298" + }, + "stop_loss": { + "stop_price": "299", + "limit_price": "300" + } +} +``` + +The type parameter must always be “limit”, indicating the take-profit order type is a limit order. The stop-loss order is a stop order if only `stop_price` is specified, and is a stop-limit order if both `limit_price` and `stop_price` are specified (i.e. `stop_price` must be present in any case). Those two orders work exactly the same way as the two legs of the bracket orders. + +Note that when you retrieve the list of orders with the `nested` parameter true, the take-profit order shows up as the parent order while the stop-loss order appears as a child order. + +Like bracket orders, order replacement is supported to update limit\_price and stop\_price. + +## OTO Orders + +OTO (One-Triggers-Other) is a variant of bracket order. It takes one of the take-profit or stop-loss order in addition to the entry order. For example, if you want to set only a stop-loss order attached to the position, without a take-profit, you may want to consider OTO orders. + +The order submission is done with the `order_class` parameter be “oto”. + +```json +{ + "side": "buy", + "symbol": "SPY", + "type": "market", + "qty": "100", + "time_in_force": "gtc", + "order_class": "oto", + "stop_loss": { + "stop_price": "299", + "limit_price": "298.5" + } +} +``` + +Either of `take_profit` or `stop_loss` must be present (the above example is for take-profit case), and the rest of requirements are the same as the bracket orders. + +Like bracket orders, order replacement is not supported yet. + +### Threshold on stop price of stop-loss orders + +For the stop-loss order leg of advanced orders, please be aware the order request can be rejected because of the restriction of the stop\_price parameter value. The stop price input has to be at least $0.01 below (for stop-loss sell, above for buy) than the “base price”. The base price is determined as follows. + +* It is the limit price of the take-profit, for OCO orders. +* It is the limit price of the entry order, for bracket or OTO orders if the entry type is limit. +* It is also the current market price for any, of OCO, OTO and bracket. + +This restriction is to avoid potential race-conditions in the order handling, but as we improve our system capability, this may be loosened in the future. + +## Trailing Stop Orders + +Trailing stop orders allow you to continuously and automatically keep updating the stop price threshold based on the stock price movement. You request a single order with a dollar offset value or percentage value as the trail and the actual stop price for this order changes as the stock price moves in your favorable way, or stay at the last level otherwise. This way, you don’t need to monitor the price movement and keep sending replace requests to update the stop price close to the latest market movement. + +Trailing stop orders keep track of the highest (for sell, lowest for buy) prices (called high water mark, or hwm) since the order was submitted, and the user-specified trail parameters determine the actual stop price to trigger relative to high water mark. Once the stop price is triggered, the order turns into a market order, and it may fill above or below the stop trigger price. + +To submit a trailing stop order, you will set the type parameter to “trailing\_stop”. There are two order submission parameters related to trailing stop, one of which is required when type is “trailing\_stop”. + +| field | type | description | +| :------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------- | +| trail\_price | string`` | a dollar value away from the highest water mark. If you set this to 2.00 for a sell trailing stop, the stop price is always `hwm - 2.00`. | +| trail\_percent | string`` | a percent value away from the highest water mark. If you set this to 1.0 for a sell trailing stop, the stop price is always `hwm \* 0.99`. | + +One of these values must be set for trailing stop orders. The following is an example of trailing order submission JSON parameter. + +```json +{ + "side": "sell", + "symbol": "SPY", + "type": "trailing_stop", + "qty": "100", + "time_in_force": "day", + "trail_price": "6.15" +} +``` + +The Order entity returned from the `GET` method has a few fields related to trailing stop orders. + +| field | type | description | +| :-------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `trail_price` | string`` | This is the same value as specified when the order was submitted. It will be null if this was not specified. | +| `trail_percent` | string`` | This is the same value as specified when the order was submitted. It will be null if this was not specified. | +| `hwm` | string`` | The high water mark value. This continuously changes as the market moves towards your favorable way. | +| `stop_price` | string`` | This is the same as stop price in the regular stop/stop limit orders, but this is derived from `hwm` and trail parameter, and continuously updates as hwm` `changes. | + +If a trailing stop order is accepted, the order status becomes “new”. While the order is pending stop price trigger, you can update the trail parameter by the PATCH method. + +| field | type | description | +| :------ | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `trail` | string`` | The new value of the `trail_price` or `trail_percent` value. Such a replace request is effective only for the order type is “trailing\_stop” before the stop price is hit. Note, you cannot change the price trailing to the percent trailing or vice versa. | + +Some notes on trailing stop orders + +* Trailing stop will not trigger outside of the regular market hours. +* Valid time-in-force values for trailing stop are “day” and “gtc”. +* Trailing stop orders are currently supported only with single orders. However, we plan to support trailing stop as the stop loss leg of bracket/OCO orders in the future. + +Proper use of Trailing Stop orders requires understanding the purpose and how they operate. The primary point to keep in mind with Trailing Stop orders is to ensure the difference between the trailing stop and the price is big enough that typical price fluctuations do not trigger a premature execution. + +In fast moving markets, the execution price may be less favorable than the stop price. The potential for such vulnerability increases for GTC orders across trading sessions or stocks experiencing trading halts. The stop price triggers a market order and therefore, it is not necessarily the case that the stop price will be the same as the execution price. + +With regard to stock splits, Alpaca reserves the right to cancel or adjust pricing and/or share quantities of trailing stop orders based upon its own discretion. Since Alpaca relies on third parties for market data, corporate actions or incorrect price data may cause a trailing stop to be triggered prematurely. + +### Notional Order Restrictions + +Notional orders (orders placed using the `notional` field instead of `qty`) cannot be replaced. Any attempt to modify a notional order via the replace (PATCH) endpoint — including changes to `limit_price`, `stop_price`, `qty`, or any other field — will be rejected. + +If you need to change any parameter on a notional order, cancel the original order and submit a new one. + +# Time in Force + +> 🚧 Crypto Time in Force +> +> For Crypto Trading, Alpaca only supports `gtc`, and `ioc`. +> +> `OPG`, `fok`, `day`, and `CLS` are not supported. + +Alpaca supports the following Time-In-Force designations: + +| time\_in\_force | description | +| :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `day` | A day order is eligible for execution only on the day it is live. By default, the order is only valid during Regular Trading Hours (9:30am - 4:00pm ET). If unfilled after the closing auction, it is automatically canceled. If submitted after the close, it is queued and submitted the following trading day. However, if marked as eligible for extended hours, the order can also execute during supported extended hours. | +| `gtc` | The order is good until canceled. Non-marketable GTC limit orders are subject to price adjustments to offset corporate actions affecting the issue. We do not currently support Do Not Reduce(DNR) orders to opt out of such price adjustments. | +| `opg` | Use this TIF with a market/limit order type to submit “market on open” (MOO) and “limit on open” (LOO) orders. This order is eligible to execute only in the market opening auction. Any unfilled orders after the open will be cancelled. OPG orders submitted after 9:28am but before 7:00pm ET will be rejected. OPG orders submitted after 7:00pm will be queued and routed to the following day’s opening auction. On open/on close orders are routed to the primary exchange. Such orders do not necessarily execute exactly at 9:30am / 4:00pm ET but execute per the exchange’s auction rules. | +| `cls` | Use this TIF with a market/limit order type to submit “market on close” (MOC) and “limit on close” (LOC) orders. This order is eligible to execute only in the market closing auction. Any unfilled orders after the close will be cancelled. CLS orders submitted after 3:50pm but before 7:00pm ET will be rejected. CLS orders submitted after 7:00pm will be queued and routed to the following day’s closing auction. Only available with API v2. | +| `ioc` | An Immediate Or Cancel (IOC) order requires all or part of the order to be executed immediately. Any unfilled portion of the order is canceled. Only available with API v2. Most market makers who receive IOC orders will attempt to fill the order on a principal basis only, and cancel any unfilled balance. On occasion, this can result in the entire order being cancelled if the market maker does not have any existing inventory of the security in question. | +| `fok` | A Fill or Kill (FOK) order is only executed if the entire order quantity can be filled, otherwise the order is canceled. Only available with API v2. | + +## Aged order policy + +Alpaca implements an aged order policy that will automatically cancel GTC orders 90 days after creation to manage risk, reduce errors, and help achieve operational efficiency.\ +The `List/Get` Orders API endpoint has a field titled `expires_at`, which provides information on the expiration.\ +On the `expires_at` date there will be a job that will submit a cancel request to cancel the GTC orders.\ +This will take place on the `expires_at` date at 4:15 pm ET. The orders will remain in pending\_cancel until canceled by the execution venue that Alpaca routed the order to for execution. + +## Order Types vs Supported Time in Force + +This section contains the tables showing time-in-force options for various order types. + +***Note**: Please contact the sales team for any TIF marked with a\** . + +### Whole qty orders (USD) + + + {` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Time in ForceMarket OrderLimit OrderStop OrderStop Limit Order
GTCYesYesYesYes
DAYYesYesYesYes
IOCYes*Yes*NoNo
FOKYes*Yes*NoNo
OPGYes*Yes*NoNo
CLSYes*Yes*NoNo
+ `} +
+ +### Fractional orders (USD) + + + {` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Time in ForceMarket OrderLimit OrderStop OrderStop Limit Order
GTCNoNoNoNo
DAYYes YesYesYes
IOCNoNoNoNo
FOKNoNoNoNo
OPGNoNoNoNo
CLSNoNoNoNo
+ `} +
+ +### LCT (Whole qty or Fractional) + + + {` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Time in ForceMarket OrderLimit OrderStop OrderStop Limit Order
GTCNoNoNoNo
DAYYes YesYesYes
IOCNoNoNoNo
FOKNoNoNoNo
OPGNoNoNoNo
CLSNoNoNoNo
+ `} +
+ +### Extended Hours orders + + + {` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Time in ForceMarket OrderLimit OrderStop OrderStop Limit Order
GTCNoYesNoNo
DAYNoYesNoNo
IOCNoNoNoNo
FOKNoNoNoNo
OPGNoNoNoNo
CLSNoNoNoNo
+ `} +
+ +### Crypto orders + + + {` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Time in ForceMarket OrderLimit OrderStop OrderStop Limit Order
GTCYesYesNoYes
DAYNoYesNoNo
IOCYesYesNoNo
FOKNoNoNoNo
OPGNoNoNoNo
CLSNoNoNoNo
+ `} +
+ +### OTC Assets + + + {` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Time in ForceMarket OrderLimit OrderStop OrderStop Limit Order
GTCYesYesYesYes
DAYYesYesYesYes
IOCNoNoNoNo
FOKNoNoNoNo
OPGNoNoNoNo
CLSNoNoNoNo
+ `} +
+ +### Options Orders + + + {` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Time in ForceMarket OrderLimit OrderStop OrderStop Limit Order
GTCYesYesYesYes
DAYYesYesYesYes
IOCNoNoNoNo
FOKNoNoNoNo
OPGNoNoNoNo
CLSNoNoNoNo
+ `} +
+ +# Order Lifecycle + +An order executed through Alpaca can experience several status changes during its lifecycle. + +![](https://files.readme.io/d7ec892-Order_status_flow.png) + +The most common statuses are described in detail below: + +| status | description | +| :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `new` | The order has been received by Alpaca, and routed to exchanges for execution. This is the usual initial state of an order. | +| `partially_filled` | The order has been partially filled. | +| `filled` | The order has been filled, and no further updates will occur for the order. | +| `done_for_day` | The order is done executing for the day, and will not receive further updates until the next trading day. | +| `canceled` | The order has been canceled, and no further updates will occur for the order. This can be either due to a cancel request by the user, or the order has been canceled by the exchanges due to its time-in-force. | +| `expired` | The order has expired, and no further updates will occur for the order. | +| `replaced` | The order was replaced by another order, or was updated due to a market event such as corporate action. | +| `pending_cancel` | The order is waiting to be canceled. | +| `pending_replace` | The order is waiting to be replaced by another order. The order will reject cancel request while in this state. | + +Less common states are described below. Note that these states only occur on very rare occasions, and most users will likely never see their orders reach these states: + +| status | description | +| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `accepted` | The order has been received by Alpaca, but hasn’t yet been routed to the execution venue. This could be seen often out side of trading session hours. | +| `pending_new` | The order has been received by Alpaca, and routed to the exchanges, but has not yet been accepted for execution. This state only occurs on rare occasions. | +| `accepted_for_bidding` | The order has been received by exchanges, and is evaluated for pricing. This state only occurs on rare occasions. | +| `stopped` | The order has been stopped, and a trade is guaranteed for the order, usually at a stated price or better, but has not yet occurred. This state only occurs on rare occasions. | +| `rejected` | The order has been rejected, and no further updates will occur for the order. This state occurs on rare occasions and may occur based on various conditions decided by the exchanges. | +| `suspended` | The order has been suspended, and is not eligible for trading. This state only occurs on rare occasions. | +| `calculated` | The order has been completed for the day (either filled or done for day), but remaining settlement calculations are still pending. This state only occurs on rare occasions. | + +An order may be canceled through the API up until the point it reaches a state of either `filled`, `canceled`, or `expired`. + +# Odd Lots and Block Trades + +When trading stocks, a round lot is typically defined as 100 shares, or a larger number that can be evenly divided by 100. An odd lot is anything that cannot be evenly divided by 100 shares (e.g. 48, 160, etc.). A block trade is typically defined as a trade that involves 10,000 shares or more. + +For trading purposes, odd lots are typically treated like round lots. However, regulatory trading rules allow odd lots to be treated differently. Similarly, block trades are usually broken up for execution and may take longer to execute due to the market having to absorb the block of shares over time rather than in one large execution. When combined with a thinly traded stock, it’s quite possible that odd lots and block trades may not get filled or execute in a timely manner, and sometimes, not at all, depending on other factors like order types used. + +# Short Sales + +A short sale is the sale of a stock that a seller does not own. In general, a short seller sells borrowed stock in anticipation of a price decline. The short seller later closes out the position by purchasing the stock. + +# Order Handling Standards at Alpaca Securities LLC + +Market and limit order orders are protected on the primary exchange opening print. We do not necessarily route retail orders to the exchange, but will route orders to market makers who will route orders on your behalf to the primary market opening auction. This protection is subject to exchange time cutoff for each exchange’s opening process. For instance, if you enter a market order between 9:28:01 and 9:29:59 on a Nasdaq security you would not receive the Nasdaq Official Opening Price (NOOP) since Nasdaq has a cutoff of 9:28 for market orders to be sent to the cross. Any market orders received before 9:28 will be filled at the Nasdaq Official Opening Price. + +Stop orders and trailing stops are elected on the consolidated print. Your sell stop order will only elect if there is a trade on the consolidated tape at or lower than your stop price and provided the electing trade is not outside of the NBBO. Your buy stop order will only elect if there is a trade on the consolidated tape that is at or above your stop price that is not outside of the NBBO. + +Limit Orders are generally subject to limit order display and protection. Protection implies that you should not see the stock trade better than your limit without you receiving an execution. Limit Order Display is bound by REG NMS Rule 611. Your orders will be displayed if they are the National Best Bid or Best Offer excluding exceptions outlined REG NMS Rule 611. Some examples are listed below: + +* An odd lot order (under a unit of trade). Most NMS securities have a unit of trade of 100 shares. +* Block Order. A block order under REG NMS is designated as an order of at least 10,000 shares or at least $200,000 notional. +* An “all or none” order +* The client requests the order to not be displayed. +* Not Held orders + +# OTC Positions + +OTC positions that are actively trading will not be automatically removed/liquidated from a user's account. The user can submit orders if the client wishes to remove or liquidate them. + +OTC markets have several market tiers and depending on the market tier there may be more considerations when placing an order to close the position. + +**OTC Market Tier**\ +*OTCQX, OTCQB, Pink (Current, limited, No information)*\ +Clients can enter market orders, limit orders and stop orders to close positions. For market orders, clients can mix whole shares and fractional shares. + +*Expert Market, Caveat Emptor*\ +Clients must enter day-limit orders when placing orders to close out an OTC expert market security. However, Alpaca will accept market orders that are only fractional shares, but it is a best practice to enter a day-limit order for your whole, mixed (whole and fraction) or fractional order. + +Example: User owns 10.53 shares of WXYZ Stock: Sell Limit Order for 10.53 shares of WXYZ Stock at a limit price. + +OTC market data is a separate data subscription. In many circumstances, a client can obtain a real-time level one quote from otcmarkets.com. Additionally, you can check the OTC market tier on that website as well. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/paper-trading.md b/DesktopBot/Documents/Alpaca/paper-trading.md new file mode 100644 index 0000000..4eff746 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/paper-trading.md @@ -0,0 +1,78 @@ +# Paper Trading + +Test your algos in our paper environment. Free and available to all Alpaca users. + +Paper trading is a real-time simulation environment where you can test your code. You can reset and test your algorithm as much as you want using free, real-time market data. Paper trading simulates crypto trading as well. Paper trading works the same way as live trading end to end - except the order is not routed a live exchange. Instead, the system simulates the order filling based on the real-time quotes. + +When you run your algorithm with the live market, there are many things that can happen that you may not see in backtesting. Orders may not be filled, prices may spike, or your network may get disconnected and retry may be needed. During the software development process, it is important to test your algorithm to catch these things in advance. + + + ### Anyone globally can create an Alpaca **Paper Only Account**! All you need to do is sign up with your email address. + + +An Alpaca **Paper Only Account** is for paper trading only. It allows you to fully utilize the Alpaca API and run your algorithm in our paper trading environment only. You won’t be trading real money, but you will be able to track your simulated activity and balance in the Alpaca web dashboard. As an Alpaca Paper Only Account holder, you are only entitled to receive and make use of IEX market data. For more information about our paper trading environment, please refer to Paper Trading Specification. + +# Paper vs Live + +We are thrilled that many users have found paper trading useful, and we continue to work on improving our paper trading assumptions so that users may have a superior experience. However, please note that paper trading is only a simulation. It provides a good approximation for what one might expect in real trading, but it is not a substitute for real trading and performance may differ. Specifically, paper trading does not account for: + +* Market impact of your orders +* Information leakage of your orders +* Price slippage due to latency +* Order queue position (for non-marketable limit orders) +* Price improvement received +* Regulatory fees +* Dividends + +If you are interested to incorporate these issues into your testing, you may do so by trading a live brokerage account. Even small amounts of real money can often provide insight into issues not seen in a simulation environment. + +## Paper vs Live + +| Feature | Paper | Live | +| :---------------------------- | :---------------- | :--- | +| Eligibility | ✅ | ✅ | +| API Access | ✅ | ✅ | +| Free IEX Real Time Data | ✅ | ✅ | +| MFA | ✅ | ✅ | +| Margin Trading | ✅ | ✅ | +| Short Selling | ✅ | ✅ | +| Premarket/After Hours Trading | ✅ | ✅ | +| Borrow Fees | ⛔️ (Coming Soon!) | ✅ | + +# Comparing Other Simulators + +Users may be interested to compare their paper trading results on Alpaca with their paper trading results on other platforms such as Quantopian or Interactive Brokers. Please note there are several factors that may lead to performance differences. Paper trading platforms may have different: + +* Fill prices +* Fill assumptions +* Liquidity assumptions +* Return calculation methodologies +* Market data sources + +# Getting Started with Paper Trading + +Your initial paper trading account is created with $100k balance as a default setting. You can reset the paper trading account at any time later with arbitrary amount as you configure. + +Your paper trading account will have a different API key from your live account, and all you need to do to start using your paper trading account is to replace your API key and API endpoint with ones for the paper trading. The API spec is the same between the paper trading and live accounts. The API endpoint (base URL) is displayed in your paper trading dashboard, and please follow the instruction about how to set it depending on the library you are using. In most cases, you need to set an environment variable `APCA_API_BASE_URL = https://paper-api.alpaca.markets` + +# Reset Your Paper Trading Account + +We've updated the dashboard to allow you to create and delete paper accounts, rather than resetting them. + +To create a new paper account, click the paper account number in the upper left corner of the dashboard and select "Open New Paper Account." + +To delete an existing paper account, click the paper account number in the upper left corner, then go to "Account Settings." Locate the paper account you'd like to remove and click the "Delete Account" button next to it. + +Don't forget to generate new API keys for any newly created account. + +# Rules and Assumptions + +* Paper trading account simulates our Pattern Day Trader checks. Orders that would generate a 4th Day Trade within 5 business days will be rejected if the real-time net worth is below $25,000. Please read the [Pattern DayTrader Protection](/docs/user-protection#pattern-day-trader-pdt-protection-at-alpaca) page for more details. +* Paper trading account **does NOT** simulate dividends. +* Paper trading account **does NOT** send order fill emails. +* Market Data API works identically. +* You cannot change the account balance after it is created, unless you reset it. +* Orders are filled only when they become marketable. This means that a non-marketable buy limit order will not be filled until its limit price is equal to or greater than the best ask price, and a non-marketable sell limit order will not be filled until its limit price is equal to or less than the best bid. +* Your order quantity is not checked against the NBBO quantities. In other words, you can submit and receive a fill for an order that is much larger than the actual available liquidity.\ + When orders are eligible to be filled, they will receive partial fills for a random size 10% of the time. If the order price is still marketable, the remaining quantity would be re-evaluated for a subsequent fill. +* Regardless of whether you have a Paper Trading Only account or a real money brokerage account, all orders submitted in paper trading will be matched against the best available current market price (NBBO). \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/position-average-entry-price-calculation.md b/DesktopBot/Documents/Alpaca/position-average-entry-price-calculation.md new file mode 100644 index 0000000..bf62ed7 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/position-average-entry-price-calculation.md @@ -0,0 +1,139 @@ +# Position Average Entry Price Calculation + +How is the average entry price of a position is calculated? + +# Description + +The average entry price and the cost basis of a position are returned in the `avg_entry_price` and `cost_basis` fields in the [positions endpoints](https://docs.alpaca.markets/reference/positions). + +```json +{ + "asset_id": "904837e3-3b76-47ec-b432-046db621571b", + "symbol": "AAPL ", + "exchange": "NASDAQ", + "asset_class": "us_equity", + "avg_entry_price": "100.0", + "qty": "5", + "qty_available": "4", + "side": "long", + "market_value": "600.0", + "cost_basis": "500.0", + "unrealized_pl": "100.0", + "unrealized_plpc": "0.20", + "unrealized_intraday_pl": "5.0", + "unrealized_intraday_plpc": "0.0084", + "current_price": "120.0", + "lastday_price": "119.0", + "change_today": "0.0084" +} +``` + +There are different methods that can be used to calculate the cost basis and the average entry price of a position such as `Strict FIFO`, `Compressed FIFO`, `Weighted Average`, and others. Each method has its own rules for calculating the cost basis and average entry price after a sell transaction. This page aims to clarify which method is Alpaca using. + +# Which Method is Alpaca Using? + +* [Weighted Average](https://docs.alpaca.markets/us/docs/position-average-entry-price-calculation#weighted-average) is used for intraday positions (positions from intraday trades) +* [Compressed FIFO](https://docs.alpaca.markets/us/docs/position-average-entry-price-calculation#compressed-fifo-first-in-first-out) is used for the end-of-day positions (positions from previous trading days) + +## Strict FIFO (First-In, First-Out) + +Under the Strict FIFO method, the first position bought is the first position sold. Let's understand how it works: + +The cost basis after the sell is calculated by deducting from the previous cost basis the price of the first open position multiplied by the sell quantity. In Strict FIFO, the sell quantity is covered using the first open position, however, if the first open position's quantity is not enough to cover the sell quantity, subsequent open positions are used. + +### Example: + +Suppose we have the following transactions: + +Day 1: + +1. Buy 100 shares at $10 per share (Cost basis = $1,000) +2. Buy 50 shares at $12 per share (Cost basis = $600) + +Day 2: + +1. Buy 30 shares at $15 per share (Cost basis = $450) + +Day 3: + +1. Sell 120 shares + +After the sell transaction: + +* Cost basis: `2050 - 100*10 - 20*12` = `$810` +* Average Entry Price: `cost_basis/qty_left` = `810/60` = `$13.5` + +## Compressed FIFO (First-In, First-Out) + +The Compressed FIFO method follows similar rules to Strict FIFO, with one key difference. It compresses intraday positions using a weighted average. Let's see how it differs: + +### Example 1: + +Using the same example from before:\ +Day 1: + +1. Buy 100 shares at $10 per share (Cost basis = $1,000) +2. Buy 50 shares at $12 per share (Cost basis = $600) + +Day 2: + +1. Buy 30 shares at $15 per share (Cost basis = $450) + +Day 3: + +1. Sell 120 shares + +After the sell transaction: + +* Cost Basis: `2050 - 120*(100*10 + 50*12)/150` = `$770` +* Average entry price: `cost_basis/qty_left` = `770/60` = `$12.83` + +As you can see the positions in Day 1 were compressed into a total of 150 shares with an average price of `(100*10 + 50*12)/150`. + +### Example 2 + +Day 1: + +1. Buy 100 shares at $10 per share (Cost basis = $1,000) +2. Buy 50 shares at $9 per share (Cost basis = $450) +3. Sell 50 shares +4. Buy 50 shares at $11 per share (Cost basis = $550) + +At the end of Day 1: + +* Cost Basis: `2000 - 50*(100*10 + 50*9 + 50*11)/200` = `$1,500` +* Average Entry Price: `1500/150` = `$10` + +## Weighted Average + +The Weighted Average method calculates the cost basis based on the weighted average price per share. Here's how it works: + +On Sell: The cost basis for the sold quantity is calculated by deducting the sell quantity multiplied by the average entry price of all the opened positions that the account holds. + +### Example: + +Using the same example from before: + +Day 1: + +1. Buy 100 shares at $10 per share (Cost basis = $1,000) +2. Buy 50 shares at $12 per share (Cost basis = $600) + +Day 2: + +1. Buy 30 shares at $15 per share (Cost basis = $450) + +Day 3: + +1. Sell 120 shares + +After the sell the calculations based on the Weighted average method would be: + +* Cost Basis: `2050 - 120*(100*10 + 50*12 + 30*15)/180` = `$683.33` +* Average Entry Price: `cost_basis/qty_left` = `683.33/60` = `$11.39` + +# FAQ + +## Why did the `avg_entry_price` and `cost_basis` of a position change the next day? + +As described in the [Which method is Alpaca using?](https://docs.alpaca.markets/us/docs/position-average-entry-price-calculation#which-method-is-alpaca-using) the calculation method for determining the `avg_entry_price` and `cost_basis` differs between the `intraday positions` and the `end-of-day positions`. Consequently, it is possible for the `avg_entry_price` and `cost_basis` fields of a position to change the day after the last trade has occurred. This change occurs when our beginning-of-day (BOD) job executes and synchronizes positions from our ledger. For details regarding the timing of the beginning-of-day (BOD) job, please refer to the [Daily Processes and Reconciliations](https://docs.alpaca.markets/us/docs/daily-processes-and-reconcilations). \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/real-time-crypto-pricing-data.md b/DesktopBot/Documents/Alpaca/real-time-crypto-pricing-data.md new file mode 100644 index 0000000..7984760 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/real-time-crypto-pricing-data.md @@ -0,0 +1,279 @@ +# Real-time Crypto Data + +Crypto Data API provides websocket streaming for trades, quotes, orderbooks, minute bars and daily bars. This helps receive the most up to date market information that could help your trading strategy to act upon certain market movements. + +Alpaca executes your crypto orders in its own exchange, and also supports Kraken, which is another crypto exchange. Therefore, `v1beta3` crypto market data endpoint distributes data from Alpaca and Kraken. + +You can find the general description of the real-time WebSocket Stream [here](https://docs.alpaca.markets/docs/streaming-market-data). This page focuses on the crypto stream. + +> 👍 Advanced Websockets Tutorial +> +> Check out our tutorial [Advanced Live Websocket Crypto Data Streams in Python](https://alpaca.markets/learn/advanced-live-websocket-crypto-data-streams-in-python/) for some tips on handling live crypto data stream in Python. + +# URL + +The URL for the crypto stream is + +``` +wss://stream.data.alpaca.markets/v1beta3/crypto/{loc} +``` + +Sandbox URL: + +``` +wss://stream.data.sandbox.alpaca.markets/v1beta3/crypto/{loc} +``` + +Possible values `{loc}` can have are: + +* us - Alpaca US +* us-1 - Kraken US +* eu-1 - Kraken EU + +The location us-1 represents the states listed below: + +1. AL (Alabama) +2. AK (Alaska) +3. AR (Arkansas) +4. CO (Colorado) +5. DC (District of Columbia) +6. DE (Delaware) +7. FL (Florida) +8. HI (Hawaii) +9. LA (Louisiana) +10. MN (Minnesota) +11. NV (Nevada) +12. NH (New Hampshire) +13. NJ (New Jersey) +14. NM (New Mexico) +15. OK (Oklahoma) +16. OR (Oregon) +17. PA (Pennsylvania) +18. PR (Puerto Rico) +19. TN (Tennessee) +20. TX (Texas) +21. VA (Virginia) +22. WI (Wisconsin) +23. WY (Wyoming) + +Please note that, at the moment `us-1` and `eu-1` stream Kraken market data (which is another crypto exchange), but the providers may change over time. + +Multiple data points may arrive in each message received from the server. These data points have the following formats, depending on their type. + +# Channels + +## Trades + +### Schema + +| Attribute | Type | Notes | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------ | +| `T` | string | message type, always “t” | +| `S` | string | symbol | +| `p` | number | trade price | +| `s` | number | trade size | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp with nanosecond precision | +| `i` | int | trade ID | +| `tks` | string | taker side: B for buyer, S for seller | + +### Example + +```json +{ + "T": "t", + "S": "AVAX/USD", + "p": 47.299, + "s": 29.205707815, + "t": "2024-03-12T10:27:48.858228144Z", + "i": 3447222699101865076, + "tks": "S" +} +``` + +## Quotes + +### Schema + +| Attribute | Type | Notes | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------ | +| `T` | string | message type, always “q” | +| `S` | string | symbol | +| `bp` | number | bid price | +| `bs` | number | bid size | +| `ap` | number | ask price | +| `as` | number | ask size | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp with nanosecond precision | + +### Example + +```json +{ + "T": "q", + "S": "BAT/USD", + "bp": 0.35718, + "bs": 13445.46, + "ap": 0.3581, + "as": 13561.902, + "t": "2024-03-12T10:29:43.111588173Z" +} +``` + +## Bars + +> 📘 Crypto bars contain quote mid-prices +> +> Due to the volatility of some currencies, including lack of trade volume at any given time, we include the quote midpoint prices in the bars to offer a better data experience. If in a bar no trade happens, the volume will be 0, but the prices will be determined by the quote prices. + +There are three separate channels where you can stream trade aggregates (bars). + +#### Minute Bars (`bars`) + +Minute bars are emitted right after each minute mark. They contain the trades and quote midpoints from the previous minute. + +#### Daily Bars (`dailyBars`) + +Daily bars are emitted right after each minute mark after the market opens. The daily bars contain all trades and quote midprices until the time they were emitted. + +#### Updated Bars (`updatedBars`) + +Updated bars are emitted after each half-minute mark if a “late” trade arrived after the previous minute mark. For example if a trade with a timestamp of `16:49:59.998` arrived right after `16:50:00`, just after `16:50:30` an updated bar with `t` set to `16:49:00` will be sent containing that trade, possibly updating the previous bar’s closing price and volume. + +### Schema + +| Attribute | Type | Description | +| :-------- | :----- | :---------------------------------------------------------------------------- | +| `T` | string | message type: “b”, “d” or “u” | +| `S` | string | symbol | +| `o` | number | open price | +| `h` | number | high price | +| `l` | number | low price | +| `c` | number | close price | +| `v` | int | volume | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp | + +### Example + +```json +{ + "T": "b", + "S": "BTC/USD", + "o": 71856.1435, + "h": 71856.1435, + "l": 71856.1435, + "c": 71856.1435, + "v": 0, + "t": "2024-03-12T10:37:00Z", + "n": 0, + "vw": 0 +} +``` + +## Orderbooks + +### Schema + +| Attribute | Type | Notes | +| :-------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `T` | string | message type, always “o” | +| `S` | string | symbol | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp with nanosecond precision | +| `b` | array | bids: array of `p` (price) and `s` pairs. If `s` is zero, it means that that bid entry was removed from the orderbook. Otherwise it was added or updated. | +| `a` | array | asks: array of `p` (price) and `s` pairs. If `s` is zero, it means that that ask entry was removed from the orderbook. Otherwise it was added or updated. | +| `r` | boolean | reset: if true, the orderbook message contains the whole server side orderbook. This indicates to the client that they should reset their orderbook. Typically sent as the first message after subscription. | + +### Example + +#### Initial full orderbook + +```json +{ + "T": "o", + "S": "BTC/USD", + "t": "2024-03-12T10:38:50.79613221Z", + "b": [ + { + "p": 71859.53, + "s": 0.27994 + }, + { + "p": 71849.4, + "s": 0.553986 + }, + { + "p": 71820.469, + "s": 0.83495 + }, + ... + ], + "a": [ + { + "p": 71939.7, + "s": 0.83953 + }, + { + "p": 71940.4, + "s": 0.28025 + }, + { + "p": 71950.715, + "s": 0.555928 + }, + ... + ], + "r": true +} +``` + +`r` is true, meaning that this message contains the whole BTC/USD orderbook. It's truncated here for readability, the actual book has a lot more bids & asks. + +#### Update message + +```json json +{ + "T": "o", + "S": "MKR/USD", + "t": "2024-03-12T10:39:39.445492807Z", + "b": [], + "a": [ + { + "p": 2614.587, + "s": 12.5308 + } + ] +} +``` + +This means that the ask price level 2614.587 was changed to 12.5308. If there were previously no 2614.587 ask entry in the orderbook, then it should be added, if there were, its size should be updated. + +#### Remove message + +```json +{ + "T": "o", + "S": "CRV/USD", + "t": "2024-03-12T10:39:40.501160019Z", + "b": [ + { + "p": 0.7904, + "s": 0 + } + ], + "a": [] +} +``` + +This means that the 0.7904 bid price level should be removed from the orderbook. + +# Example + +``` +$ wscat -c wss://stream.data.alpaca.markets/v1beta3/crypto/us +connected (press CTRL+C to quit) +< [{"T":"success","msg":"connected"}] +> {"action": "auth", "key": "**\***", "secret": "**\***"} +< [{"T":"success","msg":"authenticated"}] +> {"action": "subscribe", "bars": ["BTC/USD"]} +< [{"T":"subscription","trades":[],"quotes":[],"orderbooks":[],"bars":["BTC/USD"],"updatedBars":[],"dailyBars":[]}] +< [{"T":"b","S":"BTC/USD","o":26675.04,"h":26695.36,"l":26668.79,"c":26688.7,"v":3.227759152,"t":"2023-03-17T12:28:00Z","n":93,"vw":26679.5912436798}] +< [{"T":"b","S":"BTC/USD","o":26687.9,"h":26692.91,"l":26628.55,"c":26651.39,"v":11.568622108,"t":"2023-03-17T12:29:00Z","n":197,"vw":26651.7679765663}] +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/real-time-option-data.md b/DesktopBot/Documents/Alpaca/real-time-option-data.md new file mode 100644 index 0000000..9447c95 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/real-time-option-data.md @@ -0,0 +1,102 @@ +# Real-time Option Data + +This API provides option market data on a websocket stream. This helps receive the most up to date market information that could help your trading strategy to act upon certain market movements. If you wish to access the latest pricing data, using the stream provides much better accuracy and performance than polling the latest historical endpoints. + +You can find the general description of the real-time WebSocket Stream [here](https://docs.alpaca.markets/docs/streaming-market-data). This page focuses on the option stream. + +# URL + +The URL for the option stream is + +``` +wss://stream.data.alpaca.markets/v1beta1/{feed} +``` + +Sandbox URL: + +``` +wss://stream.data.sandbox.alpaca.markets/v1beta1/{feed} +``` + +Substitute `indicative` or `opra` for `{feed}` depending on your subscription. The capabilities and differences for the `indicative` and `opra` subscriptions can be found \[[here](https://docs.alpaca.markets/docs/about-market-data-api#options)]. + +Any attempt to access a data feed not available for your subscription will result in an error during authentication. + +# Message format + +> 🚧 Msgpack +> +> Unlike the stock and crypto stream, the option stream is only available in [msgpack](https://msgpack.org/index.html) format. The SDKs are using this format automatically. For readability, the examples in the rest of this documentation will still be in json format (because msgpack is binary encoded). + +# Channels + +## Trades + +### Schema + +| Attribute | Type | Notes | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------ | +| `T` | string | message type, always “t” | +| `S` | string | symbol | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp with nanosecond precision | +| `p` | number | trade price | +| `s` | int | trade size | +| `x` | string | exchange code where the trade occurred | +| `c` | string | trade condition | + +### Example + +```json +{ + "T": "t", + "S": "AAPL240315C00172500", + "t": "2024-03-11T13:35:35.13312256Z", + "p": 2.84, + "s": 1, + "x": "N", + "c": "S" +} +``` + +## Quotes + +### Schema + +| Attribute | Type | Notes | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------ | +| `T` | string | message type, always “q” | +| `S` | string | symbol | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp with nanosecond precision | +| `bx` | string | bid exchange code | +| `bp` | number | bid price | +| `bs` | int | bid size | +| `ax` | string | ask exchange code | +| `ap` | number | ask price | +| `as` | int | ask size | +| `c` | string | quote condition | + +### Example + +```json +{ + "T": "q", + "S": "SPXW240327P04925000", + "t": "2024-03-12T11:59:38.897261568Z", + "bx": "C", + "bp": 9.46, + "bs": 53, + "ax": "C", + "ap": 9.66, + "as": 38, + "c": "A" +} +``` + +# Errors + +Other than the [general stream errors](https://docs.alpaca.markets/docs/streaming-market-data#errors), you may receive these option-specific errors during your session: + +| Error Message | Description | +| :---------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | +| `[{"T":"error","code":412,"msg":"option messages are only available in MsgPack format"}]` | Use the `Content-Type: application/msgpack` header. | +| `[{"T":"error","code":413,"msg":"star subscription is not allowed for option quotes"}]` | You cannot subscribe to `*` for option quotes (there are simply too many of them). | \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/real-time-stock-pricing-data.md b/DesktopBot/Documents/Alpaca/real-time-stock-pricing-data.md new file mode 100644 index 0000000..c50504b --- /dev/null +++ b/DesktopBot/Documents/Alpaca/real-time-stock-pricing-data.md @@ -0,0 +1,453 @@ +# Real-time Stock Data + +This API provides stock market data on a websocket stream. This helps receive the most up to date market information that could help your trading strategy to act upon certain market movements. If you wish to access the latest pricing data, using the stream provides much better accuracy and performance than polling the latest historical endpoints. + +You can find the general description of the real-time WebSocket Stream [here](https://docs.alpaca.markets/docs/streaming-market-data). This page focuses on the stock stream. + +# URL + +The URL for the stock stream is + +``` +wss://stream.data.alpaca.markets/{version}/{feed} +``` + +Sandbox URL: + +``` +wss://stream.data.sandbox.alpaca.markets/{version}/{feed} +``` + +Substitute `{version}/{feed}` to one of the followings: + +* `v2/sip` is the SIP (Securities Information Processor) feed +* `v2/iex` is the IEX (Investors Exchange) feed +* `v2/delayed_sip` is a 15-minute delayed SIP feed +* `v1beta1/boats` is the BOATS (Blue Ocean ATS) feed +* `v1beta1/overnight` is the derived Alpaca overnight feed + +These feeds are described [here](https://docs.alpaca.markets/docs/historical-stock-data-1#feed-parameter). + +Any attempt to access a data feed not available for your subscription will result in an error during authentication. + +# Channels + +You can [subscribe](https://docs.alpaca.markets/docs/streaming-market-data#subscription) to the channels described in this section. For example + +```json +{"action":"subscribe","trades":["AAPL"],"quotes":["AMD","CLDR"],"bars":["*"]} +``` + +## Trades + +### Schema + +| Attribute | Type | Notes | +| :-------- | :-------------- | :------------------------------------------------------------------------------------------------------ | +| `T` | string | message type, always “t” | +| `S` | string | symbol | +| `i` | int | trade ID | +| `x` | string | exchange code where the trade occurred | +| `p` | number | trade price | +| `s` | int | trade size | +| `c` | array`` | trade condition | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp with nanosecond precision | +| `z` | string | tape | + +### Example + +```json +{ + "T": "t", + "i": 96921, + "S": "AAPL", + "x": "D", + "p": 126.55, + "s": 1, + "t": "2021-02-22T15:51:44.208Z", + "c": ["@", "I"], + "z": "C" +} +``` + +## Quotes + +### Schema + +| Attribute | Type | Notes | +| :-------- | :-------------- | :------------------------------------------------------------------------------------------------------ | +| `T` | string | message type, always “q” | +| `S` | string | symbol | +| `ax` | string | ask exchange code | +| `ap` | number | ask price | +| `as` | int | ask size in round lots | +| `bx` | string | bid exchange code | +| `bp` | number | bid price | +| `bs` | int | bid size in round lots | +| `c` | array`` | quote condition | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp with nanosecond precision | +| `z` | string | tape | + +### Example + +```json +{ + "T": "q", + "S": "AMD", + "bx": "U", + "bp": 87.66, + "bs": 1, + "ax": "Q", + "ap": 87.68, + "as": 4, + "t": "2021-02-22T15:51:45.335689322Z", + "c": ["R"], + "z": "C" +} +``` + +## Bars + +There are three separate channels where you can stream trade aggregates (bars). + +#### Minute Bars (`bars`) + +Minute bars are emitted right after each minute mark. They contain the trades from the previous minute. Trades from pre-market and aftermarket are also aggregated and sent out on the bars channel. + +Note: Understanding which trades are excluded from minute bars is crucial for accurate data analysis. For more detailed information on how minute bars are calculated and excluded trades, please refer to this article [Stock Minute Bars](https://alpaca.markets/learn/stock-minute-bars/). + +#### Daily Bars (`dailyBars`) + +Daily bars are emitted right after each minute mark after the market opens. The daily bars contain all trades until the time they were emitted. + +#### Updated Bars (`updatedBars`) + +Updated bars are emitted after each half-minute mark if a “late” trade arrived after the previous minute mark. For example if a trade with a timestamp of `16:49:59.998` arrived right after `16:50:00`, just after `16:50:30` an updated bar with `t` set to `16:49:00` will be sent containing that trade, possibly updating the previous bar’s closing price and volume. + +### Schema + +| Attribute | Type | Description | +| :-------- | :----- | :---------------------------------------------------------------------------- | +| `T` | string | message type: “b”, “d” or “u” | +| `S` | string | symbol | +| `o` | number | open price | +| `h` | number | high price | +| `l` | number | low price | +| `c` | number | close price | +| `v` | int | volume | +| `vw` | number | volume-weighted average price | +| `n` | int | number of trades | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp | + +### Example + +```json +{ + "T": "b", + "S": "SPY", + "o": 388.985, + "h": 389.13, + "l": 388.975, + "c": 389.12, + "v": 49378, + "n": 461, + "vw": 389.062639, + "t": "2021-02-22T19:15:00Z" +} +``` + +## Trade Corrections + +These messages indicate that a previously sent trade was incorrect and they contain the corrected trade. + +Subscription to trade corrections and cancel/errors is automatic when you subscribe to the trade channel. + +``` +{"action":"subscribe","trades":["AAPL"]} +[{"T":"subscription","trades":["AAPL"],"quotes":[],"bars":[],"updatedBars":[],"dailyBars":[],"statuses":[],"lulds":[], +"corrections":["AAPL"],"cancelErrors":["AAPL"]}] +``` + +### Schema + +| Attribute | Type | Description | +| :-------- | :-------------- | :---------------------------------------------------------------------------- | +| `T` | string | message type, always “c” | +| `S` | string | symbol | +| `x` | string | exchange code | +| `oi` | int | original trade id | +| `op` | number | original trade price | +| `os` | int | original trade size | +| `oc` | array`` | original trade conditions | +| `ci` | int | corrected trade id | +| `cp` | number | corrected trade price | +| `cs` | int | corrected trade size | +| `cc` | array`` | corrected trade conditions | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp | +| `z` | string | tape | + +### Example + +```json +{ + "T": "c", + "S": "EEM", + "x": "M", + "oi": 52983525033527, + "op": 39.1582, + "os": 440000, + "oc": [ + " ", + "7" + ], + "ci": 52983525034326, + "cp": 39.1809, + "cs": 440000, + "cc": [ + " ", + "7" + ], + "z": "B", + "t": "2023-04-06T14:25:06.542305024Z" +} +``` + +## Trade Cancels/Errors + +These messages indicate that a previously sent trade was canceled. + +Subscription to trade corrections and cancel/errors is automatic when you subscribe to the trade channel. + +``` +{"action":"subscribe","trades":["AAPL"]} +[{"T":"subscription","trades":["AAPL"],"quotes":[],"bars":[],"updatedBars":[],"dailyBars":[],"statuses":[],"lulds":[], +"corrections":["AAPL"],"cancelErrors":["AAPL"]}] +``` + +### Schema + +| Attribute | Type | Description | +| :-------- | :----- | :---------------------------------------------------------------------------- | +| `T` | string | message type, always “x” | +| `S` | string | symbol | +| `i` | int | trade id | +| `x` | string | trade exchange | +| `p` | number | trade price | +| `s` | int | trade size | +| `a` | string | action (“C” for cancel, “E” for error) | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp | +| `z` | string | tape | + +### Example + +```json +{ + "T": "x", + "S": "GOOGL", + "i": 465, + "x": "D", + "p": 105.31, + "s": 300, + "a": "C", + "z": "C", + "t": "2023-04-06T13:15:42.83540958Z" +} +``` + +## LULDs + +Limit Up - Limit Down messages provide upper and lower limit price bands to securities. + +### Schema + +| Attribute | Type | Description | +| :-------- | :----- | :---------------------------------------------------------------------------- | +| `T` | string | message type, always “l” | +| `S` | string | symbol | +| `u` | number | limit up price | +| `d` | number | limit down price | +| `i` | string | indicator | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp | +| `z` | string | tape | + +### Example + +```json +{ + "T": "l", + "S": "IONM", + "u": 3.24, + "d": 2.65, + "i": "B", + "t": "2023-04-06T13:34:45.565004401Z", + "z": "C" +} +``` + +## Trading Status + +Identifies the trading status applicable to the security and reason for the trading halt if any. The status messages can be accessed from any \{source} depending on your subscription. + +To enable market data on a production environment please reach out to our sales team. + +### Schema + +| Attribute | Type | Description | +| :-------- | :----- | :---------------------------------------------------------------------------- | +| `T` | string | message type, always “s” | +| `S` | string | symbol | +| `sc` | string | status code | +| `sm` | string | status message | +| `rc` | string | reason code | +| `rm` | string | reason message | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp | +| `z` | string | tape | + +### Example + +```json +{ + "T": "s", + "S": "AAPL", + "sc": "H", + "sm": "Trading Halt", + "rc": "T12", + "rm": "Trading Halted; For information requested by NASDAQ", + "t": "2021-02-22T19:15:00Z", + "z": "C" +} +``` + +### Status Codes + +#### Tape A & B (CTA) + +| Code | Value | +| :--- | :----------------------------- | +| 2 | Trading Halt | +| 3 | Resume | +| 5 | Price Indication | +| 6 | Trading Range Indication | +| 7 | Market Imbalance Buy | +| 8 | Market Imbalance Sell | +| 9 | Market On Close Imbalance Buy | +| A | Market On Close Imbalance Sell | +| C | No Market Imbalance | +| D | No Market On Close Imbalance | +| E | Short Sale Restriction | +| F | Limit Up-Limit Down | + +#### Tape C & O (UTP) + +| Codes | Resume | +| :---- | :----------------------- | +| H | Trading Halt | +| Q | Quotation Resumption | +| T | Trading Resumption | +| P | Volatility Trading Pause | + +### Reason Codes + +#### Tape A & B (CTA) + +| Code | Value | +| :--- | :--------------------------------------------- | +| A | Additional Information Requested | +| C | Regulatory Concern | +| D | News Released (formerly News Dissemination) | +| E | Merger Effective | +| F | ETF Component Prices Not Available | +| I | Order Imbalance | +| M | Limit Up-Limit Down (LULD) Trading Pause | +| N | Corporate Action | +| O | New Security Offering | +| P | News Pending | +| V | Intraday Indicative Value Not Available | +| X | Operational | +| Y | Sub-Penny Trading | +| 1 | Market-Wide Circuit Breaker Level 1 – Breached | +| 2 | Market-Wide Circuit Breaker Level 2 – Breached | +| 3 | Market-Wide Circuit Breaker Level 3 – Breached | + +#### Tape C & O (UTP) + +| Code | Value | +| :--- | :-------------------------------------------------------------------- | +| T1 | Halt News Pending | +| T2 | Halt News Dissemination | +| T5 | Single Stock Trading Pause In Affect | +| T6 | Regulatory Halt Extraordinary Market Activity | +| T8 | Halt ETF | +| T12 | Trading Halted; For information requested by NASDAQ | +| H4 | Halt Non Compliance | +| H9 | Halt Filings Not Current | +| H10 | Halt SEC Trading Suspension | +| H11 | Halt Regulatory Concern | +| 01 | Operations Halt, Contact Market Operations | +| IPO1 | IPO Issue not yet Trading | +| M1 | Corporate Action | +| M2 | Quotation Not Available | +| LUDP | Volatility Trading Pause | +| LUDS | Volatility Trading Pause – Straddle Condition | +| MWC1 | Market Wide Circuit Breaker Halt – Level 1 | +| MWC2 | Market Wide Circuit Breaker Halt – Level 2 | +| MWC3 | Market Wide Circuit Breaker Halt – Level 3 | +| MWC0 | Market Wide Circuit Breaker Halt – Carry over from previous day | +| T3 | News and Resumption Times | +| T7 | Single Stock Trading Pause/Quotation-Only Period | +| R4 | Qualifications Issues Reviewed/Resolved; Quotations/Trading to Resume | +| R9 | Filing Requirements Satisfied/Resolved; Quotations/Trading To Resume | +| C3 | Issuer News Not Forthcoming; Quotations/Trading To Resume | +| C4 | Qualifications Halt ended; maint. Req. met; Resume | +| C9 | Qualifications Halt Concluded; Filings Met; Quotes/Trades To Resume | +| C11 | Trade Halt Concluded By Other Regulatory Auth,; Quotes/Trades Resume | +| R1 | New Issue Available | +| R | Issue Available | +| IPOQ | IPO security released for quotation | +| IPOE | IPO security – positioning window extension | +| MWCQ | Market Wide Circuit Breaker Resumption | + +## Order imbalances + +Order imbalance is a situation resulting from an excess of buy or sell orders for a specific security on a trading exchange, making it impossible to match the orders of buyers and sellers. Order imbalance messages are typically sent during limit-up and limit-down trading halts. You have to subscribe to these messages using the `imbalances` JSON key: + +```json +{"action":"subscribe","imbalances":["INAQU"]} +``` + +### Schema + +| Attribute | Type | Notes | +| :-------- | :----- | :------------------------------------------------------------------------------------------------------ | +| `T` | string | message type, always “i” | +| `S` | string | symbol | +| `p` | number | price | +| `z` | string | tape | +| `t` | string | [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) formatted timestamp with nanosecond precision | + +### Example + +```json +{ + "T": "i", + "S": "INAQU", + "p": 9.12, + "z": "C", + "t": "2024-12-13T19:58:09.242138635Z" +} +``` + +# Example + +```json Shell +$ wscat -c wss://stream.data.alpaca.markets/v2/sip +connected (press CTRL+C to quit) +< [{"T":"success","msg":"connected"}] +> {"action": "auth", "key": "*****", "secret": "*****"} +< [{"T":"success","msg":"authenticated"}] +> {"action": "subscribe", "trades": ["AAPL"], "quotes": ["AMD", "CLDR"], "bars": ["*"],"dailyBars":["VOO"],"statuses":["*"]} +< [{"T":"subscription","trades":["AAPL"],"quotes":["AMD","CLDR"],"bars":["*"],"updatedBars":[],"dailyBars":["VOO"],"statuses":["*"],"lulds":[],"corrections":["AAPL"],"cancelErrors":["AAPL"]}] +< [{"T":"q","S":"AMD","bx":"K","bp":91.95,"bs":2,"ax":"Q","ap":91.98,"as":1,"c":["R"],"z":"C","t":"2023-04-06T11:54:21.670905508Z"}] +< [{"T":"t","S":"AAPL","i":628,"x":"K","p":162.92,"s":3,"c":["@","F","T","I"],"z":"C","t":"2023-04-06T11:54:26.838232225Z"},{"T":"t","S":"AAPL","i":75,"x":"Z","p":162.92,"s":3,"c":["@","F","T","I"],"z":"C","t":"2023-04-06T11:54:26.838562809Z"},{"T":"t","S":"AAPL","i":1465,"x":"P","p":162.91,"s":71,"c":["@","F","T","I"],"z":"C","t":"2023-04-06T11:54:26.83915973Z"}] +< [{"T":"q","S":"AMD","bx":"P","bp":91.9,"bs":1,"ax":"Q","ap":91.98,"as":1,"c":["R"],"z":"C","t":"2023-04-06T11:54:27.924933876Z"}] +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/registering-your-app.md b/DesktopBot/Documents/Alpaca/registering-your-app.md new file mode 100644 index 0000000..e0a19cc --- /dev/null +++ b/DesktopBot/Documents/Alpaca/registering-your-app.md @@ -0,0 +1,37 @@ +# Registering Your App + +
+ +Before integrating with Alpaca, you’ll need to create a new OAuth application from your Alpaca Connect Apps page. + +You can access the [Alpaca Connect](https://app.alpaca.markets/connect) page through your dashboard after logging into your Alpaca brokerage account + +### 1. Visit [Alpaca Connect](https://app.alpaca.markets/connect) on the Dashboard + +![Alpaca Connect](https://files.readme.io/224a3dc-20230627_connect.png) + +### 2. Navigate to My Developed Apps Tab + +
+ +### 3. Click on Submit Your App and Fill in the Details + +Please complete the Alpaca Connect Application for our team to review. + +![](https://files.readme.io/01492f3c44bd644cb3d1eb075f17a4228a5d44abc0573c8ab808763c8d8f5d21-image.png) + +
+ +### 3. Obtain Your Credentials + +Once you add your relevant information and create the app, you will receive your Client ID and Client Secret. Please be sure to safely store this. + +![](https://files.readme.io/7db8c14-Screenshot_2023-05-09_at_13.45.51.png) + +
+ +### 4. Application Review + +Once your application has been submitted, our team will follow up with you shortly via email to complete the remaining process. + +
\ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/regulatory-fees.md b/DesktopBot/Documents/Alpaca/regulatory-fees.md new file mode 100644 index 0000000..5e071a7 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/regulatory-fees.md @@ -0,0 +1,35 @@ +# Regulatory Fees + +## FEE types and effective rates + +The following FEEs are applied to options trades. + +| Type | When | Charged By | +| :----------------------------- | :---------------------------------- | :---------------- | +| Trading Activity Fee (TAF) | Sells only | FINRA | +| Options Regulatory Fee (ORF) | Buys and sells | Options Exchanges | +| Options Clearing Corporation | Buys and sells up to 2750 contracts | OCC | +| Consolidated Audit Trail (CAT) | Buys and sells | FINRA-CAT | + +The following FEEs are applied to equities. + +| Type | When | Charged By | +| :----------------------------- | :------------- | :--------- | +| Trading Activity Fee (TAF) | Sells only | FINRA | +| Consolidated Audit Trail (CAT) | Buys and sells | FINRA-CAT | + +For our current effective rates, please refer to our brokerage fee schedule available here: +[https://alpaca.markets/disclosures](https://alpaca.markets/disclosures) + +## How Fees are charged and reflected in account balances + +1. Alpaca's trading system keeps track of the accrued FEE amounts intraday and deducts the pending amounts from account balances. +2. At EOD, we charge each account the fees for that trading day +3. We round up total fees based on the currency’s precision to the nearest decimal place. + For example, USD has a precision of 0.01. If the total fee is calculated as 0.00083, it will be rounded up to 0.01 (i.e., 1 penny). + +## Additional resources + +* [https://www.finra.org/rules-guidance/rulebooks/industry/trading-activity-fee](https://www.finra.org/rules-guidance/rulebooks/industry/trading-activity-fee) +* [https://www.catnmsplan.com/](https://www.catnmsplan.com/) +* [https://www.sec.gov/newsroom/press-releases/2024-47](https://www.sec.gov/newsroom/press-releases/2024-47) \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/sdks-and-tools.md b/DesktopBot/Documents/Alpaca/sdks-and-tools.md new file mode 100644 index 0000000..7ea9567 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/sdks-and-tools.md @@ -0,0 +1,34 @@ +# SDKs and Tools + +# Official Client SDKs + +Alpaca provides and supports the following open-source SDKs in a number of languages. You can leverage these libraries to easily access our API in your own application code or your trading scripts. + +* **Python**: [alpaca-py](https://alpaca.markets/sdks/python/) / [PyPI](https://pypi.org/project/alpaca-py/) +* **.NET/C#**: [alpaca-trade-api-csharp](https://github.com/alpacahq/alpaca-trade-api-csharp/) / [NuGet](https://www.nuget.org/packages/Alpaca.Markets/) +* **Node**: [alpaca-trade-api-js](https://github.com/alpacahq/alpaca-trade-api-js/) / [npm](https://www.npmjs.com/package/@alpacahq/alpaca-trade-api) +* **Go**: [alpaca-trade-api-go](https://github.com/alpacahq/alpaca-trade-api-go/) +* **Python (legacy)**: [alpaca-trade-api-python](https://github.com/alpacahq/alpaca-trade-api-python/) / [PyPI](https://pypi.org/project/alpaca-trade-api/) + +# Alpaca-py (Python SDK) + +[**Alpaca-py**](https://alpaca.markets/sdks/python/getting_started.html) provides an interface for interacting with the API products Alpaca offers. These API products are provided as various REST, WebSocket and SSE endpoints that allow you to do everything from streaming market data to creating your own trading apps. Here are some things you can do with Alpaca-py: + +* [**Market Data API**](https://alpaca.markets/sdks/python/api_reference/data_api.html): Access live and historical market data for 5000+ stocks and 20+ crypto. +* [**Trading API**](https://alpaca.markets/sdks/python/api_reference/trading_api.html): Trade stock and crypto with lightning fast execution speeds. +* [**Broker API & Connect**](https://alpaca.markets/sdks/python/api_reference/broker_api.html): Build investment apps - from robo-advisors to brokerages. + +# Community-Made SDKs + +In addition to the SDKs directly supported by Alpaca, individual members of our community have created and contributed their own wrappers for these other languages. We are providing these links as a courtesy to the community and to our users who are looking for the API wrapper in other languages or variants. Please be sure to carefully review any code you use to access our financial trading API and/or trust your account credentials to. + +Made your own wrapper for a language not listed? Join our community Slack and let us know about it! + +* **C++**: [alpaca-trade-api-cpp](https://github.com/marpaia/alpaca-trade-api-cpp) +* **Java**: [alpaca-java](https://github.com/Petersoj/alpaca-java) +* **Node.js** (TypeScript): [alpaca-ts](https://github.com/alpacahq/alpaca-ts) +* **R**: [alpaca-for-r](https://github.com/yogat3ch/AlpacaforR) +* **Rust**: [apca](https://github.com/d-e-s-o/apca) (SDK) & [apcacli](https://github.com/d-e-s-o/apcacli) (CLI) +* **Scala**: [Alpaca Scala](https://github.com/cynance/alpaca-scala) +* **Ruby**: [alpaca-trade-api](https://github.com/ccjr/alpaca-trade-api) +* **Elixir**: [alpaca\_elixir](https://github.com/jrusso1020/alpaca_elixir) \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/streaming-market-data.md b/DesktopBot/Documents/Alpaca/streaming-market-data.md new file mode 100644 index 0000000..a1e7919 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/streaming-market-data.md @@ -0,0 +1,378 @@ +# WebSocket Stream + +This API provides a [WebSocket](https://en.wikipedia.org/wiki/WebSocket) stream for real-time market data. This allows you to receive the most up-to-date market information, which can be used to power your trading strategies. + +The WebSocket stream provides real-time updates of the following market data: + +* [Stocks](https://docs.alpaca.markets/docs/real-time-stock-pricing-data) +* [Crypto](https://docs.alpaca.markets/docs/real-time-crypto-pricing-data) +* [Options](https://docs.alpaca.markets/docs/real-time-option-data) +* [News](https://docs.alpaca.markets/docs/streaming-real-time-news) + +# Steps to use the stream + +To use the WebSocket stream follow these steps: + +## Connection + +To establish a connection use the stream URL depending on the data you'd like to consume. The general schema of the URL is + +``` +wss://stream.data.alpaca.markets/{version}/{feed} +``` + +Sandbox URL: + +``` +wss://stream.data.sandbox.alpaca.markets/{version}/{feed} +``` + +Any attempt to access a data feed not available for your subscription will result in an error during authentication. + +> 📘 Test stream +> +> We provide a test stream that is available all the time, even outside market hours, on this URL: +> +> ``` +> wss://stream.data.alpaca.markets/v2/test +> ``` +> +> Use the symbol "FAKEPACA" when trying out this test stream. + +Upon successfully connecting, you will receive the welcome message: + +```json +[{"T":"success","msg":"connected"}] +``` + +> 🚧 Connection limit +> +> The number of connections to a single endpoint from a user is limited based on the user's subscription, but in most subscriptions (including Algo Trader Plus) this limit is 1. If you try to open a second connection, you'll get this error: +> +> ```json +> [{"T":"error","code":406,"msg":"connection limit exceeded"}] +> ``` + +## Authentication + +You need to authenticate yourself using your credentials. This can be done multiple ways + +### For the Trading API, Authenticate with HTTP headers + +You can set the same headers used for the historical market data and trading endpoints: + +* `APCA-API-KEY-ID` +* `APCA-API-SECRET-KEY` + +Here's an example using a WebSocket client called [websocat](https://github.com/vi/websocat): + +``` +$ websocat wss://stream.data.alpaca.markets/v2/test \ + -H="APCA-API-KEY-ID: {KEY_ID}" -H="APCA-API-SECRET-KEY: {SECRET}" +``` + +### For the Broker API, Authenticate with Basic Authentication + +You can use the same Basic Authentication header used for the historical market data and trading endpoints: + +* `Authorization` = `base64encode({KEY}:{SECRET})` + +**Note:** `base64encode({KEY_ID}:{SECRET})` is the base64 encoding of the `{KEY}:{SECRET}` string. + +### For both Trading & Broker API, Authenticate with a message + +Alternatively, for both the trading & broker API, you can authenticate with a message after connection: + +```json +{"action": "auth", "key": "{KEY_ID}", "secret": "{SECRET}"} +``` + +Keep in mind though, that you only have 10 seconds to do so after connecting. + +If you provided correct credentials you will receive another success message: + +```json +[{"T":"success","msg":"authenticated"}] +``` + +### For OAuth applications, Authenticate with a message + +For an OAuth integration, authenticate with a message and use “oauth” as your key, and user token as the “secret”. (do NOT use your Client Secret) + +```json json +{"action": "auth", "key": "oauth", "secret": "{TOKEN}"} +``` + +Keep in mind that most users can have only 1 active stream connection. If that connection is used by another 3rd party application, you will receive an error: 406 and “connection limit exceeded” message. Similarly, if the user wants to access their stream from an API or another 3rd party application, they will also receive the same error message. + +## Subscription + +Congratulations, you are ready to receive real-time market data! + +You can send one or more subscription messages. The general format of the subscribe message is this: + +```json +{ + "action": "subscribe", + "": [""], + "": ["",""], + "": ["*"] +} +``` + +You can subscribe to a particular symbol or to every symbol using the `*` wildcard. A subscribe message should contain what subscription you want to add to your current subscriptions in your session so you don’t have to send what you’re already subscribed to. + +For example in the test stream, you can send this message: + +```json +{"action":"subscribe","trades":["FAKEPACA"]} +``` + +The available channels are described for each streaming endpoints separately. + +Much like subscribe you can also send an unsubscribe message that subtracts the list of subscriptions specified from your current set of subscriptions. + +```json +{"action":"unsubscribe","quotes":["FAKEPACA"]} +``` + +After subscribing or unsubscribing you will receive a message that describes your current list of subscriptions. + +```json +[{"T":"subscription","trades":["AAPL"],"quotes":["AMD","CLDR"],"bars":["*"],"updatedBars":[],"dailyBars":["VOO"],"statuses":["*"],"lulds":[],"corrections":["AAPL"],"cancelErrors":["AAPL"]}] +``` + +You will always receive your entire list of subscriptions, as illustrated by the sample communication excerpt below: + +```json +> {"action": "subscribe", "trades": ["AAPL"], "quotes": ["AMD", "CLDR"], "bars": ["*"]} +< [{"T":"subscription","trades":["AAPL"],"quotes":["AMD","CLDR"],"bars":["*"],"updatedBars":[],"dailyBars":[],"statuses":[],"lulds":[],"corrections":["AAPL"],"cancelErrors":["AAPL"]}] +... +> {"action": "unsubscribe", "bars": ["*"]} +< [{"T":"subscription","trades":["AAPL"],"quotes":["AMD","CLDR"],"bars":[],"updatedBars":[],"dailyBars":[],"statuses":[],"lulds":[],"corrections":["AAPL"],"cancelErrors":["AAPL"]}] +``` + +# Messages + +## Format + +Every message you receive from the server will be in the format: + +```json json +[{"T": "{message_type}", {contents}},...] +``` + +Control messages (i.e. where `T` is `error`, `success` or `subscription`) always arrive in arrays of size one to make their processing easier. + +Data points however may arrive in arrays that have a length that is greater than one. This is to facilitate clients whose connection is not fast enough to handle data points sent one by one. Our server buffers the outgoing messages but slow clients may get disconnected if their buffer becomes full. + +## Content type + +You can use the `Content-Type` header to switch between text and binary message [data frame](https://datatracker.ietf.org/doc/html/rfc6455#section-5.6): + +* `Content-Type: application/json` +* `Content-Type: application/msgpack` + +## Encoding and Compression + +Messages over the websocket are in encoded as clear text. + +To reduce bandwidth requirements we have implemented compression as per [RFC-7692](https://datatracker.ietf.org/doc/html/rfc7692). [Our SDKs](https://docs.alpaca.markets/us/docs/sdks-and-tools) handle this for you so in most cases you won’t have to implement anything yourself. + +## Errors + +You may receive an error during your session. Below are the general errors you may run into. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Code + + Message + + Description +
+ 400 + + invalid syntax + + The message you sent to the server did not follow the specification.\ + :warning: This can also be sent if the symbol in your subscription message is in invalid format. +
+ 401 + + not authenticated + + You have attempted to subscribe or unsubscribe before [authentication](https://docs.alpaca.markets/docs/streaming-market-data#authentication). +
+ 402 + + auth failed + + You have provided invalid authentication credentials. +
+ 403 + + already authenticated + + You have already successfully authenticated during your current session. +
+ 404 + + auth timeout + + You failed to successfully authenticate after connecting. You only have a few seconds to authenticate after connecting. +
+ 405 + + symbol limit exceeded + + The symbol subscription request you sent would put you over the limit set by your subscription package. If this happens your symbol subscriptions are the same as they were before you sent the request that failed. +
+ 406 + + connection limit exceeded + + You already have the number of sessions allowed by your subscription. +
+ 407 + + slow client + + You may receive this if you are too slow to process the messages sent by the server. Please note that this is not guaranteed to arrive before you are disconnected to avoid keeping slow connections active forever. +
+ 409 + + insufficient subscription + + You have attempted to access a data source not available in your subscription package. +
+ 410 + + invalid subscribe action for this feed + + You tried to subscribe to channels not available in the stream, for example to `bars` in the [option stream](https://docs.alpaca.markets/docs/real-time-option-data) or to `trades` in the [news stream](https://docs.alpaca.markets/docs/streaming-real-time-news). +
+ 500 + + internal error + + An unexpected error occurred on our end. Please let us know if this happens. +
+ +Beside these there can be some endpoint specific errors, for example in the [option stream](https://docs.alpaca.markets/docs/real-time-option-data#errors). + +# Example + +Here's a complete example of the test stream using the [wscat](https://github.com/websockets/wscat) cli tool: + +```json +$ wscat -c wss://stream.data.alpaca.markets/v2/test +Connected (press CTRL+C to quit) +< [{"T":"success","msg":"connected"}] +> {"action":"auth","key":"","secret":""} +< [{"T":"success","msg":"authenticated"}] +> {"action":"subscribe","bars":["FAKEPACA"],"quotes":["FAKEPACA"]} +< [{"T":"subscription","trades":[],"quotes":["FAKEPACA"],"bars":["FAKEPACA"]}] +< [{"T":"q","S":"FAKEPACA","bx":"O","bp":133.85,"bs":4,"ax":"R","ap":135.77,"as":5,"c":["R"],"z":"A","t":"2024-07-24T07:56:53.639713735Z"}] +< [{"T":"q","S":"FAKEPACA","bx":"O","bp":133.85,"bs":4,"ax":"R","ap":135.77,"as":5,"c":["R"],"z":"A","t":"2024-07-24T07:56:58.641207127Z"}] +< [{"T":"b","S":"FAKEPACA","o":132.65,"h":136,"l":132.12,"c":134.65,"v":205,"t":"2024-07-24T07:56:00Z","n":16,"vw":133.7}] +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/streaming-real-time-news.md b/DesktopBot/Documents/Alpaca/streaming-real-time-news.md new file mode 100644 index 0000000..940ddc2 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/streaming-real-time-news.md @@ -0,0 +1,67 @@ +# Real-time News + +This API provides stock market news on a websocket stream. You can find the general description of the real-time WebSocket Stream [here](https://docs.alpaca.markets/docs/streaming-market-data). This page focuses on the news stream. + +# URL + +The URL for the news stream is + +``` +wss://stream.data.alpaca.markets/v1beta1/news +``` + +Sandbox URL: + +``` +wss://stream.data.sandbox.alpaca.markets/v1beta1/news +``` + +# Channels + +## News + +### Schema + +| Attribute | Type | Notes | +| :---------- | :-------------- | :------------------------------------------------------------------------------------------- | +| T | string | Type of message (“n” for news) | +| id | int | News article ID | +| headline | string | Headline or title of the article | +| summary | string | Summary text for article (may be first sentence of content) | +| author | string | Original author of news article | +| created\_at | string | Date article was created in [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) format | +| updated\_at | string | Date article was updated in [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339) format | +| content | string | Content of news article (might contain HTML) | +| url | string | URL of article (if applicable) | +| symbols | array`` | List of related or mentioned symbols | +| source | string | Source where the news originated from (e.g. Benzinga) | + +### Example + +```json +{ + "T": "n", + "id": 24918784, + "headline": "Corsair Reports Purchase Of Majority Ownership In iDisplay, No Terms Disclosed", + "summary": "Corsair Gaming, Inc. (NASDAQ:CRSR) (“Corsair”), a leading global provider and innovator of high-performance gear for gamers and content creators, today announced that it acquired a 51% stake in iDisplay", + "author": "Benzinga Newsdesk", + "created_at": "2022-01-05T22:00:37Z", + "updated_at": "2022-01-05T22:00:38Z", + "url": "https://www.benzinga.com/m-a/22/01/24918784/corsair-reports-purchase-of-majority-ownership-in-idisplay-no-terms-disclosed", + "content": "\u003cp\u003eCorsair Gaming, Inc. (NASDAQ:\u003ca class=\"ticker\" href=\"https://www.benzinga.com/stock/CRSR#NASDAQ\"\u003eCRSR\u003c/a\u003e) (\u0026ldquo;Corsair\u0026rdquo;), a leading global ...", + "symbols": ["CRSR"], + "source": "benzinga" +} +``` + +# Example + +```json +$ websocat -H="APCA-API-KEY-ID: ${APCA_API_KEY_ID}" -H="APCA-API-SECRET-KEY: ${APCA_API_SECRET_KEY}" \ + "${APCA_API_STREAM_URL}/v1beta1/news" +[{"T":"success","msg":"connected"}] +[{"T":"success","msg":"authenticated"}] +{"action":"subscribe","news":["*"]} +[{"T":"subscription","news":["*"]}] +[{"T":"n","id":40892639,"headline":"VinFast Officially Launches VF 3 Electric Vehicle In The Philippines","summary":"VinFast Auto has officially opened pre-orders for the VF 3 in the Philippines. From September 19 to 30, early customers who reserve the VF 3 will enjoy several attractive incentives and privileges, including a","author":"Benzinga Newsdesk","created_at":"2024-09-17T09:02:44Z","updated_at":"2024-09-17T09:02:45Z","url":"https://www.benzinga.com/news/24/09/40892639/vinfast-officially-launches-vf-3-electric-vehicle-in-the-philippines","content":"\u003cp\u003eVinFast Auto has officially opened pre-orders for the VF 3 in the Philippines.\u003c/p\u003e\u003cp\u003e\u0026nbsp;\u003c/p\u003e\u003cp\u003eFrom September 19 to 30, early customers who reserve the VF 3 will enjoy several attractive incentives and privileges, including a special price of 605,000 pesos (battery subscription) or 705,000 pesos (battery included). After this period, the prices will revert to the MSRP of 645,000 pesos (battery subscription) and 745,000 pesos (battery included).\u003cbr\u003e\u003cbr\u003eAdditionally, early VF 3 customers will have the privilege of choosing from nine striking exterior paint colors, including four base colors and five premium options, free of charge. Premium paint colors will cost an additional 20,000 pesos after this period.\u003cbr\u003e\u003cbr\u003eMoreover, from September 19 to 30, for only 40,000 pesos, early customers can customize their car's paint beyond the nine available colors. This will be the only time VinFast offers this exclusive privilege for the VF 3.\u003cbr\u003e\u003cbr\u003eVinFast is accepting deposits of 5,000 pesos through its official website or at authorized dealerships (refundable under VinFast's terms).\u003cbr\u003e\u0026nbsp;\u003c/p\u003e","symbols":["VFS"],"source":"benzinga"}] +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/the-intraday-margin-rule.md b/DesktopBot/Documents/Alpaca/the-intraday-margin-rule.md new file mode 100644 index 0000000..fa6d855 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/the-intraday-margin-rule.md @@ -0,0 +1,63 @@ +# The Intraday Margin Rule + +
+ +### **Q: What is the Intraday Margin Rule?** + +The Intraday Margin Rule is a FINRA-mandated framework that replaces the legacy Pattern Day Trader (PDT) system. The rule shifts the regulatory focus from **trade frequency** (counting transactions) to **real-time risk exposure** (measuring actual financial risk during the trading day). + +### **Q: Has the Pattern Day Trader (PDT) designation been removed?** + +Yes. The definition of a "pattern day trader" has been eliminated. Brokers are no longer required to track "round-trip" counts or restrict accounts that execute more than three day trades in a five-day period. + +### **Q: Is the $25,000 minimum equity requirement still in effect?** + +No. The specific $25,000 equity minimum required to day trade has been removed. Standard Regulation T requirements still apply; for example, margin-enabled accounts typically require a minimum of $2,000 in equity to maintain intraday debits or short positions. + +## **Rule Comparison** + +*** + +| Feature | Legacy PDT Rule | New Intraday Margin Rule | +| :--------------------- | :--------------------------------------- | :-------------------------------------------------- | +| **Trade Limits** | Max 3 day trades per 5 days (under $25k) | **Unlimited day trades** | +| **Buying Power** | Fixed (based on previous day's close) | **Dynamic** (updates in real-time) | +| **Equity Calculation** | Limited to brokerage assets | Includes **FDIC bank sweeps** & same-day deposits | +| **Profit Treatment** | Intraday gains ignored for buying power | **Intraday P\&L** factored into margin availability | + +## **Risk Measurement & Definitions** + +### --- + +**Q: How is intraday risk calculated under the new rule?** + +Risk is monitored via three primary metrics: + +* **Intraday Margin Level (IML):** A running balance of your account’s maintenance margin excess or deficit, updated continuously based on transaction activity. +* **IML-Reducing Transaction:** Any action that lowers your IML, such as purchasing securities, shorting options, or withdrawing funds. +* **Intraday Margin Deficit (IMD):** The peak negative margin deficiency recorded immediately following an IML-reducing transaction at any point during the trading day. + +## **Deficits and Restrictions** + +### --- + +**Q: What happens if an account incurs an Intraday Margin Deficit (IMD)?** + +If an IMD occurs, a margin call will be issued. This call must be satisfied within **two business days**. If an IMD remains unmet by the close of the fifth business day, the account will be subject to a **90-day freeze**, restricting the account from increasing short positions or creating new debit balances. + +### **Q: Are there "De Minimis" exceptions to the new rule?** + +Yes. An intraday margin call is generally not triggered if the unmet deficit is less than **$1,000 or 5% of account equity** (whichever is lower) + +## **Key Benefits for Traders** + +- *** + + **Real-Time Capital Access:** Unlike the old rule, which ignored intraday gains, the new rule allows you to use realized and unrealized intraday profits to increase your margin availability immediately. +- **Collateral Flexibility:** You can now count FDIC-insured bank sweeps and net deposits made on the same day toward your equity, providing a more accurate and favorable picture of your account's health. +- **Reduced Administrative Friction:** The removal of "counting" eliminates the risk of accidental account flags and the 90-day "PDT lock" that previously frustrated active traders. +- **Capital Efficiency:** Traders with less than $25,000 are no longer sidelined; capital can be reallocated as many times as necessary, provided the risk (IML) remains within manageable bounds. + +**Margin trading involves significant risk and is not suitable for all investors.** Before considering a margin loan, it is crucial that you carefully consider how borrowing fits with your investment objectives and risk tolerance. + +*When trading on margin, you assume higher market risk, and potential losses can exceed the collateral value in your account. Alpaca may sell any securities in your account, without prior notice, to satisfy a margin call. Alpaca may also change its “house” maintenance margin requirements at any time without advance written notice. You are not entitled to an extension of time on a margin call. Please review the Firm’s [Margin Disclosure Statement](https://files.alpaca.markets/disclosures/library/MarginDiscStmt.pdf) before investing.* \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/trading-api.md b/DesktopBot/Documents/Alpaca/trading-api.md new file mode 100644 index 0000000..fcbc39e --- /dev/null +++ b/DesktopBot/Documents/Alpaca/trading-api.md @@ -0,0 +1,29 @@ +# About Trading API + +Trade stocks & crypto with Alpaca’s easy to use Trading API. Up to 4X intraday & 2X overnight buying power. Short selling. Advanced order types. All packaged and delivered through our API. + +# Paper Trading + +Paper trading is free and available to all Alpaca users. Paper trading is a real-time simulation environment where you can test your code. You can reset and test your algorithm as much as you want using free, real-time market data. For more click [here](/docs/paper-trading). + +# Account Plans + +Anyone globally can create an paper only account. All you need to do is sign up with your email address. Alpaca also offers live brokerage accounts (with real money) for individuals and businesses plus crypto accounts. For more click [here](/docs/account-plans). + +# Crypto Trading + +Alpaca offers crypto trading through our API and the Alpaca web dashboard! Trade all day, seven days a week, as frequently as you’d like. For more click [here](/docs/crypto-trading). + +# Understand Orders + +Our Trading API supports different order types such as market, limit and stop orders plus more complex ones. For more click [here](/docs/orders-at-alpaca). + +# Fractional Trading + +Fractional shares are fractions of a whole share, meaning that you don’t need to buy a whole share to own a portion of a company. You can now buy as little as $1 worth of shares for over 2,000 US equities. For more click [here](/docs/fractional-trading). + +We only allow market orders for fractional trading at the moment. + +# User Protections + +We have enabled several types of protections to enhance your trading experience. For more click [here](/docs/user-protection). \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/understanding-finras-new-intraday-margin-rule-and-the-end-of-pdt.md b/DesktopBot/Documents/Alpaca/understanding-finras-new-intraday-margin-rule-and-the-end-of-pdt.md new file mode 100644 index 0000000..ffcbc7a --- /dev/null +++ b/DesktopBot/Documents/Alpaca/understanding-finras-new-intraday-margin-rule-and-the-end-of-pdt.md @@ -0,0 +1,82 @@ +# Understanding FINRA’s New Intraday Margin Rule and the End of PDT + +
+ +### **1. Introduction to the Regulatory Shift** + +The Financial Industry Regulatory Authority (FINRA) has approved a comprehensive overhaul of Rule 4210, marking a fundamental pivot in the governance of active retail trading. As part of the "FINRA Forward" initiative to modernize oversight and adapt to technological advancements, the legacy Pattern Day Trading (PDT) framework is being replaced with a modern "intraday margin" system. + +The primary objective of this transition is to move away from arbitrary trade-counting metrics toward a risk-sensitive system that accurately reflects intraday market exposure. By eliminating the $25,000 equity threshold and the "four trades in five days" rule, FINRA aims to reduce unnecessary financial burdens on smaller investors while maintaining robust risk oversight. + +### **2. Rationale for Reform: The Technical Underpinnings** + +The shift to an intraday margin framework is necessitated by structural changes in the financial landscape and academic evidence regarding intraday volatility: + +* **Obsolescence of the $25,000 Threshold:** Originally designed to protect investors from high commission costs, the $25,000 requirement is viewed by industry participants—including firms like Charles Schwab and Morgan Stanley—as anachronistic. In the era of zero-commission trading, this barrier often forces retail investors to hold positions overnight to avoid PDT status, inadvertently increasing their risk. +* **Volatility and High-Frequency Risk:** Academic research (Cotter & Longin) demonstrates that daily closing prices often neglect the dynamics investors face during the trading day. High-frequency data suggests that risk levels during intraday volatility can be 50% higher than what daily data implies. The new rule addresses this by focusing on real-time maintenance margin excess. +* **Dynamic Price Discovery:** As noted by David Russell (TradeStation), active trading provides essential liquidity and enables "dynamic price discovery in lesser-known or emerging securities." The new rule acknowledges that "one man’s speculation is another man’s liquidity." +* **Capturing Modern Risks (0DTE Options):** The legacy PDT rule failed to effectively capture the risks of "Zero Day to Expiration" (0DTE) options. Under the new framework, the expiration of a long option is explicitly classified as an IML-reducing transaction, ensuring these instruments are properly margined. PM + +### **3. Key Definitions: Foundations of Intraday Margin** + +The new regulatory framework relies on three technical definitions to determine compliance: + +* **Intraday Margin Level (IML):** The running balance of an account’s maintenance margin excess or deficit during the day, defined by a dual-pronged approach: + * (A) The amount of cash a customer could withdraw while maintaining the required margin; or + * (B) The amount of additional cash (expressed as a negative number) the customer would need to deposit to satisfy maintenance margin requirements. +* **IML-Reducing Transaction:** Any transaction that decreases the account’s IML. This includes security purchases, short sales of options, the expiration of long options, and the withdrawal of cash or securities. +* **Intraday Margin Deficit (IMD):** The largest negative IML recorded during the trading day following an IML-reducing transaction. Crucially, an IMD is **not** triggered by mark-to-market (market-related) losses; if a negative IML results solely from market movement rather than customer-initiated activity, it does not necessitate an IMD satisfaction. + +### **4. Comparison Table: Legacy PDT Rule vs. New Intraday Margin Rule** + +| *Feature* | *Legacy PDT Rule* | *New Intraday Margin Rule* | +| :----------------------------- | :------------------------------------------------- | :----------------------------------------------------------- | +| **Minimum Equity Requirement** | $25,000 for Pattern Day Traders | Eliminated (Standard $2,000 Reg T/Rule 4210 Minimum applies) | +| **Trade Counting** | 4 trades in 5 days (unless \le 6% of total trades) | Eliminated; focus shifted to real-time risk exposure | +| **Buying Power Calculation** | Based on previous day’s close (DTBP) | Real-time or retrospective IML calculation | +| **Trading into a Margin Call** | No (for PDTs) | Yes | +| **Hold on Deposited Funds** | 2-day hold required | None | +| **Intraday P\&L Inclusion** | Excluded | Included (by recalculating the account) | +| **Guaranteed Accounts** | Prohibited for PDT requirements | Allowed | +| **0DTE Option Margin** | No specific intraday requirement | Required; treated as IML-reducing activity | +| **Account Restrictions** | 90-day "cash only" freeze for unmet calls | 90-day freeze on *new margin debits* and *short positions* | +| **Treatment of Bank Sweeps** | Excluded from buying power | Included (requires written firm policy) | + +### **5. Mechanics of Satisfaction and Compliance** + +Firms are empowered to use real-time monitoring to block trades that would create IMDs, though retrospective end-of-day calculation is permitted. + +**Satisfaction of Deficits** A customer satisfies an IMD through net deposits of cash/securities or activities that increase the account’s IML (e.g., liquidating positions to release maintenance margin). + +**Timeline and Capital Charge Mitigation** + +* **Prompt Satisfaction:** IMDs must be satisfied "as promptly as possible." +* **5-Day Rule:** If an IMD is not satisfied within five business days, the firm must take a capital charge starting on the 6th business day. +* **Expiration:** The capital charge is capped at 10 days because the deficit itself expires for capital purposes after 15 business days. + +**The "Freeze" Protocol** If a customer fails to meet an IMD within five days and demonstrates a practice of such failures, a 90-day restriction is triggered: + +* **Scope:** The customer is prohibited from creating **new debits** or **short positions** (closing existing short positions is permitted). +* **De Minimis Exceptions:** Restrictions are waived if the IMD is less than $1,000 or less than 5% of account equity. + +### **6. Portfolio Margin (PM) and Special Cases** + +The intraday margin requirements are modified for Portfolio Margin accounts to reflect their unique risk profiles: + +* **Equity Thresholds:** The intraday margin requirement does **not** apply to PM accounts with **$5 million or more in equity**, provided the firm has the technological capability to monitor intraday risk. +* **Standard PM Accounts:** PM accounts with less than $5 million in equity must maintain margin for intraday risk that is substantially similar to the margin required for end-of-day positions. +* **Bank Sweep Integration:** To mitigate unnecessary deficiencies, firms may include FDIC-insured bank sweep balances as credit balances, provided they maintain a **written policy** for this treatment. +* **"As of" Transactions:** Firms may allocate "as of" trades to the time of processing or the actual occurrence time, provided this procedure is designed for accuracy and not for the sole purpose of reducing margin requirements. + +### **7. Implementation Timeline** + +FINRA recognizes the need for firms to update technological infrastructure and internal risk oversight programs. + +* **Effective Date & Interim Period:** Following SEC approval, a 12-month interim transition period will commence. +* **Transition Strategy:** During this 12-month window, member firms may apply either the legacy PDT rules or the new intraday margin rules (by account or across the firm) as they transition their systems. + +**Margin trading involves significant risk and is not suitable for all investors.** Before considering a margin loan, it is crucial that you carefully consider how borrowing fits with your investment objectives and risk tolerance. + +*When trading on margin, you assume higher market risk, and potential losses can exceed the collateral value in your account. Alpaca may sell any securities in your account, without prior notice, to satisfy a margin call. Alpaca may also change its “house” maintenance margin requirements at any time without advance written notice. You are not entitled to an extension of time on a margin call. Please review the Firm’s [Margin Disclosure Statement](https://files.alpaca.markets/disclosures/library/MarginDiscStmt.pdf) before investing.* + +
\ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/understanding-the-new-intraday-margin-rule.md b/DesktopBot/Documents/Alpaca/understanding-the-new-intraday-margin-rule.md new file mode 100644 index 0000000..bef0da3 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/understanding-the-new-intraday-margin-rule.md @@ -0,0 +1,31 @@ +# Intraday Margin Rule for Non-Leverage Margin Accounts + +### **Q: What is the Intraday Margin Rule?** + +The Intraday Margin Rule is a regulatory update from FINRA designed to replace the legacy Pattern Day Trader (PDT) framework. While the old rules focused on the **frequency** of trades, the new rule focuses on **real-time risk exposure**. It measures the financial risk of positions held during the day that are closed before the market settles. + +### **Q: Is the Pattern Day Trader (PDT) rule still in effect?** + +No. The "pattern day trader" designation has been abolished. Brokers are no longer required to monitor or flag accounts that execute four or more day trades within a five-business-day window. + +### **Q: Does the new rule limit the number of trades I can make?** + +No. The previous "4-trade limit" has been eliminated. You may enter and exit positions as frequently as you choose, provided your account has sufficient equity to cover the intraday risk requirements set by your broker. + +### **Q: Is the $25,000 minimum equity requirement still required?** + +No. The specific $25,000 minimum previously required to maintain "Pattern Day Trader" status has been removed. However, individual firms may still maintain their own "house" requirements for risk management. + +### **Q: I only trade with my own cash. Why does this matter to me?** + +Even without using leverage, accounts enabled for margin trading are often utilized to avoid the delays associated with T+1 settlement. The new rule allows you to benefit from the speed of a margin account without being constrained by the intricate calculation rules or trading limitations that characterized the former Pattern Day Trader (PDT) system. Consequently, PDT restrictions are no longer necessary, and clients will no longer encounter trade rejections stemming from PDT protection and restrictions. + +### **Key Advantages** + +* **Capital Efficiency:** Traders can pivot strategies or re-enter positions instantly without worrying about "locking" their account for the week due to trade frequency. +* **Elimination of "Day Trading Calls":** Since the PDT status is gone, traders are no longer at risk of account freezes or "equity maintenance calls" triggered simply by the number of trades made. +* **Focus on Strategy, Not Math:** You can focus entirely on market movement rather than keeping a manual tally of "round-trip" trades to avoid a 90-day account restriction. + +**Margin trading involves significant risk and is not suitable for all investors.** Before considering a margin loan, it is crucial that you carefully consider how borrowing fits with your investment objectives and risk tolerance. + +*When trading on margin, you assume higher market risk, and potential losses can exceed the collateral value in your account. Alpaca may sell any securities in your account, without prior notice, to satisfy a margin call. Alpaca may also change its “house” maintenance margin requirements at any time without advance written notice. You are not entitled to an extension of time on a margin call. Please review the Firm’s [Margin Disclosure Statement](https://files.alpaca.markets/disclosures/library/MarginDiscStmt.pdf) before investing.* \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/user-protection.md b/DesktopBot/Documents/Alpaca/user-protection.md new file mode 100644 index 0000000..0255f11 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/user-protection.md @@ -0,0 +1,220 @@ +# User Protection + +We have enabled several types of protections to enhance your trading experience. + +1. Pattern Day Trader (PDT) Protection +2. Day Trade Margin Call (DTMC) Protection +3. Preventing Wash Trades +4. Limit order price away sanity check + +Please note that these do not apply to crypto trading as cryptocurrencies are not marginable. Pattern Day Trading rule does not apply to crypto trading either. Preventing Wash Trades does apply to crypto trading. + +# Pattern Day Trader (PDT) Protection at Alpaca + +In order to prevent Alpaca Brokerage Account customers from unintentionally being designated as a Pattern Day Trader (PDT), the Alpaca Trading platform checks the PDT rule condition every time an order is submitted from a customer. If the order could potentially result in the account being flagged as a PDT, the order is rejected, and API returns error with HTTP status code 403 (Forbidden). + +## The Rule + +A day trade is defined as a round-trip pair of trades within the same day (including extended hours). This is best described as an initial or opening transaction that is subsequently closed later in the same calendar day. For long positions, this would consist of a buy and then sell. For short positions, selling a security short and buying it back to cover the short position on the same day would also be considered a day trade. + +An account is designated as a Pattern Day Trader if it makes four (4) or more day trades within five (5) business days, and the number of day trades represents more than six percent (6%) of the total trades within the same five (5) business days window. Day trades less than this criteria will not flag the account for PDT. + +Cryptocurrency trading is not subject to the PDT rule. As a result, crypto orders are not evaluated by PDT protection logic and round-trip crypto trades on the same day do not contribute to the day trade count. + +Day trades are counted regardless of share quantity or frequency throughout the day. Here are some FINRA-provided examples: + +Example A: + +09:30 Buy 250 ABC\ +09:31 Buy 250 ABC\ +13:00 Sell 500 ABC\ +The customer has executed one day trade. + +Example B:\ +09:30 Buy 100 ABC\ +09:31 Sell 100 ABC\ +09:32 Buy 100 ABC\ +13:00 Sell 100 ABC\ +The customer has executed two day trades. + +Example C:\ +09:30 Buy 500 ABC\ +13:00 Sell 100 ABC\ +13:01 Sell 100 ABC\ +13:03 Sell 300 ABC\ +The customer has executed one day trade. + +Example D:\ +09:30 Buy 250 ABC\ +09:31 Buy 300 ABC\ +13:01 Buy 100 ABC\ +13:02 Sell 150 ABC\ +13:03 Sell 175 ABC\ +The customer has executed one day trade. + +Example E:\ +09:30 Buy 199 ABC\ +09:31 Buy 142 ABC\ +13:00 Sell 1 ABC\ +13:01 Buy 45 ABC\ +13:02 Sell 100 ABC\ +13:03 Sell 200 ABC\ +The customer has executed two day trades. + +Example F:\ +09:30 Buy 200 ABC\ +09:30 Buy 100 XYZ\ +13:00 Sell 100 ABC\ +13:00 Sell 100 XYZ + +The customer has executed two day trades. + +For further information, please visit [Regulatory Notice 21-13 | FINRA.org ](https://www.finra.org/rules-guidance/notices/21-13) + +## Alpaca’s Order Rejection + +Alpaca Trading platform monitors the number of day trades for the account for the past 5 business days and rejects a newly submitted orders on exit of a position if it could potentially result in the account being flagged for PDT. This protection triggers only when the previous day’s closing account equity is less than $25,000 at the time of order submission. + +In addition to the filled orders, the system also takes into consideration pending orders in the account. In this case, regardless of the order of pending orders, a pair of buy and sell orders is counted as a potential day trade. This is because orders that are active (pending) in the marketplace may fill in random orders. Therefore, even if your sell limit order is submitted first (without being filled yet) and another buy order on the same security is submitted later, this buy order will be blocked if your account already has 3 day trades in the last 5 business days. + +
+ +## Pattern Day Trader (PDT) Restriction at Alpaca + +If an account falls below the required **$25,000 securities equity** at the end of the trading day, it will be flagged for not meeting the **Pattern Day Trader (PDT)** requirements. When this happens, the account will be restricted from placing any new day trades on the following business day. Day trading will remain blocked until either: + +**A PDT Restriction lift is granted, or** + +**A One-Time PDT Removal is granted, or** + +**The account equity once again meets the $25,000 minimum requirement by the end of the trading day.** + +To request a **PDT Restriction lift or One-Time PDT Removal**\* in order to place a day trade, please contact our support team at [support@alpaca.markets](mailto:support@alpaca.markets). + +\***Note:** The **One-Time PDT Removal** applies for the life of all accounts at Alpaca. By requesting this, you are removing the PDT flag from your account and agree not to engage in future pattern day trading. +Please also note that once the restriction is lifted and you place a day trade, your account will be limited to **liquidation-only activity for 90 days**, or until the **$25,000 equity requirement** is met. + +
+ +## Paper Trading + +The same protection triggers in your paper trading account. It is advised to test your algorithm with the realistic balance amount you would manage when going live, to make sure your assumption works under this PDT protection as well. + +> For more details of Pattern Day Trader rule, please visit the [FINRA website](https://www.finra.org/investors/investing/investment-products/stocks/day-trading). + +# Day Trade Margin Call (DTMC) Protection at Alpaca + +In order to prevent Alpaca Brokerage Account customers from unintentionally receiving day trading margin calls, Alpaca implements two forms of DTMC protection. + +## The Rule + +Day traders are required to have a minimum of $25,000 OR 25% of the total market value of securities (whichever is higher) maintained in their account. + +The buying power of a pattern day trader is 4x the excess of the maintenance margin from the closing of the previous day. If you exceed this amount, you will receive a day trading margin call. + +## How Alpaca’s DTMC Protection Settings Work + +Users only receive day trading buying power when marked as a pattern day trader. If the user is designated a pattern day trader, the account.multiplier is equal to 4. + +Daytrading buying power cannot increase beyond its start of day value. In other words, closing an overnight position will not add to your daytrading buying power. + +The following scenarios and protections are applicable only for accounts that are designated as pattern day traders. Please check your Account API result for the multiplier field. + +Every trading day, you start with the new `daytrading_buying_power`. This beginning value is calculated as `4 * (last_equity - last_maintenance_margin)`. The last\_equity and last\_maintenance\_margin values can be accessed through Account API. These values are stored from the end of the previous trading day. + +Throughout the day, each time you enter a new position, your `daytrading_buying_power` is reduced by that amount. When you exit that position within the same day, that same amount is credited back, regardless of position’s P/L. + +At the end of the trading day, on close, the maximum exposure of your day trading position is checked. A Day Trade Margin Call (DTMC) is issued the next day if the maximum exposure of day trades exceeded your day trading buying power from the beginning of that day. + +The buying\_power value is the larger of `regt_buying_power` and `daytrading_buying_power`. Since the basic buying power check runs on this buying\_power value, you could be exceeding your `daytrading_buying_power` when you enter the position if `regt_buying_power` is larger than your `daytrading_buying_power` at one point in the day. + +The following is an example scenario: + +1. Your equity is $50k +2. You hold overnight positions up to $100k +3. Your maintenance margin is $30k (\~30%), therefore your day trading buying power at the beginning of day is $80k using the calculation of 4 \* ($50k - $30k) +4. You sell all of the overnight positions ($100k value) in the morning, which brings your `regt_buying_power` up to $100k +5. You now buy and sell the same security up to $100k +6. At the end of the day, you have a $20k Day Trade Margin Call ($100k - $80k) + +By default, Alpaca users have DTMC protections on entry of a position. This means that if your entering order would exceed `daytrading_buying_power` at the moment, it will be blocked, even if `regt_buying_power` still has room for it. This is based on the assumption that any entering position could be day trades later in the day. This option is the more conservative of the two DTMC protections that our users have. + +The second DTMC protection option is protection on exit of a position. This means that Alpaca will block the exit of positions that would cause a Day Trading Margin Call. This may cause users to be unable to liquidate a position until the next day. + +Neither of the DTMC protection options evaluate crypto orders since crypto cannot be purchased using margin. + +One of the two protections will be enabled for all users (you cannot have both protections disabled). If you would like to switch your protection option, please contact our support. + +We are working towards features to allow users to change their DTMC protection setting on their own without support help. + +## Equity/Order Ratio Validation Check + +In order to help Alpaca Brokerage Account customers from placing orders larger than the calculated buying power, Alpaca has instituted a control on the account independent of the buying power for the account. Alpaca will restrict the account to closing transactions when an account has a position that is 600% larger than the equity in the account. The account will remain restricted for closing transactions until a member of Alpaca’s trading team reviews the account. The trading team will either clear the alert by allowing opening transactions or will notify the client of the restriction and take corrective actions as necessary. + +## Paper Trading + +he same protection triggers in your paper trading account. It is advised to test your algorithm with the realistic balance amount you would manage when going live, to make sure your assumption works under this DTMC protection as well. + +For more details of Pattern Day Trader rule, please read [FINRA’s margin requirements](https://www.finra.org/investors/learn-to-invest/advanced-investing/day-trading-margin-requirements-know-rules-rot). For more details on day trade margins, please read [FINRA’s Mind Your Margin](https://www.finra.org/investors/day-trading) article. + +# Preventing Wash Trades at Alpaca + +At Alpaca, we want to help our customers avoid making unintentional wash trades. A wash trade happens when a customer buys and sells the same security at the same time, which can be seen as a form of market manipulation. To prevent this, the Alpaca Trading platform checks for potential wash trades every time a customer places an order. If we detect a possible wash trade, we reject the order and send back an error message with the HTTP status code 403 (Forbidden). + +## The Rule + +A wash trade occurs when a customer's two orders could potentially interact with each other. Here are a couple of examples: + +* A customer places an order to buy 1 share at $10 (a limit order). Then, the same customer places an order to sell 100 shares at $10 (another limit order). These orders could potentially interact, which would be a wash trade. + +* A customer places an order to sell 100 shares at the market open (a market order). Then, the same customer places an order to buy 100 shares at $10 (a limit order). Again, these orders could potentially interact, which would be a wash trade. + +## How Alpaca Handles Potential Wash Trades + +The Alpaca Trading platform is always on the lookout for potential wash trades. If we determine that an order could result in a wash trade, we trigger our protection measures, reject the order, and send back an error message with the HTTP status code 403 (Forbidden). + +If a customer wants to set up a 'take profit' and a 'stop loss' situation, we recommend using a bracket or OCO (One Cancels the Other) order. These complex orders and trailing stop orders are exceptions to our wash trade protection. + +Here's a table that shows when we would reject an order to prevent a potential wash trade: + +| Existing Order | New Order | Reject Condition | +| ---------------- | ---------------- | ----------------------------------------------- | +| market buy | market sell | always rejected | +| market buy | limit sell | always rejected | +| market buy | stop sell | always rejected | +| market buy | stop\_limit sell | always rejected | +| market sell | market buy | always rejected | +| market sell | limit buy | always rejected | +| market sell | stop buy | always rejected | +| market sell | stop\_limit buy | always rejected | +| stop buy | market sell | always rejected | +| stop buy | limit sell | always rejected | +| stop buy | stop sell | always rejected | +| stop buy | stop\_limit sell | always rejected | +| stop sell | market buy | always rejected | +| stop sell | limit buy | always rejected | +| stop sell | stop buy | always rejected | +| stop sell | stop\_limit buy | always rejected | +| limit buy | market sell | always rejected | +| limit buy | limit sell | rejected if buy limit price >= sell limit price | +| limit buy | stop sell | always rejected | +| limit buy | stop\_limit sell | rejected if buy limit price >= sell limit price | +| limit sell | market buy | always rejected | +| limit sell | limit buy | rejected if buy limit price >= sell limit price | +| limit sell | stop buy | always rejected | +| limit sell | stop\_limit buy | rejected if buy limit price >= sell limit price | +| stop\_limit buy | market sell | always rejected | +| stop\_limit buy | limit sell | rejected if buy limit price >= sell limit price | +| stop\_limit buy | stop sell | always rejected | +| stop\_limit buy | stop\_limit sell | rejected if buy limit price >= sell limit price | +| stop\_limit sell | market buy | always rejected | +| stop\_limit sell | limit buy | rejected if buy limit price >= sell limit price | +| stop\_limit sell | stop buy | always rejected | +| stop\_limit sell | stop\_limit buy | rejected if buy limit price >= sell limit price | + +## Paper Trading + +Our wash trade protection also applies to your paper trading account. We recommend testing your trading algorithm with a realistic balance amount. This way, you can make sure your strategy works under our wash trade protection rules before you start live trading. + +For more details of wash trade rule, please read\ +[FINRA's self-trades requirements](https://www.finra.org/rules-guidance/rulebooks/finra-rules/5210). \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/using-oauth2-and-trading-api.md b/DesktopBot/Documents/Alpaca/using-oauth2-and-trading-api.md new file mode 100644 index 0000000..658121f --- /dev/null +++ b/DesktopBot/Documents/Alpaca/using-oauth2-and-trading-api.md @@ -0,0 +1,155 @@ +# Using OAuth2 and Trading API + +Alpaca implements OAuth 2.0 to allow third party applications to access Alpaca Trading API on behalf of the end-users. This document describes how you can integrate with Alpaca through OAuth. + +By default once you have a valid client\_id and client\_secret, any paper account and the live account associated with the OAuth Client will be available to connect to your app. We welcome developers to build applications and products that are powered by Alpaca while also protecting the privacy and security of our users. To build using Alpaca’s APIs, please follow the guide below. + +> ℹ️ Note +> +> An single Alpaca OAuth token may authorize access to either: +> +> * One live account +> * One paper account +> * One live account and one paper account +> +> For users with multiple paper accounts, the user must go through the authorization flow separately for each account they want to connect. + +# Getting the Access Token + +At a high level the flow looks like this, we will go into detail about each step + +1. User requests a connection between your application and Alpaca +2. User is redirected to Alpaca to login and authorize the application from inside the dashboard +3. Alpaca grants an authorization token to your application through user-agent +4. You application then makes an access token request +5. Alpaca returns an access token grant + +## 1. Request for Connection on Behalf of User + +When redirecting a user to Alpaca to authorize access to your application, you’ll need to construct the authorization URL with the correct parameters and scopes. + +``` +GET https://app.alpaca.markets/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URL&state=SOMETHING_RANDOM&scope=account:write%20trading&env=live +``` + +Here’s a list of parameters you should always specify: + +| Parameter | Required? | Description | +| :-------------- | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------- | +| `response_type` | Required | Must be `code` to request an authorization code. | +| `client_id` | Required | The `client_id` you were provided with when registering your app | +| `redirect_uri` | Required | The redirect URL where the user will be sent after authorization. It must match one of the whitelisted redirect URIs for your application. | +| `state` | Optional | An unguessable random string, used to protect against request forgery attacks. | +| `scope` | Optional | A space-delimited list of scopes your application requests access to. Read-only endpoint access is assumed by default. | +| `env` | Optional | If provided, must be one of `live` or `paper`. If not specified, the user will be prompted to authorized both a live and a paper account. | + +**Allowed Scopes** + +| Scope | Description | +| :-------------- | :------------------------------------------------------ | +| `account:write` | Write access for account configurations and watchlists. | +| `trading` | Place, cancel or modify orders. | +| `data` | Access to the Data API. | + +## 2. Users Authorizing App to Access Alpaca Account + +After you redirect a user to Alpaca, we will display the following OAuth consent screen and ask the user to authorize your app to connect to their Alpaca account. + + + +If you specify a value for the `env` parameter when redirecting to us, we will ask the user to authorize only a live or a paper account, depending on whether you specified `live` or `paper` respectivley. + +For example, if specifying `env=paper` as a query parameter, we will show the following consent screen. + + + +## 3. Alpaca Redirect Back to App + +If the user approves access, Alpaca will redirect them back to your `redirect_uri` with a temporary `code` parameter. If you specified a state parameter in step 1, it will be returned as well. The parameter will always match the value specified in step 1. If the values don’t match, the request should not be trusted. + +Example + +``` +GET https://example.com/oauth/callback?code=67f74f5a-a2cc-4ebd-88b4-22453fe07994&state=8e02c9c6a3484fadaaf841fb1df290e1 +``` + +## 4. App Receives Authorization Code + +You can then use this code to exchange for an access token. + +## 5. App Exchanges Auth Code with Access Token + +After you have received the temporary `code`, you can exchange it for an access token. This can be done by making a `POST` call to `https://api.alpaca.markets/oauth/token` + +### Parameters (All Required) + +| Parameter | Description | +| :-------------- | :------------------------------------------------------------------ | +| `grant_type` | Must be set to authorization\_code for an access token request. | +| `code` | The authorization code received in step 4 | +| `client_id` | The Client ID you received when you registered the application. | +| `client_secret` | The Client Secret you received when you registered the application. | +| `redirect_uri` | The redirect URI you used for the authorization code request. | + +> 🚧 Note +> +> This request should take place behind-the-scenes from your backend server and shouldn’t be visible to the end users for security purposes. + +The content type must be application/x-www-form-urlencoded as defined in RFC. + +Example request: + +```curl +curl -X POST https://api.alpaca.markets/oauth/token \ + -d 'grant_type=authorization_code&code=67f74f5a-a2cc-4ebd-88b4-22453fe07994&client_id=fc9c55efa3924f369d6c1148e668bbe8&client_secret=5b8027074d8ab434882c0806833e76508861c366&redirect_uri=https://example.com/oauth/callback' +``` + +After a successful request, a valid access token will be returned in the response: + +```json +{ + "access_token": "79500537-5796-4230-9661-7f7108877c60", + "token_type": "bearer", + "scope": "account:write trading" +} +``` + +# API Calls + +Once you have integrated and have a valid access token you can start make calls to Alpaca Trading API v2 on behalf of the end-user. + +## Example Requests + +A + +```curl +curl https://api.alpaca.markets/v2/account / + -H 'Authorization: Bearer 79500537-5796-4230-9661-7f7108877c60' +``` + +```curl +curl https://paper-api.alpaca.markets/v2/orders / + -H 'Authorization: Bearer 79500537-5796-4230-9661-7f7108877c60' + +``` + +The OAuth token can also be used for the trade update websockets stream. + +``` +{ + "action": "authenticate", + "data": { + "oauth_token": "79500537-5796-4230-9661-7f7108877c60" + } +} +``` + +The OAuth Token can be used to authenticate for Market Data Stream. Please note that most users can have only 1 active stream connection. If that connection is used by another 3rd party application, you will receive an error: 406 and “connection limit exceeded” message. + +``` +{ +"action": "auth", +"key": "oauth", +"secret": "79500537-5796-4230-9661-7f7108877c60" +} +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/websocket-streaming.md b/DesktopBot/Documents/Alpaca/websocket-streaming.md new file mode 100644 index 0000000..0a5195a --- /dev/null +++ b/DesktopBot/Documents/Alpaca/websocket-streaming.md @@ -0,0 +1,369 @@ +# Websocket Streaming + +Learn how to stream market data using Websockets. + +Alpaca’s API offers WebSocket streaming for trade, account, and order updates which follows the [RFC6455 WebSocket protocol](https://datatracker.ietf.org/doc/html/rfc6455). + +To connect to the WebSocket follow the standard opening handshake as defined by the RFC specification to `wss://paper-api.alpaca.markets/stream` or `wss://api.alpaca.markets/stream`. Alpaca’s streaming service supports both JSON and MessagePack codecs. + +Once the connection is authorized, the client can listen to the `trade_updates` stream to get updates on trade, account, and order changes. + +> 📘 Note: +> +> The `trade_updates` stream coming from `wss://paper-api.alpaca.markets/stream` uses binary frames, which differs from the text frames that comes from the `wss://data.alpaca.markets/stream` stream. + +In order to listen to streams, the client sends a `listen` message to the server as follows: + +```json +{ + "action": "listen", + "data": { + "streams": ["trade_updates"] + } +} +``` + +The server acknowledges by replying a message in the listening stream. + +```json +{ + "stream": "listening", + "data": { + "streams": ["trade_updates"] + } +} +``` + +If any of the requested streams are not available, they will not appear in the streams list in the acknowledgement. Note that the streams field in the listen message is to tell the set of streams to listen, so if you want to stop receiving updates from the stream, you must send an empty list of streams values as a listen message. Similarly, if you want to add more streams to get updates in addition to the ones you are already doing so, you must send all the stream names, not only the new ones. + +Subscribing to real-time trade updates ensures that a user always has the most up-to-date picture of their account actvivity. + +> 📘 Note +> +> To request with MessagePack, add the header: Content-Type: `application/msgpack`. + +# Authentication + +The WebSocket client can be authenticated using the same API key when making HTTP requests. Upon connecting to the WebSocket, client must send an authentication message over the WebSocket connection with the API key and secret key as its payload. + +```json +{ + "action": "auth", + "key": "{YOUR_API_KEY_ID}", + "secret": "{YOUR_API_SECRET_KEY}" +} +``` + +The server will then authorize the connection and respond with either an authorized (successful) response + +```json +{ + "stream": "authorization", + "data": { + "status": "authorized", + "action": "authenticate" + } +} +``` + +or an unauthorized (unsuccessful) response: + +```json +{ + "stream": "authorization", + "data": { + "status": "unauthorized", + "action": "authenticate" + } +} +``` + +In the case that the socket connection is not authorized yet, a new message under the authorization stream is issued in response to the listen request. + +```json +{ + "stream": "authorization", + "data": { + "status": "unauthorized", + "action": "listen" + } +} +``` + +# Trade Updates + +With regards to the account associated with the authorized API keys, updates for orders placed at Alpaca are dispatched over the WebSocket connection under the event trade\_updates. These messages include any data pertaining to orders that are executed with Alpaca. This includes order fills, partial fills, cancellations and rejections of orders. Clients may listen to this stream by sending a listen message: + +```json +{ + "action": "listen", + "data": { + "streams": ["trade_updates"] + } +} +``` + +Any listen messages received by the server will be acknowledged via a message on the listening stream. The message’s data payload will include the list of streams the client is currently listening to: + +```json +{ + "stream": "listening", + "data": { + "streams": ["trade_updates"] + } +} +``` + +The fields present in a message sent over the `trade_updates` stream depend on the type of event they are communicating. All messages contain an `event` type and an `order` field, which is the same as the order object that is returned from the REST API. Potential event types and additional fields that will be in their messages are listed below. + +## Common Events + +These are the events that are the expected results of actions you may have taken by sending API requests. + +* `new`: Sent when an order has been routed to exchanges for execution. +* `fill`: Sent when an order has been completely filled. + * `timestamp`: The time at which the order was filled. + * `price`: The price per share for the fill event. This may be different from the average fill price for the order if there were partial fills. + * `qty`: The number of shares for the fill event. This will be different from the filled quantity for the order if there were partial fills. + * `position_qty`: The total size of your position after this event, in shares. Positive for long positions, negative for short positions. +* `partial_fill`: Sent when a number of shares less than the total remaining quantity on your order has been filled. + * `timestamp`: The time at which the order was partially filled. + * `price`: The price per share for the partial fill event. + * `qty`: The number of shares for the partial fill event. + * `position_qty`: The total size of your position after this event, in shares. Positive for long positions, negative for short positions. +* `canceled`: Sent when your requested cancelation of an order is processed. + * `timestamp`: The time at which the order was canceled. +* `expired`: Sent when an order has reached the end of its lifespan, as determined by the order’s time in force value. + * `timestamp`: The time at which the order was expired. +* `done_for_day`: Sent when the order is done executing for the day, and will not receive further updates until the next trading day. +* `replaced`: Sent when your requested replacement of an order is processed. + * `timestamp`: The time at which the order was replaced. + +## Less Common Events + +These are events that may rarely be sent due to uncommon circumstances on the exchanges. It is unlikely you will need to design your code around them, but you may still wish to account for the possibility that they can occur. + +* `accepted`: Sent when your order has been received by Alpaca, but hasn’t yet been routed to the execution venue. +* `rejected`: Sent when your order has been rejected. + * `timestamp`: The time at which the order was rejected. +* `pending_new`: Sent when the order has been received by Alpaca and routed to the exchanges, but has not yet been accepted for execution. +* `stopped`: Sent when your order has been stopped, and a trade is guaranteed for the order, usually at a stated price or better, but has not yet occurred. +* `pending_cancel`: Sent when the order is awaiting cancelation. Most cancelations will occur without the order entering this state. +* `pending_replace`: Sent when the order is awaiting replacement. +* `calculated`: Sent when the order has been completed for the day - it is either “filled” or “done\_for\_day” - but remaining settlement calculations are still pending. +* `suspended`: Sent when the order has been suspended and is not eligible for trading. +* `order_replace_rejected`: Sent when the order replace has been rejected. +* `order_cancel_rejected`: Sent when the order cancel has been rejected. + +# Example + +An example of a message sent over the `trade_updates` stream looks like: + +```json +{ + "stream": "trade_updates", + "data": { + "event": "fill", + "execution_id": "2f63ea93-423d-4169-b3f6-3fdafc10c418", + "order": { + "asset_class": "crypto", + "asset_id": "1cf35270-99ee-44e2-a77f-6fab902c7f80", + "cancel_requested_at": null, + "canceled_at": null, + "client_order_id": "4642fd68-d59a-47d7-a9ac-e22f536828d1", + "created_at": "2022-04-19T13:45:04.981350886-04:00", + "expired_at": null, + "extended_hours": false, + "failed_at": null, + "filled_at": "2022-04-19T17:45:05.024916716Z", + "filled_avg_price": "105.8988475", + "filled_qty": "1790.86", + "hwm": null, + "id": "a5be8f5e-fdfa-41f5-a644-7a74fe947a8f", + "legs": null, + "limit_price": null, + "notional": null, + "order_class": "", + "order_type": "market", + "qty": "1790.86", + "replaced_at": null, + "replaced_by": null, + "replaces": null, + "side": "sell", + "status": "filled", + "stop_price": null, + "submitted_at": "2022-04-19T13:45:04.980944666-04:00", + "symbol": "SOLUSD", + "time_in_force": "gtc", + "trail_percent": null, + "trail_price": null, + "type": "market", + "updated_at": "2022-04-19T13:45:05.027690731-04:00" + }, + "position_qty": "0", + "price": "105.8988475", + "qty": "1790.86", + "timestamp": "2022-04-19T17:45:05.024916716Z" + } +} + +``` + +An example message for MultilegOptionsOrder fill event: + +```json +{ + "stream": "trade_updates", + "data": { + "at": "2025-01-21T07:32:40.70095Z", + "event_id": "01JJ3WE73W5PG672TC4XACXH5R", + "event": "fill", + "timestamp": "2025-01-21T07:32:40.695569506Z", + "order": { + "id": "31cd620f-3bd5-41b7-8bb2-6834524679d0", + "client_order_id": "fe999618-6435-497b-9fdd-a63d3da3615f", + "created_at": "2025-01-21T07:32:40.678963102Z", + "updated_at": "2025-01-21T07:32:40.699359002Z", + "submitted_at": "2025-01-21T07:32:40.691562346Z", + "filled_at": "2025-01-21T07:32:40.695569506Z", + "expired_at": null, + "cancel_requested_at": null, + "canceled_at": null, + "failed_at": null, + "replaced_at": null, + "replaced_by": null, + "replaces": null, + "asset_id": "00000000-0000-0000-0000-000000000000", + "symbol": "", + "asset_class": "", + "notional": null, + "qty": "1", + "filled_qty": "1", + "filled_avg_price": "1.62", + "order_class": "mleg", + "order_type": "limit", + "type": "limit", + "side": "buy", + "time_in_force": "day", + "limit_price": "2", + "stop_price": null, + "status": "filled", + "extended_hours": false, + "legs": [ + { + "id": "3cbe69ef-241c-43ba-9d8c-09361930a1af", + "client_order_id": "e868fb88-ce92-442b-91be-4b16defbc883", + "created_at": "2025-01-21T07:32:40.678963102Z", + "updated_at": "2025-01-21T07:32:40.697474882Z", + "submitted_at": "2025-01-21T07:32:40.687356797Z", + "filled_at": "2025-01-21T07:32:40.695564076Z", + "expired_at": null, + "cancel_requested_at": null, + "canceled_at": null, + "failed_at": null, + "replaced_at": null, + "replaced_by": null, + "replaces": null, + "asset_id": "925af3ed-5c00-4ef1-b89b-e4bd05f04486", + "symbol": "AAPL250321P00200000", + "asset_class": "us_option", + "notional": null, + "qty": "1", + "filled_qty": "1", + "filled_avg_price": "1.6", + "order_class": "mleg", + "order_type": "", + "type": "", + "side": "buy", + "time_in_force": "day", + "limit_price": null, + "stop_price": null, + "status": "filled", + "extended_hours": false, + "legs": null, + "trail_percent": null, + "trail_price": null, + "hwm": null, + "ratio_qty": "1" + }, + { + "id": "ec694de5-5028-4347-8f89-d8ea00c9341f", + "client_order_id": "0a1bf1e1-6992-4c23-85a6-9469bbe05f1a", + "created_at": "2025-01-21T07:32:40.678963102Z", + "updated_at": "2025-01-21T07:32:40.699294952Z", + "submitted_at": "2025-01-21T07:32:40.691562346Z", + "filled_at": "2025-01-21T07:32:40.695569506Z", + "expired_at": null, + "cancel_requested_at": null, + "canceled_at": null, + "failed_at": null, + "replaced_at": null, + "replaced_by": null, + "replaces": null, + "asset_id": "9f8c3d65-f5f7-42cd-acbc-9636cc32d3b5", + "symbol": "AAPL250321C00380000", + "asset_class": "us_option", + "notional": null, + "qty": "1", + "filled_qty": "1", + "filled_avg_price": "0.02", + "order_class": "mleg", + "order_type": "", + "type": "", + "side": "buy", + "time_in_force": "day", + "limit_price": null, + "stop_price": null, + "status": "filled", + "extended_hours": false, + "legs": null, + "trail_percent": null, + "trail_price": null, + "hwm": null, + "ratio_qty": "1" + } + ], + "trail_percent": null, + "trail_price": null, + "hwm": null + }, + "price": "1.62", + "qty": "1", + "position_qtys": { + "AAPL250321P00200000": "1", + "AAPL250321C00380000": "1" + }, + "legs": [ + { + "execution_id": "69a70e98-f370-427d-bcd3-834dc4800aed", + "qty": "1", + "price": "1.6", + "order_id": "3cbe69ef-241c-43ba-9d8c-09361930a1af", + "symbol": "AAPL250321P00200000", + "timestamp": "2025-01-21T07:32:40.695564076Z" + }, + { + "execution_id": "fb878d87-569e-49f3-b42e-a09ad06e3d3a", + "qty": "1", + "price": "0.02", + "order_id": "ec694de5-5028-4347-8f89-d8ea00c9341f", + "symbol": "AAPL250321C00380000", + "timestamp": "2025-01-21T07:32:40.695569506Z" + } + ] + } +} +``` + +## Error messages + +In the case of in-stream errors, the server sends an `error` action before closing the connection. + +```json +{ + "action": "error", + "data": { + "error_message": "internal server error" + } +} +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/working-with-account.md b/DesktopBot/Documents/Alpaca/working-with-account.md new file mode 100644 index 0000000..da3f893 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/working-with-account.md @@ -0,0 +1,199 @@ +# Working with /account + +Learn how to use the /account endpoint to learn about the state of your account + +# View Account Information + +By sending a `GET` request to our `/v2/account` endpoint, you can see various information about your account, such as the amount of buying power available or whether or not it has a PDT flag. + +```Text Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import GetAssetsRequest + +trading_client = TradingClient('api-key', 'secret-key') + +# Get our account information. +account = trading_client.get_account() + +# Check if our account is restricted from trading. +if account.trading_blocked: + print('Account is currently restricted from trading.') + +# Check how much money we can use to open new positions. +print('${} is available as buying power.'.format(account.buying_power)) +``` + +```Text JavaScript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Get our account information. +alpaca.getAccount().then((account) => { + // Check if our account is restricted from trading. + if (account.trading_blocked) { + console.log("Account is currently restricted from trading."); + } + + // Check how much money we can use to open new positions. + console.log(`$${account.buying_power} is available as buying power.`); +}); +``` + +```Text C# +using Alpaca.Markets; +using System; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Get our account information. + var account = await client.GetAccountAsync(); + + // Check if our account is restricted from trading. + if (account.IsTradingBlocked) + { + Console.WriteLine("Account is currently restricted from trading."); + } + + Console.WriteLine(account.BuyingPower + " is available as buying power."); + + Console.Read(); + } + } +} +``` + +```Text Go +package main + +import ( + "fmt" + + "github.com/alpacahq/alpaca-trade-api-go/alpaca" +) + +func init() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") +} + +func main() { + // Get our account information. + account, err := alpaca.GetAccount() + if err != nil { + panic(err) + } + + // Check if our account is restricted from trading. + if account.TradingBlocked { + fmt.Println("Account is currently restricted from trading.") + } + + // Check how much money we can use to open new positions. + fmt.Printf("%v is available as buying power.\n", account.BuyingPower) +} +``` + +# View Gain/Loss of Portfolio + +You can use the information from the account endpoint to do things like calculating the daily profit or loss of your account. + +```Text Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import GetAssetsRequest + +trading_client = TradingClient('api-key', 'secret-key') + +# Get our account information. +account = trading_client.get_account() + +# Check our current balance vs. our balance at the last market close +balance_change = float(account.equity) - float(account.last_equity) +print(f'Today\'s portfolio balance change: ${balance_change}') +``` + +```Text JavaScript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Get account information. +alpaca.getAccount().then((account) => { + // Calculate the difference between current balance and balance at the last market close. + const balanceChange = account.equity - account.last_equity; + + console.log("Today's portfolio balance change:", balanceChange); +}); +``` + +```Text C# +using Alpaca.Markets; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +// With the Alpaca API, you can check on your daily profit or loss by +// comparing your current balance to yesterday's balance. + +namespace GetPnLExample +{ + internal class GetPnL + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Get account info + var account = await client.GetAccountAsync(); + + // Check our current balance vs. our balance at the last market close + var balance_change = account.Equity - account.LastEquity; + + Console.WriteLine($"Today's portfolio balance change: ${balance_change}"); + } + } +} +``` + +```Text Go +package main + +import ( + "fmt" + "log" + + "github.com/alpacahq/alpaca-trade-api-go/alpaca" +) + +func main() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") + + // Get account information. + account, err := alpaca.GetAccount() + if err != nil { + log.Fatalln(err) + } + + // Calculate the difference between current balance and balance at the last market close. + balanceChange := account.Equity.Sub(account.LastEquity) + + fmt.Println("Today's portfolio balance change:", balanceChange) +} +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/working-with-assets.md b/DesktopBot/Documents/Alpaca/working-with-assets.md new file mode 100644 index 0000000..774ba2e --- /dev/null +++ b/DesktopBot/Documents/Alpaca/working-with-assets.md @@ -0,0 +1,191 @@ +# Working with /assets + +Learn how to use the `/assets` endpoint to learn more about assets available on Alpaca. Both Securities and Crypto can be retrieved from the `/assets` endpoint. + +# Get a List of Assets + +If you send a `GET` request to our `/v2/assets` endpoint, you’ll receive a list of US equities. + +```python Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import GetAssetsRequest +from alpaca.trading.enums import AssetClass + +trading_client = TradingClient('api-key', 'secret-key') + +# search for US equities +search_params = GetAssetsRequest(asset_class=AssetClass.US_EQUITY) + +assets = trading_client.get_all_assets(search_params) +``` + +```javascript JavaScript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Get a list of all active assets. +const activeAssets = alpaca + .getAssets({ + status: "active", + }) + .then((activeAssets) => { + // Filter the assets down to just those on NASDAQ. + const nasdaqAssets = activeAssets.filter( + (asset) => asset.exchange == "NASDAQ" + ); + console.log(nasdaqAssets); + }); +``` + +```csharp C# +using Alpaca.Markets; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Get a list of all active assets. + var assets = await client.ListAssetsAsync( + new AssetsRequest { AssetStatus = AssetStatus.Active }); + + // Filter the assets down to just those on NASDAQ. + var nasdaqAssets = assets.Where(asset => asset.Exchange == Exchange.NyseMkt); + + Console.Read(); + } + } +} +``` + +```go Go +package main + +import ( + "github.com/alpacahq/alpaca-trade-api-go/alpaca" +) + +func init() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") +} + +func main() { + // Get a list of all active assets. + status := "active" + assets, err := alpaca.ListAssets(&status) + if err != nil { + panic(err) + } + + // Filter the assets down to just those on NASDAQ. + nasdaq_assets := []alpaca.Asset{} + for _, asset := range assets { + if asset.Exchange == "NASDAQ" { + nasdaq_assets = append(nasdaq_assets, asset) + } + } +} +``` + +# See If a Particular Asset is Tradable on Alpaca + +By sending a symbol along with our request, we can get the information about just one asset. This is useful if we just want to make sure that a particular asset is tradable before we attempt to buy it. + +```python Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import GetAssetsRequest + +trading_client = TradingClient('api-key', 'secret-key') + +# search for AAPL +aapl_asset = trading_client.get_asset('AAPL') + +if aapl_asset.tradable: + print('We can trade AAPL.') +``` + +```javascript JavaScript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Check if AAPL is tradable on the Alpaca platform. +alpaca.getAsset("AAPL").then((aaplAsset) => { + if (aaplAsset.tradable) { + console.log("We can trade AAPL."); + } +}); +``` + +```csharp C# +using Alpaca.Markets; +using System; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Check if AAPL is tradable on the Alpaca platform. + try + { + var asset = await client.GetAssetAsync("AAPL"); + if (asset.IsTradable) + { + Console.WriteLine("We can trade AAPL"); + } + } + catch (Exception) + { + Console.WriteLine("Asset not found for AAPL."); + } + + Console.Read(); + } + } +} +``` + +```go Go +package main + +import ( + "fmt" + "github.com/alpacahq/alpaca-trade-api-go/alpaca" +) + +func init() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") +} + +func main() { + // Check if AAPL is tradable on the Alpaca platform. + asset, err := alpaca.GetAsset("AAPL") + if err != nil { + fmt.Println("Asset not found for AAPL.") + } else if asset.Tradable { + fmt.Println("We can trade AAPL.") + } +} +``` \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/working-with-orders.md b/DesktopBot/Documents/Alpaca/working-with-orders.md new file mode 100644 index 0000000..5b9fc4d --- /dev/null +++ b/DesktopBot/Documents/Alpaca/working-with-orders.md @@ -0,0 +1,998 @@ +# Working with /orders + +Learn how to submit orders to Alpaca. + +This page contains examples of some of the things you can do with order objects through our API. For additional help understanding different types of orders and how they behave once they’re placed, please check out the Orders on Alpaca page. + +# Place New Orders + +Orders can be placed with a `POST` request to our `/v2/orders` endpoint. + +```python Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import MarketOrderRequest, LimitOrderRequest +from alpaca.trading.enums import OrderSide, TimeInForce + +trading_client = TradingClient('api-key', 'secret-key', paper=True) + +# preparing market order +market_order_data = MarketOrderRequest( + symbol="SPY", + qty=0.023, + side=OrderSide.BUY, + time_in_force=TimeInForce.DAY + ) + +# Market order +market_order = trading_client.submit_order( + order_data=market_order_data + ) + +# preparing limit order +limit_order_data = LimitOrderRequest( + symbol="BTC/USD", + limit_price=17000, + notional=4000, + side=OrderSide.SELL, + time_in_force=TimeInForce.FOK + ) + +# Limit order +limit_order = trading_client.submit_order( + order_data=limit_order_data + ) +``` + +```javascript JavaScript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Submit a market order to buy 1 share of Apple at market price +alpaca.createOrder({ + symbol: "AAPL", + qty: 1, + side: "buy", + type: "market", + time_in_force: "day", +}); + +// Submit a limit order to attempt to sell 1 share of AMD at a +// particular price ($20.50) when the market opens +alpaca.createOrder({ + symbol: "AMD", + qty: 1, + side: "sell", + type: "limit", + time_in_force: "opg", + limit_price: 20.5, +}); +``` + +```csharp C# +using Alpaca.Markets; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Submit a market order to buy 1 share of Apple at market price + var order = await client.PostOrderAsync(MarketOrder.Buy("AAPL", 1)); + + // Submit a limit order to attempt to sell 1 share of AMD at a + // particular price ($20.50) when the market opens + order = await client.PostOrderAsync( + LimitOrder.Sell("AMD", 1, 20.50M).WithDuration(TimeInForce.Opg)); + + Console.Read(); + } + } +} +``` + +```go Go +package main + +import ( + "github.com/alpacahq/alpaca-trade-api-go/alpaca" + "github.com/shopspring/decimal" +) + +func init() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") +} + +func main() { + // Submit a market order to buy 1 share of Apple at market price + symbol := "AAPL" + alpaca.PlaceOrder(alpaca.PlaceOrderRequest{ + AssetKey: &symbol, + Qty: decimal.NewFromFloat(1), + Side: alpaca.Buy, + Type: alpaca.Market, + TimeInForce: alpaca.Day, + }) + + // Submit a limit order to attempt to sell 1 share of AMD at a + // particular price ($20.50) when the market opens + symbol = "AMD" + alpaca.PlaceOrder(alpaca.PlaceOrderRequest{ + AssetKey: &symbol, + Qty: decimal.NewFromFloat(1), + Side: alpaca.Sell, + Type: alpaca.Limit, + TimeInForce: alpaca.OPG, + LimitPrice: decimal.NewFromFloat(20.50), + }) +} +``` + +# Submit Shorts + +Short orders can also be placed for securities which you do not hold an open long position in. + +```python Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import MarketOrderRequest +from alpaca.trading.enums import OrderSide, TimeInForce + +trading_client = TradingClient('api-key', 'secret-key', paper=True) + +# preparing orders +market_order_data = MarketOrderRequest( + symbol="SPY", + qty=1, + side=OrderSide.SELL, + time_in_force=TimeInForce.GTC + ) + +# Market order +market_order = trading_client.submit_order( + order_data=market_order_data + ) +``` + +```csharp C# +using Alpaca.Markets; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +// With the Alpaca API, you can open a short position by submitting a sell +// order for a security that you have no open long position with. + +namespace ShortingExample +{ + internal class ShortProgram + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + // The security we'll be shorting + private static string symbol = "TSLA"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var tradingClient = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + var dataClient = Alpaca.Markets.Environments.Paper + .GetAlpacaDataClient(new SecretKey(API_KEY, API_SECRET)); + + // Submit a market order to open a short position of one share + var order = await tradingClient.PostOrderAsync(MarketOrder.Sell(symbol, 1)); + Console.WriteLine("Market order submitted."); + + // Submit a limit order to attempt to grow our short position + // First, get an up-to-date price for our security + var snapshot = await dataClient.GetSnapshotAsync(symbol); + var price = snapshot.MinuteBar.Close; + + // Submit another order for one share at that price + order = await tradingClient.PostOrderAsync(LimitOrder.Sell(symbol, 1, price)); + Console.WriteLine($"Limit order submitted. Limit price = {order.LimitPrice}"); + + // Wait a few seconds for our orders to fill... + Thread.Sleep(2000); + + // Check on our position + var position = await tradingClient.GetPositionAsync(symbol); + if (position.Quantity < 0) + { + Console.WriteLine($"Short position open for {symbol}"); + } + } + } +} +``` + +# Using Client Order IDs + +`client_order_id` can be used to organize and track specific orders in your client program. Unique `client_order_ids` for different strategies is a good way of running parallel algos across the same account. + +```python Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import MarketOrderRequest +from alpaca.trading.enums import OrderSide, TimeInForce + +trading_client = TradingClient('api-key', 'secret-key', paper=True) + +# preparing orders +market_order_data = MarketOrderRequest( + symbol="SPY", + qty=0.023, + side=OrderSide.BUY, + time_in_force=TimeInForce.DAY, + client_order_id='my_first_order', + ) + +# Market order +market_order = trading_client.submit_order( + order_data=market_order_data + ) + +# Get our order using its Client Order ID. +my_order = trading_client.get_order_by_client_id('my_first_order') +print('Got order #{}'.format(my_order.id)) +``` + +```javascript +const Alpaca = require('@alpacahq/alpaca-trade-api') +const alpaca = new Alpaca() + +// Submit a market order and assign it a Client Order ID. +alpaca.createOrder({ + symbol: 'AAPL', + qty: 1, + side: 'buy', + type: 'market', + time_in_force: 'day', + client_order_id='my_first_order' +}) + +// Get our order using its Client Order ID. +alpaca.getOrderByClientOrderId('my_first_order') + .then((myOrder) => { + console.log(`Got order #${myOrder.id}.`) + }) +``` + +```csharp C# +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + private static string CLIENT_ORDER_ID = "my_first_order"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Submit a market order and assign it a Client Order ID + await client.PostOrderAsync( + MarketOrder.Buy("AAPL", 1) + .WithClientOrderId(CLIENT_ORDER_ID)); + + // Get our order using its Client Order ID + var order = await client.GetOrderAsync(CLIENT_ORDER_ID); + + Console.WriteLine($"Got order #{order.ClientOrderId}"); + Console.Read(); + } + } +} +``` + +# Submitting Bracket Orders + +Bracket orders allow you to create a chain of orders that react to execution and stock price. For more details, go to Bracket Order Overview. + +```python Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import MarketOrderRequest, LimitOrderRequest, TakeProfitRequest, StopLossRequest +from alpaca.trading.enums import OrderSide, TimeInForce, OrderClass + +trading_client = TradingClient('api-key', 'secret-key', paper=True) + +# preparing bracket order with both stop loss and take profit +bracket__order_data = MarketOrderRequest( + symbol="SPY", + qty=5, + side=OrderSide.BUY, + time_in_force=TimeInForce.DAY, + order_class=OrderClass.BRACKET, + take_profit=TakeProfitRequest(limit_price=400), + stop_loss=StopLossRequest(stop_price=300) + ) + +bracket_order = trading_client.submit_order( + order_data=bracket__order_data + ) + +# preparing oto order with stop loss +oto_order_data = LimitOrderRequest( + symbol="SPY", + qty=5, + limit_price=350, + side=OrderSide.BUY, + time_in_force=TimeInForce.DAY, + order_class=OrderClass.OTO, + stop_loss=StopLossRequest(stop_price=300) + ) + +# Market order +oto_order = trading_client.submit_order( + order_data=oto_order_data + ) +``` + +```javascript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +const symbol = "AAPL"; +alpaca + .getBars("minute", symbol, { + limit: 5, + }) + .then((barset) => { + const currentPrice = barset[symbol].slice(-1)[0].closePrice; + + // We could buy a position and add a stop-loss and a take-profit of 5 % + alpaca.createOrder({ + symbol: symbol, + qty: 1, + side: "buy", + type: "limit", + time_in_force: "gtc", + limit_price: currentPrice, + order_class: "bracket", + stop_loss: { + stop_price: currentPrice * 0.95, + limit_price: currentPrice * 0.94, + }, + take_profit: { + limit_price: currentPrice * 1.05, + }, + }); + + // We could buy a position and just add a stop loss of 5 % (OTO Orders) + alpaca.createOrder({ + symbol: symbol, + qty: 1, + side: "buy", + type: "limit", + time_in_force: "gtc", + limit_price: currentPrice, + order_class: "oto", + stop_loss: { + stop_price: currentPrice * 0.95, + }, + }); + + // We could split it to 2 orders. first buy a stock, + // and then add the stop/profit prices (OCO Orders) + alpaca.createOrder({ + symbol: symbol, + qty: 1, + side: "buy", + type: "limit", + time_in_force: "gtc", + limit_price: currentPrice, + }); + + // wait for it to buy position and then + alpaca.createOrder({ + symbol: symbol, + qty: 1, + side: "sell", + type: "limit", + time_in_force: "gtc", + limit_price: currentPrice, + order_class: "oco", + stop_loss: { + stop_price: currentPrice * 0.95, + }, + take_profit: { + limit_price: currentPrice * 1.05, + }, + }); + }); +``` + +```csharp C# +using Alpaca.Markets; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace ShortingExample +{ + internal class ShortProgram + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + private static string symbol = "APPL"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var tradingClient = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + var dataClient = Alpaca.Markets.Environments.Paper + .GetAlpacaDataClient(new SecretKey(API_KEY, API_SECRET)); + + var snapshot = await dataClient.GetSnapshotAsync(symbol); + var price = snapshot.MinuteBar.Close; + + // We could buy a position and add a stop-loss and a take-profit of 5 % + await tradingClient.PostOrderAsync( + MarketOrder.Buy(symbol, 1) + .WithDuration(TimeInForce.Gtc) + .Bracket( + stopLossStopPrice: price * 0.95M, + takeProfitLimitPrice: price * 0.94M, + stopLossLimitPrice: price * 1.05M)); + + // We could buy a position and just add a stop loss of 5 % (OTO Orders) + await tradingClient.PostOrderAsync( + MarketOrder.Buy(symbol, 1) + .WithDuration(TimeInForce.Gtc) + .StopLoss( + stopLossStopPrice: price * 0.95M)); + + // We could split it to 2 orders. first buy a stock, + // and then add the stop/profit prices (OCO Orders) + await tradingClient.PostOrderAsync( + LimitOrder.Buy(symbol, 1, price)); + + await tradingClient.PostOrderAsync( + LimitOrder.Sell(symbol, 1, price) + .WithDuration(TimeInForce.Gtc) + .OneCancelsOther( + stopLossStopPrice: price * 0.95M, + stopLossLimitPrice: price * 1.05M)); + } + } +} +``` + +```go +package main + +import ( + "github.com/alpacahq/alpaca-trade-api-go/alpaca" + "github.com/alpacahq/alpaca-trade-api-go/common" + "github.com/shopspring/decimal" +) + +func init() { + API_KEY := "YOUR_API_KEY_HERE" + API_SECRET := "YOUR_API_SECRET_HERE" + BASE_URL := "https://paper-api.alpaca.markets" + // Check for environment variables + if common.Credentials().ID == "" { + os.Setenv(common.EnvApiKeyID, API_KEY) + } + if common.Credentials().Secret == "" { + os.Setenv(common.EnvApiSecretKey, API_SECRET) + } + alpaca.SetBaseUrl(BASE_URL) +} + +func main() { + // Submit a limit order to buy 1 share of Apple and add + // StopLoss and TakeProfit orders. + client := alpaca.NewClient(common.Credentials()) + symbol := "AAPL" + tpp := decimal.NewFromFloat(320.) + spp := decimal.NewFromFloat(317.) + limit := decimal.NewFromFloat(318.) + tp := &alpaca.TakeProfit{LimitPrice: &tpp} + sl := &alpaca.StopLoss{ + LimitPrice: nil, + StopPrice: &spp, + } + req := alpaca.PlaceOrderRequest{ + AccountID: common.Credentials().ID, + AssetKey: &symbol, + Qty: decimal.New(1, 0), + Side: alpaca.Buy, + LimitPrice: &limit, + TimeInForce: alpaca.GTC, + Type: alpaca.Limit, + OrderClass: alpaca.Bracket, + TakeProfit: tp, + StopLoss: sl, + } + order, err := client.PlaceOrder(req) + fmt.Println(order) + fmt.Println(err) + + // We could buy a position and just add a stop loss (OTO Orders) + spp := decimal.NewFromFloat(317.) + limit := decimal.NewFromFloat(318.) + sl := &alpaca.StopLoss{ + StopPrice: &spp, + } + req := alpaca.PlaceOrderRequest{ + AccountID: common.Credentials().ID, + AssetKey: &symbol, + Qty: decimal.New(1, 0), + Side: alpaca.Buy, + LimitPrice: &limit, + TimeInForce: alpaca.GTC, + Type: alpaca.Limit, + OrderClass: alpaca.Oto, + StopLoss: sl, + } + order, err := client.PlaceOrder(req) + fmt.Println(order) + fmt.Println(err) + + // We could split it to 2 orders. first buy a stock, + // and then add the stop/profit prices (OCO Orders) + limit := decimal.NewFromFloat(318.) + req := alpaca.PlaceOrderRequest{ + AccountID: common.Credentials().ID, + AssetKey: &symbol, + Qty: decimal.New(1, 0), + Side: alpaca.Buy, + LimitPrice: &limit, + TimeInForce: alpaca.GTC, + Type: alpaca.Limit, + OrderClass: alpaca.Simple, + } + order, err := client.PlaceOrder(req) + fmt.Println(order) + fmt.Println(err) + + // wait for it to complete and then + tpp := decimal.NewFromFloat(320.) + spp := decimal.NewFromFloat(317.) + tp := &alpaca.TakeProfit{LimitPrice: &tpp} + sl := &alpaca.StopLoss{ + LimitPrice: nil, + StopPrice: &spp, + } + req := alpaca.PlaceOrderRequest{ + AccountID: common.Credentials().ID, + AssetKey: &symbol, + Qty: decimal.New(1, 0), + Side: alpaca.Sell, + TimeInForce: alpaca.GTC, + Type: alpaca.Limit, + OrderClass: alpaca.Oco, + TakeProfit: tp, + StopLoss: sl, + } + order, err := client.PlaceOrder(req) + fmt.Println(order) + fmt.Println(err) +} +``` + +# Submitting Trailing Stop Orders + +Trailing stop orders allow you to create a stop order that automatically changes the stop price allowing you to maximize your profits while still protecting your position with a stop price. For more details, go to Trailing Stop Order Overview. + +```python Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import TrailingStopOrderRequest +from alpaca.trading.enums import OrderSide, TimeInForce + +trading_client = TradingClient('api-key', 'secret-key', paper=True) + + +trailing_percent_data = TrailingStopOrderRequest( + symbol="SPY", + qty=1, + side=OrderSide.SELL, + time_in_force=TimeInForce.GTC, + trail_percent=1.00 # hwm * 0.99 + ) + +trailing_percent_order = trading_client.submit_order( + order_data=trailing_percent_data + ) + + +trailing_price_data = TrailingStopOrderRequest( + symbol="SPY", + qty=1, + side=OrderSide.SELL, + time_in_force=TimeInForce.GTC, + trail_price=1.00 # hwm - $1.00 + ) + +trailing_price_order = trading_client.submit_order( + order_data=trailing_price_data + ) +``` + +```javascript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Submit a market order to buy 1 share of Apple at market price +alpaca.createOrder({ + symbol: "AAPL", + qty: 1, + side: "buy", + type: "market", + time_in_force: "day", +}); + +// Submit a trailing stop order to sell 1 share of Apple at a +// trailing stop of +alpaca.createOrder({ + symbol: "AAPL", + qty: 1, + side: "sell", + type: "trailing_stop", + trail_price: 1.0, // stop price will be hwm - 1.00$ + time_in_force: "day", +}); + +// Alternatively, you could use trail_percent: +alpaca.createOrder({ + symbol: "AAPL", + qty: 1, + side: "sell", + type: "trailing_stop", + trail_percent: 1.0, // stop price will be hwm*0.99 + time_in_force: "day", +}); +``` + +```csharp C# +using Alpaca.Markets; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Submit a market order to buy 1 share of Apple at market price + var order = await client.PostOrderAsync( + new NewOrderRequest("AAPL", 1, OrderSide.Buy, OrderType.Market, TimeInForce.Day)); + + // Submit a trailing stop order to sell 1 share of Apple at a + // trailing stop of + order = await client.PostOrderAsync( + TrailingStopOrder.Sell("AAPL", 1, TrailOffset.InDollars(1.00M))); // stop price will be hwm - 1.00$ + + /** + // Alternatively, you could use trail_percent: + order = await client.PostOrderAsync( + TrailingStopOrder.Sell("AAPL", 1, TrailOffset.InPercent(0.99M))); // stop price will be hwm * 0.99 + */ + + Console.Read(); + } + } +} +``` + +```go +package main + +import ( + "github.com/alpacahq/alpaca-trade-api-go/alpaca" + "github.com/shopspring/decimal" +) + +func init() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") +} + +func main() { + // Submit a market order to buy 1 share of Apple at market price + symbol := "AAPL" + alpaca.PlaceOrder(alpaca.PlaceOrderRequest{ + AssetKey: &symbol, + Qty: decimal.NewFromFloat(1), + Side: alpaca.Buy, + Type: alpaca.Market, + TimeInForce: alpaca.Day, + }) + + // Submit a trailing stop order to sell 1 share of Apple at a + // trailing stop of + symbol = "AAPL" + alpaca.PlaceOrder(alpaca.PlaceOrderRequest{ + AssetKey: &symbol, + Qty: decimal.NewFromFloat(1), + Side: alpaca.Sell, + Type: alpaca.TrailingStop, + StopPrice: decimal.NewFromFloat(1.00), // stop price will be hwm - 1.00$ + TimeInForce: alpaca.Day, + }) + + // Alternatively, you could use trail_percent: + symbol = "AAPL" + alpaca.PlaceOrder(alpaca.PlaceOrderRequest{ + AssetKey: &symbol, + Qty: decimal.NewFromFloat(1), + Side: alpaca.Sell, + Type: alpaca.TrailingStop, + TrailPercent: decimal.NewFromFloat(1.0), // stop price will be hwm*0.99 + TimeInForce: alpaca.Day, + }) +} +``` + +# Retrieve All Orders + +If you’d like to see a list of your existing orders, you can send a `GET` request to our `/v2/orders` endpoint. + +```python Python +from alpaca.trading.client import TradingClient +from alpaca.trading.requests import GetOrdersRequest +from alpaca.trading.enums import QueryOrderStatus + +trading_client = TradingClient('api-key', 'secret-key', paper=True) + +# Get the last 100 closed orders +get_orders_data = GetOrdersRequest( + status=QueryOrderStatus.CLOSED, + limit=100, + nested=True # show nested multi-leg orders +) + +trading_client.get_orders(filter=get_orders_data) +``` + +```javascript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Get the last 100 of our closed orders +const closedOrders = alpaca + .getOrders({ + status: "closed", + limit: 100, + nested: true, // show nested multi-leg orders + }) + .then((closedOrders) => { + // Get only the closed orders for a particular stock + const closedAaplOrders = closedOrders.filter( + (order) => order.symbol == "AAPL" + ); + console.log(closedAaplOrders); + }); +``` + +```csharp C# +using Alpaca.Markets; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Get the last 100 of our closed orders + var closedOrders = await client.ListOrdersAsync( + new ListOrdersRequest + { + LimitOrderNumber = 100, + OrderStatusFilter = OrderStatusFilter.Closed + }); + + // Get only the closed orders for a particular stock + var aaplOrders = closedOrders.Where(order => order.Symbol.Equals("AAPL")); + + Console.Read(); + } + } +} +``` + +```go +package main + +import ( + "github.com/alpacahq/alpaca-trade-api-go/alpaca" +) + +func init() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") +} + +func main() { + // Get the last 100 of our closed orders + status := "closed" + limit := 100 + nested := true // show nested multi-leg orders + closed_orders, err := alpaca.ListOrders(&status, nil, &limit, &nested) + if err != nil { + panic(err) + } + + // Get only the closed orders for a particular stock + var aaplOrders []alpaca.Order + for _, order := range closed_orders { + if order.Symbol == "AAPL" { + aaplOrders = append(aaplOrders, order) + } + } +} +``` + +# Listen for Updates to Orders + +You can use Websockets to receive real-time updates about the status of your orders as they change. You can see the full documentation here. + +```python Python +from alpaca.trading.stream import TradingStream + +stream = TradingStream('api-key', 'secret-key', paper=True) + + +@conn.on(client_order_id) +async def on_msg(data): + # Print the update to the console. + print("Update for {}. Event: {}.".format(data.event)) + +stream.subscribe_trade_updates(on_msg) +# Start listening for updates. +stream.run() +``` + +```javascript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Prepare the websocket connection and subscribe to trade_updates. +alpaca.websocket.onConnect(function () { + console.log("Connected"); + client.subscribe(["trade_updates"]); + setTimeout(() => { + client.disconnect(); + }, 30 * 1000); +}); +alpaca.websocket.onDisconnect(() => { + console.log("Disconnected"); +}); +alpaca.websocket.onStateChange((newState) => { + console.log(`State changed to ${newState}`); +}); + +// Handle updates on an order you've given a Client Order ID. +const client_order_id = "my_client_order_id"; +alpaca.websocket.onOrderUpdate((orderData) => { + const event = orderData["event"]; + if (orderData["order"]["client_order_id"] == client_order_id) { + console.log(`Update for ${client_order_id}. Event: ${event}.`); + } +}); + +// Start listening for updates. +alpaca.websocket.connect(); +``` + +```csharp C# +using Alpaca.Markets; +using System; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + // Print a message to the console when the account's status changes. + public static async Task Main(string[] args) + { + // Prepare the websocket connection and subscribe to trade_updates. + var alpacaStreamingClient = Environments.Paper + .GetAlpacaStreamingClient(new SecretKey(API_KEY, API_SECRET)); + + await alpacaStreamingClient.ConnectAndAuthenticateAsync(); + alpacaStreamingClient.OnTradeUpdate += HandleTradeUpdate; + + Console.Read(); + } + + // Handle incoming trade updates. + private static void HandleTradeUpdate(ITradeUpdate update) + { + if (update.Order.ClientOrderId.Equals("my_client_order_id")) + { + Console.WriteLine($"Update for {update.Order.ClientOrderId}. Event: {update.Event}"); + } + } + } +} +``` + +```go +package main + +import ( + "fmt" + + "github.com/alpacahq/alpaca-trade-api-go/alpaca" + "github.com/alpacahq/alpaca-trade-api-go/stream" +) + +func init() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") +} + +func main() { + if err := stream.Register(alpaca.TradeUpdates, tradeHandler); err != nil { + panic(err) + } +} + +func tradeHandler(msg interface{}) { + tradeupdate := msg.(alpaca.TradeUpdate) + if tradeupdate.Order.ClientOrderID == "my_client_order_id" { + fmt.Printf("Update for {}. Event: {}.\n", tradeupdate.Order.ClientOrderID, tradeupdate.Event) + } +} +``` + +# FAQ + +**What should I do if I receive a timeout message from Alpaca when submitting an order?** + +The order may have been sent to the market for execution. You should not attempt to resend the order or mark the timed-out order as canceled until confirmed by Alpaca Support or the trading team. Before taking any action on the timed-out order you should check the broker dashboard and contact Alpaca support.\ +Please contact our Support Team at [support@alpaca.markets](mailto:support@alpaca.markets) to verify if the order was successfully submitted and routed to the market. \ No newline at end of file diff --git a/DesktopBot/Documents/Alpaca/working-with-positions.md b/DesktopBot/Documents/Alpaca/working-with-positions.md new file mode 100644 index 0000000..fc459e1 --- /dev/null +++ b/DesktopBot/Documents/Alpaca/working-with-positions.md @@ -0,0 +1,117 @@ +# Working with /positions + +You can view the positions in your portfolio by making a `GET` request to the `/v2/positions` endpoint. If you specify a symbol, you’ll see only your position for the associated stock. + +```python +from alpaca.trading.client import TradingClient + +trading_client = TradingClient('api-key', 'secret-key') + +# Get our position in AAPL. +aapl_position = trading_client.get_open_position('AAPL') + +# Get a list of all of our positions. +portfolio = trading_client.get_all_positions() + +# Print the quantity of shares for each position. +for position in portfolio: + print("{} shares of {}".format(position.qty, position.symbol)) +``` + +```javascript +const Alpaca = require("@alpacahq/alpaca-trade-api"); +const alpaca = new Alpaca(); + +// Get our position in AAPL. +aaplPosition = alpaca.getPosition("AAPL"); + +// Get a list of all of our positions. +alpaca.getPositions().then((portfolio) => { + // Print the quantity of shares for each position. + portfolio.forEach(function (position) { + console.log(`${position.qty} shares of ${position.symbol}`); + }); +}); +``` + +```csharp +using Alpaca.Markets; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace CodeExamples +{ + internal static class Example + { + private static string API_KEY = "your_api_key"; + + private static string API_SECRET = "your_secret_key"; + + public static async Task Main(string[] args) + { + // First, open the API connection + var client = Alpaca.Markets.Environments.Paper + .GetAlpacaTradingClient(new SecretKey(API_KEY, API_SECRET)); + + // Get our position in AAPL. + var aaplPosition = await client.GetPositionAsync("AAPL"); + + // Get a list of all of our positions. + var positions = await client.ListPositionsAsync(); + + // Print the quantity of shares for each position. + foreach (var position in positions) + { + Console.WriteLine($"{position.Quantity} shares of {position.Symbol}."); + } + + Console.Read(); + } + } +} +``` + +```go +package main + +import ( + "fmt" + "github.com/alpacahq/alpaca-trade-api-go/alpaca" +) + +func init() { + alpaca.SetBaseUrl("https://paper-api.alpaca.markets") +} + +func main() { + // Get our position in AAPL. + aapl_position, err := alpaca.GetPosition("AAPL") + if err != nil { + fmt.Println("No AAPL position.") + } else { + fmt.Printf("AAPL position: %v shares.\n", aapl_position.Qty) + } + + // Get a list of all of our positions. + positions, err := alpaca.ListPositions() + if err != nil { + fmt.Println("No positions found.") + } else { + // Print the quantity of shares for each position. + for _, position := range positions { + fmt.Printf("%v shares in %s", position.Qty, position.Symbol) + } + } +} +``` + +The current price reflected will be based on the following: + +**4:00 am ET - 9:30 am ET** - Last trade based on the premarket + +**9:30 am ET - 4pm ET** - Last trade + +**4:00 pm ET - 10:00 pm ET** - Last trade based on after-hours trading + +**10 pm ET - 4:00 am ET next trading day** - Official closing price from the primary exchange at 4 pm ET. \ No newline at end of file diff --git a/DesktopBot/Documents/Code/gemini-code-1779811123559.cs b/DesktopBot/Documents/Code/gemini-code-1779811123559.cs new file mode 100644 index 0000000..759f3f2 --- /dev/null +++ b/DesktopBot/Documents/Code/gemini-code-1779811123559.cs @@ -0,0 +1,19 @@ +using Alpaca.Markets; + +public class AlpacaTradingService +{ + private IAlpacaTradingClient _tradingClient; + private IAlpacaDataClient _dataClient; + + public void Initialize(string apiKey, string apiSecret, bool isPaper) + { + // Seleziona l'ambiente corretto + var environments = isPaper ? Environments.Paper : Environments.Live; + + // Inizializza il client per il trading (ordini, posizioni, conto) + _tradingClient = environments.GetAlpacaTradingClient(new SecretKey(apiKey, apiSecret)); + + // Inizializza il client per i dati di mercato (prezzi, candele) + _dataClient = environments.GetAlpacaDataClient(new SecretKey(apiKey, apiSecret)); + } +} \ No newline at end of file diff --git a/DesktopBot/Documents/Code/gemini-code-1779811125057.cs b/DesktopBot/Documents/Code/gemini-code-1779811125057.cs new file mode 100644 index 0000000..186fc13 --- /dev/null +++ b/DesktopBot/Documents/Code/gemini-code-1779811125057.cs @@ -0,0 +1,8 @@ +public async Task GetAvailableEquityAsync() +{ + // Recupera le informazioni del conto + IAccount account = await _tradingClient.GetAccountAsync(); + + // Ritorna il valore totale del portafoglio o la liquidità disponibile + return account.Equity; +} \ No newline at end of file diff --git a/DesktopBot/Documents/Code/gemini-code-1779811126091.cs b/DesktopBot/Documents/Code/gemini-code-1779811126091.cs new file mode 100644 index 0000000..e41e0e1 --- /dev/null +++ b/DesktopBot/Documents/Code/gemini-code-1779811126091.cs @@ -0,0 +1,13 @@ +public async Task> GetHistoricalBarsAsync(string symbol) +{ + var request = new HistoricalBarsRequest(symbol, BarTimeFrame.Hour, Into.Past(TimeSpan.FromDays(7))) + { + // Limitiamo i dati per mantenere l'app leggera + PageSize = 50 + }; + + IMultiPage barsPage = await _dataClient.ListHistoricalBarsAsync(request); + + // Restituisce la lista di candele per il simbolo specificato + return barsPage.Items[symbol].ToList(); +} \ No newline at end of file diff --git a/DesktopBot/Documents/Code/gemini-code-1779811127546.cs b/DesktopBot/Documents/Code/gemini-code-1779811127546.cs new file mode 100644 index 0000000..fbc2992 --- /dev/null +++ b/DesktopBot/Documents/Code/gemini-code-1779811127546.cs @@ -0,0 +1,13 @@ +public async Task PlaceBracketOrderAsync(string symbol, int quantity, decimal entryPrice, decimal takeProfitPrice, decimal stopLossPrice) +{ + // 1. Ordine principale di acquisto (Buy) a mercato o limite + var entryOrder = MarketOrder.Buy(symbol, quantity); + + // 2. Configura il Bracket Order inserendo Take Profit e Stop Loss + var bracketOrder = entryOrder.TakeProfit(takeProfitPrice).StopLoss(stopLossPrice); + + // 3. Invia l'ordine ad Alpaca + IOrder order = await _tradingClient.PostOrderAsync(bracketOrder); + + Console.WriteLine($"Ordine inviato con successo. ID: {order.OrderId}"); +} \ No newline at end of file diff --git a/DesktopBot/Documents/Code/gemini-code-1779811128687.cs b/DesktopBot/Documents/Code/gemini-code-1779811128687.cs new file mode 100644 index 0000000..72be024 --- /dev/null +++ b/DesktopBot/Documents/Code/gemini-code-1779811128687.cs @@ -0,0 +1,13 @@ +public async Task HasOpenPositionAsync(string symbol) +{ + try + { + IPosition position = await _tradingClient.GetPositionAsync(symbol); + return position != null && position.Quantity > 0; + } + catch (AlpacaRestException ex) when (ex.ErrorCode == 40410000) + { + // Il codice 40410000 indica che non ci sono posizioni aperte per quel simbolo + return false; + } +} \ No newline at end of file diff --git a/DesktopBot/Engine/AutomatedBotEngine.cs b/DesktopBot/Engine/AutomatedBotEngine.cs new file mode 100644 index 0000000..5f7a773 --- /dev/null +++ b/DesktopBot/Engine/AutomatedBotEngine.cs @@ -0,0 +1,611 @@ +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)); + } + } +} diff --git a/DesktopBot/Engine/BtcUsdAlgorithm.cs b/DesktopBot/Engine/BtcUsdAlgorithm.cs new file mode 100644 index 0000000..82a9633 --- /dev/null +++ b/DesktopBot/Engine/BtcUsdAlgorithm.cs @@ -0,0 +1,459 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Alpaca.Markets; +using DesktopBot.Models; + +namespace DesktopBot.Engine +{ + // ═══════════════════════════════════════════════════════════════════════════ + // BTC/USD ADVANCED ALGORITHM — v2.0 + // Strategia combinata multi-segnale ad alta frequenza (barre da 1 minuto). + // + // ARCHITETTURA: + // ┌─────────────────────────────────────────────────────────────────────┐ + // │ 1. REGIME DETECTOR (ADX + Trend Slope) │ + // │ Classifica il mercato in: TREND_UP / TREND_DOWN / RANGING │ + // │ Attiva le strategie più adatte al regime corrente. │ + // │ │ + // │ 2. ADAPTIVE MOMENTUM ENGINE (Dual-EMA + MACD + RSI divergence) │ + // │ Confluenza di tre oscillatori su barre 1min. │ + // │ Punteggio: 0–3. Richiede >= 2 per generare segnale. │ + // │ │ + // │ 3. KALMAN FAIR-VALUE FILTER (filtro adattivo di Kalman) │ + // │ Stima il fair-value in tempo reale. Genera segnale quando │ + // │ il prezzo si discosta di > KalmanZEntry deviazioni standard. │ + // │ │ + // │ 4. VOLATILITY BREAKOUT GATE (Keltner Channel + RVOL) │ + // │ Filtra i breakout falsi richiedendo volume relativo >= 2x. │ + // │ In regime RANGING sostituisce il momentum come segnale. │ + // │ │ + // │ 5. ATR DYNAMIC POSITION SIZING (ATR-based SL/TP) │ + // │ Stop Loss = entry − 1.5 × ATR(14) │ + // │ Take Profit = entry + 2.5 × ATR(14) → Risk/Reward = 1:1.67 │ + // │ Max drawdown del segnale capped al 4% del prezzo di entrata. │ + // └─────────────────────────────────────────────────────────────────────┘ + // + // LOGICA DI DECISIONE: + // ───────────────────── + // BUY se TUTTI i seguenti: + // • Regime == TREND_UP OPPURE (Regime == RANGING e breakout bullish) + // • MomentumScore >= 2 + // • Kalman Z-Score <= −KalmanZEntry (prezzo sotto fair-value) + // • Nessuna posizione aperta + // + // SELL se UNO dei seguenti: + // • Regime == TREND_DOWN AND MomentumScore <= 0 (tutti bearish) + // • Kalman Z-Score >= +KalmanZEntry (prezzo sopra fair-value) + // • Breakout della banda Keltner inferiore con RVOL >= 2x + // • Posizione aperta con perdita >= 4% (trailing risk-gate) + // + // PARAMETRI PRE-CALIBRATI PER BTC/USD 1-MINUTO: + // ─────────────────────────────────────────────── + // FastEma = 5 (5 minuti) + // SlowEma = 20 (20 minuti) + // RsiPeriod = 14 + // MacdFast = 8, MacdSlow = 21, MacdSignal = 5 + // AdxPeriod = 14 (soglia trend: ADX > 25) + // AtrPeriod = 14 + // KalmanDelta = 5e-6 + // KalmanZ_Entry = 1.8 + // KeltnerPeriod = 20, KeltnerMult = 2.0 + // RvolMin = 2.0x + // ═══════════════════════════════════════════════════════════════════════════ + + public sealed class BtcUsdAlgorithm + { + // ── Parametri fissi ottimizzati per BTC/USD 1min ───────────────────── + private const int FastEmaPeriod = 5; + private const int SlowEmaPeriod = 20; + private const int RsiPeriod = 14; + private const int MacdFast = 8; + private const int MacdSlow = 21; + private const int MacdSignal = 5; + private const int AdxPeriod = 14; + private const int AtrPeriod = 14; + private const int KeltnerPeriod = 20; + private const double KalmanDelta = 5e-6; + private const double KalmanObsVar = 1.0; + private const double KalmanZEntry = 1.8; + private const double KalmanZExit = 0.3; + private const decimal KeltnerMult = 2.0m; + private const decimal RvolMin = 2.0m; + private const decimal AdxTrendLevel = 25m; // ADX > 25 → mercato in trend + private const decimal MaxDrawdownPct = 0.04m; // 4% max drawdown + + // ── Stato del filtro di Kalman (persistente tra tick) ──────────────── + private double _kalmanX; + private double _kalmanP = 1.0; + private readonly Queue _spreads = new Queue(25); + private double _kalmanStd = 1.0; + + // ── Stato del regime corrente ──────────────────────────────────────── + public MarketRegime CurrentRegime { get; private set; } = MarketRegime.Unknown; + + // ── Ultimo segnale prodotto ────────────────────────────────────────── + public BtcSignalResult LastResult { get; private set; } = new BtcSignalResult(); + + // ════════════════════════════════════════════════════════════════════ + // METODO PRINCIPALE + // ════════════════════════════════════════════════════════════════════ + + /// + /// Analizza le barre più recenti e restituisce il segnale di trading. + /// Richiede almeno 50 barre da 1 minuto per calcolare tutti gli indicatori. + /// + public BtcSignalResult Analyze(List bars) + { + var result = new BtcSignalResult(); + + if (bars == null || bars.Count < 50) + { + result.Reason = "Dati insufficienti (richiesti min. 50 bar 1min)"; + result.Type = SignalType.None; + return LastResult = result; + } + + var closes = bars.Select(b => b.Close).ToList(); + var highs = bars.Select(b => b.High).ToList(); + var lows = bars.Select(b => b.Low).ToList(); + var volumes = bars.Select(b => b.Volume).ToList(); + decimal price = closes.Last(); + + // 1. ── REGIME DETECTOR ────────────────────────────────────────── + decimal adx = CalculateAdx(bars, AdxPeriod); + decimal trendSlope = CalculateTrendSlope(closes, 10); // slope EMA20 ultimi 10 bar + CurrentRegime = DetectRegime(adx, trendSlope); + result.Regime = CurrentRegime; + result.Adx = adx; + + // 2. ── ADAPTIVE MOMENTUM ──────────────────────────────────────── + int momentumScore = 0; + string momentumInfo = ""; + + // 2a. EMA dual-cross + var fastEma = CalculateEma(closes, FastEmaPeriod); + var slowEma = CalculateEma(closes, SlowEmaPeriod); + bool emaBull = fastEma.Count >= 2 && slowEma.Count >= 2 + && fastEma[fastEma.Count - 2] <= slowEma[slowEma.Count - 2] && fastEma.Last() > slowEma.Last(); + bool emaBear = fastEma.Count >= 2 && slowEma.Count >= 2 + && fastEma[fastEma.Count - 2] >= slowEma[slowEma.Count - 2] && fastEma.Last() < slowEma.Last(); + bool emaAbove = fastEma.Count > 0 && slowEma.Count > 0 + && fastEma.Last() > slowEma.Last(); + + if (emaAbove) { momentumScore++; momentumInfo += "EMA↑ "; } + else { momentumScore--; momentumInfo += "EMA↓ "; } + + // 2b. MACD + decimal macdLine, signalLine; + CalculateMacd(closes, MacdFast, MacdSlow, MacdSignal, out macdLine, out signalLine); + bool macdBull = macdLine > signalLine && macdLine > 0; + bool macdBear = macdLine < signalLine && macdLine < 0; + if (macdBull) { momentumScore++; momentumInfo += "MACD↑ "; } + else if (macdBear) { momentumScore--; momentumInfo += "MACD↓ "; } + + // 2c. RSI con divergenza + decimal rsi = CalculateRsi(closes, RsiPeriod); + bool rsiBull = rsi > 45 && rsi < 65; // RSI in territorio bullish ma non overbought + bool rsiBear = rsi < 55 && rsi > 35; // RSI in territorio bearish ma non oversold + bool rsiExtremeBull = rsi < 30; // Ipervenduto → opportunità contrarian + bool rsiExtremeBear = rsi > 70; // Ipercomprato → uscita + if (rsiBull || rsiExtremeBull) { momentumScore++; momentumInfo += $"RSI({rsi:F0})↑ "; } + else if (rsiExtremeBear) { momentumScore--; momentumInfo += $"RSI({rsi:F0})↓ "; } + + result.MomentumScore = momentumScore; + result.MomentumInfo = momentumInfo.Trim(); + result.Rsi = rsi; + result.MacdLine = macdLine; + + // 3. ── KALMAN FAIR-VALUE ───────────────────────────────────────── + // Feed tutte le barre nel filtro (aggiorna lo stato persistente) + foreach (var bar in bars) UpdateKalman(bar.Close); + double fairValue = _kalmanX; + double zScore = _kalmanStd > 0 + ? (double)(price - (decimal)fairValue) / _kalmanStd + : 0.0; + result.FairValue = (decimal)fairValue; + result.KalmanZ = zScore; + + // 4. ── VOLATILITY BREAKOUT ─────────────────────────────────────── + decimal atr = CalculateAtr(bars, AtrPeriod); + decimal midEma = CalculateEma(closes.Skip(closes.Count - KeltnerPeriod - 5).ToList(), KeltnerPeriod).Last(); + decimal upper = midEma + KeltnerMult * atr; + decimal lower = midEma - KeltnerMult * atr; + + decimal avgVol = volumes.Count > 1 + ? volumes.Take(volumes.Count - 1) + .Select(v => (decimal)v).Average() + : 1m; + decimal rvol = avgVol > 0 ? (decimal)volumes.Last() / avgVol : 0m; + bool vbBull = price > upper && rvol >= RvolMin; + bool vbBear = price < lower; + result.Rvol = rvol; + result.Upper = upper; + result.Lower = lower; + + // 5. ── DECISIONE FINALE ────────────────────────────────────────── + decimal sl = Math.Max(price - 1.5m * atr, price * (1 - MaxDrawdownPct)); + decimal tp = price + 2.5m * atr; + + // ── BUY conditions ─── + bool buyByTrend = CurrentRegime == MarketRegime.TrendUp && momentumScore >= 2 && zScore <= -KalmanZEntry; + bool buyByBreakout = CurrentRegime == MarketRegime.Ranging && vbBull && momentumScore >= 1; + bool buyByKalman = zScore <= -(KalmanZEntry + 0.5) && momentumScore >= 1; // Strong Kalman segnale standalone + + // ── SELL conditions ─── + bool sellByTrend = CurrentRegime == MarketRegime.TrendDown && momentumScore <= -1; + bool sellByKalman = zScore >= KalmanZEntry; + bool sellByBreakout = vbBear && rvol >= RvolMin; + + if (buyByTrend || buyByBreakout || buyByKalman) + { + result.Type = SignalType.Buy; + if (buyByTrend) result.Reason = $"[TREND_UP] Confluenza: {momentumInfo} | Z={zScore:F2}"; + else if (buyByBreakout) result.Reason = $"[BREAKOUT] Keltner UP | RVOL={rvol:F1}x | Score={momentumScore}"; + else result.Reason = $"[KALMAN] Forte deviazione Z={zScore:F2} | {momentumInfo}"; + + result.Confidence = CalculateConfidence(momentumScore, zScore, rvol, CurrentRegime, isBuy: true); + result.StopLoss = sl; + result.TakeProfit = tp; + } + else if (sellByTrend || sellByKalman || sellByBreakout) + { + result.Type = SignalType.Sell; + if (sellByTrend) result.Reason = $"[TREND_DOWN] Momentum={momentumInfo} | ADX={adx:F1}"; + else if (sellByKalman) result.Reason = $"[KALMAN] Prezzo sopra fair-value Z={zScore:F2}"; + else result.Reason = $"[BREAKOUT_DN] Keltner DOWN | RVOL={rvol:F1}x"; + + result.Confidence = CalculateConfidence(momentumScore, zScore, rvol, CurrentRegime, isBuy: false); + } + else + { + result.Type = SignalType.Hold; + result.Reason = $"HOLD — Regime={CurrentRegime} Score={momentumScore} Z={zScore:F2} ADX={adx:F1}"; + } + + result.Price = price; + result.Atr = atr; + return LastResult = result; + } + + // ════════════════════════════════════════════════════════════════════ + // REGIME DETECTOR + // ════════════════════════════════════════════════════════════════════ + + private static MarketRegime DetectRegime(decimal adx, decimal trendSlope) + { + if (adx < AdxTrendLevel) return MarketRegime.Ranging; + if (trendSlope > 0) return MarketRegime.TrendUp; + if (trendSlope < 0) return MarketRegime.TrendDown; + return MarketRegime.Ranging; + } + + // ════════════════════════════════════════════════════════════════════ + // FILTRO DI KALMAN (persistente tra i tick) + // ════════════════════════════════════════════════════════════════════ + + private void UpdateKalman(decimal observed) + { + double z = (double)observed; + if (_kalmanX == 0) { _kalmanX = z; return; } + + double q = KalmanDelta / (1.0 - KalmanDelta); + double pPred = _kalmanP + q; + double k = pPred / (pPred + KalmanObsVar); + _kalmanX = _kalmanX + k * (z - _kalmanX); + _kalmanP = (1.0 - k) * pPred; + + _spreads.Enqueue(z - _kalmanX); + if (_spreads.Count > 20) _spreads.Dequeue(); + + if (_spreads.Count >= 2) + { + var list = _spreads.ToArray(); + double mean = list.Average(); + _kalmanStd = Math.Sqrt(list.Sum(v => (v - mean) * (v - mean)) / (list.Length - 1)); + if (_kalmanStd < 1e-9) _kalmanStd = 1.0; + } + } + + // ════════════════════════════════════════════════════════════════════ + // INDICATORI TECNICI + // ════════════════════════════════════════════════════════════════════ + + private static List CalculateEma(List prices, int period) + { + var r = new List(); + if (prices.Count < period) return r; + decimal k = 2m / (period + 1); + decimal cur = prices.Take(period).Average(); + r.Add(cur); + for (int i = period; i < prices.Count; i++) + { cur = prices[i] * k + cur * (1 - k); r.Add(cur); } + return r; + } + + private static decimal CalculateRsi(List closes, int period) + { + if (closes.Count < period + 1) return 50m; + decimal ag = 0, al = 0; + for (int i = 1; i <= period; i++) + { decimal d = closes[i] - closes[i - 1]; if (d > 0) ag += d; else al -= d; } + ag /= period; al /= period; + for (int i = period + 1; i < closes.Count; i++) + { + decimal d = closes[i] - closes[i - 1]; + ag = (ag * (period - 1) + Math.Max(d, 0)) / period; + al = (al * (period - 1) + Math.Max(-d, 0)) / period; + } + return al == 0 ? 100m : 100m - (100m / (1 + ag / al)); + } + + private static void CalculateMacd(List closes, int fast, int slow, int signal, + out decimal macdLine, out decimal signalLine) + { + macdLine = 0; signalLine = 0; + var emaF = CalculateEma(closes, fast); + var emaS = CalculateEma(closes, slow); + int len = Math.Min(emaF.Count, emaS.Count); + if (len < signal + 2) return; + var macd = new List(); + for (int i = 0; i < len; i++) + macd.Add(emaF[emaF.Count - len + i] - emaS[emaS.Count - len + i]); + var sig = CalculateEma(macd, signal); + macdLine = macd.Last(); + signalLine = sig.Count > 0 ? sig.Last() : 0; + } + + 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 tr = Math.Max(bars[i].High - bars[i].Low, + Math.Max(Math.Abs(bars[i].High - bars[i - 1].Close), + Math.Abs(bars[i].Low - bars[i - 1].Close))); + atr += tr; + } + atr /= period; + for (int i = period + 1; i < bars.Count; i++) + { + decimal tr = Math.Max(bars[i].High - bars[i].Low, + Math.Max(Math.Abs(bars[i].High - bars[i - 1].Close), + Math.Abs(bars[i].Low - bars[i - 1].Close))); + atr = (atr * (period - 1) + tr) / period; + } + return atr; + } + + /// ADX basato su True Range e Directional Movement. + private static decimal CalculateAdx(List bars, int period) + { + if (bars.Count < period * 2) return 0m; + + var dmPlus = new List(); + var dmMinus = new List(); + var trList = new List(); + + for (int i = 1; i < bars.Count; i++) + { + decimal upMove = bars[i].High - bars[i - 1].High; + decimal downMove = bars[i - 1].Low - bars[i].Low; + decimal tr = Math.Max(bars[i].High - bars[i].Low, + Math.Max(Math.Abs(bars[i].High - bars[i - 1].Close), + Math.Abs(bars[i].Low - bars[i - 1].Close))); + trList.Add(tr); + dmPlus.Add(upMove > downMove && upMove > 0 ? upMove : 0); + dmMinus.Add(downMove > upMove && downMove > 0 ? downMove : 0); + } + + // Wilder smoothing + decimal smoothTr = trList.Take(period).Sum(); + decimal smoothPlus = dmPlus.Take(period).Sum(); + decimal smoothMinus = dmMinus.Take(period).Sum(); + + var dx = new List(); + for (int i = period; i < trList.Count; i++) + { + smoothTr = smoothTr - smoothTr / period + trList[i]; + smoothPlus = smoothPlus - smoothPlus / period + dmPlus[i]; + smoothMinus = smoothMinus - smoothMinus / period + dmMinus[i]; + + if (smoothTr == 0) { dx.Add(0); continue; } + decimal diPlus = 100m * smoothPlus / smoothTr; + decimal diMinus = 100m * smoothMinus / smoothTr; + decimal diSum = diPlus + diMinus; + dx.Add(diSum == 0 ? 0 : 100m * Math.Abs(diPlus - diMinus) / diSum); + } + + return dx.Count >= period ? dx.Skip(dx.Count - period).Average() : 0m; + } + + /// Slope della EMA(period) negli ultimi n bar (normalizzato per prezzo). + private static decimal CalculateTrendSlope(List closes, int lookback) + { + var ema = CalculateEma(closes, SlowEmaPeriod); + if (ema.Count < lookback + 1) return 0m; + decimal first = ema[ema.Count - lookback - 1]; + decimal last = ema.Last(); + return first == 0 ? 0m : (last - first) / first; + } + + // ════════════════════════════════════════════════════════════════════ + // CONFIDENCE SCORE (0–100) + // ════════════════════════════════════════════════════════════════════ + + private static int CalculateConfidence(int momentumScore, double z, + decimal rvol, MarketRegime regime, bool isBuy) + { + int score = 50; + score += momentumScore * 10; + score += (int)(Math.Min(Math.Abs(z), 3.0) * 8); + score += (int)(Math.Min((double)rvol, 4.0) * 4); + if (isBuy && regime == MarketRegime.TrendUp) score += 10; + if (!isBuy && regime == MarketRegime.TrendDown) score += 10; + return Math.Min(Math.Max(score, 1), 99); + } + } + + // ══════════════════════════════════════════════════════════════════════ + // TIPI DI SUPPORTO + // ══════════════════════════════════════════════════════════════════════ + + /// Regime del mercato classificato dal Regime Detector. + public enum MarketRegime { Unknown, TrendUp, TrendDown, Ranging } + + /// Risultato dettagliato prodotto da BtcUsdAlgorithm.Analyze(). + public sealed class BtcSignalResult + { + public SignalType Type { get; set; } = SignalType.None; + public string Reason { get; set; } = ""; + public int Confidence { get; set; } + public decimal Price { get; set; } + public decimal StopLoss { get; set; } + public decimal TakeProfit { get; set; } + + // Diagnostica indicatori + public MarketRegime Regime { get; set; } + public decimal Adx { get; set; } + public int MomentumScore { get; set; } + public string MomentumInfo { get; set; } = ""; + public decimal Rsi { get; set; } + public decimal MacdLine { get; set; } + public decimal FairValue { get; set; } + public double KalmanZ { get; set; } + public decimal Rvol { get; set; } + public decimal Upper { get; set; } + public decimal Lower { get; set; } + public decimal Atr { get; set; } + + /// Stringa diagnostica completa per il log operativo. + public string ToLogString() => + $"[{Type}] {Reason} | " + + $"Regime={Regime} ADX={Adx:F1} Score={MomentumScore} " + + $"RSI={Rsi:F1} MACD={MacdLine:F2} " + + $"FV={FairValue:F1} Z={KalmanZ:F2} RVOL={Rvol:F1}x ATR={Atr:F1} " + + $"Conf={Confidence}%"; + } +} diff --git a/DesktopBot/Engine/PositionRiskManager.cs b/DesktopBot/Engine/PositionRiskManager.cs new file mode 100644 index 0000000..c48c422 --- /dev/null +++ b/DesktopBot/Engine/PositionRiskManager.cs @@ -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 _logInfo; + private readonly Action _logWarn; + + public PositionRiskManager( + ITradingService service, + BotConfiguration config, + Action logInfo, + Action logWarn) + { + _svc = service; + _cfg = config; + _logInfo = logInfo; + _logWarn = logWarn; + } + + // ── Verifica se è possibile aprire un nuovo ordine ────────────────── + /// true = ok aprire, false = bloccato dal risk manager + public async Task 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 ──────────── + /// + /// Ritorna la quantità da comprare in base al capitale disponibile e + /// alla quota assegnabile a questa specifica posizione. + /// + public async Task 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 positions; + try { positions = await _svc.GetBotPositionsAsync(); } + catch { positions = new List(); } + + 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) ─── + /// + /// 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. + /// + 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}"); + } + } + } +} diff --git a/DesktopBot/Engine/StrategyAdvisor.cs b/DesktopBot/Engine/StrategyAdvisor.cs new file mode 100644 index 0000000..668dd1e --- /dev/null +++ b/DesktopBot/Engine/StrategyAdvisor.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using DesktopBot.Models; + +namespace DesktopBot.Engine +{ + /// + /// Motore di raccomandazione strategia. + /// Per ogni combinazione asset-class / volatilità / tipologia di mercato restituisce + /// la strategia ottimale e i parametri pre-calibrati. + /// + /// Logica ispirata agli approcci professionali: + /// Crypto → alta volatilità 24/7 → VOLATILITY_BREAKOUT + /// Equity → orario di mercato → EMA_CROSSOVER o MACD + /// ETF → bassa volatilità → KALMAN_MEAN_REVERSION + /// FX/FX-alike → mean-reverting → RSI + /// + public static class StrategyAdvisor + { + // ── Profili per asset class ───────────────────────────────────────── + + private static readonly Dictionary _profiles + = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // ── Crypto ────────────────────────────────────────────────────── + ["crypto"] = new[] + { + new StrategyProfile( + TradingStrategy.VOLATILITY_BREAKOUT, + "Volatility Breakout (1min)", + "Strategia ad alta frequenza per crypto: breakout su barre da 1 minuto con filtro RVOL e CVD.", + "🚀", "#FF6D00", + isRecommended: true, + cfg => + { + cfg.KeltnerPeriod = 20; + cfg.KeltnerMultiplier = 2.0m; + cfg.RvolMinThreshold = 2.5m; // Soglia RVOL più alta per crypto ad alta volatilità + cfg.AtrStopMultiplier = 1.0m; + cfg.CheckIntervalSeconds = 60; // Tick ogni 60 secondi + cfg.AnalysisTimeFrame = Alpaca.Markets.BarTimeFrame.Minute; // Barre da 1 minuto + cfg.HistoricalBarCount = 200; // 200 barre = 3.3 ore di storico + cfg.StopLossPercentage = 0.03m; // 3% SL per volatilità crypto + cfg.TakeProfitPercentage = 0.06m; // 6% TP, ratio 1:2 + }), + + new StrategyProfile( + TradingStrategy.KALMAN_MEAN_REVERSION, + "Kalman Mean Reversion (1min)", + "Mean-reversion adattiva su crypto range-bound con analisi a 1 minuto.", + "🔬", "#40C4FF", + isRecommended: false, + cfg => + { + cfg.KalmanDelta = 1e-5; + cfg.KalmanObservationVariance = 1.0; + cfg.KalmanEntryZScore = 2.0; + cfg.KalmanExitZScore = 0.25; + cfg.CheckIntervalSeconds = 120; + cfg.AnalysisTimeFrame = Alpaca.Markets.BarTimeFrame.Minute; + cfg.HistoricalBarCount = 300; // 5 ore di storico + cfg.StopLossPercentage = 0.03m; + cfg.TakeProfitPercentage = 0.06m; + }), + + new StrategyProfile( + TradingStrategy.EMA_CROSSOVER, + "EMA Crossover (1min)", + "Trend-following veloce su barre da 1 minuto — ottimale per BTC/USD in trend forte.", + "⚡", "#00E676", + isRecommended: false, + cfg => + { + cfg.FastEmaPeriod = 5; // EMA veloce 5 periodi (5 minuti) + cfg.SlowEmaPeriod = 15; // EMA lenta 15 periodi (15 minuti) + cfg.CheckIntervalSeconds = 60; // Tick ogni 60 secondi + cfg.AnalysisTimeFrame = Alpaca.Markets.BarTimeFrame.Minute; + cfg.HistoricalBarCount = 150; // 2.5 ore di storico + cfg.StopLossPercentage = 0.025m; // 2.5% SL + cfg.TakeProfitPercentage = 0.05m; // 5% TP + }), + }, + + // ── US Equity ──────────────────────────────────────────────────── + ["us_equity"] = new[] + { + new StrategyProfile( + TradingStrategy.EMA_CROSSOVER, + "EMA Crossover", + "Strategia trend-following classica, ottimale su azioni con trend chiari.", + "📈", "#00E676", + isRecommended: true, + cfg => + { + cfg.FastEmaPeriod = 9; + cfg.SlowEmaPeriod = 21; + cfg.CheckIntervalSeconds = 60; + cfg.StopLossPercentage = 0.02m; + cfg.TakeProfitPercentage = 0.04m; + }), + + new StrategyProfile( + TradingStrategy.MACD, + "MACD", + "Momentum con istogramma MACD — eccellente su mid/large cap con trend.", + "⚡", "#EA80FC", + isRecommended: false, + cfg => + { + cfg.MacdFastPeriod = 12; + cfg.MacdSlowPeriod = 26; + cfg.MacdSignalPeriod = 9; + cfg.CheckIntervalSeconds = 60; + cfg.StopLossPercentage = 0.02m; + cfg.TakeProfitPercentage = 0.04m; + }), + + new StrategyProfile( + TradingStrategy.RSI, + "RSI Reversal", + "Ottimale per azioni in range o in correzione — compra ipervenduto.", + "📊", "#FFFF00", + isRecommended: false, + cfg => + { + cfg.RsiPeriod = 14; + cfg.RsiOversoldThreshold = 30m; + cfg.RsiOverboughtThreshold = 70m; + cfg.CheckIntervalSeconds = 120; + cfg.StopLossPercentage = 0.02m; + cfg.TakeProfitPercentage = 0.035m; + }), + }, + + // ── ETF (mappato come us_equity con parametri diversi) ─────────── + ["etf"] = new[] + { + new StrategyProfile( + TradingStrategy.KALMAN_MEAN_REVERSION, + "Kalman Mean Reversion", + "ETF tendono a mean-revert: il filtro Kalman ne stima il fair value.", + "🔬", "#40C4FF", + isRecommended: true, + cfg => + { + cfg.KalmanDelta = 1e-5; + cfg.KalmanObservationVariance = 0.8; + cfg.KalmanEntryZScore = 1.8; + cfg.KalmanExitZScore = 0.2; + cfg.CheckIntervalSeconds = 300; + cfg.StopLossPercentage = 0.015m; + cfg.TakeProfitPercentage = 0.03m; + }), + + new StrategyProfile( + TradingStrategy.EMA_CROSSOVER, + "EMA Crossover", + "Trend-following conservativo su ETF ad alta liquidità.", + "📈", "#00E676", + isRecommended: false, + cfg => + { + cfg.FastEmaPeriod = 12; + cfg.SlowEmaPeriod = 26; + cfg.CheckIntervalSeconds = 120; + cfg.StopLossPercentage = 0.015m; + cfg.TakeProfitPercentage = 0.03m; + }), + }, + }; + + // ── Fallback: usa equity ───────────────────────────────────────────── + private static StrategyProfile[] GetProfiles(string assetClass) + { + if (string.IsNullOrEmpty(assetClass)) return _profiles["us_equity"]; + var key = NormalizeClass(assetClass); + return _profiles.TryGetValue(key, out var p) ? p : _profiles["us_equity"]; + } + + /// Restituisce tutti i profili strategia disponibili per la classe dell'asset. + public static StrategyProfile[] GetAvailableProfiles(string assetClass) + => GetProfiles(assetClass); + + /// Restituisce il profilo raccomandato (primo marcato IsRecommended). + public static StrategyProfile GetRecommended(string assetClass) + { + var profiles = GetProfiles(assetClass); + foreach (var p in profiles) + if (p.IsRecommended) return p; + return profiles[0]; + } + + /// + /// Applica i parametri ottimali del profilo raccomandato alla configurazione. + /// Mantiene Symbol, Quantity e Name invariati. + /// + public static void ApplyOptimalConfig(BotConfiguration cfg, string assetClass) + { + var profile = GetRecommended(assetClass); + cfg.Strategy = profile.Strategy; + profile.ApplyParameters(cfg); + } + + private static string NormalizeClass(string assetClass) + { + // Alpaca restituisce "us_equity", "crypto", ecc. + var lower = assetClass.ToLowerInvariant().Replace("-", "_"); + if (lower.Contains("crypto")) return "crypto"; + if (lower.Contains("etf")) return "etf"; + return lower; + } + } + + // ── StrategyProfile ────────────────────────────────────────────────────── + + /// Descrive una strategia disponibile per una classe di asset, con i parametri pre-calibrati. + public sealed class StrategyProfile + { + public TradingStrategy Strategy { get; } + public string DisplayName { get; } + public string Description { get; } + public string Icon { get; } + public string AccentColor { get; } + public bool IsRecommended { get; } + + private readonly Action _applyParameters; + + public StrategyProfile( + TradingStrategy strategy, + string displayName, + string description, + string icon, + string accentColor, + bool isRecommended, + Action applyParameters) + { + Strategy = strategy; + DisplayName = displayName; + Description = description; + Icon = icon; + AccentColor = accentColor; + IsRecommended = isRecommended; + _applyParameters = applyParameters; + } + + public void ApplyParameters(BotConfiguration cfg) => _applyParameters(cfg); + } +} diff --git a/DesktopBot/Files/Icon/favicon.ico b/DesktopBot/Files/Icon/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d74895ea6bbcd6bf9c141bed0ce6c3393fc27345 GIT binary patch literal 162081 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�{Hx5&oov?n?6KM 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

+{w-2%=YC?MBs5pvy@Akl9n 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 ui4t|eqGV| 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 + + + + diff --git a/DesktopBot/MainWindow.xaml.cs b/DesktopBot/MainWindow.xaml.cs new file mode 100644 index 0000000..d1d22b3 --- /dev/null +++ b/DesktopBot/MainWindow.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace DesktopBot +{ + ///

+ /// Logica di interazione per MainWindow.xaml + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + } + } +} diff --git a/DesktopBot/Models/AssetSearchResult.cs b/DesktopBot/Models/AssetSearchResult.cs new file mode 100644 index 0000000..6537c09 --- /dev/null +++ b/DesktopBot/Models/AssetSearchResult.cs @@ -0,0 +1,19 @@ +namespace DesktopBot.Models +{ + /// + /// Risultato semplificato della ricerca asset su Alpaca. + /// + public class AssetSearchResult + { + public string Symbol { get; set; } + public string Name { get; set; } + public string AssetClass { get; set; } + public string Exchange { get; set; } + public bool Tradable { get; set; } + + /// True quando questo asset è selezionato nel Market Watch panel. + public bool IsSelected { get; set; } + + public override string ToString() => $"{Symbol} — {Name}"; + } +} diff --git a/DesktopBot/Models/BotConfiguration.cs b/DesktopBot/Models/BotConfiguration.cs new file mode 100644 index 0000000..baf25be --- /dev/null +++ b/DesktopBot/Models/BotConfiguration.cs @@ -0,0 +1,236 @@ +using System; +using Alpaca.Markets; + +namespace DesktopBot.Models +{ + /// + /// Modello per la configurazione del bot + /// + public class BotConfiguration + { + /// + /// Simbolo ticker da tradare (es. "AAPL", "TSLA") + /// + public string Symbol { get; set; } = "AAPL"; + + /// + /// Quantità di azioni da acquistare per operazione + /// + public int Quantity { get; set; } = 1; + + /// + /// Intervallo in secondi tra ogni controllo del bot + /// + public int CheckIntervalSeconds { get; set; } = 60; + + /// + /// Timeframe delle barre da analizzare: Minute (1min), Hour (1h), Day (1d). + /// Default: Day per compatibilità con strategie esistenti. + /// Per crypto ad alta frequenza, usare Minute. + /// + public BarTimeFrame AnalysisTimeFrame { get; set; } = BarTimeFrame.Day; + + /// + /// Numero di periodi (barre) da richiedere per l'analisi. + /// Default: automatico in base alla strategia. + /// Per strategie su 1min: 200-300 barre = 3-5 ore di storico. + /// + public int HistoricalBarCount { get; set; } = 0; // 0 = automatico + + /// + /// Percentuale di Stop Loss (es. 0.02 = 2%) + /// + public decimal StopLossPercentage { get; set; } = 0.02m; + + /// + /// Percentuale di Take Profit (es. 0.05 = 5%) + /// + public decimal TakeProfitPercentage { get; set; } = 0.05m; + + /// + /// Percentuale massima del portfolio da utilizzare per operazione (0.1 = 10%) + /// + public decimal MaxPositionSizePercent { get; set; } = 0.10m; + + // ─── EMA CROSSOVER ──────────────────────────────────────────────── + /// Periodo EMA veloce (default 9) + public int FastEmaPeriod { get; set; } = 9; + + /// Periodo EMA lenta (default 21) + public int SlowEmaPeriod { get; set; } = 21; + + // ─── RSI ────────────────────────────────────────────────────────── + /// Periodo RSI (default 14) + public int RsiPeriod { get; set; } = 14; + + /// Soglia ipervenduto (default 30) + public decimal RsiOversoldThreshold { get; set; } = 30; + + /// Soglia ipercomprato (default 70) + public decimal RsiOverboughtThreshold { get; set; } = 70; + + // ─── MACD ───────────────────────────────────────────────────────── + /// Periodo EMA veloce MACD (default 12) + public int MacdFastPeriod { get; set; } = 12; + + /// Periodo EMA lenta MACD (default 26) + public int MacdSlowPeriod { get; set; } = 26; + + /// Periodo Signal Line MACD (default 9) + public int MacdSignalPeriod { get; set; } = 9; + + // ─── VOLATILITY BREAKOUT (Keltner + RVOL + CVD) ────────────────── + /// Periodo ATR e canale Keltner (default 20) + public int KeltnerPeriod { get; set; } = 20; + + /// + /// Moltiplicatore ATR per larghezza canale Keltner (default 2.0) + /// Bande = EMA ± KeltnerMultiplier × ATR + /// + public decimal KeltnerMultiplier { get; set; } = 2.0m; + + /// + /// Soglia minima RVOL (Volume Relativo) per validare il breakout (default 2.0 = 2× il volume medio). + /// Un valore elevato filtra le rotture false prive di supporto volumetrico istituzionale. + /// + public decimal RvolMinThreshold { get; set; } = 2.0m; + + /// + /// Moltiplicatore ATR per lo Stop Loss nei breakout (default 1.0). + /// SL = punto di breakout ± AtrStopMultiplier × ATR + /// + public decimal AtrStopMultiplier { get; set; } = 1.0m; + + // ─── KALMAN MEAN REVERSION ──────────────────────────────────────── + /// + /// Fattore di decadimento δ (delta) del rumore di transizione Kalman (default 1e-5). + /// W = δ/(1-δ) × I₂ — controlla la velocità di adattamento dell'hedge ratio. + /// Valori piccoli → hedge ratio stabile; valori grandi → adattamento aggressivo. + /// + public double KalmanDelta { get; set; } = 1e-5; + + /// + /// Varianza del rumore di osservazione Kalman (default 1.0). + /// Modella l'incertezza della misura di prezzo rispetto al fair value stimato. + /// + public double KalmanObservationVariance { get; set; } = 1.0; + + /// + /// Soglia Z-Score per entrata Long (default -2.0): il prezzo è sufficientemente sotto il fair value. + /// + public double KalmanEntryZScore { get; set; } = 2.0; + + /// + /// Soglia Z-Score per uscita / take profit (default 0.25): spread converge alla media. + /// + public double KalmanExitZScore { get; set; } = 0.25; + + // ─── RISK MANAGEMENT ───────────────────────────────────────────────── + /// + /// Numero massimo di posizioni aperte contemporaneamente (default 3). + /// Se il numero di posizioni aperte raggiunge questo limite, non verranno aperti nuovi ordini. + /// + public int MaxOpenPositions { get; set; } = 3; + + /// + /// Numero massimo di posizioni aperte per lo stesso asset (default 10). + /// Impedisce di accumulare troppe posizioni sullo stesso simbolo. + /// + public int MaxOpenPositionsPerAsset { get; set; } = 10; + + /// + /// Percentuale massima del capitale totale che può essere allocata in posizioni aperte (default 30%). + /// Evita di impegnare troppo capitale su tante posizioni piccole. + /// Es: con equity=100k e MaxCapitalAllocatedPercent=0.30, non si può superare 30k allocati. + /// + public decimal MaxCapitalAllocatedPercent { get; set; } = 0.30m; + + /// + /// Soglia di profitto percentuale oltre la quale si considera di chiudere la posizione (lock profit). + /// Es: 0.03 = chiude quando la posizione è in profitto di almeno il 3%. + /// Se 0, il take-profit è gestito solo dall'ordine limit TP. + /// + public decimal ProfitLockPercent { get; set; } = 0.03m; + + /// + /// Perdita massima percentuale tollerata su una singola posizione aperta prima di tagliarla (default 2%). + /// Questo agisce come stop-loss dinamico di seconda linea, indipendente dall'ordine stop piazzato. + /// Es: 0.02 = chiude se la posizione perde più del 2% rispetto all'ingresso. + /// + public decimal MaxLossPercent { get; set; } = 0.02m; + + /// + /// Se true, sposta lo stop-loss al punto di pareggio (break-even) quando il profitto supera ProfitLockPercent/2. + /// Protegge il capitale una volta che la posizione è in verde. + /// + public bool UseBreakEvenStop { get; set; } = true; + + // ─── ASSET-STRATEGY OPTIMIZATION ───────────────────────────────────── + /// + /// Strategia raccomandata dal StrategyAdvisor per la classe d'asset corrente. + /// Viene aggiornata automaticamente quando si associa un asset al bot. + /// + public TradingStrategy? RecommendedStrategy { get; set; } = null; + + /// + /// True quando la strategia attiva corrisponde a quella raccomandata per l'asset. + /// Usato dalla UI per mostrare il badge "Ottimizzato". + /// + public bool IsOptimizedForAsset + => RecommendedStrategy.HasValue && Strategy == RecommendedStrategy.Value; + + /// + /// True quando la configurazione è stata bloccata a seguito dell'associazione con un asset. + /// Dopo il lock, i parametri critici della strategia non possono essere modificati liberamente. + /// + public bool IsLocked { get; set; } = false; + + /// + /// Timestamp del momento in cui la configurazione è stata bloccata. + /// Null se la configurazione non è ancora stata bloccata. + /// + public DateTime? LockedAt { get; set; } = null; + + // ─── STRATEGIA ATTIVA ───────────────────────────────────────────── + /// Strategia di trading da utilizzare + public TradingStrategy Strategy { get; set; } = TradingStrategy.EMA_CROSSOVER; + + /// + /// Blocca la configurazione corrente, impedendo modifiche successive alla strategia. + /// Questo viene chiamato quando un bot viene associato a un asset. + /// + public void Lock() + { + if (!IsLocked) + { + IsLocked = true; + LockedAt = DateTime.Now; + } + } + + // ─── LOGGING CONFIGURATION ────────────────────────────────────────── + /// Configurazione dei limiti per i log e i dati storici. + public LoggingConfiguration LoggingConfig { get; set; } = new LoggingConfiguration(); + } + + /// + /// Tipo di strategia di trading + /// + public enum TradingStrategy + { + /// Crossover di medie mobili esponenziali (EMA veloce vs EMA lenta) + EMA_CROSSOVER, + + /// RSI (Relative Strength Index) – ipervenduto/ipercomprato + RSI, + + /// MACD – divergenza tra EMA veloce e lenta con signal line + MACD, + + /// Volatility Breakout – Keltner Channel con filtri RVOL e CVD (Cap.3 del report) + VOLATILITY_BREAKOUT, + + /// Mean Reversion con Filtro di Kalman – stima dinamica del fair value (Cap.4 del report) + KALMAN_MEAN_REVERSION + } +} diff --git a/DesktopBot/Models/BotInstance.cs b/DesktopBot/Models/BotInstance.cs new file mode 100644 index 0000000..a1de63e --- /dev/null +++ b/DesktopBot/Models/BotInstance.cs @@ -0,0 +1,74 @@ +using System; + +namespace DesktopBot.Models +{ + /// + /// Rappresenta una singola istanza di bot configurata e associata a un asset. + /// L'ID univoco viene incorporato nel ClientOrderId di ogni ordine Alpaca, + /// così le posizioni aperte rimangono associabili al bot anche dopo un riavvio. + /// + /// Formato ClientOrderId: "BOT_{BotId}_{timestamp}" + /// Esempio: "BOT_3f2a1b_20250610T143022" + /// + /// VINCOLO: Ogni bot è strettamente associato a un SINGOLO asset con una SINGOLA strategia. + /// Una volta assegnato l'asset, il bot viene bloccato (IsAssetLocked=true) e non è più possibile + /// modificare Symbol, AssetClass o Strategy. + /// + public class BotInstance + { + /// ID univoco del bot (6 hex chars, stabile nel tempo) + public string BotId { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 6); + + /// Nome leggibile assegnato dall'utente (es. "EMA AAPL #1") + public string Name { get; set; } = "Nuovo Bot"; + + /// Simbolo ticker associato (es. "AAPL", "MSFT") + public string Symbol { get; set; } = ""; + + /// Nome dell'asset completo (es. "Apple Inc.") + public string AssetName { get; set; } = ""; + + /// Classe dell'asset: "us_equity", "crypto", ecc. + public string AssetClass { get; set; } = "us_equity"; + + /// + /// True quando il bot è stato associato a un asset e la configurazione è bloccata. + /// Dopo il lock, Symbol, AssetClass e Strategy non possono più essere modificati. + /// + public bool IsAssetLocked { get; set; } = false; + + /// + /// Timestamp del momento in cui il bot è stato associato all'asset e bloccato. + /// Null se il bot non è ancora stato associato a un asset. + /// + public DateTime? LockedAt { get; set; } = null; + + /// Bot attivo o sospeso + public bool IsEnabled { get; set; } = true; + + /// Configurazione della strategia + public BotConfiguration Config { get; set; } = new BotConfiguration(); + + /// Data e ora di creazione del bot + public DateTime CreatedAt { get; set; } = DateTime.Now; + + /// Note libere dell'utente + public string Notes { get; set; } = ""; + + /// Colore badge nella UI (hex, es. "#00E676") + public string BadgeColor { get; set; } = "#00E676"; + + /// + /// Blocca il bot associandolo definitivamente all'asset corrente. + /// Dopo questa operazione, Symbol, AssetClass e Strategy diventano immutabili. + /// + public void LockToAsset() + { + if (!IsAssetLocked && !string.IsNullOrWhiteSpace(Symbol)) + { + IsAssetLocked = true; + LockedAt = DateTime.Now; + } + } + } +} diff --git a/DesktopBot/Models/BotInstanceStore.cs b/DesktopBot/Models/BotInstanceStore.cs new file mode 100644 index 0000000..49a05f5 --- /dev/null +++ b/DesktopBot/Models/BotInstanceStore.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Newtonsoft.Json; + +namespace DesktopBot.Models +{ + /// + /// Gestisce la persistenza delle istanze di bot in un file JSON locale. + /// Percorso: %AppData%\TradingBot\bots.json + /// + public static class BotInstanceStore + { + private static readonly string StorePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "TradingBot", + "bots.json" + ); + + /// Carica tutti i bot salvati dal disco. Ritorna lista vuota se il file non esiste. + public static List Load() + { + try + { + if (!File.Exists(StorePath)) + return new List(); + + var json = File.ReadAllText(StorePath, Encoding.UTF8); + return JsonConvert.DeserializeObject>(json) ?? new List(); + } + catch + { + return new List(); + } + } + + /// Salva tutti i bot su disco. + public static void Save(IEnumerable bots) + { + try + { + var dir = Path.GetDirectoryName(StorePath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + var json = JsonConvert.SerializeObject(new List(bots), Formatting.Indented); + File.WriteAllText(StorePath, json, Encoding.UTF8); + } + catch + { + // Logging silenzioso: non bloccare la UI per errori di I/O + } + } + } +} + diff --git a/DesktopBot/Models/BotLogEntry.cs b/DesktopBot/Models/BotLogEntry.cs new file mode 100644 index 0000000..5c215d1 --- /dev/null +++ b/DesktopBot/Models/BotLogEntry.cs @@ -0,0 +1,34 @@ +using System; + +namespace DesktopBot.Models +{ + /// + /// Modello per un log del bot + /// + public class BotLogEntry + { + public DateTime Timestamp { get; set; } + public LogLevel Level { get; set; } + public string Message { get; set; } + public string Details { get; set; } + + public BotLogEntry(LogLevel level, string message, string details = "") + { + Timestamp = DateTime.Now; + Level = level; + Message = message; + Details = details; + } + } + + /// + /// Livello di log + /// + public enum LogLevel + { + Info, + Success, + Warning, + Error + } +} diff --git a/DesktopBot/Models/BotTradeRecord.cs b/DesktopBot/Models/BotTradeRecord.cs new file mode 100644 index 0000000..60e7562 --- /dev/null +++ b/DesktopBot/Models/BotTradeRecord.cs @@ -0,0 +1,47 @@ +using System; + +namespace DesktopBot.Models +{ + /// + /// Record di un trade (aperto o chiuso) effettuato dal bot. + /// + public class BotTradeRecord + { + public string Symbol { get; set; } + public string Side { get; set; } // "BUY" / "SELL" + public decimal EntryPrice { get; set; } + public decimal ExitPrice { get; set; } // 0 se ancora aperta + public decimal Quantity { get; set; } + public decimal StopLoss { get; set; } + public decimal TakeProfit { get; set; } + public decimal PnL { get; set; } // 0 se ancora aperta + public DateTime OpenedAt { get; set; } + public DateTime? ClosedAt { get; set; } + public bool IsOpen => ClosedAt == null; + public int Confidence { get; set; } + + /// PnL percentuale sull'investimento iniziale. + public decimal PnLPercent => + EntryPrice > 0 && Quantity > 0 + ? PnL / (EntryPrice * Quantity) * 100m + : 0m; + + /// + /// Badge testuale: APERTA / +X.XX% / -X.XX% + /// + public string PnLBadge => + IsOpen + ? "APERTA" + : $"{PnL:+0.00;-0.00;0.00} ({PnLPercent:+0.0;-0.0;0.0}%)"; + + /// Stringa di stato colorabile via DataTrigger. + public string Status => IsOpen ? "APERTA" : (PnL >= 0 ? "CHIUSA +" : "CHIUSA -"); + + /// + /// Categoria PnL per i DataTrigger XAML ("open" / "profit" / "loss"). + /// + public string PnLCategory => + IsOpen ? "open" : + PnL > 0 ? "profit" : "loss"; + } +} diff --git a/DesktopBot/Models/LoggingConfiguration.cs b/DesktopBot/Models/LoggingConfiguration.cs new file mode 100644 index 0000000..f96370b --- /dev/null +++ b/DesktopBot/Models/LoggingConfiguration.cs @@ -0,0 +1,56 @@ +using System; + +namespace DesktopBot.Models +{ + /// + /// Configurazione globale per i limiti di memorizzazione dei log e dati storici. + /// I valori sono conservativamente alti per mantenere il massimo di informazioni possibili. + /// + public class LoggingConfiguration + { + /// + /// Numero massimo di elementi nel log del bot (BotLog). + /// Default: 5000 (mantiene ~8-10 ore di trading con CheckIntervalSeconds=60). + /// + public int MaxBotLogEntries { get; set; } = 5000; + + /// + /// Numero massimo di elementi nello storico trade (TradeHistory). + /// Default: 2000 (mantiene mesi di operazioni). + /// + public int MaxTradeHistoryEntries { get; set; } = 2000; + + /// + /// Numero massimo di elementi nel log attività della dashboard (ActivityLog). + /// Default: 5000 (cronologia completa della sessione di trading). + /// + public int MaxActivityLogEntries { get; set; } = 5000; + + /// + /// Numero massimo di elementi nel log live globale (LiveLog). + /// Default: 10000 (log dettagliato completo di tutte le operazioni). + /// + public int MaxLiveLogEntries { get; set; } = 10000; + + /// + /// Numero massimo di punti dati nel grafico dei prezzi (PriceData). + /// Default: 3000 (mantiene ore di dati a 1min, giorni a 15min). + /// + public int MaxPriceDataPoints { get; set; } = 3000; + + /// + /// Clona la configurazione corrente. + /// + public LoggingConfiguration Clone() + { + return new LoggingConfiguration + { + MaxBotLogEntries = this.MaxBotLogEntries, + MaxTradeHistoryEntries = this.MaxTradeHistoryEntries, + MaxActivityLogEntries = this.MaxActivityLogEntries, + MaxLiveLogEntries = this.MaxLiveLogEntries, + MaxPriceDataPoints = this.MaxPriceDataPoints + }; + } + } +} diff --git a/DesktopBot/Models/TradingSignal.cs b/DesktopBot/Models/TradingSignal.cs new file mode 100644 index 0000000..4de9f21 --- /dev/null +++ b/DesktopBot/Models/TradingSignal.cs @@ -0,0 +1,40 @@ +using System; +using Alpaca.Markets; + +namespace DesktopBot.Models +{ + /// + /// Segnale di trading generato da una strategia. + /// + public class TradingSignal + { + public SignalType Type { get; set; } + public string Symbol { get; set; } + public decimal Price { get; set; } + public DateTime Timestamp { get; set; } + public string Reason { get; set; } + /// Confidenza del segnale (0–100). + public int Confidence { get; set; } + /// + /// Livello di Stop Loss calcolato dalla strategia (0 = non impostato, + /// viene usato il default percentuale di BotConfiguration). + /// + public decimal StopLoss { get; set; } + /// + /// Livello di Take Profit calcolato dalla strategia (0 = non impostato). + /// + public decimal TakeProfit { get; set; } + + /// Lato dell'ordine Alpaca derivato dal tipo di segnale. + public OrderSide Side => Type == SignalType.Sell ? OrderSide.Sell : OrderSide.Buy; + } + + public enum SignalType + { + None, + Buy, + Sell, + Hold + } +} + diff --git a/DesktopBot/Properties/AssemblyInfo.cs b/DesktopBot/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ad29ba2 --- /dev/null +++ b/DesktopBot/Properties/AssemblyInfo.cs @@ -0,0 +1,52 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// Le informazioni generali relative a un assembly sono controllate dal seguente +// set di attributi. Modificare i valori di questi attributi per modificare le informazioni +// associate a un assembly. +[assembly: AssemblyTitle("DesktopBot")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DesktopBot")] +[assembly: AssemblyCopyright("Copyright © 2026")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Se si imposta ComVisible su false, i tipi in questo assembly non saranno visibili +// ai componenti COM. Se è necessario accedere a un tipo in questo assembly da +// COM, impostare su true l'attributo ComVisible per tale tipo. +[assembly: ComVisible(false)] + +//Per iniziare a creare applicazioni localizzabili, impostare +//CultureYouAreCodingWith nel file .csproj +//all'interno di un . Ad esempio, se si utilizza l'inglese (Stati Uniti) +//nei file di origine, impostare su en-US. Rimuovere quindi il commento dall'attributo +//NeutralResourceLanguage riportato di seguito. Aggiornare "en-US" nella +//riga sottostante in modo che corrisponda all'impostazione UICulture nel file di progetto. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //dove si trovano i dizionari delle risorse specifiche del tema + //(da usare se nella pagina non viene trovata una risorsa, + // oppure nei dizionari delle risorse dell'applicazione) + ResourceDictionaryLocation.SourceAssembly //dove si trova il dizionario delle risorse generiche + //(da usare se nella pagina non viene trovata una risorsa, + // nell'applicazione o nei dizionari delle risorse specifiche del tema) +)] + + +// Le informazioni sulla versione di un assembly sono costituite dai seguenti quattro valori: +// +// Versione principale +// Versione secondaria +// Numero di build +// Revisione +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DesktopBot/Properties/Resources.Designer.cs b/DesktopBot/Properties/Resources.Designer.cs new file mode 100644 index 0000000..3d93172 --- /dev/null +++ b/DesktopBot/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// Codice generato da uno strumento. +// Versione runtime:4.0.30319.42000 +// +// Le modifiche apportate a questo file possono causare un comportamento non corretto e andranno perse se +// il codice viene rigenerato. +// +//------------------------------------------------------------------------------ + +namespace DesktopBot.Properties +{ + + + /// + /// Classe di risorse fortemente tipizzata per la ricerca di stringhe localizzate e così via. + /// + // Questa classe è stata generata automaticamente dalla classe StronglyTypedResourceBuilder + // tramite uno strumento quale ResGen o Visual Studio. + // Per aggiungere o rimuovere un membro, modificare il file .ResX, quindi eseguire di nuovo ResGen + // con l'opzione /str oppure ricompilare il progetto VS. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Restituisce l'istanza di ResourceManager memorizzata nella cache e usata da questa classe. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DesktopBot.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Esegue l'override della proprietà CurrentUICulture del thread corrente per tutte + /// le ricerche di risorse che utilizzano questa classe di risorse fortemente tipizzata. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/DesktopBot/Properties/Resources.resx b/DesktopBot/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/DesktopBot/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/DesktopBot/Properties/Settings.Designer.cs b/DesktopBot/Properties/Settings.Designer.cs new file mode 100644 index 0000000..e216878 --- /dev/null +++ b/DesktopBot/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DesktopBot.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/DesktopBot/Properties/Settings.settings b/DesktopBot/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/DesktopBot/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/DesktopBot/Services/AlpacaPingService.cs b/DesktopBot/Services/AlpacaPingService.cs new file mode 100644 index 0000000..740e73d --- /dev/null +++ b/DesktopBot/Services/AlpacaPingService.cs @@ -0,0 +1,101 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace DesktopBot.Services +{ + /// + /// Misura la latenza (ping) verso l'endpoint di trading Alpaca. + /// Usa una richiesta GET su /v2/clock che è leggera e non richiede + /// autenticazione nella versione pubblica. + /// + public class AlpacaPingService : IDisposable + { + // endpoint pubblico leggero di Alpaca (non richiede auth, risponde 200) + private const string PingUrlPaper = "https://paper-api.alpaca.markets/v2/clock"; + private const string PingUrlLive = "https://api.alpaca.markets/v2/clock"; + + private readonly HttpClient _http; + private CancellationTokenSource _cts; + private bool _isPaper = true; + + public event EventHandler PingCompleted; + + public AlpacaPingService() + { + _http = new HttpClient { Timeout = TimeSpan.FromSeconds(8) }; + } + + public void SetEnvironment(bool isPaper) => _isPaper = isPaper; + + public void Start(int intervalSeconds = 10) + { + _cts?.Cancel(); + _cts = new CancellationTokenSource(); + _ = PingLoopAsync(intervalSeconds, _cts.Token); + } + + public void Stop() => _cts?.Cancel(); + + private async Task PingLoopAsync(int intervalSeconds, CancellationToken ct) + { + // Prima misura immediata + await MeasureAsync(); + while (!ct.IsCancellationRequested) + { + try { await Task.Delay(TimeSpan.FromSeconds(intervalSeconds), ct); } + catch (OperationCanceledException) { return; } + await MeasureAsync(); + } + } + + private async Task MeasureAsync() + { + string url = _isPaper ? PingUrlPaper : PingUrlLive; + var sw = Stopwatch.StartNew(); + try + { + using var req = new HttpRequestMessage(HttpMethod.Head, url); + using var resp = await _http.SendAsync(req).ConfigureAwait(false); + sw.Stop(); + PingCompleted?.Invoke(this, new PingResult + { + LatencyMs = (int)sw.ElapsedMilliseconds, + IsSuccess = resp.IsSuccessStatusCode || (int)resp.StatusCode == 403 // 403 = raggiungibile ma senza auth + }); + } + catch + { + sw.Stop(); + PingCompleted?.Invoke(this, new PingResult + { + LatencyMs = -1, + IsSuccess = false + }); + } + } + + public void Dispose() + { + _cts?.Cancel(); + _http?.Dispose(); + } + } + + public class PingResult + { + /// Latenza in millisecondi. -1 se non raggiungibile. + public int LatencyMs { get; set; } + public bool IsSuccess { get; set; } + + public PingStatus Status => + !IsSuccess ? PingStatus.Offline : + LatencyMs < 80 ? PingStatus.Good : + LatencyMs < 250 ? PingStatus.Fair : + PingStatus.Poor; + } + + public enum PingStatus { Good, Fair, Poor, Offline } +} diff --git a/DesktopBot/Services/AlpacaTradingService.cs b/DesktopBot/Services/AlpacaTradingService.cs new file mode 100644 index 0000000..80b4634 --- /dev/null +++ b/DesktopBot/Services/AlpacaTradingService.cs @@ -0,0 +1,541 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Alpaca.Markets; +using DesktopBot.Models; +using Newtonsoft.Json.Linq; + +namespace DesktopBot.Services +{ + /// + /// Implementazione del servizio di trading per Alpaca Markets API v2. + /// + /// Conformità documentazione Alpaca: + /// - Autenticazione: SecretKey (header APCA-API-KEY-ID / APCA-API-SECRET-KEY) + /// - Paper Trading: Environments.Paper → paper-api.alpaca.markets + /// - Live Trading: Environments.Live → api.alpaca.markets + /// - Market Data: data.alpaca.markets (sempre, sia paper che live) + /// - Feed dati: "iex" (piano Basic gratuito; "sip" richiede Algo Trader Plus) + /// - Rate limit: 200 req/min Market Data, ~100 req/min Trading (Basic plan) + /// + /// + /// Implementazione locale di IBar per dati crypto deserializzati dall'API REST. + /// + internal sealed class SimpleBar : IBar + { + public string Symbol { get; set; } + public DateTime TimeUtc { get; set; } + public decimal Open { get; set; } + public decimal High { get; set; } + public decimal Low { get; set; } + public decimal Close { get; set; } + public decimal Volume { get; set; } + public decimal Vwap { get; set; } + public ulong TradeCount { get; set; } + } + + public class AlpacaTradingService : ITradingService + { + private IAlpacaTradingClient _tradingClient; + private IAlpacaDataClient _dataClient; + private IAlpacaCryptoDataClient _cryptoDataClient; + private string _apiKey; + private string _apiSecret; + private bool _isInitialized; + + private static readonly HttpClient _httpClient = new HttpClient(); + private const string CryptoDataBaseUrl = "https://data.alpaca.markets"; + + /// Contatore e rate-limiter per le chiamate API Alpaca. + public ApiCallCounterService ApiCounter { get; } = new ApiCallCounterService(); + + /// + /// Inizializza i client per il trading e i dati di mercato. + /// Autenticazione tramite API Key/Secret (header APCA-API-KEY-ID / APCA-API-SECRET-KEY). + /// + public void Initialize(string apiKey, string apiSecret, bool isPaper) + { + if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(apiSecret)) + throw new ArgumentException("API Key e Secret sono obbligatori"); + + // Paper: paper-api.alpaca.markets (Trading) + data.alpaca.markets (Market Data) + // Live: api.alpaca.markets (Trading) + data.alpaca.markets (Market Data) + var environments = isPaper ? Environments.Paper : Environments.Live; + var secretKey = new SecretKey(apiKey, apiSecret); + + _apiKey = apiKey; + _apiSecret = apiSecret; + + _tradingClient = environments.GetAlpacaTradingClient(secretKey); + _dataClient = environments.GetAlpacaDataClient(secretKey); + _cryptoDataClient = environments.GetAlpacaCryptoDataClient(secretKey); + + _isInitialized = true; + } + + /// + /// Recupera l'equity disponibile del conto. + /// + public async Task GetAvailableEquityAsync() + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetAvailableEquity").ConfigureAwait(false); + IAccount account = await _tradingClient.GetAccountAsync(); + return account.Equity ?? 0m; + } + + /// + /// Recupera le informazioni complete del conto. + /// + public async Task GetAccountAsync() + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetAccount").ConfigureAwait(false); + return await _tradingClient.GetAccountAsync(); + } + + /// + /// Recupera le barre storiche per un simbolo. + /// Per simboli crypto (contengono '/') usa l'endpoint REST v1beta3/crypto/us/bars + /// con paginazione automatica. Per simboli equity usa il feed IEX. + /// + public async Task> GetHistoricalBarsAsync(string symbol, BarTimeFrame timeframe, int barCount) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.MarketData, "GetHistoricalBars").ConfigureAwait(false); + + bool isCrypto = symbol.Contains("/") || + symbol.Equals("BTCUSD", StringComparison.OrdinalIgnoreCase) || + symbol.Equals("BTC/USD", StringComparison.OrdinalIgnoreCase); + + if (isCrypto) + return await GetCryptoHistoricalBarsAsync(symbol, timeframe, barCount).ConfigureAwait(false); + + // --- Equity: usa IAlpacaDataClient + feed IEX --- + DateTime endTime = DateTime.UtcNow; + DateTime startTime = CalculateStartTime(endTime, timeframe, (int)(barCount * 1.5)); + var interval = new Interval(startTime, endTime); + var request = new HistoricalBarsRequest(symbol, timeframe, interval) + { + Feed = MarketDataFeed.Iex + }; + IPage page = await _dataClient.ListHistoricalBarsAsync(request).ConfigureAwait(false); + return page.Items.ToList(); + } + + /// + /// Recupera barre storiche crypto tramite chiamata diretta REST + /// GET /v1beta3/crypto/us/bars con paginazione automatica. + /// + private async Task> GetCryptoHistoricalBarsAsync( + string symbol, BarTimeFrame timeframe, int barCount) + { + string apiSymbol = NormalizeCryptoSymbol(symbol); + + // Margine del 50% per compensare ore notturne/festive senza candele + DateTime end = DateTime.UtcNow; + DateTime start = CalculateStartTime(end, timeframe, (int)(barCount * 1.5)); + + string tf = BarTimeFrameToString(timeframe); + string startStr = start.ToString("yyyy-MM-ddTHH:mm:ssZ"); + string endStr = end.ToString("yyyy-MM-ddTHH:mm:ssZ"); + + var allBars = new List(); + string nextToken = null; + + do + { + string url = string.Format( + "{0}/v1beta3/crypto/us/bars?symbols={1}&timeframe={2}&start={3}&end={4}&limit=1000&sort=asc", + CryptoDataBaseUrl, + Uri.EscapeDataString(apiSymbol), + tf, + Uri.EscapeDataString(startStr), + Uri.EscapeDataString(endStr)); + + if (nextToken != null) + url += "&page_token=" + Uri.EscapeDataString(nextToken); + + using (var reqMsg = new HttpRequestMessage(HttpMethod.Get, url)) + { + reqMsg.Headers.Add("APCA-API-KEY-ID", _apiKey); + reqMsg.Headers.Add("APCA-API-SECRET-KEY", _apiSecret); + reqMsg.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + HttpResponseMessage resp = await _httpClient.SendAsync(reqMsg).ConfigureAwait(false); + string json = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (!resp.IsSuccessStatusCode) + throw new InvalidOperationException( + string.Format("Alpaca crypto bars API error {0}: {1}", (int)resp.StatusCode, json)); + + JObject root = JObject.Parse(json); + nextToken = root["next_page_token"]?.Value(); + + JToken barsToken = root["bars"]?[apiSymbol]; + if (barsToken != null) + { + foreach (JToken b in barsToken) + { + allBars.Add(new SimpleBar + { + Symbol = apiSymbol, + TimeUtc = b["t"].Value().ToUniversalTime(), + Open = b["o"].Value(), + High = b["h"].Value(), + Low = b["l"].Value(), + Close = b["c"].Value(), + Volume = b["v"].Value(), + Vwap = b["vw"] != null ? b["vw"].Value() : 0m, + TradeCount = b["n"] != null ? (ulong)b["n"].Value() : 0UL + }); + } + } + } + } + while (nextToken != null && allBars.Count < barCount * 2); + + // Restituisce le ultime barCount barre (le più recenti) + if (allBars.Count > barCount) + return allBars.Skip(allBars.Count - barCount).ToList(); + return allBars; + } + + private static string NormalizeCryptoSymbol(string symbol) + { + if (symbol.Equals("BTCUSD", StringComparison.OrdinalIgnoreCase)) return "BTC/USD"; + if (symbol.Equals("ETHUSD", StringComparison.OrdinalIgnoreCase)) return "ETH/USD"; + return symbol; + } + + private static string BarTimeFrameToString(BarTimeFrame timeframe) + { + if (timeframe == BarTimeFrame.Minute) return "1Min"; + if (timeframe == BarTimeFrame.Hour) return "1Hour"; + if (timeframe == BarTimeFrame.Day) return "1Day"; + return timeframe.ToString(); + } + + private static DateTime CalculateStartTime(DateTime end, BarTimeFrame timeframe, int count) + { + if (timeframe == BarTimeFrame.Minute) return end.AddMinutes(-count); + if (timeframe == BarTimeFrame.Hour) return end.AddHours(-count); + return end.AddDays(-count); + } + + /// + /// Piazza un ordine bracket con Take Profit e Stop Loss. + /// Conforme alla documentazione Alpaca: MarketOrder + TakeProfit + StopLoss. + /// + public async Task PlaceBracketOrderAsync(string symbol, decimal quantity, decimal entryPrice, + decimal takeProfitPrice, decimal stopLossPrice, OrderSide side) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "PlaceBracketOrder").ConfigureAwait(false); + + // Usa OrderQuantity.Fractional per crypto (es. 0.001 BTC), intero per equity + var orderQty = quantity == Math.Floor(quantity) + ? OrderQuantity.FromInt64((long)quantity) + : OrderQuantity.Fractional(quantity); + + var entryOrder = side == OrderSide.Buy + ? MarketOrder.Buy(symbol, orderQty) + : MarketOrder.Sell(symbol, orderQty); + + var bracketOrder = entryOrder + .TakeProfit(takeProfitPrice) + .StopLoss(stopLossPrice); + + return await _tradingClient.PostOrderAsync(bracketOrder); + } + + public async Task PlaceMarketOrderAsync(string symbol, decimal quantity, OrderSide side) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "PlaceMarketOrder").ConfigureAwait(false); + var orderQty = quantity == Math.Floor(quantity) + ? OrderQuantity.FromInt64((long)quantity) + : OrderQuantity.Fractional(quantity); + var order = side == OrderSide.Buy + ? MarketOrder.Buy(symbol, orderQty) + : MarketOrder.Sell(symbol, orderQty); + order.Duration = TimeInForce.Gtc; + order.ClientOrderId = "BOT_" + Guid.NewGuid().ToString("N").Substring(0, 16); + return await _tradingClient.PostOrderAsync(order); + } + + public async Task PlaceStopOrderAsync(string symbol, decimal quantity, decimal stopPrice, OrderSide side) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "PlaceStopOrder").ConfigureAwait(false); + var orderQty = quantity == Math.Floor(quantity) + ? OrderQuantity.FromInt64((long)quantity) + : OrderQuantity.Fractional(quantity); + var order = side == OrderSide.Buy + ? StopOrder.Buy(symbol, orderQty, stopPrice) + : StopOrder.Sell(symbol, orderQty, stopPrice); + order.Duration = TimeInForce.Gtc; + order.ClientOrderId = "BOT_" + Guid.NewGuid().ToString("N").Substring(0, 16); + return await _tradingClient.PostOrderAsync(order); + } + + public async Task PlaceLimitOrderAsync(string symbol, decimal quantity, decimal limitPrice, OrderSide side) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "PlaceLimitOrder").ConfigureAwait(false); + var orderQty = quantity == Math.Floor(quantity) + ? OrderQuantity.FromInt64((long)quantity) + : OrderQuantity.Fractional(quantity); + var order = side == OrderSide.Buy + ? LimitOrder.Buy(symbol, orderQty, limitPrice) + : LimitOrder.Sell(symbol, orderQty, limitPrice); + order.Duration = TimeInForce.Gtc; + order.ClientOrderId = "BOT_" + Guid.NewGuid().ToString("N").Substring(0, 16); + return await _tradingClient.PostOrderAsync(order); + } + + /// + /// Verifica se esiste una posizione aperta per un simbolo. + /// Il codice 40410000 indica assenza di posizione (non è un errore reale). + /// + public async Task HasOpenPositionAsync(string symbol) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "HasOpenPosition").ConfigureAwait(false); + try + { + IPosition position = await _tradingClient.GetPositionAsync(symbol); + return position != null && Math.Abs(position.Quantity) > 0; + } + catch (RestClientErrorException ex) when (ex.ErrorCode == 40410000) + { + return false; + } + } + + /// + /// Recupera la posizione aperta per un simbolo specifico. + /// Ritorna null se non esiste (codice 40410000 = no position). + /// + public async Task GetPositionAsync(string symbol) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetPosition").ConfigureAwait(false); + try + { + return await _tradingClient.GetPositionAsync(symbol); + } + catch (RestClientErrorException ex) when (ex.ErrorCode == 40410000) + { + return null; + } + } + + /// + /// Recupera tutte le posizioni aperte. + /// + public async Task> GetAllPositionsAsync() + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetAllPositions").ConfigureAwait(false); + return await _tradingClient.ListPositionsAsync(); + } + + /// + /// Recupera l'ultimo prezzo disponibile per un simbolo. + /// Per crypto usa IAlpacaCryptoDataClient.GetLatestBarAsync (endpoint /v1beta3/crypto/us/latest/bars). + /// Per equity usa IAlpacaDataClient.GetLatestTradeAsync con feed IEX. + /// + public async Task GetLatestPriceAsync(string symbol) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.MarketData, "GetLatestPrice").ConfigureAwait(false); + + bool isCrypto = symbol.Contains("/") || + symbol.Equals("BTCUSD", StringComparison.OrdinalIgnoreCase) || + symbol.Equals("BTC/USD", StringComparison.OrdinalIgnoreCase); + + if (isCrypto) + { + string apiSymbol = NormalizeCryptoSymbol(symbol); + // ListLatestBarsAsync restituisce un dizionario symbol -> IBar + var listRequest = new LatestDataListRequest(new[] { apiSymbol }); + var barsDict = await _cryptoDataClient.ListLatestBarsAsync(listRequest).ConfigureAwait(false); + if (barsDict != null && barsDict.ContainsKey(apiSymbol)) + return barsDict[apiSymbol].Close; + throw new InvalidOperationException( + string.Format("Nessun dato latest disponibile per {0}", apiSymbol)); + } + + // Equity: feed IEX gratuito + var request = new LatestMarketDataRequest(symbol) + { + Feed = MarketDataFeed.Iex + }; + var latestTrade = await _dataClient.GetLatestTradeAsync(request).ConfigureAwait(false); + return latestTrade.Price; + } + + /// + /// Recupera solo le posizioni aperte originate da ordini piazzati dal bot. + /// Strategia: lista gli ordini recenti (filled, lato Buy) con ClientOrderId + /// che inizia per "BOT_", ne estrae i simboli univoci e incrocia con le + /// posizioni effettivamente aperte su Alpaca. + /// + public async Task> GetBotPositionsAsync() + { + EnsureInitialized(); + + // 1. Posizioni live + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetBotPositions_Pos").ConfigureAwait(false); + var allPositions = await _tradingClient.ListPositionsAsync().ConfigureAwait(false); + if (allPositions == null || allPositions.Count == 0) + return new List(); + + // 2. Ordini recenti (ultimi 500 filled) per trovare quelli del bot + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetBotPositions_Ord").ConfigureAwait(false); + var ordersRequest = new ListOrdersRequest + { + OrderStatusFilter = OrderStatusFilter.Closed, + LimitOrderNumber = 500 + }; + IReadOnlyList recentOrders; + try + { + recentOrders = await _tradingClient.ListOrdersAsync(ordersRequest).ConfigureAwait(false); + } + catch + { + // In caso di errore fallback: restituisce tutte le posizioni + return allPositions; + } + + // 3. Simboli con almeno un ordine Buy filled piazzato dal bot + var botSymbols = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var o in recentOrders) + { + if (o.OrderSide == OrderSide.Buy && + o.OrderStatus == OrderStatus.Filled && + o.ClientOrderId != null && + o.ClientOrderId.StartsWith("BOT_", StringComparison.OrdinalIgnoreCase)) + { + botSymbols.Add(o.Symbol); + } + } + + if (botSymbols.Count == 0) + { + // Nessun ordine bot trovato: fallback su tutte le posizioni + // (compatibilità con posizioni aperte prima dell'introduzione del prefisso) + return allPositions; + } + + // 4. Filtra le posizioni per i simboli del bot + var botPositions = new List(); + foreach (var p in allPositions) + { + if (botSymbols.Contains(p.Symbol)) + botPositions.Add(p); + } + + return botPositions; + } + + /// + /// Chiude tutte le posizioni aperte. + /// + public async Task CloseAllPositionsAsync() + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "CloseAllPositions").ConfigureAwait(false); + await _tradingClient.DeleteAllPositionsAsync(); + } + + public async Task> GetOrdersAsync(OrderStatusFilter statusFilter = OrderStatusFilter.Open, int limit = 50) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetOrders").ConfigureAwait(false); + var request = new ListOrdersRequest + { + OrderStatusFilter = statusFilter, + LimitOrderNumber = limit + }; + return await _tradingClient.ListOrdersAsync(request); + } + + public async Task CancelOrderAsync(Guid orderId) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "CancelOrder").ConfigureAwait(false); + await _tradingClient.CancelOrderAsync(orderId); + } + + /// + /// Chiude una singola posizione per simbolo (DELETE /v2/positions/{symbol}). + /// Ignora silenziosamente se la posizione non esiste già (già chiusa). + /// + public async Task ClosePositionAsync(string symbol) + { + EnsureInitialized(); + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "ClosePosition").ConfigureAwait(false); + try + { + await _tradingClient.DeletePositionAsync(new DeletePositionRequest(symbol)); + } + catch (Alpaca.Markets.RestClientErrorException ex) when (ex.Message.Contains("not found")) + { + // Posizione già chiusa o non esiste; ok + } + } + + /// + /// Cerca asset tradabili per simbolo o nome (case-insensitive, starts-with / contains). + /// Usa ListAssetsAsync con AssetStatus.Active + AssetClass.UsEquity o Crypto. + /// La ricerca viene effettuata localmente sul risultato della lista asset. + /// + public async Task> SearchAssetsAsync(string query, int maxResults = 20) + { + EnsureInitialized(); + if (string.IsNullOrWhiteSpace(query)) + return new List(); + + await ApiCounter.ThrottleAsync(ApiCategory.Trading, "SearchAssets").ConfigureAwait(false); + + var request = new AssetsRequest { AssetStatus = AssetStatus.Active }; + var assets = await _tradingClient.ListAssetsAsync(request).ConfigureAwait(false); + + var q = query.Trim().ToUpperInvariant(); + + return assets + .Where(a => a.IsTradable && + (a.Symbol.StartsWith(q, StringComparison.OrdinalIgnoreCase) || + (a.Name != null && a.Name.IndexOf(q, StringComparison.OrdinalIgnoreCase) >= 0))) + .OrderBy(a => a.Symbol.StartsWith(q, StringComparison.OrdinalIgnoreCase) ? 0 : 1) + .ThenBy(a => a.Symbol) + .Take(maxResults) + .Select(a => new AssetSearchResult + { + Symbol = a.Symbol, + Name = a.Name ?? a.Symbol, + AssetClass = a.Class.ToString(), + Exchange = a.Exchange.ToString(), + Tradable = a.IsTradable + }) + .ToList(); + } + + /// + /// Verifica che il servizio sia stato inizializzato + /// + private void EnsureInitialized() + { + if (!_isInitialized) + { + throw new InvalidOperationException("Il servizio non è stato inizializzato. Chiamare Initialize() prima di usarlo."); + } + } + } +} diff --git a/DesktopBot/Services/ApiCallCounterService.cs b/DesktopBot/Services/ApiCallCounterService.cs new file mode 100644 index 0000000..7892a13 --- /dev/null +++ b/DesktopBot/Services/ApiCallCounterService.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace DesktopBot.Services +{ + /// + /// Categoria di chiamata API verso Alpaca. + /// + public enum ApiCategory + { + /// Trading API: ordini, posizioni, conto (paper-api / api.alpaca.markets) + Trading, + /// Market Data API: barre storiche, prezzi real-time (data.alpaca.markets) + MarketData + } + + /// + /// Monitora e limita la frequenza delle chiamate verso le API Alpaca. + /// + /// Limiti piano Basic (gratuito) da documentazione ufficiale Alpaca: + /// - Market Data API (Historical): 200 req/min + /// - Trading API: nessun limite documentato pubblicamente, + /// usa 100 req/min come valore conservativo + /// + /// Fonte: https://docs.alpaca.markets/docs/about-market-data-api#subscription-plans + /// + public class ApiCallCounterService : INotifyPropertyChanged + { + // ── Limiti per piano Basic ────────────────────────────────────────────── + public const int MarketDataRpmLimit = 200; // req/min – Basic plan (gratuito) + public const int TradingRpmLimit = 100; // req/min – valore conservativo + + // ── Finestra sliding di 60 secondi ───────────────────────────────────── + private static readonly TimeSpan _window = TimeSpan.FromSeconds(60); + + private readonly ConcurrentQueue _marketDataTimestamps = new ConcurrentQueue(); + private readonly ConcurrentQueue _tradingTimestamps = new ConcurrentQueue(); + + // ── Totali cumulativi ─────────────────────────────────────────────────── + private long _totalMarketData; + private long _totalTrading; + private long _throttledCalls; + + private readonly object _uiLock = new object(); + + // ── Proprietà notificabili per la UI ─────────────────────────────────── + + private int _marketDataRpm; + /// Chiamate Market Data API negli ultimi 60 secondi. + public int MarketDataRpm + { + get => _marketDataRpm; + private set { if (_marketDataRpm != value) { _marketDataRpm = value; OnPropertyChanged(); OnPropertyChanged(nameof(MarketDataUsagePercent)); } } + } + + private int _tradingRpm; + /// Chiamate Trading API negli ultimi 60 secondi. + public int TradingRpm + { + get => _tradingRpm; + private set { if (_tradingRpm != value) { _tradingRpm = value; OnPropertyChanged(); OnPropertyChanged(nameof(TradingUsagePercent)); } } + } + + /// Totale cumulativo chiamate Market Data dalla creazione. + public long TotalMarketDataCalls => Interlocked.Read(ref _totalMarketData); + + /// Totale cumulativo chiamate Trading dalla creazione. + public long TotalTradingCalls => Interlocked.Read(ref _totalTrading); + + /// Totale chiamate rallentate per rate-limit. + public long ThrottledCalls => Interlocked.Read(ref _throttledCalls); + + /// Percentuale utilizzo finestra 60s per Market Data (0–100). + public double MarketDataUsagePercent => MarketDataRpm * 100.0 / MarketDataRpmLimit; + + /// Percentuale utilizzo finestra 60s per Trading (0–100). + public double TradingUsagePercent => TradingRpm * 100.0 / TradingRpmLimit; + + /// True se Market Data è sopra il 90% del limite. + public bool IsMarketDataNearLimit => MarketDataRpm >= MarketDataRpmLimit * 0.9; + + /// True se Trading è sopra il 90% del limite. + public bool IsTradingNearLimit => TradingRpm >= TradingRpmLimit * 0.9; + + // ── Statistiche per categoria ─────────────────────────────────────────── + private readonly ConcurrentDictionary _callsByEndpoint = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + // ── Costruttore ───────────────────────────────────────────────────────── + + public ApiCallCounterService() + { + // Timer per aggiornare la UI ogni secondo + var timer = new Timer(_ => RefreshRpmCounters(), null, + TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); + } + + // ── API pubblica ──────────────────────────────────────────────────────── + + /// + /// Attende se necessario rispettando il rate limit, poi registra la chiamata. + /// Deve essere chiamato PRIMA di ogni richiesta HTTP verso Alpaca. + /// + /// Categoria della chiamata. + /// Nome endpoint per statistiche (es. "GetAccount"). + /// Token di cancellazione. + public async Task ThrottleAsync(ApiCategory category, string endpoint = "", + CancellationToken cancellationToken = default) + { + var queue = category == ApiCategory.MarketData + ? _marketDataTimestamps : _tradingTimestamps; + var limit = category == ApiCategory.MarketData + ? MarketDataRpmLimit : TradingRpmLimit; + + // Attendi finché la finestra non è libera + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + Purge(queue); + + if (queue.Count < limit) + break; + + Interlocked.Increment(ref _throttledCalls); + // Attende il tempo residuo prima che il più vecchio timestamp esca dalla finestra + DateTime oldest; + if (queue.TryPeek(out oldest)) + { + var wait = _window - (DateTime.UtcNow - oldest); + if (wait > TimeSpan.Zero) + await Task.Delay(wait, cancellationToken).ConfigureAwait(false); + } + } + + // Registra la chiamata + queue.Enqueue(DateTime.UtcNow); + if (category == ApiCategory.MarketData) + Interlocked.Increment(ref _totalMarketData); + else + Interlocked.Increment(ref _totalTrading); + + if (!string.IsNullOrEmpty(endpoint)) + _callsByEndpoint.AddOrUpdate(endpoint, 1, (_, v) => v + 1); + + RefreshRpmCounters(); + } + + /// + /// Restituisce le statistiche per endpoint (nome → conteggio totale). + /// + public IReadOnlyDictionary GetEndpointStats() + => new Dictionary(_callsByEndpoint); + + /// + /// Azzera i contatori cumulativi (non il rate-limiter sliding). + /// + public void Reset() + { + Interlocked.Exchange(ref _totalMarketData, 0); + Interlocked.Exchange(ref _totalTrading, 0); + Interlocked.Exchange(ref _throttledCalls, 0); + _callsByEndpoint.Clear(); + RefreshRpmCounters(); + OnPropertyChanged(nameof(TotalMarketDataCalls)); + OnPropertyChanged(nameof(TotalTradingCalls)); + OnPropertyChanged(nameof(ThrottledCalls)); + } + + // ── Privati ───────────────────────────────────────────────────────────── + + private void Purge(ConcurrentQueue queue) + { + var cutoff = DateTime.UtcNow - _window; + DateTime ts; + while (queue.TryPeek(out ts) && ts < cutoff) + queue.TryDequeue(out _); + } + + private void RefreshRpmCounters() + { + Purge(_marketDataTimestamps); + Purge(_tradingTimestamps); + + lock (_uiLock) + { + MarketDataRpm = _marketDataTimestamps.Count; + TradingRpm = _tradingTimestamps.Count; + } + + OnPropertyChanged(nameof(TotalMarketDataCalls)); + OnPropertyChanged(nameof(TotalTradingCalls)); + OnPropertyChanged(nameof(ThrottledCalls)); + OnPropertyChanged(nameof(IsMarketDataNearLimit)); + OnPropertyChanged(nameof(IsTradingNearLimit)); + } + + // ── INotifyPropertyChanged ────────────────────────────────────────────── + + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string name = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } +} diff --git a/DesktopBot/Services/CredentialService.cs b/DesktopBot/Services/CredentialService.cs new file mode 100644 index 0000000..2379117 --- /dev/null +++ b/DesktopBot/Services/CredentialService.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace DesktopBot.Services +{ + /// + /// Gestisce il salvataggio e il caricamento sicuro delle credenziali Alpaca. + /// Cifra con AES-256 usando una chiave derivata da MachineName + UserName. + /// + public static class CredentialService + { + private static readonly string SettingsFolder = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TradingBot"); + + private static readonly string CredentialsFile = + Path.Combine(SettingsFolder, "credentials.dat"); + + private static byte[] DeriveKey() + { + var seed = Environment.MachineName + Environment.UserName + "TradingBotSalt_v1"; + using (var sha = SHA256.Create()) + return sha.ComputeHash(Encoding.UTF8.GetBytes(seed)); + } + + public static void SaveCredentials(string apiKey, string apiSecret, bool isPaper) + { + if (!Directory.Exists(SettingsFolder)) + Directory.CreateDirectory(SettingsFolder); + + var plainText = apiKey + "|" + apiSecret + "|" + isPaper; + var plainBytes = Encoding.UTF8.GetBytes(plainText); + var key = DeriveKey(); + + using (var aes = Aes.Create()) + { + aes.Key = key; + aes.GenerateIV(); + using (var ms = new MemoryStream()) + { + ms.Write(aes.IV, 0, aes.IV.Length); + using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write)) + cs.Write(plainBytes, 0, plainBytes.Length); + File.WriteAllBytes(CredentialsFile, ms.ToArray()); + } + } + } + + public static AlpacaCredentials LoadCredentials() + { + if (!File.Exists(CredentialsFile)) + return null; + try + { + var data = File.ReadAllBytes(CredentialsFile); + var key = DeriveKey(); + using (var aes = Aes.Create()) + { + aes.Key = key; + var iv = new byte[16]; + Array.Copy(data, 0, iv, 0, 16); + aes.IV = iv; + using (var ms = new MemoryStream(data, 16, data.Length - 16)) + using (var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read)) + using (var reader = new StreamReader(cs, Encoding.UTF8)) + { + var plain = reader.ReadToEnd(); + var parts = plain.Split('|'); + if (parts.Length != 3) return null; + return new AlpacaCredentials + { + ApiKey = parts[0], + ApiSecret = parts[1], + IsPaper = bool.Parse(parts[2]) + }; + } + } + } + catch { return null; } + } + + public static bool HasCredentials() => File.Exists(CredentialsFile); + + public static void DeleteCredentials() + { + if (File.Exists(CredentialsFile)) + File.Delete(CredentialsFile); + } + } + + /// Modello credenziali Alpaca + public class AlpacaCredentials + { + public string ApiKey { get; set; } + public string ApiSecret { get; set; } + public bool IsPaper { get; set; } + } +} diff --git a/DesktopBot/Services/ITradingService.cs b/DesktopBot/Services/ITradingService.cs new file mode 100644 index 0000000..ac4dd3b --- /dev/null +++ b/DesktopBot/Services/ITradingService.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Alpaca.Markets; +using DesktopBot.Models; + +namespace DesktopBot.Services +{ + /// + /// Interfaccia per il servizio di trading - permette disaccoppiamento e testing + /// + public interface ITradingService + { + /// + /// Inizializza il client di trading con le credenziali API + /// + /// Chiave API Alpaca + /// Secret API Alpaca + /// True per Paper Trading, False per Live + void Initialize(string apiKey, string apiSecret, bool isPaper); + + /// + /// Recupera il saldo disponibile del conto + /// + Task GetAvailableEquityAsync(); + + /// + /// Recupera le informazioni complete del conto (saldo, P&L, buying power, ecc.) + /// + Task GetAccountAsync(); + + /// + /// Recupera le barre storiche (candele) per un simbolo + /// + /// Simbolo ticker (es. "AAPL") + /// Timeframe delle candele + /// Numero di barre da recuperare (convertito in intervallo temporale appropriato) + Task> GetHistoricalBarsAsync(string symbol, BarTimeFrame timeframe, int barCount); + + /// + /// Piazza un ordine bracket (con Take Profit e Stop Loss automatici). + /// NON supportato per crypto su Alpaca: usare PlaceMarketOrderAsync + SL/TP separati. + /// + Task PlaceBracketOrderAsync(string symbol, decimal quantity, decimal entryPrice, + decimal takeProfitPrice, decimal stopLossPrice, OrderSide side); + + /// + /// Piazza un ordine market semplice (buy o sell). Usare per crypto. + /// + Task PlaceMarketOrderAsync(string symbol, decimal quantity, OrderSide side); + + /// + /// Piazza un ordine stop-loss separato su una posizione aperta (per crypto). + /// + Task PlaceStopOrderAsync(string symbol, decimal quantity, decimal stopPrice, OrderSide side); + + /// + /// Piazza un ordine limit (take-profit) separato su una posizione aperta (per crypto). + /// + Task PlaceLimitOrderAsync(string symbol, decimal quantity, decimal limitPrice, OrderSide side); + + /// + /// Verifica se esiste una posizione aperta per un simbolo + /// + Task HasOpenPositionAsync(string symbol); + + /// + /// Recupera tutte le posizioni aperte + /// + Task> GetAllPositionsAsync(); + + /// + /// Recupera la posizione aperta per un simbolo specifico. + /// Ritorna null se non esiste nessuna posizione aperta per quel simbolo. + /// + Task GetPositionAsync(string symbol); + + /// + /// Recupera l'ultimo prezzo di un simbolo + /// + Task GetLatestPriceAsync(string symbol); + + /// + /// Chiude tutte le posizioni aperte + /// + Task CloseAllPositionsAsync(); + + /// + /// Recupera gli ordini (aperti, chiusi o tutti) + /// + /// Filtro stato: Open, Closed, All + /// Numero massimo di ordini da recuperare + Task> GetOrdersAsync(OrderStatusFilter statusFilter = OrderStatusFilter.Open, int limit = 50); + + /// + /// Cancella un ordine specifico per ID + /// + Task CancelOrderAsync(Guid orderId); + + /// + /// Chiude una singola posizione aperta per simbolo + /// + Task ClosePositionAsync(string symbol); + + /// + /// Recupera solo le posizioni aperte originate da ordini del bot + /// (riconosce gli ordini del bot tramite il prefisso BOT_ nel ClientOrderId). + /// Ritorna un sottoinsieme di GetAllPositionsAsync filtrato sui simboli + /// per cui esiste almeno un ordine buy filled con ClientOrderId che inizia per "BOT_". + /// + Task> GetBotPositionsAsync(); + + /// + /// Cerca asset disponibili su Alpaca per simbolo o nome. + /// Ritorna al massimo risultati tradabili. + /// + Task> SearchAssetsAsync(string query, int maxResults = 20); + } +} diff --git a/DesktopBot/Services/MarketHoursService.cs b/DesktopBot/Services/MarketHoursService.cs new file mode 100644 index 0000000..b891865 --- /dev/null +++ b/DesktopBot/Services/MarketHoursService.cs @@ -0,0 +1,113 @@ +using System; + +namespace DesktopBot.Services +{ + /// + /// Helper per determinare se un mercato è aperto in base all'asset class e all'orario locale. + /// + /// Regole approssimative (senza chiamata API aggiuntiva): + /// - US Equity : Lun–Ven 09:30–16:00 ET (UTC-5 standard / UTC-4 daylight) + /// - Crypto : sempre aperto (24/7) + /// - Altro : assume sempre aperto + /// + /// Per una verifica precisa (pre-market, post-market, festivi) usare + /// IAlpacaTradingClient.GetClockAsync() — disponibile ma costa una chiamata API. + /// + public static class MarketHoursService + { + // ── Orari NYSE/NASDAQ in Eastern Time ──────────────────────────────── + private static readonly TimeSpan MarketOpen = new TimeSpan(9, 30, 0); + private static readonly TimeSpan MarketClose = new TimeSpan(16, 0, 0); + + // Fuso Eastern (tiene conto automaticamente del DST di .NET) + private static readonly TimeZoneInfo EasternTz = + TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); + + /// + /// Ritorna true se il mercato relativo all'asset class indicata è + /// presumibilmente aperto adesso. + /// + public static bool IsMarketOpen(string assetClass) + { + if (string.IsNullOrEmpty(assetClass)) + return true; + + var cls = assetClass.ToLowerInvariant(); + + // Crypto: 24/7 + if (cls.Contains("crypto")) + return true; + + // US Equity: Lun–Ven, orario NYSE + var nowEt = TimeZoneInfo.ConvertTime(DateTime.UtcNow, EasternTz); + if (nowEt.DayOfWeek == DayOfWeek.Saturday || nowEt.DayOfWeek == DayOfWeek.Sunday) + return false; + + var tod = nowEt.TimeOfDay; + return tod >= MarketOpen && tod < MarketClose; + } + + /// + /// Testo descrittivo dello stato mercato per la UI. + /// + public static string GetMarketStatusLabel(string assetClass) + { + if (string.IsNullOrEmpty(assetClass)) + return ""; + + var cls = assetClass.ToLowerInvariant(); + if (cls.Contains("crypto")) + return "Mercato 24/7 — sempre aperto"; + + var nowEt = TimeZoneInfo.ConvertTime(DateTime.UtcNow, EasternTz); + if (nowEt.DayOfWeek == DayOfWeek.Saturday || nowEt.DayOfWeek == DayOfWeek.Sunday) + return $"Mercato chiuso (weekend) — riapertura lunedì {MarketOpen:hh\\:mm} ET"; + + var tod = nowEt.TimeOfDay; + if (tod < MarketOpen) + return $"Mercato chiuso — apertura alle {MarketOpen:hh\\:mm} ET"; + if (tod >= MarketClose) + return $"Mercato chiuso — riapertura domani {MarketOpen:hh\\:mm} ET"; + + var remaining = MarketClose - tod; + return $"Mercato aperto — chiude tra {(int)remaining.TotalHours}h {remaining.Minutes}m ET"; + } + + /// + /// Colore suggerito per il badge stato mercato. + /// + public static string GetMarketStatusColor(string assetClass) + => IsMarketOpen(assetClass) ? "#00E676" : "#FFC107"; + + /// + /// Secondi da attendere prima della prossima apertura (utile per sleep nel loop). + /// Ritorna 0 se il mercato è già aperto. + /// + public static double SecondsUntilOpen(string assetClass) + { + if (IsMarketOpen(assetClass)) return 0; + + var cls = (assetClass ?? "").ToLowerInvariant(); + if (cls.Contains("crypto")) return 0; + + var nowEt = TimeZoneInfo.ConvertTime(DateTime.UtcNow, EasternTz); + + DateTime nextOpen; + if (nowEt.TimeOfDay < MarketOpen) + { + nextOpen = nowEt.Date + MarketOpen; + } + else + { + // Dopo la chiusura → prossimo giorno lavorativo + var candidate = nowEt.Date.AddDays(1) + MarketOpen; + while (candidate.DayOfWeek == DayOfWeek.Saturday || candidate.DayOfWeek == DayOfWeek.Sunday) + candidate = candidate.AddDays(1); + nextOpen = candidate; + } + + var diff = nextOpen - nowEt; + return diff.TotalSeconds > 0 ? diff.TotalSeconds : 0; + } + } +} diff --git a/DesktopBot/Themes/DarkTheme.xaml b/DesktopBot/Themes/DarkTheme.xaml new file mode 100644 index 0000000..c3c8a9d --- /dev/null +++ b/DesktopBot/Themes/DarkTheme.xaml @@ -0,0 +1,414 @@ + + + + + + + + + #FF121214 + #FF1E1E24 + #FF27272F + #FF00E676 + #FFFF1744 + #FFEAEAEA + #FF8888A0 + #FF3A3A4A + #FF2D2D38 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DesktopBot/ViewModels/AccountViewModels.cs b/DesktopBot/ViewModels/AccountViewModels.cs new file mode 100644 index 0000000..f429c33 --- /dev/null +++ b/DesktopBot/ViewModels/AccountViewModels.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using Alpaca.Markets; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + // ─── BalanceViewModel ──────────────────────────────────────────────────────── + + public class BalanceViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private bool _isLoading; + private string _errorMessage; + private bool _hasError; + + public bool IsLoading + { + get => _isLoading; + set => SetProperty(ref _isLoading, value); + } + public string ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + public bool HasError + { + get => _hasError; + set => SetProperty(ref _hasError, value); + } + + public ObservableCollection BalanceRows { get; } + = new ObservableCollection(); + + public ICommand RefreshCommand { get; } + + public BalanceViewModel(ITradingService tradingService) + { + _tradingService = tradingService; + RefreshCommand = new RelayCommand(async _ => await LoadAsync()); + } + + public async Task LoadAsync() + { + IsLoading = true; + HasError = false; + try + { + var account = await _tradingService.GetAccountAsync(); + Application.Current.Dispatcher.Invoke(() => + { + BalanceRows.Clear(); + AddSection("Buying Power"); + AddRow("RegT Buying Power", account.RegulationBuyingPower, account.RegulationBuyingPower); + AddRow("Day Trading Buying Power", account.DayTradingBuyingPower, account.DayTradingBuyingPower); + AddRow("Effective Buying Power", account.BuyingPower, account.BuyingPower); + AddRow("Non-Marginable Buying Power", account.NonMarginableBuyingPower, account.NonMarginableBuyingPower); + AddSection("Margin"); + AddRow("Initial Margin", account.InitialMargin, account.InitialMargin); + AddRow("Maintenance Margin", account.MaintenanceMargin, account.MaintenanceMargin); + AddSection("Cash"); + AddRow("Cash", account.TradableCash, account.TradableCash); + AddSection("Positions"); + AddRow("Equity", account.LastEquity, account.Equity ?? 0m, bold: true); + AddRow("Long Market Value", account.LongMarketValue, account.LongMarketValue); + AddRow("Short Market Value", account.ShortMarketValue, account.ShortMarketValue); + }); + } + catch (Exception ex) + { + Application.Current.Dispatcher.Invoke(() => + { + HasError = true; + ErrorMessage = "Errore: " + ex.Message; + }); + } + finally + { + Application.Current.Dispatcher.Invoke(() => IsLoading = false); + } + } + + private void AddSection(string title) + => BalanceRows.Add(new BalanceRowViewModel { Label = title, IsSection = true }); + private void AddRow(string label, decimal? last, decimal? curr, bool bold = false) + => BalanceRows.Add(new BalanceRowViewModel + { + Label = label, + LastClose = last.HasValue ? "$" + last.Value.ToString("N2") : "-", + Current = curr.HasValue ? "$" + curr.Value.ToString("N2") : "-", + IsBold = bold + }); + private void AddRow(string label, decimal last, decimal curr, bool bold = false) + => BalanceRows.Add(new BalanceRowViewModel + { + Label = label, + LastClose = "$" + last.ToString("N2"), + Current = "$" + curr.ToString("N2"), + IsBold = bold + }); + } + + // ─── PositionsViewModel ────────────────────────────────────────────────────── + + public class PositionsViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private bool _isLoading; + private string _errorMessage; + private bool _hasError; + + public bool IsLoading + { + get => _isLoading; + set => SetProperty(ref _isLoading, value); + } + public string ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + public bool HasError + { + get => _hasError; + set => SetProperty(ref _hasError, value); + } + + public ObservableCollection Positions { get; } + = new ObservableCollection(); + + public ICommand RefreshCommand { get; } + public ICommand CloseAllCommand { get; } + public ICommand ClosePositionCommand { get; } + + public PositionsViewModel(ITradingService tradingService) + { + _tradingService = tradingService; + RefreshCommand = new RelayCommand(async _ => await LoadAsync()); + CloseAllCommand = new RelayCommand(async _ => + { + await _tradingService.CloseAllPositionsAsync(); + await LoadAsync(); + }); + ClosePositionCommand = new RelayCommand(async p => + { + if (p is string symbol) + { + await _tradingService.ClosePositionAsync(symbol); + await LoadAsync(); + } + }); + } + + public async Task LoadAsync() + { + IsLoading = true; + HasError = false; + try + { + var positions = await _tradingService.GetAllPositionsAsync(); + Application.Current.Dispatcher.Invoke(() => + { + Positions.Clear(); + foreach (var p in positions) + Positions.Add(new PositionViewModel(p)); + }); + } + catch (Exception ex) + { + Application.Current.Dispatcher.Invoke(() => + { + HasError = true; + ErrorMessage = "Errore: " + ex.Message; + }); + } + finally + { + Application.Current.Dispatcher.Invoke(() => IsLoading = false); + } + } + } + + // ─── OrdersViewModel ──────────────────────────────────────────────────────── + + public class OrdersViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private bool _isLoading; + private string _errorMessage; + private bool _hasError; + + public bool IsLoading + { + get => _isLoading; + set => SetProperty(ref _isLoading, value); + } + public string ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + public bool HasError + { + get => _hasError; + set => SetProperty(ref _hasError, value); + } + + public ObservableCollection Orders { get; } + = new ObservableCollection(); + + public ICommand RefreshCommand { get; } + public ICommand CancelOrderCommand { get; } + + public OrdersViewModel(ITradingService tradingService) + { + _tradingService = tradingService; + RefreshCommand = new RelayCommand(async _ => await LoadAsync()); + CancelOrderCommand = new RelayCommand(async p => + { + if (p is Guid id) + { + await _tradingService.CancelOrderAsync(id); + await LoadAsync(); + } + }); + } + + public async Task LoadAsync() + { + IsLoading = true; + HasError = false; + try + { + var orders = await _tradingService.GetOrdersAsync(OrderStatusFilter.All, 100); + Application.Current.Dispatcher.Invoke(() => + { + Orders.Clear(); + foreach (var o in orders) + Orders.Add(new OrderViewModel(o)); + }); + } + catch (Exception ex) + { + Application.Current.Dispatcher.Invoke(() => + { + HasError = true; + ErrorMessage = "Errore: " + ex.Message; + }); + } + finally + { + Application.Current.Dispatcher.Invoke(() => IsLoading = false); + } + } + } +} diff --git a/DesktopBot/ViewModels/BaseViewModel.cs b/DesktopBot/ViewModels/BaseViewModel.cs new file mode 100644 index 0000000..d373672 --- /dev/null +++ b/DesktopBot/ViewModels/BaseViewModel.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Input; +using DesktopBot.Models; + +namespace DesktopBot.ViewModels +{ + /// + /// Base ViewModel con implementazione INotifyPropertyChanged + /// + public class BaseViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (Equals(field, value)) + return false; + + field = value; + OnPropertyChanged(propertyName); + return true; + } + } + + /// + /// Implementazione semplice di ICommand + /// + public class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func _canExecute; + + public event EventHandler CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + + public RelayCommand(Action execute, Func canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); + + public void Execute(object parameter) => _execute(parameter); + } + + /// + /// Command senza parametri + /// + public class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func _canExecute; + + public event EventHandler CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + + public RelayCommand(Action execute, Func canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public bool CanExecute(object parameter) => _canExecute == null || _canExecute((T)parameter); + + public void Execute(object parameter) => _execute((T)parameter); + } +} diff --git a/DesktopBot/ViewModels/BotConfigViewModel.cs b/DesktopBot/ViewModels/BotConfigViewModel.cs new file mode 100644 index 0000000..e97d0e6 --- /dev/null +++ b/DesktopBot/ViewModels/BotConfigViewModel.cs @@ -0,0 +1,85 @@ +using System; +using System.Windows.Input; +using DesktopBot.Engine; +using DesktopBot.Models; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + /// + /// ViewModel per la configurazione del bot + /// + public class BotConfigViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private readonly AutomatedBotEngine _botEngine; + + private BotConfiguration _config = new BotConfiguration(); + private bool _isRunning; + private string _statusMessage; + + public BotConfiguration Config + { + get => _config; + set => SetProperty(ref _config, value); + } + + public bool IsRunning + { + get => _isRunning; + set => SetProperty(ref _isRunning, value); + } + + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public ICommand StartBotCommand { get; } + public ICommand StopBotCommand { get; } + + public BotConfigViewModel(ITradingService tradingService, AutomatedBotEngine botEngine) + { + _tradingService = tradingService; + _botEngine = botEngine; + + StartBotCommand = new RelayCommand( + async _ => await StartBotAsync(), + _ => !IsRunning + ); + + StopBotCommand = new RelayCommand( + _ => StopBot(), + _ => IsRunning + ); + } + + public void LoadConfiguration() + { + Config = new BotConfiguration(); + } + + private async System.Threading.Tasks.Task StartBotAsync() + { + try + { + IsRunning = true; + StatusMessage = $"Bot avviato - {Config.Symbol} - Strategia: {Config.Strategy}"; + await _botEngine.StartAsync(Config); + } + catch (Exception ex) + { + StatusMessage = $"Errore: {ex.Message}"; + IsRunning = false; + } + } + + private void StopBot() + { + _botEngine.Stop(); + IsRunning = false; + StatusMessage = "Bot arrestato"; + } + } +} diff --git a/DesktopBot/ViewModels/BotInstanceViewModel.cs b/DesktopBot/ViewModels/BotInstanceViewModel.cs new file mode 100644 index 0000000..b0a19cc --- /dev/null +++ b/DesktopBot/ViewModels/BotInstanceViewModel.cs @@ -0,0 +1,548 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using DesktopBot.Engine; +using DesktopBot.Models; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + public class BotInstanceViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private AutomatedBotEngine _engine; + + private bool _isRunning; + private bool _isWaitingMarket; + private string _statusMessage = "Inattivo"; + private string _lastSignal = "---"; + private decimal _lastPrice; + + public ObservableCollection BotLog { get; } = new ObservableCollection(); + public ObservableCollection OpenPositions { get; } = new ObservableCollection(); + public ObservableCollection TradeHistory { get; } = new ObservableCollection(); + + // CancellationToken per il polling periodico delle posizioni + private CancellationTokenSource _positionPollCts; + + private void AppendLog(string message, Models.LogLevel level = Models.LogLevel.Info) + { + var entry = new BotLogEntry(level, message); + var dispatcher = System.Windows.Application.Current?.Dispatcher; + if (dispatcher != null && !dispatcher.CheckAccess()) + dispatcher.Invoke(() => { BotLog.Add(entry); TrimLog(); }); + else + { BotLog.Add(entry); TrimLog(); } + } + + private void TrimLog() + { + int maxEntries = Model?.Config?.LoggingConfig?.MaxBotLogEntries ?? 5000; + while (BotLog.Count > maxEntries) BotLog.RemoveAt(0); + } + + public BotInstance Model { get; } + + public string BotId => Model.BotId; + public string BadgeColor => Model.BadgeColor; + + public string Name + { + get => Model.Name; + set { Model.Name = value; OnPropertyChanged(); OnPropertyChanged(nameof(DisplayTitle)); } + } + + public string Symbol + { + get => Model.Symbol; + set + { + // Impedisce la modifica del simbolo se il bot è bloccato + if (Model.IsAssetLocked) + { + AppendLog($"⚠ Tentativo di modifica del simbolo ignorato: il bot è bloccato su {Model.Symbol}", Models.LogLevel.Warning); + return; + } + + Model.Symbol = value; + Model.Config.Symbol = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(DisplayTitle)); + RefreshMarketStatus(); + } + } + + public string AssetName => Model.AssetName; + + public string AssetClass + { + get => Model.AssetClass; + set + { + // Impedisce la modifica della classe se il bot è bloccato + if (Model.IsAssetLocked) + { + AppendLog($"⚠ Tentativo di modifica della classe asset ignorato: il bot è bloccato", Models.LogLevel.Warning); + return; + } + + Model.AssetClass = value; + OnPropertyChanged(); + RefreshMarketStatus(); + RefreshStrategyProfiles(); + } + } + + public bool IsEnabled + { + get => Model.IsEnabled; + set { Model.IsEnabled = value; OnPropertyChanged(); } + } + + public string Notes + { + get => Model.Notes; + set { Model.Notes = value; OnPropertyChanged(); } + } + + public BotConfiguration Config => Model.Config; + + /// + /// Indica se il bot è bloccato a un asset specifico (immutabile). + /// + public bool IsAssetLocked => Model.IsAssetLocked; + + /// + /// Timestamp del blocco dell'asset. + /// + public string LockedAtLabel => Model.LockedAt?.ToString("dd/MM/yyyy HH:mm") ?? "---"; + + /// + /// Indica se la configurazione della strategia è bloccata. + /// + public bool IsConfigLocked => Model.Config.IsLocked; + + /// + /// Aggiorna le proprietà relative al lock status. + /// Chiamato dopo l'associazione dell'asset. + /// + public void RefreshLockStatus() + { + OnPropertyChanged(nameof(IsAssetLocked)); + OnPropertyChanged(nameof(IsConfigLocked)); + OnPropertyChanged(nameof(LockedAtLabel)); + } + + public bool IsRunning + { + get => _isRunning; + private set + { + SetProperty(ref _isRunning, value); + OnPropertyChanged(nameof(StatusColor)); + OnPropertyChanged(nameof(RunButtonLabel)); + OnPropertyChanged(nameof(StatusBadge)); + } + } + + public bool IsWaitingMarket + { + get => _isWaitingMarket; + private set + { + SetProperty(ref _isWaitingMarket, value); + OnPropertyChanged(nameof(StatusColor)); + OnPropertyChanged(nameof(StatusBadge)); + } + } + + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public string LastSignal + { + get => _lastSignal; + private set => SetProperty(ref _lastSignal, value); + } + + public decimal LastPrice + { + get => _lastPrice; + private set => SetProperty(ref _lastPrice, value); + } + + public bool IsMarketOpen => MarketHoursService.IsMarketOpen(Model.AssetClass); + public string MarketStatusLabel => MarketHoursService.GetMarketStatusLabel(Model.AssetClass); + public string MarketStatusColor => MarketHoursService.GetMarketStatusColor(Model.AssetClass); + + private void RefreshMarketStatus() + { + OnPropertyChanged(nameof(IsMarketOpen)); + OnPropertyChanged(nameof(MarketStatusLabel)); + OnPropertyChanged(nameof(MarketStatusColor)); + } + + public string StatusColor + { + get + { + if (!IsRunning) return "#555566"; + if (IsWaitingMarket) return "#FFC107"; + return "#00E676"; + } + } + + public string StatusBadge + { + get + { + if (!IsRunning) return "FERMO"; + if (IsWaitingMarket) return "IN ATTESA"; + return "ATTIVO"; + } + } + + public bool IsSelected + { + get => _isSelected; + set => SetProperty(ref _isSelected, value); + } + private bool _isSelected; + + public string RunButtonLabel => IsRunning ? "STOP" : "START"; + public string DisplayTitle => $"{Name} [{Symbol}]"; + public string CreatedAtLabel => Model.CreatedAt.ToString("dd/MM/yyyy HH:mm"); + + public ObservableCollection StrategyProfiles { get; } + = new ObservableCollection(); + + public bool IsUsingRecommendedStrategy => Model.Config.IsOptimizedForAsset; + + public string RecommendedStrategyLabel + { + get + { + if (!Model.Config.RecommendedStrategy.HasValue) return string.Empty; + var profiles = StrategyAdvisor.GetAvailableProfiles(Model.AssetClass); + foreach (var p in profiles) + if (p.Strategy == Model.Config.RecommendedStrategy.Value) + return $"{p.Icon} {p.DisplayName}"; + return Model.Config.RecommendedStrategy.Value.ToString(); + } + } + + private void RefreshStrategyProfiles() + { + var profiles = StrategyAdvisor.GetAvailableProfiles(Model.AssetClass); + StrategyProfiles.Clear(); + foreach (var p in profiles) + StrategyProfiles.Add(new StrategyProfileViewModel(p, this)); + OnPropertyChanged(nameof(RecommendedStrategyLabel)); + OnPropertyChanged(nameof(IsUsingRecommendedStrategy)); + RefreshStrategySelection(); + } + + public void RefreshStrategySelection() + { + foreach (var sp in StrategyProfiles) + sp.RefreshIsActive(); + OnPropertyChanged(nameof(IsUsingRecommendedStrategy)); + OnPropertyChanged(nameof(StrategyDescription)); + OnPropertyChanged(nameof(RecommendedStrategyLabel)); + } + + internal void ApplyStrategyProfile(StrategyProfile profile) + { + // Impedisce il cambio di strategia se il bot è bloccato + if (Model.Config.IsLocked) + { + AppendLog($"⚠ Tentativo di cambio strategia ignorato: la configurazione è bloccata su {Model.Config.Strategy}", Models.LogLevel.Warning); + StatusMessage = $"❌ Impossibile cambiare strategia: bot bloccato su {Model.Config.Strategy}"; + return; + } + + profile.ApplyParameters(Model.Config); + Model.Config.Strategy = profile.Strategy; + if (profile.IsRecommended) + Model.Config.RecommendedStrategy = profile.Strategy; + RefreshStrategySelection(); + OnPropertyChanged(nameof(Config)); + } + + public string StrategyDescription + { + get + { + var c = Model.Config; + switch (c.Strategy) + { + case TradingStrategy.EMA_CROSSOVER: + return $"EMA CROSSOVER -- EMA veloce ({c.FastEmaPeriod} p.) vs EMA lenta ({c.SlowEmaPeriod} p.).\n" + + $"ACQUISTO al crossover rialzista -- VENDITA al ribassista.\n" + + $"SL: {c.StopLossPercentage * 100:N1}% x TP: {c.TakeProfitPercentage * 100:N1}% x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s"; + case TradingStrategy.RSI: + return $"RSI ({c.RsiPeriod} p.) -- oscillatore di momentum 0-100.\n" + + $"ACQUISTO: RSI < {c.RsiOversoldThreshold} x VENDITA: RSI > {c.RsiOverboughtThreshold}.\n" + + $"SL: {c.StopLossPercentage * 100:N1}% x TP: {c.TakeProfitPercentage * 100:N1}% x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s"; + case TradingStrategy.MACD: + return $"MACD -- EMA({c.MacdFastPeriod}) - EMA({c.MacdSlowPeriod}) con Signal EMA({c.MacdSignalPeriod}).\n" + + $"ACQUISTO al crossover rialzista MACD > Signal.\n" + + $"SL: {c.StopLossPercentage * 100:N1}% x TP: {c.TakeProfitPercentage * 100:N1}% x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s"; + case TradingStrategy.VOLATILITY_BREAKOUT: + return $"VOLATILITY BREAKOUT -- Keltner EMA({c.KeltnerPeriod}) +/- {c.KeltnerMultiplier}xATR.\n" + + $"Breakout: close > banda AND RVOL >= {c.RvolMinThreshold}x AND CVD slope > 0.\n" + + $"SL = min barra - {c.AtrStopMultiplier}xATR x TP simmetrico x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s"; + case TradingStrategy.KALMAN_MEAN_REVERSION: + return $"KALMAN MEAN REVERSION -- fair value dinamico via filtro di Kalman.\n" + + $"ACQUISTO: Z-Score < -{c.KalmanEntryZScore:F1} x USCITA: |Z| < {c.KalmanExitZScore:F2}.\n" + + $"delta={c.KalmanDelta:G3} x SL: {c.StopLossPercentage * 100:N1}% x TP: {c.TakeProfitPercentage * 100:N1}% x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s"; + default: + return "Strategia non riconosciuta."; + } + } + } + + public void RefreshStrategyDescription() => OnPropertyChanged(nameof(StrategyDescription)); + + public ICommand ToggleRunCommand { get; } + public ICommand RemoveCommand { get; } + public ICommand CopyLogCommand { get; } + public event EventHandler RemoveRequested; + + public BotInstanceViewModel(BotInstance model, ITradingService tradingService) + { + Model = model ?? throw new ArgumentNullException(nameof(model)); + _tradingService = tradingService ?? throw new ArgumentNullException(nameof(tradingService)); + _engine = new AutomatedBotEngine(_tradingService); + + _engine.LogGenerated += (s, log) => + { + StatusMessage = $"[{log.Level}] {log.Message}"; + AppendLog(log.Message, log.Level); + }; + _engine.SignalGenerated += (s, signal) => + { + var reason = signal.Reason ?? signal.Type.ToString(); + LastSignal = reason; + AppendLog($"SEGNALE: {reason}", Models.LogLevel.Success); + + // Dopo ogni segnale rilevante aggiorna le posizioni chiedendo conferma ad Alpaca + if (signal.Type == SignalType.Buy || signal.Type == SignalType.Sell) + _ = RefreshOpenPositionsAsync(signal); + }; + _engine.EquityUpdated += (s, eq) => + { + LastPrice = eq; + AppendLog($"Equity: {eq:N2}", Models.LogLevel.Info); + }; + _engine.WaitingForMarketChanged += (s, waiting) => + { + IsWaitingMarket = waiting; + AppendLog(waiting + ? $"Mercato chiuso per {Model.Symbol} -- in attesa apertura" + : $"Mercato aperto -- ripresa operativita su {Model.Symbol}", + Models.LogLevel.Info); + }; + + ToggleRunCommand = new RelayCommand(_ => _ = ToggleRunAsync()); + RemoveCommand = new RelayCommand( + _ => RemoveRequested?.Invoke(this, EventArgs.Empty), + _ => !Model.IsAssetLocked); // il bot fisso BTC/USD non può essere rimosso + CopyLogCommand = new RelayCommand(_ => + { + if (BotLog.Count == 0) return; + var sb = new StringBuilder(); + foreach (var entry in BotLog) + sb.AppendLine($"[{entry.Timestamp:dd-MM-yyyy HH:mm:ss}] [{entry.Level}] {entry.Message}"); + Clipboard.SetText(sb.ToString()); + }); + + RefreshStrategyProfiles(); + } + + private async Task ToggleRunAsync() + { + if (IsRunning) StopBot(); + else await StartBotAsync(); + } + + private async Task StartBotAsync() + { + if (string.IsNullOrWhiteSpace(Model.Symbol)) + { + StatusMessage = "Nessun asset associato. Seleziona un simbolo prima di avviare."; + AppendLog("Avvio fallito: nessun asset associato.", Models.LogLevel.Error); + return; + } + try + { + IsRunning = true; + _engine.AssetClass = Model.AssetClass ?? "us_equity"; + StatusMessage = $"Avvio... {Model.Symbol}"; + AppendLog($"Avvio bot su {Model.Symbol} -- strategia: {Model.Config.Strategy}", Models.LogLevel.Info); + + // Prima di avviare: leggi le posizioni correnti da Alpaca + await RefreshOpenPositionsAsync(); + + // Avvia polling periodico posizioni (ogni 30 secondi) + _positionPollCts = new CancellationTokenSource(); + _ = StartPositionPollingAsync(_positionPollCts.Token); + + await _engine.StartAsync(Model.Config); + } + catch (Exception ex) + { + StatusMessage = $"Errore: {ex.Message}"; + AppendLog($"Errore durante l avvio: {ex.Message}", Models.LogLevel.Error); + IsRunning = false; + } + } + + private void StopBot() + { + _engine.Stop(); + _positionPollCts?.Cancel(); + _positionPollCts = null; + IsRunning = false; + IsWaitingMarket = false; + StatusMessage = "Fermato manualmente"; + AppendLog("Bot fermato manualmente.", Models.LogLevel.Warning); + // Refresh finale per mostrare lo stato reale dopo lo stop + _ = RefreshOpenPositionsAsync(); + } + + /// + /// Interroga Alpaca per le posizioni aperte sul simbolo del bot e aggiorna OpenPositions. + /// Se viene fornito un segnale di chiusura (Sell), registra il trade nello storico. + /// Nessuno stato viene mantenuto in locale: la sorgente di verità è sempre Alpaca. + /// + private async Task RefreshOpenPositionsAsync(TradingSignal closedSignal = null) + { + try + { + var position = await _tradingService.GetPositionAsync(Model.Symbol); + + var d = System.Windows.Application.Current?.Dispatcher; + void RunOnUi(Action a) { if (d != null && !d.CheckAccess()) d.Invoke(a); else a(); } + + RunOnUi(() => + { + if (position != null && Math.Abs(position.Quantity) > 0) + { + // Posizione aperta confermata da Alpaca + var existing = OpenPositions.FirstOrDefault(p => p.Symbol == Model.Symbol); + if (existing == null) + { + // Nuova posizione: aggiungila + var record = new BotTradeRecord + { + Symbol = position.Symbol, + Side = position.Quantity > 0 ? "BUY" : "SELL", + EntryPrice = position.AverageEntryPrice, + Quantity = Math.Abs(position.Quantity), + OpenedAt = DateTime.Now + }; + OpenPositions.Insert(0, record); + } + else + { + // Aggiorna quantità e prezzo medio (possono cambiare) + existing.Quantity = Math.Abs(position.Quantity); + existing.EntryPrice = position.AverageEntryPrice; + } + } + else + { + // Nessuna posizione aperta su Alpaca: rimuovila dalla lista + // Se c'era una posizione aperta la registriamo come chiusa nello storico + var wasOpen = OpenPositions.FirstOrDefault(p => p.Symbol == Model.Symbol); + if (wasOpen != null) + { + OpenPositions.Remove(wasOpen); + wasOpen.ClosedAt = DateTime.Now; + wasOpen.ExitPrice = closedSignal?.Price > 0 ? closedSignal.Price : wasOpen.EntryPrice; + wasOpen.PnL = wasOpen.Quantity > 0 + ? (wasOpen.ExitPrice - wasOpen.EntryPrice) * wasOpen.Quantity + : wasOpen.ExitPrice - wasOpen.EntryPrice; + TradeHistory.Insert(0, wasOpen); + int maxTradeEntries = Model?.Config?.LoggingConfig?.MaxTradeHistoryEntries ?? 2000; + if (TradeHistory.Count > maxTradeEntries) TradeHistory.RemoveAt(TradeHistory.Count - 1); + } + } + }); + } + catch (Exception ex) + { + AppendLog($"[Warn] Impossibile aggiornare posizioni da Alpaca: {ex.Message}", Models.LogLevel.Warning); + } + } + + /// + /// Polling periodico: aggiorna le posizioni ogni 30 secondi durante l'esecuzione del bot. + /// + private async Task StartPositionPollingAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); + if (!ct.IsCancellationRequested) + await RefreshOpenPositionsAsync().ConfigureAwait(false); + } + } + catch (TaskCanceledException) { /* atteso alla cancellazione */ } + } + + public void ForceStop() + { + if (IsRunning) StopBot(); + } + + public void StartBot() + { + if (!IsRunning) _ = StartBotAsync(); + } + } + + public class StrategyProfileViewModel : BaseViewModel + { + private readonly StrategyProfile _profile; + private readonly BotInstanceViewModel _owner; + + public string Icon => _profile.Icon; + public string DisplayName => _profile.DisplayName; + public string Description => _profile.Description; + public string AccentColor => _profile.AccentColor; + public bool IsRecommended => _profile.IsRecommended; + public TradingStrategy Strategy => _profile.Strategy; + + private bool _isActive; + public bool IsActive + { + get => _isActive; + private set => SetProperty(ref _isActive, value); + } + + public ICommand SelectCommand { get; } + + public StrategyProfileViewModel(StrategyProfile profile, BotInstanceViewModel owner) + { + _profile = profile; + _owner = owner; + SelectCommand = new RelayCommand(_ => _owner.ApplyStrategyProfile(_profile)); + RefreshIsActive(); + } + + public void RefreshIsActive() + => IsActive = _owner.Config.Strategy == _profile.Strategy; + } +} diff --git a/DesktopBot/ViewModels/BotsManagerViewModel.cs b/DesktopBot/ViewModels/BotsManagerViewModel.cs new file mode 100644 index 0000000..fa17b49 --- /dev/null +++ b/DesktopBot/ViewModels/BotsManagerViewModel.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using Alpaca.Markets; +using DesktopBot.Engine; +using DesktopBot.Models; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + /// + /// ViewModel del pannello Bot Manager. + /// Contiene un unico bot BTC/USD fisso, precaricato all'avvio. + /// L'utente può soltanto avviarlo e fermarlo. + /// + public class BotsManagerViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + + // ── Collezione (sempre 1 elemento: il bot BTC/USD fisso) ───────────── + public ObservableCollection Bots { get; } + = new ObservableCollection(); + + /// Shortcut diretto al bot fisso BTC/USD. + public BotInstanceViewModel BtcBot => Bots.FirstOrDefault(); + + // ── Grafico prezzi BTC/USD ───────────────────────────────────────── + public PriceChartViewModel ChartVM { get; } + + // ── Stato UI ───────────────────────────────────────────────────────── + private string _statusMessage; + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + // ── Comandi ────────────────────────────────────────────────────────── + public ICommand StartBotCommand { get; } + public ICommand StopBotCommand { get; } + public ICommand SaveAllCommand { get; } + + // ── Ctor ───────────────────────────────────────────────────────────── + public BotsManagerViewModel(ITradingService tradingService) + { + _tradingService = tradingService ?? throw new ArgumentNullException(nameof(tradingService)); + + ChartVM = new PriceChartViewModel(tradingService); + + StartBotCommand = new RelayCommand(_ => BtcBot?.StartBot(), _ => BtcBot != null && !BtcBot.IsRunning); + StopBotCommand = new RelayCommand(_ => BtcBot?.ForceStop(), _ => BtcBot != null && BtcBot.IsRunning); + SaveAllCommand = new RelayCommand(_ => SaveAll()); + + EnsureBtcBotExists(); + + // Avvia streaming grafico in background + _ = ChartVM.StartStreamingAsync(); + } + + // ── Preload bot BTC/USD fisso ───────────────────────────────────────── + private void EnsureBtcBotExists() + { + // Tenta di caricare da disco + var saved = BotInstanceStore.Load(); + var btcModel = saved.FirstOrDefault(m => + string.Equals(m.Symbol, "BTCUSD", StringComparison.OrdinalIgnoreCase)); + + if (btcModel == null) + { + // Prima esecuzione: crea il bot BTC/USD con parametri ottimali + btcModel = CreateBtcUsdBot(); + BotInstanceStore.Save(new[] { btcModel }); + } + + var vm = new BotInstanceViewModel(btcModel, _tradingService); + // Il bot fisso non può essere rimosso — ignoriamo RemoveRequested + Bots.Add(vm); + StatusMessage = "Bot BTC/USD caricato — pronto all'avvio."; + } + + private static BotInstance CreateBtcUsdBot() + { + var config = new BotConfiguration + { + Symbol = "BTCUSD", + Quantity = 1, + CheckIntervalSeconds = 60, + AnalysisTimeFrame = BarTimeFrame.Minute, + HistoricalBarCount = 200, + + // EMA ottimizzate per 1min BTC + FastEmaPeriod = 5, + SlowEmaPeriod = 20, + + // RSI + RsiPeriod = 14, + RsiOversoldThreshold = 30, + RsiOverboughtThreshold = 70, + + // MACD + MacdFastPeriod = 8, + MacdSlowPeriod = 21, + MacdSignalPeriod = 5, + + // Keltner / RVOL + KeltnerPeriod = 20, + KeltnerMultiplier = 2.0m, + RvolMinThreshold = 2.0m, + AtrStopMultiplier = 1.5m, + + // Kalman + KalmanDelta = 5e-6, + KalmanObservationVariance = 1.0, + KalmanEntryZScore = 1.8, + KalmanExitZScore = 0.3, + + // Risk + StopLossPercentage = 0.02m, + TakeProfitPercentage = 0.04m, + MaxPositionSizePercent = 0.10m, + + // Algoritmo BTC/USD avanzato (dispatch automatico nell'engine) + Strategy = TradingStrategy.VOLATILITY_BREAKOUT, + RecommendedStrategy = TradingStrategy.VOLATILITY_BREAKOUT, + IsLocked = true, + LockedAt = DateTime.Now + }; + + var model = new BotInstance + { + Name = "BTC/USD — Algoritmo Avanzato", + BadgeColor = "#F7931A", // arancione Bitcoin + Config = config, + Notes = "Bot predefinito BTC/USD. Algoritmo multi-segnale: Regime Detector + Kalman + ATR Sizing.", + CreatedAt = DateTime.Now, + IsEnabled = true + }; + + // Blocca asset e config (bot fisso) + model.Symbol = "BTCUSD"; + model.AssetName = "Bitcoin / US Dollar"; + model.AssetClass = "crypto"; + model.LockToAsset(); + + return model; + } + + // ── Persistenza ────────────────────────────────────────────────────── + public void SaveAll() + { + BotInstanceStore.Save(Bots.Select(vm => vm.Model)); + StatusMessage = $"Salvato — {DateTime.Now:HH:mm:ss}"; + } + + // ── Start / Stop esposto ai comandi ─────────────────────────────────── + private async Task StartAllAsync() + { + if (BtcBot != null && !BtcBot.IsRunning) + BtcBot.StartBot(); + await Task.CompletedTask; + } + + private void StopAll() + { + if (BtcBot?.IsRunning == true) + BtcBot.ForceStop(); + StatusMessage = "Bot fermato."; + } + } +} + diff --git a/DesktopBot/ViewModels/DashboardViewModel.cs b/DesktopBot/ViewModels/DashboardViewModel.cs new file mode 100644 index 0000000..61d3856 --- /dev/null +++ b/DesktopBot/ViewModels/DashboardViewModel.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using Alpaca.Markets; +using DesktopBot.Engine; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + /// Voce del log attività mostrata nella dashboard. + public class ActivityLogEntry + { + public DateTime Timestamp { get; set; } + public string Level { get; set; } + public string Message { get; set; } + public string LevelColor { get; set; } + public string LevelBg { get; set; } + + public static ActivityLogEntry Create(string level, string message) + { + string color, bg; + switch (level.ToUpperInvariant()) + { + case "SUCCESS": color = "#00E676"; bg = "#1A00E676"; break; + case "ERROR": color = "#FF1744"; bg = "#1AFF1744"; break; + case "WARNING": color = "#FFC107"; bg = "#1AFFC107"; break; + case "BUY": color = "#00E676"; bg = "#1A003A00"; break; + case "SELL": color = "#FF1744"; bg = "#1A3A0000"; break; + default: color = "#9E9EAF"; bg = "#1A3A3A4A"; break; + } + return new ActivityLogEntry + { + Timestamp = DateTime.Now, + Level = level.ToUpperInvariant(), + Message = message, + LevelColor = color, + LevelBg = bg + }; + } + } + + public class BalanceRowViewModel : BaseViewModel + { + public string Label { get; set; } + public string LastClose { get; set; } + public string Current { get; set; } + public bool IsBold { get; set; } + public bool IsSection { get; set; } + } + + public class OrderViewModel : BaseViewModel + { + public Guid OrderId { get; } + public string Symbol { get; } + public string OrderType { get; } + public string Side { get; } + public decimal Qty { get; } + public decimal FilledQty { get; } + public string AvgFillPrice { get; } + public string Status { get; } + public string SubmittedAt { get; } + public string FilledAt { get; } + public string LimitPrice { get; } + public string StopPrice { get; } + public bool IsOpen { get; } + public string SideBadgeColor { get; } + + public OrderViewModel(IOrder order) + { + OrderId = order.OrderId; + Symbol = order.Symbol; + OrderType = order.OrderType.ToString(); + Side = order.OrderSide.ToString().ToUpperInvariant(); + Qty = order.Quantity ?? 0m; + FilledQty = order.FilledQuantity; + AvgFillPrice = order.AverageFillPrice.HasValue ? "$" + order.AverageFillPrice.Value.ToString("N2") : "-"; + Status = order.OrderStatus.ToString(); + SubmittedAt = order.SubmittedAtUtc.HasValue ? order.SubmittedAtUtc.Value.ToLocalTime().ToString("dd/MM HH:mm:ss") : "-"; + FilledAt = order.FilledAtUtc.HasValue ? order.FilledAtUtc.Value.ToLocalTime().ToString("dd/MM HH:mm:ss") : "-"; + LimitPrice = order.LimitPrice.HasValue ? "$" + order.LimitPrice.Value.ToString("N2") : "-"; + StopPrice = order.StopPrice.HasValue ? "$" + order.StopPrice.Value.ToString("N2") : "-"; + IsOpen = order.OrderStatus == OrderStatus.New + || order.OrderStatus == OrderStatus.PartiallyFilled + || order.OrderStatus == OrderStatus.PendingNew + || order.OrderStatus == OrderStatus.Accepted; + SideBadgeColor = order.OrderSide == OrderSide.Buy ? "#1A3A1A" : "#3A1A1A"; + } + } + + public class DashboardViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private readonly AutomatedBotEngine _botEngine; + + private decimal _equity; + private decimal _buyingPower; + private decimal _dailyPnL; + private decimal _dailyPnLPercent; + private int _openPositionsCount; + private int _openOrdersCount; + private string _botStatus; + private bool _isBotRunning; + private bool _isLoading; + private string _accountStatus; + private bool _isPaperTrading; + private string _accountNumber; + private string _currency; + private string _errorMessage; + private bool _hasError; + private int _selectedTabIndex; + + public int SelectedTabIndex + { + get => _selectedTabIndex; + set => SetProperty(ref _selectedTabIndex, value); + } + public decimal Equity + { + get => _equity; + set => SetProperty(ref _equity, value); + } + public decimal BuyingPower + { + get => _buyingPower; + set => SetProperty(ref _buyingPower, value); + } + public decimal DailyPnL + { + get => _dailyPnL; + set { if (SetProperty(ref _dailyPnL, value)) OnPropertyChanged(nameof(IsPnLPositive)); } + } + public decimal DailyPnLPercent + { + get => _dailyPnLPercent; + set => SetProperty(ref _dailyPnLPercent, value); + } + public bool IsPnLPositive => DailyPnL >= 0; + public int OpenPositionsCount + { + get => _openPositionsCount; + set => SetProperty(ref _openPositionsCount, value); + } + public int OpenOrdersCount + { + get => _openOrdersCount; + set => SetProperty(ref _openOrdersCount, value); + } + public string BotStatus + { + get => _botStatus; + set => SetProperty(ref _botStatus, value); + } + public bool IsBotRunning + { + get => _isBotRunning; + set => SetProperty(ref _isBotRunning, value); + } + public bool IsLoading + { + get => _isLoading; + set => SetProperty(ref _isLoading, value); + } + public string AccountStatus + { + get => _accountStatus; + set => SetProperty(ref _accountStatus, value); + } + public bool IsPaperTrading + { + get => _isPaperTrading; + set => SetProperty(ref _isPaperTrading, value); + } + public string AccountNumber + { + get => _accountNumber; + set => SetProperty(ref _accountNumber, value); + } + public string Currency + { + get => _currency; + set => SetProperty(ref _currency, value); + } + public string ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + public bool HasError + { + get => _hasError; + set => SetProperty(ref _hasError, value); + } + + public ObservableCollection BalanceRows { get; } = new ObservableCollection(); + public ObservableCollection Positions { get; } = new ObservableCollection(); + public ObservableCollection Orders { get; } = new ObservableCollection(); + public ObservableCollection ActivityLog { get; } = new ObservableCollection(); + + /// + /// Statistiche e rate-limiter per le chiamate API Alpaca. + /// Disponibile per il binding in dashboard (null se il servizio non è ancora inizializzato). + /// + public ApiCallCounterService ApiCounter => + (_tradingService as AlpacaTradingService)?.ApiCounter; + + public System.Windows.Input.ICommand RefreshCommand { get; } + public System.Windows.Input.ICommand CancelOrderCommand { get; } + public System.Windows.Input.ICommand CloseAllCommand { get; } + public System.Windows.Input.ICommand CopyActivityLogCommand { get; } + + public DashboardViewModel(ITradingService tradingService, AutomatedBotEngine botEngine) + { + _tradingService = tradingService; + _botEngine = botEngine; + BotStatus = "In attesa"; + AccountStatus = "--"; + + RefreshCommand = new RelayCommand(async _ => await LoadAsync()); + CancelOrderCommand = new RelayCommand(async p => + { + if (p is Guid id) await CancelOrderAsync(id); + }); + CloseAllCommand = new RelayCommand(async _ => + { + await _tradingService.CloseAllPositionsAsync(); + await LoadAsync(); + }); + CopyActivityLogCommand = new RelayCommand(_ => + { + if (ActivityLog.Count == 0) return; + var sb = new StringBuilder(); + foreach (var entry in ActivityLog.Reverse()) + sb.AppendLine(string.Format("[{0:HH:mm:ss dd/MM/yy}] [{1}] {2}", + entry.Timestamp, entry.Level, entry.Message)); + Clipboard.SetText(sb.ToString()); + }); + } + + public async Task LoadAsync() + { + IsLoading = true; + HasError = false; + try + { + var accountTask = _tradingService.GetAccountAsync(); + var positionsTask = _tradingService.GetAllPositionsAsync(); + var ordersTask = _tradingService.GetOrdersAsync(OrderStatusFilter.All, 100); + + await Task.WhenAll(accountTask, positionsTask, ordersTask); + + var account = accountTask.Result; + var positions = positionsTask.Result; + var orders = ordersTask.Result; + + var equity = account.Equity ?? 0m; + var lastEquity = account.LastEquity; + var pnl = equity - lastEquity; + var pnlPct = lastEquity > 0 ? (pnl / lastEquity) * 100m : 0m; + + Application.Current.Dispatcher.Invoke(() => + { + Equity = equity; + BuyingPower = account.BuyingPower ?? 0m; + DailyPnL = pnl; + DailyPnLPercent = pnlPct; + OpenPositionsCount = positions.Count; + AccountStatus = account.IsTradingBlocked ? "BLOCCATO" : "ATTIVO"; + AccountNumber = account.AccountNumber; + Currency = account.Currency; + + BalanceRows.Clear(); + AddSection("Buying Power"); + AddRow("RegT Buying Power", account.RegulationBuyingPower, account.RegulationBuyingPower); + AddRow("Day Trading Buying Power", account.DayTradingBuyingPower, account.DayTradingBuyingPower); + AddRow("Effective Buying Power", account.BuyingPower, account.BuyingPower); + AddRow("Non-Marginable Buying Power", account.NonMarginableBuyingPower, account.NonMarginableBuyingPower); + AddSection("Margin"); + AddRow("Initial Margin", account.InitialMargin, account.InitialMargin); + AddRow("Maintenance Margin", account.MaintenanceMargin, account.MaintenanceMargin); + AddSection("Cash"); + AddRow("Cash", account.TradableCash, account.TradableCash); + AddSection("Positions"); + AddRow("Equity", lastEquity, equity, bold: true); + AddRow("Long Market Value", account.LongMarketValue, account.LongMarketValue); + AddRow("Short Market Value", account.ShortMarketValue, account.ShortMarketValue); + + Positions.Clear(); + foreach (var p in positions) + Positions.Add(new PositionViewModel(p)); + + Orders.Clear(); + int openCount = 0; + foreach (var o in orders) + { + var ovm = new OrderViewModel(o); + Orders.Add(ovm); + if (ovm.IsOpen) openCount++; + } + OpenOrdersCount = openCount; + AddActivityLog("SUCCESS", $"Dati aggiornati — Equity: ${equity:N2} | Posizioni: {positions.Count} | Ordini aperti: {openCount}"); + }); + } + catch (Exception ex) + { + Application.Current.Dispatcher.Invoke(() => + { + HasError = true; + ErrorMessage = "Errore: " + ex.Message; + AddActivityLog("ERROR", ex.Message); + }); + } + finally + { + Application.Current.Dispatcher.Invoke(() => IsLoading = false); + } + } + + public void UpdateEquity(decimal equity) + { + Equity = equity; + AddActivityLog("INFO", $"Equity aggiornata: ${equity:N2}"); + } + + /// Aggiunge una voce al log attività (thread-safe via Dispatcher, limiti parametrizzati). + public void AddActivityLog(string level, string message) + { + Application.Current.Dispatcher.Invoke(() => + { + ActivityLog.Insert(0, ActivityLogEntry.Create(level, message)); + // Recupera il limite dalla configurazione del bot, fallback a 5000 + int maxActivityEntries = _botEngine?.Model?.Config?.LoggingConfig?.MaxActivityLogEntries ?? 5000; + while (ActivityLog.Count > maxActivityEntries) + ActivityLog.RemoveAt(ActivityLog.Count - 1); + }); + } + + private void AddSection(string title) + => BalanceRows.Add(new BalanceRowViewModel { Label = title, IsSection = true }); + + private void AddRow(string label, decimal? last, decimal? curr, bool bold = false) + => BalanceRows.Add(new BalanceRowViewModel + { + Label = label, + LastClose = last.HasValue ? "$" + last.Value.ToString("N2") : "-", + Current = curr.HasValue ? "$" + curr.Value.ToString("N2") : "-", + IsBold = bold + }); + + private void AddRow(string label, decimal last, decimal curr, bool bold = false) + => BalanceRows.Add(new BalanceRowViewModel + { + Label = label, + LastClose = "$" + last.ToString("N2"), + Current = "$" + curr.ToString("N2"), + IsBold = bold + }); + + private async Task CancelOrderAsync(Guid orderId) + { + try + { + await _tradingService.CancelOrderAsync(orderId); + await LoadAsync(); + } + catch (Exception ex) + { + Application.Current.Dispatcher.Invoke(() => + { + HasError = true; + ErrorMessage = "Errore cancellazione: " + ex.Message; + }); + } + } + } +} diff --git a/DesktopBot/ViewModels/LiveLogViewModel.cs b/DesktopBot/ViewModels/LiveLogViewModel.cs new file mode 100644 index 0000000..516b09d --- /dev/null +++ b/DesktopBot/ViewModels/LiveLogViewModel.cs @@ -0,0 +1,58 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Windows; +using System.Windows.Input; +using DesktopBot.Engine; +using DesktopBot.Models; + +namespace DesktopBot.ViewModels +{ + /// + /// ViewModel per il Live Log + /// + public class LiveLogViewModel : BaseViewModel + { + private ObservableCollection _logs = new ObservableCollection(); + + public ObservableCollection Logs + { + get => _logs; + set => SetProperty(ref _logs, value); + } + + public ICommand ClearLogsCommand { get; } + public ICommand CopyLogsCommand { get; } + + public LiveLogViewModel(AutomatedBotEngine botEngine) + { + ClearLogsCommand = new RelayCommand(_ => Logs.Clear()); + CopyLogsCommand = new RelayCommand(_ => + { + if (Logs.Count == 0) return; + var sb = new StringBuilder(); + // I log sono inseriti in testa (più recente prima): li copiamo in ordine cronologico + foreach (var entry in Logs.Reverse()) + sb.AppendLine(string.Format("[{0:HH:mm:ss dd/MM}] [{1}] {2}", + entry.Timestamp, entry.Level, entry.Message)); + Clipboard.SetText(sb.ToString()); + }); + AddLog(new BotLogEntry(LogLevel.Info, "Sistema avviato - in attesa di configurazione")); + } + + /// + /// Aggiunge un log entry in cima alla lista, sul UI thread (limiti parametrizzati) + /// + public void AddLog(BotLogEntry logEntry) + { + Application.Current?.Dispatcher.Invoke(() => + { + Logs.Insert(0, logEntry); + // Limite massimo per il live log (default 10000) + int maxLiveLogEntries = 10000; + while (Logs.Count > maxLiveLogEntries) + Logs.RemoveAt(Logs.Count - 1); + }); + } + } +} diff --git a/DesktopBot/ViewModels/LoggingSettingsViewModel.cs b/DesktopBot/ViewModels/LoggingSettingsViewModel.cs new file mode 100644 index 0000000..5a7333d --- /dev/null +++ b/DesktopBot/ViewModels/LoggingSettingsViewModel.cs @@ -0,0 +1,67 @@ +using System.Windows.Input; +using DesktopBot.Models; + +namespace DesktopBot.ViewModels +{ + /// + /// ViewModel per la configurazione dei limiti di logging. + /// + public class LoggingSettingsViewModel : BaseViewModel + { + private LoggingConfiguration _config; + + public LoggingSettingsViewModel(LoggingConfiguration config) + { + _config = config?.Clone() ?? new LoggingConfiguration(); + } + + /// Numero massimo di elementi nel log del bot. + public int MaxBotLogEntries + { + get => _config.MaxBotLogEntries; + set { _config.MaxBotLogEntries = value; OnPropertyChanged(); } + } + + /// Numero massimo di elementi nello storico trade. + public int MaxTradeHistoryEntries + { + get => _config.MaxTradeHistoryEntries; + set { _config.MaxTradeHistoryEntries = value; OnPropertyChanged(); } + } + + /// Numero massimo di elementi nel log attività della dashboard. + public int MaxActivityLogEntries + { + get => _config.MaxActivityLogEntries; + set { _config.MaxActivityLogEntries = value; OnPropertyChanged(); } + } + + /// Numero massimo di elementi nel log live globale. + public int MaxLiveLogEntries + { + get => _config.MaxLiveLogEntries; + set { _config.MaxLiveLogEntries = value; OnPropertyChanged(); } + } + + /// Numero massimo di punti dati nel grafico dei prezzi. + public int MaxPriceDataPoints + { + get => _config.MaxPriceDataPoints; + set { _config.MaxPriceDataPoints = value; OnPropertyChanged(); } + } + + /// Restituisce la configurazione modificata. + public LoggingConfiguration GetUpdatedConfig() => _config.Clone(); + + /// Ripristina i valori di default. + public void ResetToDefaults() + { + var defaults = new LoggingConfiguration(); + MaxBotLogEntries = defaults.MaxBotLogEntries; + MaxTradeHistoryEntries = defaults.MaxTradeHistoryEntries; + MaxActivityLogEntries = defaults.MaxActivityLogEntries; + MaxLiveLogEntries = defaults.MaxLiveLogEntries; + MaxPriceDataPoints = defaults.MaxPriceDataPoints; + } + } +} diff --git a/DesktopBot/ViewModels/MainViewModel.cs b/DesktopBot/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..ed5a070 --- /dev/null +++ b/DesktopBot/ViewModels/MainViewModel.cs @@ -0,0 +1,184 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using DesktopBot.Engine; +using DesktopBot.Models; +using DesktopBot.Services; +namespace DesktopBot.ViewModels +{ + public class MainViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private readonly AutomatedBotEngine _botEngine; + private readonly AlpacaPingService _pingService; + + public PingViewModel PingVM { get; } = new PingViewModel(); + + // ── Auto-refresh periodico ───────────────────────────────────────── + /// Intervallo (secondi) per l'aggiornamento frequente: posizioni e ordini. + private const int FastRefreshSeconds = 30; + /// Intervallo (secondi) per l'aggiornamento lento: balance, wallet, dashboard. + private const int SlowRefreshSeconds = 60; + + private CancellationTokenSource _autoRefreshCts; + private int _refreshCycle; // contatore cicli per sfalsare il refresh lento + + private BaseViewModel _currentViewModel; + private string _selectedTab; + + public DashboardViewModel DashboardVM { get; } + public BotsManagerViewModel BotsVM { get; } + public LiveLogViewModel LiveLogVM { get; } + public WalletViewModel WalletVM { get; } + public SettingsViewModel SettingsVM { get; } + public BalanceViewModel BalanceVM { get; } + public PositionsViewModel PositionsVM { get; } + public OrdersViewModel OrdersVM { get; } + + public BaseViewModel CurrentViewModel + { + get => _currentViewModel; + set => SetProperty(ref _currentViewModel, value); + } + + public string SelectedTab + { + get => _selectedTab; + set + { + if (SetProperty(ref _selectedTab, value)) + NavigateToTab(value); + } + } + + public ICommand NavigateCommand { get; } + + public MainViewModel() + { + _tradingService = new AlpacaTradingService(); + _botEngine = new AutomatedBotEngine(_tradingService); + _pingService = new AlpacaPingService(); + _pingService.PingCompleted += (_, r) => PingVM.Update(r); + + DashboardVM = new DashboardViewModel(_tradingService, _botEngine); + BotsVM = new BotsManagerViewModel(_tradingService); + LiveLogVM = new LiveLogViewModel(_botEngine); + WalletVM = new WalletViewModel(_tradingService); + SettingsVM = new SettingsViewModel(_tradingService, OnCredentialsSaved); + BalanceVM = new BalanceViewModel(_tradingService); + PositionsVM = new PositionsViewModel(_tradingService); + OrdersVM = new OrdersViewModel(_tradingService); + + NavigateCommand = new RelayCommand(param => SelectedTab = param?.ToString()); + SubscribeToBotEvents(); + + var saved = CredentialService.LoadCredentials(); + if (saved != null) + { + _tradingService.Initialize(saved.ApiKey, saved.ApiSecret, saved.IsPaper); + _pingService.SetEnvironment(saved.IsPaper); + _pingService.Start(10); + CurrentViewModel = DashboardVM; + SelectedTab = "Dashboard"; + _ = LoadInitialDataAsync(); + } + else + { + CurrentViewModel = SettingsVM; + SelectedTab = "Settings"; + } + } + + private void NavigateToTab(string tabName) + { + CurrentViewModel = tabName switch + { + "Dashboard" => (BaseViewModel)DashboardVM, + "Bot" => BotsVM, + "LiveLog" => LiveLogVM, + "Wallet" => WalletVM, + "Settings" => SettingsVM, + "Balance" => BalanceVM, + "Positions" => PositionsVM, + "Orders" => OrdersVM, + _ => DashboardVM + }; + } + + private void SubscribeToBotEvents() + { + _botEngine.LogGenerated += (sender, log) => LiveLogVM.AddLog(log); + _botEngine.EquityUpdated += (sender, equity) => DashboardVM.UpdateEquity(equity); + } + + private async Task LoadInitialDataAsync() + { + await Task.WhenAll( + DashboardVM.LoadAsync(), + WalletVM.LoadAsync(), + BalanceVM.LoadAsync(), + PositionsVM.LoadAsync(), + OrdersVM.LoadAsync() + ); + StartAutoRefresh(); + } + + // ── Auto-refresh periodico ───────────────────────────────────────── + private void StartAutoRefresh() + { + _autoRefreshCts?.Cancel(); + _autoRefreshCts = new CancellationTokenSource(); + _refreshCycle = 0; + _ = AutoRefreshLoopAsync(_autoRefreshCts.Token); + } + + private async Task AutoRefreshLoopAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(FastRefreshSeconds), ct); + if (ct.IsCancellationRequested) break; + + _refreshCycle++; + + // Refresh frequente: posizioni aperte e ordini recenti + var fastTasks = new System.Collections.Generic.List + { + SafeRefresh(PositionsVM.LoadAsync), + SafeRefresh(OrdersVM.LoadAsync) + }; + + // Refresh lento (ogni 2 cicli = 60s): balance, wallet, dashboard + if (_refreshCycle % 2 == 0) + { + fastTasks.Add(SafeRefresh(BalanceVM.LoadAsync)); + fastTasks.Add(SafeRefresh(WalletVM.LoadAsync)); + fastTasks.Add(SafeRefresh(DashboardVM.LoadAsync)); + } + + await Task.WhenAll(fastTasks); + } + } + catch (OperationCanceledException) { /* atteso */ } + } + + private static async Task SafeRefresh(Func loader) + { + try { await loader(); } + catch { /* non bloccare il ciclo per errori singoli di rete */ } + } + + private void OnCredentialsSaved(string apiKey, string apiSecret, bool isPaper) + { + SelectedTab = "Dashboard"; + _ = LoadInitialDataAsync(); + } + public void InitializeWithCredentials(string apiKey, string apiSecret, bool isPaper) + { + _tradingService.Initialize(apiKey, apiSecret, isPaper); + } + } +} diff --git a/DesktopBot/ViewModels/PingViewModel.cs b/DesktopBot/ViewModels/PingViewModel.cs new file mode 100644 index 0000000..24e5a8f --- /dev/null +++ b/DesktopBot/ViewModels/PingViewModel.cs @@ -0,0 +1,84 @@ +using System.Windows.Media; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + /// + /// ViewModel per l'indicatore di ping verso Alpaca. + /// Esposto in MainViewModel e mostrato nella sidebar. + /// + public class PingViewModel : BaseViewModel + { + private int _latencyMs = -1; + private bool _isOnline; + private string _statusText = "---"; + private Brush _statusColor; + + private static readonly Brush BrushGood = new SolidColorBrush(Color.FromRgb(0x00, 0xC8, 0x5A)); // verde + private static readonly Brush BrushFair = new SolidColorBrush(Color.FromRgb(0xFF, 0xBF, 0x00)); // giallo + private static readonly Brush BrushPoor = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x00)); // arancione + private static readonly Brush BrushOffline = new SolidColorBrush(Color.FromRgb(0xFF, 0x33, 0x33)); // rosso + private static readonly Brush BrushIdle = new SolidColorBrush(Color.FromRgb(0x66, 0x66, 0x80)); // grigio + + public int LatencyMs + { + get => _latencyMs; + private set => SetProperty(ref _latencyMs, value); + } + + public bool IsOnline + { + get => _isOnline; + private set => SetProperty(ref _isOnline, value); + } + + public string StatusText + { + get => _statusText; + private set => SetProperty(ref _statusText, value); + } + + public Brush StatusColor + { + get => _statusColor ??= BrushIdle; + private set => SetProperty(ref _statusColor, value); + } + + public void Update(PingResult result) + { + var dispatcher = System.Windows.Application.Current?.Dispatcher; + if (dispatcher != null && !dispatcher.CheckAccess()) + { + dispatcher.Invoke(() => Apply(result)); + return; + } + Apply(result); + } + + private void Apply(PingResult result) + { + LatencyMs = result.LatencyMs; + IsOnline = result.IsSuccess; + + switch (result.Status) + { + case PingStatus.Good: + StatusText = $"{result.LatencyMs} ms"; + StatusColor = BrushGood; + break; + case PingStatus.Fair: + StatusText = $"{result.LatencyMs} ms"; + StatusColor = BrushFair; + break; + case PingStatus.Poor: + StatusText = $"{result.LatencyMs} ms"; + StatusColor = BrushPoor; + break; + default: + StatusText = "OFFLINE"; + StatusColor = BrushOffline; + break; + } + } + } +} diff --git a/DesktopBot/ViewModels/PriceChartViewModel.cs b/DesktopBot/ViewModels/PriceChartViewModel.cs new file mode 100644 index 0000000..e17e8d2 --- /dev/null +++ b/DesktopBot/ViewModels/PriceChartViewModel.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using Alpaca.Markets; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + /// + /// ViewModel per la visualizzazione del grafico dei prezzi in tempo reale. + /// Supporta streaming via WebSocket per crypto e azioni, con buffer storico. + /// + public class PriceChartViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private CancellationTokenSource _streamCts; + private Task _streamTask; + + // ── Dati del grafico ───────────────────────────────────────────────── + /// Collezione di punti prezzo per il grafico (timestamp, prezzo) + public ObservableCollection PriceData { get; } + = new ObservableCollection(); + + /// Limite massimo dei punti dati nel grafico (parametrizzato, default 3000) + public int MaxDataPoints { get; set; } = 3000; + + // ── Simbolo monitorato ─────────────────────────────────────────────── + private string _symbol = "BTCUSD"; + public string Symbol + { + get => _symbol; + set + { + if (SetProperty(ref _symbol, value)) + { + _ = RestartStreamAsync(); + } + } + } + + // ── Prezzo corrente ────────────────────────────────────────────────── + private decimal _currentPrice; + public decimal CurrentPrice + { + get => _currentPrice; + private set => SetProperty(ref _currentPrice, value); + } + + private decimal _priceChange; + public decimal PriceChange + { + get => _priceChange; + private set => SetProperty(ref _priceChange, value); + } + + private decimal _priceChangePercent; + public decimal PriceChangePercent + { + get => _priceChangePercent; + private set => SetProperty(ref _priceChangePercent, value); + } + + public string PriceChangeColor => PriceChange >= 0 ? "#00E676" : "#FF5252"; + public string PriceChangeIcon => PriceChange >= 0 ? "▲" : "▼"; + + public System.Windows.Media.Brush PriceChangeBrush => + new System.Windows.Media.SolidColorBrush( + (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(PriceChangeColor)); + + // ── Stato streaming ────────────────────────────────────────────────── + private bool _isStreaming; + public bool IsStreaming + { + get => _isStreaming; + private set => SetProperty(ref _isStreaming, value); + } + + private string _statusMessage = "Pronto"; + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + // ── Timeframe ──────────────────────────────────────────────────────── + private ChartTimeFrame _selectedTimeFrame = ChartTimeFrame.OneMinute; + public ChartTimeFrame SelectedTimeFrame + { + get => _selectedTimeFrame; + set + { + if (SetProperty(ref _selectedTimeFrame, value)) + { + _ = LoadHistoricalDataAsync(); + } + } + } + + // ── Comandi ────────────────────────────────────────────────────────── + public ICommand StartStreamCommand { get; } + public ICommand StopStreamCommand { get; } + + // ── Costruttore ────────────────────────────────────────────────────── + public PriceChartViewModel(ITradingService tradingService) + { + _tradingService = tradingService ?? throw new ArgumentNullException(nameof(tradingService)); + + StartStreamCommand = new RelayCommand(_ => _ = StartStreamingAsync(), _ => !IsStreaming); + StopStreamCommand = new RelayCommand(_ => StopStreaming(), _ => IsStreaming); + } + + // ── Caricamento dati storici ───────────────────────────────────────── + private async Task LoadHistoricalDataAsync() + { + try + { + StatusMessage = $"Caricamento storico {Symbol}..."; + + var timeFrame = SelectedTimeFrame == ChartTimeFrame.OneMinute + ? BarTimeFrame.Minute + : BarTimeFrame.Hour; + + int barCount = SelectedTimeFrame == ChartTimeFrame.OneMinute ? 200 : 100; + + var bars = await _tradingService.GetHistoricalBarsAsync(Symbol, timeFrame, barCount); + if (bars == null || bars.Count == 0) + { + StatusMessage = "Nessun dato storico disponibile"; + return; + } + + PriceData.Clear(); + decimal? firstPrice = null; + + foreach (var bar in bars.OrderBy(b => b.TimeUtc)) + { + if (!firstPrice.HasValue) firstPrice = bar.Close; + PriceData.Add(new PricePoint + { + Timestamp = bar.TimeUtc.ToLocalTime(), + Price = bar.Close + }); + } + + if (PriceData.Count > 0) + { + var last = PriceData.Last(); + CurrentPrice = last.Price; + if (firstPrice.HasValue && firstPrice.Value > 0) + { + PriceChange = CurrentPrice - firstPrice.Value; + PriceChangePercent = (PriceChange / firstPrice.Value) * 100m; + } + } + + StatusMessage = $"Caricati {bars.Count} punti storici"; + OnPropertyChanged(nameof(PriceChangeColor)); + OnPropertyChanged(nameof(PriceChangeBrush)); + OnPropertyChanged(nameof(PriceChangeIcon)); + } + catch (Exception ex) + { + StatusMessage = $"Errore caricamento: {ex.Message}"; + } + } + + // ── Streaming real-time ────────────────────────────────────────────── + public async Task StartStreamingAsync() + { + if (IsStreaming) return; + + await LoadHistoricalDataAsync(); + + IsStreaming = true; + _streamCts = new CancellationTokenSource(); + StatusMessage = $"Streaming {Symbol} attivo..."; + + // TODO: Implementare WebSocket streaming con Alpaca + // Per ora: polling ogni 5 secondi come fallback + _streamTask = Task.Run(() => PollingLoopAsync(_streamCts.Token)); + } + + private async Task PollingLoopAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(5), ct); + if (ct.IsCancellationRequested) break; + + decimal price = await _tradingService.GetLatestPriceAsync(Symbol); + UpdatePrice(price); + } + catch (OperationCanceledException) { break; } + catch (Exception ex) + { + StatusMessage = $"Errore stream: {ex.Message}"; + await Task.Delay(TimeSpan.FromSeconds(10), ct); + } + } + } + + private void UpdatePrice(decimal price) + { + var dispatcher = System.Windows.Application.Current?.Dispatcher; + if (dispatcher != null && !dispatcher.CheckAccess()) + { + dispatcher.Invoke(() => UpdatePriceInternal(price)); + } + else + { + UpdatePriceInternal(price); + } + } + + private void UpdatePriceInternal(decimal price) + { + var firstPrice = PriceData.Count > 0 ? PriceData.First().Price : price; + PriceChange = price - CurrentPrice; + CurrentPrice = price; + + if (firstPrice > 0) + { + PriceChangePercent = ((price - firstPrice) / firstPrice) * 100m; + } + + PriceData.Add(new PricePoint + { + Timestamp = DateTime.Now, + Price = price + }); + + // Limita dimensione buffer + while (PriceData.Count > MaxDataPoints) + PriceData.RemoveAt(0); + + OnPropertyChanged(nameof(PriceChangeColor)); + OnPropertyChanged(nameof(PriceChangeBrush)); + OnPropertyChanged(nameof(PriceChangeIcon)); + } + + public void StopStreaming() + { + _streamCts?.Cancel(); + IsStreaming = false; + StatusMessage = "Stream fermato"; + } + + private async Task RestartStreamAsync() + { + if (IsStreaming) + { + StopStreaming(); + await Task.Delay(500); + await StartStreamingAsync(); + } + } + + // ── Cleanup ────────────────────────────────────────────────────────── + public void Dispose() + { + StopStreaming(); + _streamCts?.Dispose(); + } + } + + // ── Modello dati punto prezzo ──────────────────────────────────────────── + public class PricePoint + { + public DateTime Timestamp { get; set; } + public decimal Price { get; set; } + } + + // ── Enum timeframe grafico ─────────────────────────────────────────────── + public enum ChartTimeFrame + { + OneMinute, + FiveMinutes, + OneHour + } +} diff --git a/DesktopBot/ViewModels/SettingsViewModel.cs b/DesktopBot/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..57180dc --- /dev/null +++ b/DesktopBot/ViewModels/SettingsViewModel.cs @@ -0,0 +1,223 @@ +using System; +using System.Reflection; +using System.Windows.Input; +using DesktopBot.Models; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + /// + /// ViewModel per la schermata di configurazione credenziali Alpaca e logging + /// + public class SettingsViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + private readonly Action _onCredentialsSaved; + private readonly Action _onLoggingConfigSaved; + private LoggingSettingsViewModel _loggingSettings; + + private string _apiKey; + private string _apiSecret; + private bool _isPaper = true; + private string _statusMessage; + private bool _isConnecting; + private bool _isConnected; + + public string ApiKey + { + get => _apiKey; + set => SetProperty(ref _apiKey, value); + } + + /// + /// Il secret non è bindabile direttamente (PasswordBox non supporta binding). + /// Viene impostato dal code-behind tramite questa proprietà. + /// + public string ApiSecret + { + get => _apiSecret; + set => SetProperty(ref _apiSecret, value); + } + + public bool IsPaper + { + get => _isPaper; + set => SetProperty(ref _isPaper, value); + } + + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public bool IsConnecting + { + get => _isConnecting; + set => SetProperty(ref _isConnecting, value); + } + + public bool IsConnected + { + get => _isConnected; + set => SetProperty(ref _isConnected, value); + } + + /// ViewModel per la configurazione dei log + public LoggingSettingsViewModel LoggingSettings + { + get => _loggingSettings; + set => SetProperty(ref _loggingSettings, value); + } + + public ICommand SaveCommand { get; } + public ICommand TestConnectionCommand { get; } + public ICommand DeleteCredentialsCommand { get; } + public ICommand SaveLoggingCommand { get; } + public ICommand ResetLoggingCommand { get; } + + // ── Informazioni applicazione ────────────────────────────────────────── + public string AppVersion { get; } = Assembly.GetExecutingAssembly() + .GetName().Version?.ToString() ?? "1.0.0.0"; + public string BuildDate { get; } = System.IO.File.GetLastWriteTime( + Assembly.GetExecutingAssembly().Location) + .ToString("dd/MM/yyyy HH:mm"); + public string Author { get; } = "Alberto Balbo"; + public string Framework { get; } = ".NET Framework 4.8.1"; + public string Description { get; } = "Trading Bot automatico per Alpaca Markets API v2. " + + "Supporta paper trading e live trading con strategia EMA Crossover + RSI."; + + public SettingsViewModel(ITradingService tradingService, + Action onCredentialsSaved, + Action onLoggingConfigSaved = null) + { + _tradingService = tradingService; + _onCredentialsSaved = onCredentialsSaved; + _onLoggingConfigSaved = onLoggingConfigSaved; + + SaveCommand = new RelayCommand( + _ => SaveCredentials(), + _ => !string.IsNullOrWhiteSpace(ApiKey) && !string.IsNullOrWhiteSpace(ApiSecret) + ); + + TestConnectionCommand = new RelayCommand( + async _ => await TestConnectionAsync(), + _ => !string.IsNullOrWhiteSpace(ApiKey) && !string.IsNullOrWhiteSpace(ApiSecret) && !IsConnecting + ); + + DeleteCredentialsCommand = new RelayCommand(_ => DeleteCredentials()); + + SaveLoggingCommand = new RelayCommand(_ => SaveLoggingConfiguration()); + ResetLoggingCommand = new RelayCommand(_ => ResetLoggingConfiguration()); + + // Carica credenziali esistenti (mostra solo la key, non il secret) + var saved = CredentialService.LoadCredentials(); + if (saved != null) + { + ApiKey = saved.ApiKey; + ApiSecret = saved.ApiSecret; + IsPaper = saved.IsPaper; + StatusMessage = "✓ Credenziali caricate da configurazione salvata"; + IsConnected = true; + } + else + { + StatusMessage = "Inserisci le credenziali API di Alpaca"; + } + + // Inizializza logging settings con configurazione di default + LoggingSettings = new LoggingSettingsViewModel(new LoggingConfiguration()); + } + + /// + /// Imposta la configurazione di logging iniziale dal bot + /// + public void SetLoggingConfiguration(LoggingConfiguration config) + { + LoggingSettings = new LoggingSettingsViewModel(config); + } + + /// + /// Salva la configurazione di logging + /// + private void SaveLoggingConfiguration() + { + var updatedConfig = LoggingSettings.GetUpdatedConfig(); + _onLoggingConfigSaved?.Invoke(updatedConfig); + StatusMessage = "✓ Configurazione log salvata"; + } + + /// + /// Ripristina i limiti di logging ai valori di default + /// + private void ResetLoggingConfiguration() + { + LoggingSettings.ResetToDefaults(); + StatusMessage = "✓ Limiti log ripristinati ai valori di default"; + } + + /// + /// Salva le credenziali cifrate e notifica il ViewModel principale + /// + private void SaveCredentials() + { + if (string.IsNullOrWhiteSpace(ApiKey) || string.IsNullOrWhiteSpace(ApiSecret)) + { + StatusMessage = "⚠ API Key e Secret sono obbligatori"; + return; + } + + try + { + CredentialService.SaveCredentials(ApiKey, ApiSecret, IsPaper); + _tradingService.Initialize(ApiKey, ApiSecret, IsPaper); + _onCredentialsSaved?.Invoke(ApiKey, ApiSecret, IsPaper); + StatusMessage = "✓ Credenziali salvate e connessione configurata"; + IsConnected = true; + } + catch (Exception ex) + { + StatusMessage = $"✗ Errore nel salvataggio: {ex.Message}"; + } + } + + /// + /// Testa la connessione alle API Alpaca con le credenziali inserite + /// + private async System.Threading.Tasks.Task TestConnectionAsync() + { + IsConnecting = true; + IsConnected = false; + StatusMessage = "⟳ Test connessione in corso..."; + + try + { + _tradingService.Initialize(ApiKey, ApiSecret, IsPaper); + var equity = await _tradingService.GetAvailableEquityAsync(); + StatusMessage = $"✓ Connessione riuscita! Equity disponibile: ${equity:N2}"; + IsConnected = true; + } + catch (Exception ex) + { + StatusMessage = $"✗ Connessione fallita: {ex.Message}"; + IsConnected = false; + } + finally + { + IsConnecting = false; + } + } + + /// + /// Elimina le credenziali salvate + /// + private void DeleteCredentials() + { + CredentialService.DeleteCredentials(); + ApiKey = string.Empty; + ApiSecret = string.Empty; + IsConnected = false; + StatusMessage = "Credenziali eliminate. Inserisci nuove credenziali."; + } + } +} diff --git a/DesktopBot/ViewModels/WalletViewModel.cs b/DesktopBot/ViewModels/WalletViewModel.cs new file mode 100644 index 0000000..8b4d757 --- /dev/null +++ b/DesktopBot/ViewModels/WalletViewModel.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Input; +using DesktopBot.Services; + +namespace DesktopBot.ViewModels +{ + /// + /// ViewModel per il Wallet e le posizioni aperte + /// + public class WalletViewModel : BaseViewModel + { + private readonly ITradingService _tradingService; + + private decimal _equity; + private bool _isLoading; + private string _errorMessage; + private bool _hasError; + private ObservableCollection _positions = new ObservableCollection(); + + public decimal Equity + { + get => _equity; + set => SetProperty(ref _equity, value); + } + + public bool IsLoading + { + get => _isLoading; + set => SetProperty(ref _isLoading, value); + } + + public string ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + + public bool HasError + { + get => _hasError; + set => SetProperty(ref _hasError, value); + } + + public ObservableCollection Positions + { + get => _positions; + set => SetProperty(ref _positions, value); + } + + public ICommand RefreshCommand { get; } + public ICommand CloseAllCommand { get; } + + public WalletViewModel(ITradingService tradingService) + { + _tradingService = tradingService; + RefreshCommand = new RelayCommand(async _ => await RefreshAsync()); + CloseAllCommand = new RelayCommand(async _ => await CloseAllPositionsAsync()); + } + + private async System.Threading.Tasks.Task RefreshAsync() + { + await LoadAsync(); + } + + /// + /// Carica equity e posizioni aperte da Alpaca + /// + public async System.Threading.Tasks.Task LoadAsync() + { + IsLoading = true; + HasError = false; + try + { + var accountTask = _tradingService.GetAccountAsync(); + var positionsTask = _tradingService.GetAllPositionsAsync(); + + await System.Threading.Tasks.Task.WhenAll(accountTask, positionsTask); + + var account = accountTask.Result; + var positions = positionsTask.Result; + + Application.Current?.Dispatcher.Invoke(() => + { + Equity = account.Equity ?? 0m; + Positions.Clear(); + foreach (var pos in positions) + Positions.Add(new PositionViewModel(pos)); + }); + } + catch (Exception ex) + { + Application.Current?.Dispatcher.Invoke(() => + { + HasError = true; + ErrorMessage = "Errore: " + ex.Message; + }); + } + finally + { + Application.Current?.Dispatcher.Invoke(() => IsLoading = false); + } + } + + private async System.Threading.Tasks.Task CloseAllPositionsAsync() + { + try + { + await _tradingService.CloseAllPositionsAsync(); + await RefreshAsync(); + } + catch { /* Gestito silenziosamente */ } + } + } + + /// + /// ViewModel per una singola posizione aperta + /// + public class PositionViewModel : BaseViewModel + { + public string Symbol { get; } + public decimal Quantity { get; } + public string Side { get; } + public decimal EntryPrice { get; } + public decimal CurrentPrice { get; } + public decimal MarketValue { get; } + public decimal UnrealizedPnL { get; } + public decimal UnrealizedPnLPercent { get; } + public bool IsProfit { get; } + + public PositionViewModel(Alpaca.Markets.IPosition position) + { + Symbol = position.Symbol; + Quantity = Math.Abs(position.Quantity); + Side = position.Quantity >= 0 ? "LONG" : "SHORT"; + EntryPrice = position.AverageEntryPrice; + CurrentPrice = position.AssetCurrentPrice ?? 0m; + MarketValue = position.MarketValue ?? 0m; + UnrealizedPnL = position.UnrealizedProfitLoss ?? 0m; + var cost = position.CostBasis != 0 ? position.CostBasis : 1m; + UnrealizedPnLPercent = (cost != 0) ? (UnrealizedPnL / Math.Abs(cost)) * 100m : 0m; + IsProfit = UnrealizedPnL >= 0; + } + } +} diff --git a/DesktopBot/Views/BalanceView.xaml b/DesktopBot/Views/BalanceView.xaml new file mode 100644 index 0000000..e335b21 --- /dev/null +++ b/DesktopBot/Views/BalanceView.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DesktopBot/Views/MainView.xaml.cs b/DesktopBot/Views/MainView.xaml.cs new file mode 100644 index 0000000..0ac1c4b --- /dev/null +++ b/DesktopBot/Views/MainView.xaml.cs @@ -0,0 +1,17 @@ +using System.Windows; +using DesktopBot.ViewModels; + +namespace DesktopBot.Views +{ + /// + /// MainView - Finestra principale con navigazione verticale dark mode + /// + public partial class MainView : Window + { + public MainView() + { + InitializeComponent(); + DataContext = new MainViewModel(); + } + } +} diff --git a/DesktopBot/Views/OrdersView.xaml b/DesktopBot/Views/OrdersView.xaml new file mode 100644 index 0000000..914b810 --- /dev/null +++ b/DesktopBot/Views/OrdersView.xaml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + +