Compare commits

..

21 Commits

Author SHA1 Message Date
Alby96 e3c0bd51b2 Sviluppo TradingBot 2026-06-09 18:29:41 +02:00
Alby96 61f1e59964 Rimuovi cartella docs\deployment\ da TradingBot.csproj
Eliminata la voce relativa alla cartella docs\deployment\ dal file di progetto per semplificare la struttura e rimuovere riferimenti non più necessari alla documentazione di deployment.
2026-05-26 17:49:25 +02:00
Alby96 64f3511695 Nuove: multi-strategy, indicatori avanzati, posizioni
- Sidebar portfolio con metriche dettagliate (Totale, Investito, Disponibile, P&L, ROI) e aggiornamento real-time
- Sistema multi-strategia: 8 strategie assegnabili per asset, voting decisionale, pagina Trading Control
- Nuova pagina Posizioni: gestione, chiusura manuale, P&L non realizzato, notifiche
- Sistema indicatori tecnici: 7+ indicatori configurabili, segnali real-time, raccomandazioni, storico segnali
- Refactoring TradingBotService per capitale, P&L, ROI, eventi
- Nuovi modelli e servizi per strategie/indicatori, persistenza configurazioni
- UI/UX: navigazione aggiornata, widget, modali, responsive
- Aggiornamento README e CHANGELOG con tutte le novità
2026-01-06 17:49:07 +01:00
Alby96 c229c50f1d chore: Bump version to 1.5.2 - Add detailed capital metrics in sidebar: Total, Invested, Available, P&L, ROI 2025-12-23 10:51:06 +01:00
Alby96 0809c9af87 chore: Bump version to 1.5.1 - Add Positions management page with manual close functionality 2025-12-22 21:04:16 +01:00
Alby96 ae5f8f9249 chore: Bump version to 1.5.0 - Multi-strategy trading system with 8 famous strategies 2025-12-22 18:16:22 +01:00
Alby96 f21adf3313 chore: Bump version to 1.4.0 - Add comprehensive indicators system with configuration 2025-12-22 15:54:55 +01:00
Alby96 92c8e57a8c Persistenza dati e logging avanzato con UI e Unraid
- Aggiunto TradeHistoryService per persistenza trade/posizioni attive su disco (JSON, auto-save/restore)
- Logging centralizzato (LoggingService) con livelli, categorie, simbolo e buffer circolare (500 log)
- Nuova pagina Logs: monitoraggio real-time, filtri avanzati, cancellazione log, colorazione livelli
- Sezione "Dati Persistenti" in Settings: conteggio trade, dimensione dati, reset con conferma modale
- Background service per salvataggio sicuro su shutdown/stop container
- Aggiornata sidebar, stili modali/bottoni danger, .gitignore e documentazione (README, CHANGELOG, UNRAID_INSTALL, checklist)
- Versione 1.3.0
2025-12-22 11:24:17 +01:00
Alby96 d7ae3e5d44 chore: Bump version to 1.3.0 - Add comprehensive logs page with real-time monitoring 2025-12-22 00:43:45 +01:00
Alby96 54cfe05687 chore: Bump version to 1.2.0 - Add trade history persistence and data management 2025-12-21 18:48:41 +01:00
Alby96 0e64afa1f2 Refactor documentazione, versioning e deployment
- Riorganizzato README.md con badge versione, changelog, guida rapida e istruzioni semplificate per Docker/Unraid
- Creato CHANGELOG.md secondo standard Keep a Changelog/SemVer
- Aggiunto script bump-version.ps1 per gestione automatica versioni e tagging Git
- Aggiornate guide deployment: PUBLISHING_GUIDE.md, UNRAID_INSTALL.md e README.md in /deployment
- Modificato unraid-template.xml: porta WebUI configurabile (default 8888), volumi e variabili ambiente semplificati
- Aggiornata PROJECT_STRUCTURE.md con nuova struttura e best practices
- Migliorata chiarezza, professionalità e automazione del workflow di rilascio
2025-12-21 18:31:00 +01:00
Alby96 121324dfc7 chore: Bump version to 1.1.0 - Add automated deployment system, versioning, and comprehensive documentation 2025-12-17 23:34:36 +01:00
Alby96 cc34d2b57f Riorganizzazione deployment, doc e publish automatico
- Spostata tutta la configurazione di deployment in /deployment (docker-compose, unraid-template, guide)
- Aggiunte e aggiornate guide dettagliate: publishing su Gitea, installazione Unraid, struttura progetto
- Migliorato target MSBuild: publish automatico su Gitea Registry da Visual Studio, log dettagliati, condizioni più robuste
- Aggiornato e ampliato .gitignore per escludere build, dati e file locali
- Rimossi file obsoleti dalla root (ora tutto in /deployment)
- Struttura più chiara, zero script esterni, documentazione completa e workflow di deploy semplificato
2025-12-17 23:15:46 +01:00
Alby96 5532ad2473 Supporto Unraid/Docker nativo, healthcheck e template
- Configurazione Kestrel ottimizzata per ambienti Docker/Unraid: porta 8080 in produzione, HTTPS redirect solo in sviluppo
- Endpoint /health sempre attivo per healthcheck automatici
- Aggiunti file docker-compose.yml e unraid-template.xml per deploy e gestione nativa su Unraid (senza Portainer)
- Nuova guida UNRAID_NATIVE_INSTALL.md per installazione, update e troubleshooting su Unraid
- Logging e appsettings separati per Development/Production
- launchSettings.json aggiornato e semplificato
- Rimosso package Azure Containers Tools dal csproj; aggiunto target MSBuild per push automatico su Gitea Registry dopo publish
- Algoritmo SMA più robusto: filtra dati nulli/invalidi e gestisce casi di dati insufficienti
- Pronto per deploy professionale, aggiornamento e gestione semplificata in ambienti containerizzati
2025-12-17 14:34:52 +01:00
Alby96 8ee8dc7e71 Refactoring Docker: integrazione con Visual Studio
Rimosse configurazioni e script manuali per Docker, build e documentazione. Riscritto il Dockerfile per supportare il flusso di lavoro Visual Studio/.NET 10 con multi-stage build semplificato. Aggiunte impostazioni di pubblicazione Docker in TradingBot.csproj e nuovo profilo "Docker" in launchSettings.json. Eliminati file di configurazione e script non più necessari; aggiunto Dockerfile.original come riferimento legacy. Ottimizzato il progetto per la pubblicazione tramite strumenti Microsoft.
2025-12-15 15:46:26 +01:00
Alby96 d933c7e812 Semplifica configurazione Docker e gestione porta UI
Riorganizza .env.example lasciando solo EXTERNAL_PORT e spostando tutte le altre impostazioni applicative nella UI web. Il mapping della porta in docker-compose.yml ora usa la variabile EXTERNAL_PORT per una personalizzazione più semplice. Rimosse variabili e opzioni avanzate non essenziali dal compose. Aggiornata la documentazione per riflettere la nuova gestione centralizzata delle impostazioni tramite interfaccia web.
2025-12-15 11:38:36 +01:00
Alby96 f69d5dd567 Configura Kestrel e accesso browser per Docker/Unraid
- Kestrel ora ascolta su 0.0.0.0:8080 per compatibilità Docker
- HTTPS redirect attivo solo in sviluppo, disabilitato in prod
- Aggiunta sezione "Kestrel" in appsettings.json e nuovo appsettings.Production.json con limiti di sicurezza
- Healthcheck Docker ora usa wget su /health (porta 8080)
- Aggiunta documentazione dettagliata in BROWSER_ACCESS_CONFIG.md
- Migliorata accessibilità browser, supporto reverse proxy e SignalR
2025-12-15 11:32:26 +01:00
Alby96 c93ccd5e4a Migliora robustezza Dockerfile e controlli TradingBotService
- Installa wget e aggiorna healthcheck in Dockerfile (usa wget invece di curl, UID 1001 per utente non-root)
- Aggiunti controlli di nullità e validità su simboli, prezzi e segnali in TradingBotService
- Migliorata gestione delle eccezioni con stampa dello stack trace
- Filtrati dati non validi prima del calcolo degli indicatori
- Aumentata la sicurezza e la resilienza contro dati corrotti o incompleti
2025-12-15 10:37:31 +01:00
Alby96 e414123cd0 Aggiorna e riorganizza la documentazione del progetto
- Sostituito README.md con versione avanzata e strutturata (indice, badge, quick start, deployment, troubleshooting, roadmap, credits)
- Aggiunto .gitignore completo per .NET, Docker, VSCode, log, dati locali e secrets
- Creato .env.example con tutte le variabili d’ambiente documentate per Docker/Unraid
- Aggiunti script organize-docs.ps1/.sh per strutturare e spostare la documentazione in docs/
- Aggiornate e migliorate tutte le guide tecniche (Docker, Unraid, Git workflow, troubleshooting, verifica finale)
- Documentazione ora pronta per ambienti di produzione, collaborazione e manutenzione
2025-12-13 00:24:58 +01:00
Alby96 b2f04b6600 Supporto Docker/Unraid: build, healthcheck, docs
Aggiunti Dockerfile multi-stage, .dockerignore e docker-compose.yml per deployment containerizzato (con healthcheck, volumi persistenti, limiti risorse). Script di build per Linux/Mac e Windows. In Program.cs aggiunto endpoint /health e health checks per orchestrazione. Documentazione estesa: guide Unraid, quickstart Docker, workflow Git/DevOps, best practices su sicurezza, backup, monitoring. Progetto ora pronto per deploy e gestione professionale in ambienti Docker/Unraid.
2025-12-12 23:40:34 +01:00
Alby96 d25b4443c0 Aggiunta Bootstrap 5.3.3 (CSS, JS, RTL, mappe) al progetto
Sono stati aggiunti tutti i file principali di Bootstrap 5.3.3, inclusi CSS, JavaScript (bundle, ESM, UMD, minificati), versioni RTL, utility, reboot, griglia e relative mappe delle sorgenti. Questi file abilitano un sistema di design moderno, responsive e accessibile, con supporto per layout LTR e RTL, debugging avanzato tramite source map e tutte le funzionalità di Bootstrap per lo sviluppo dell’interfaccia utente. Nessuna modifica ai file esistenti.
2025-12-12 23:27:28 +01:00
258 changed files with 101360 additions and 2 deletions
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.1.2"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
+24
View File
@@ -0,0 +1,24 @@
<Application x:Class="DesktopBot.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DesktopBot"
xmlns:conv="clr-namespace:DesktopBot.Converters"
StartupUri="Views/MainView.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/DesktopBot;component/Themes/DarkTheme.xaml"/>
</ResourceDictionary.MergedDictionaries>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
<conv:InverseBoolConverter x:Key="InverseBoolConverter"/>
<conv:BoolVisibilityConverter x:Key="BoolVisibilityConverter"/>
<conv:InvertedBoolVisibilityConverter x:Key="InvertedBoolVisibilityConverter"/>
<conv:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
<conv:NotNullToVisibilityConverter x:Key="NotNullToVisibilityConverter"/>
<conv:EmptyStringToVisibilityConverter x:Key="EmptyStringToVisibilityConverter"/>
<conv:ZeroToCollapsedConverter x:Key="ZeroToCollapsedConverter"/>
<conv:ProfitColorConverter x:Key="ProfitColorConverter"/>
<conv:BoolToStreamColorConverter x:Key="BoolToStreamColorConverter"/>
</ResourceDictionary>
</Application.Resources>
</Application>
+17
View File
@@ -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
{
/// <summary>
/// Logica di interazione per App.xaml
/// </summary>
public partial class App : Application
{
}
}
+221
View File
@@ -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
{
/// <summary>
/// Line chart WPF nativo (nessuna dipendenza esterna).
/// Legge una <see cref="System.Collections.ObjectModel.ObservableCollection{PricePoint}"/>
/// e ridisegna automaticamente ad ogni variazione.
/// </summary>
public class PriceLineChart : FrameworkElement
{
// ── Dependency Properties ────────────────────────────────────────────
public static readonly DependencyProperty PriceDataProperty =
DependencyProperty.Register(
nameof(PriceData),
typeof(IEnumerable<PricePoint>),
typeof(PriceLineChart),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.AffectsRender,
OnPriceDataChanged));
public IEnumerable<PricePoint> PriceData
{
get => (IEnumerable<PricePoint>)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<PricePoint>().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));
}
}
}
+177
View File
@@ -0,0 +1,177 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace DesktopBot.Converters
{
/// <summary>
/// Converter che restituisce lo stile NavItemActive se il tab corrisponde,
/// altrimenti NavItem (usato nella barra di navigazione laterale)
/// </summary>
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();
}
}
/// <summary>
/// Converter per i colori dei log in base al livello
/// </summary>
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();
}
}
/// <summary>Inverte un valore booleano (usato per il radio button Live/Paper)</summary>
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;
}
}
/// <summary>bool → Visibility (true=Visible, false=Collapsed)</summary>
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();
}
/// <summary>bool → Visibility invertito (true=Collapsed, false=Visible)</summary>
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();
}
/// <summary>null → Visible, not-null → Collapsed (placeholder "nessun bot selezionato")</summary>
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();
}
/// <summary>not-null → Visible, null → Collapsed (mostra pannello dettaglio)</summary>
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();
}
/// <summary>stringa vuota/null → Visible (placeholder search), non-vuota → Collapsed</summary>
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();
}
/// <summary>int 0 → Collapsed, >0 → Visible (usato per nascondere la lista risultati vuota)</summary>
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();
}
/// <summary>bool IsProfit → verde o rosso per P&amp;L</summary>
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();
}
/// <summary>
/// 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.
/// </summary>
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();
}
/// <summary>
/// Converter per il colore dell'indicatore di streaming.
/// True (streaming attivo) → Verde, False (fermo) → Grigio
/// </summary>
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();
}
}
@@ -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
{
/// <summary>
/// Converte una IEnumerable in una collezione limitata ai primi N elementi.
/// Parametro: numero massimo di elementi da visualizzare (default 50).
/// </summary>
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<object>().Take(maxItems).ToList();
return new ObservableCollection<object>(list);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
+340
View File
@@ -0,0 +1,340 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{4ED6728E-EAC8-4745-A106-476DCBDFB710}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>DesktopBot</RootNamespace>
<AssemblyName>DesktopBot</AssemblyName>
<TargetFrameworkVersion>v4.8.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>favicon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Security" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="Newtonsoft.Json">
<HintPath>packages\Newtonsoft.Json.13.0.3\lib\netstandard2.0\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="Polly">
<HintPath>packages\Polly.8.5.0\lib\net462\Polly.dll</HintPath>
</Reference>
<Reference Include="Polly.Core">
<HintPath>packages\Polly.Core.8.5.0\lib\net462\Polly.Core.dll</HintPath>
</Reference>
<Reference Include="Portable.System.DateTimeOnly">
<HintPath>packages\Portable.System.DateTimeOnly.9.0.0\lib\net462\Portable.System.DateTimeOnly.dll</HintPath>
</Reference>
<Reference Include="System.IO.Pipelines">
<HintPath>packages\System.IO.Pipelines.9.0.0\lib\net462\System.IO.Pipelines.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http.WinHttpHandler">
<HintPath>packages\System.Net.Http.WinHttpHandler.9.0.0\lib\net462\System.Net.Http.WinHttpHandler.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Channels">
<HintPath>packages\System.Threading.Channels.9.0.0\lib\net462\System.Threading.Channels.dll</HintPath>
</Reference>
<Reference Include="System.Text.Json">
<HintPath>packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll</HintPath>
</Reference>
<Reference Include="Alpaca.Markets">
<HintPath>packages\Alpaca.Markets.7.2.0\lib\net462\Alpaca.Markets.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Bcl.AsyncInterfaces">
<HintPath>packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions">
<HintPath>packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Memory">
<HintPath>packages\System.Memory.4.5.5\lib\net461\System.Memory.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Buffers">
<HintPath>packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Numerics.Vectors">
<HintPath>packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Runtime.CompilerServices.Unsafe">
<HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Text.Json">
<HintPath>packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
</ApplicationDefinition>
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<None Include="Documents\Algorithm\Report Strategie Trading Algoritmico Avanzato.md" />
<None Include="Documents\Code\gemini-code-1779811123559.cs" />
<None Include="Documents\Code\gemini-code-1779811125057.cs" />
<None Include="Documents\Code\gemini-code-1779811126091.cs" />
<None Include="Documents\Code\gemini-code-1779811127546.cs" />
<None Include="Documents\Code\gemini-code-1779811128687.cs" />
<Compile Include="Controls\PriceLineChart.cs" />
<Compile Include="Converters\MaxItemsConverter.cs" />
<Compile Include="Engine\BtcUsdAlgorithm.cs" />
<Compile Include="Engine\PositionRiskManager.cs" />
<Compile Include="Engine\StrategyAdvisor.cs" />
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="Views\BotsManagerView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\MainView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\DashboardView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\BotConfigView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\LiveLogView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\PriceChartView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\SettingsView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\WalletView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\BalanceView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\PositionsView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\OrdersView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Themes\DarkTheme.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="Models\AssetSearchResult.cs" />
<Compile Include="Models\BotInstance.cs" />
<Compile Include="Models\BotInstanceStore.cs" />
<Compile Include="Models\BotTradeRecord.cs" />
<Compile Include="Models\LoggingConfiguration.cs" />
<Compile Include="Services\AlpacaPingService.cs" />
<Compile Include="Services\ApiCallCounterService.cs" />
<Compile Include="Services\CredentialService.cs" />
<Compile Include="Services\MarketHoursService.cs" />
<Compile Include="ViewModels\BotConfigViewModel.cs" />
<Compile Include="ViewModels\BotInstanceViewModel.cs" />
<Compile Include="ViewModels\BotsManagerViewModel.cs" />
<Compile Include="ViewModels\DashboardViewModel.cs" />
<Compile Include="ViewModels\LiveLogViewModel.cs" />
<Compile Include="ViewModels\LoggingSettingsViewModel.cs" />
<Compile Include="ViewModels\PingViewModel.cs" />
<Compile Include="ViewModels\PriceChartViewModel.cs" />
<Compile Include="ViewModels\SettingsViewModel.cs" />
<Compile Include="ViewModels\WalletViewModel.cs" />
<Compile Include="Views\BotsManagerView.xaml.cs">
<DependentUpon>BotsManagerView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\MainView.xaml.cs">
<DependentUpon>MainView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\DashboardView.xaml.cs">
<DependentUpon>DashboardView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\BotConfigView.xaml.cs">
<DependentUpon>BotConfigView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\LiveLogView.xaml.cs">
<DependentUpon>LiveLogView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PriceChartView.xaml.cs">
<DependentUpon>PriceChartView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\SettingsView.xaml.cs">
<DependentUpon>SettingsView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\WalletView.xaml.cs">
<DependentUpon>WalletView.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModels\AccountViewModels.cs" />
<Compile Include="Views\BalanceView.xaml.cs">
<DependentUpon>BalanceView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PositionsView.xaml.cs">
<DependentUpon>PositionsView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\OrdersView.xaml.cs">
<DependentUpon>OrdersView.xaml</DependentUpon>
</Compile>
<Compile Include="Services\ITradingService.cs" />
<Compile Include="Services\AlpacaTradingService.cs" />
<Compile Include="Engine\AutomatedBotEngine.cs" />
<Compile Include="Models\BotConfiguration.cs" />
<Compile Include="Models\TradingSignal.cs" />
<Compile Include="Models\BotLogEntry.cs" />
<Compile Include="ViewModels\BaseViewModel.cs" />
<Compile Include="ViewModels\MainViewModel.cs" />
<Compile Include="Converters\Converters.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="Documents\Alpaca\245-trading-for-trading-api.md" />
<None Include="Documents\Alpaca\5f36ea05-b1b4-5fd1-91b5-ef51db361a4b.md" />
<None Include="Documents\Alpaca\about-alpaca.md" />
<None Include="Documents\Alpaca\about-connect-api.md" />
<None Include="Documents\Alpaca\about-market-data-api.md" />
<None Include="Documents\Alpaca\account-activities.md" />
<None Include="Documents\Alpaca\account-plans.md" />
<None Include="Documents\Alpaca\additional-resources.md" />
<None Include="Documents\Alpaca\alpaca-api-platform.md" />
<None Include="Documents\Alpaca\alpaca-elite-smart-router.md" />
<None Include="Documents\Alpaca\alpaca-mcp-server.md" />
<None Include="Documents\Alpaca\authentication.md" />
<None Include="Documents\Alpaca\crypto-fees.md" />
<None Include="Documents\Alpaca\crypto-orders.md" />
<None Include="Documents\Alpaca\crypto-pricing-data.md" />
<None Include="Documents\Alpaca\crypto-trading.md" />
<None Include="Documents\Alpaca\fix-messages.md" />
<None Include="Documents\Alpaca\fractional-trading.md" />
<None Include="Documents\Alpaca\getting-started-with-alpaca-market-data.md" />
<None Include="Documents\Alpaca\getting-started-with-trading-api.md" />
<None Include="Documents\Alpaca\getting-started.md" />
<None Include="Documents\Alpaca\historical-api.md" />
<None Include="Documents\Alpaca\historical-crypto-data-1.md" />
<None Include="Documents\Alpaca\historical-news-data.md" />
<None Include="Documents\Alpaca\historical-option-data.md" />
<None Include="Documents\Alpaca\historical-stock-data-1.md" />
<None Include="Documents\Alpaca\margin-and-short-selling.md" />
<None Include="Documents\Alpaca\market-data-faq.md" />
<None Include="Documents\Alpaca\non-trade-activities-for-option-events.md" />
<None Include="Documents\Alpaca\options-level-3-trading.md" />
<None Include="Documents\Alpaca\options-orders.md" />
<None Include="Documents\Alpaca\options-trading.md" />
<None Include="Documents\Alpaca\orders-at-alpaca.md" />
<None Include="Documents\Alpaca\paper-trading.md" />
<None Include="Documents\Alpaca\position-average-entry-price-calculation.md" />
<None Include="Documents\Alpaca\real-time-crypto-pricing-data.md" />
<None Include="Documents\Alpaca\real-time-option-data.md" />
<None Include="Documents\Alpaca\real-time-stock-pricing-data.md" />
<None Include="Documents\Alpaca\registering-your-app.md" />
<None Include="Documents\Alpaca\regulatory-fees.md" />
<None Include="Documents\Alpaca\sdks-and-tools.md" />
<None Include="Documents\Alpaca\streaming-market-data.md" />
<None Include="Documents\Alpaca\streaming-real-time-news.md" />
<None Include="Documents\Alpaca\the-intraday-margin-rule.md" />
<None Include="Documents\Alpaca\trading-api.md" />
<None Include="Documents\Alpaca\understanding-finras-new-intraday-margin-rule-and-the-end-of-pdt.md" />
<None Include="Documents\Alpaca\understanding-the-new-intraday-margin-rule.md" />
<None Include="Documents\Alpaca\user-protection.md" />
<None Include="Documents\Alpaca\using-oauth2-and-trading-api.md" />
<None Include="Documents\Alpaca\websocket-streaming.md" />
<None Include="Documents\Alpaca\working-with-account.md" />
<None Include="Documents\Alpaca\working-with-assets.md" />
<None Include="Documents\Alpaca\working-with-orders.md" />
<None Include="Documents\Alpaca\working-with-positions.md" />
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Resource Include="Files\Icon\favicon.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="favicon.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>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)"</PostBuildEvent>
</PropertyGroup>
</Project>
@@ -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** \<vector\>
\#**include** \<cmath\>
\#**include** \<numeric\>
class JohansenVECMEngine {
private:
std::vector\<double\> 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\<double\> beta, double mean, double std)
: beta\_vector(beta), mean\_spread(mean), std\_spread(std) {}
double calculate\_spread(const std::vector\<double\>& 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\<double\>& raw\_prices) {
std::vector\<double\> 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\<double\>& 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 LongTerm 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]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAaCAYAAABVX2cEAAABHElEQVR4XmNgGAWUAkcgfg3E/6F4BxBzIsnzAfEuJHkQXgfE3EhqUAAjEM8C4l9A/BOILVGlwSAIiNcwoFqEFQgC8UIgzmeA2DyFAWIBMigC4mg0MaxAH4j7gVgSiK8D8RMgVkSSZwHi2VB1BAHIxnQou4EB4rocuCwDgwgDxOUgHxAEfUBsDGXrAPF7ID4BxPxQMRsgngxl4wWw8ALZDgIgLy0H4n9A7AEVA7mapPBCDnCQISDDQIaCYo+s8IIBkPdA3gR514mByPACuQYUFqboEkAQwwCJiGtA3IkmhxWghxcyEGeAJBOQgUSFF8gLoKzBhS4BBQ1A/BaINdHEUYALEH9hQOQ1UBbyRlEBAaBkAsqrBMNrFIyCIQMA260zNBT6yKgAAAAASUVORK5CYII=>
[image2]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACUAAAAaCAYAAAAwspV7AAACPklEQVR4Xu2WMUgcQRSGnxhBUZFgUEQbxUYIiEgignYpFLExjRKChaAhpQFtFUmZEBQsbKwkTZoUWth4IoiQJhamCyYhKApaaSUk+f+bG5l7szN7Fzyr++Djjjc7M29n3s6uSJn7oQFW6WAEXss+JWMEvpPik/oAx3RDjAl4Cf86XsFX7kWgB+7AJhWvgH1wGa6KSbwy7wqRRrgFn6p4FA68Dv/AZ6qN1MDPcFzF2W8O7sJ2MZNvwDXxV3MIbsM6FQ/yEH6BP2BrflMWDngIm1W8F57BASfWAX+K6ePCutoX/8aCcPBr+Ak+UG12Fbk9mrdiEmhxYvVwT0wf9nXh9UlzJPJCTC3N6gbwCB6JX6jVcFP8pLg9GTErzx1wGRZzfZuKe9iVuJH8bbBwFX/lfl3s5KGkdJxwjN/wiYp72Hr6Ln7NkFF4KqZWXDghJ9aTx5KyfThmlFg9EQ6QNAFvgDei2wpJiuUSJVZPJJRUaPJQnNikQnNlSasnEkqKq8rV1W02KT6BfBJdCtq+tPOJMNkT2K0bwDy8gF1OzD6tSUcI65Jj8SkMklZPhBNyoKSTvlPM0+QeiIPwHPY7MQufOtahexO3vBQzkfu+40BT7kU57HbMqLiF59cxnIaT8Bt8Lf7BSVi/GSniVRNjQeKryXce64TyfxLs+1HMWHcCt+krfKwbioBjHOR+74w38L0kb0sa7LMo5ovif/oH4afICnyuGwqAxc/tL8kXaC1cEv+VE4NHDb8OSpJQmTIh/gG6mHPsgva0pQAAAABJRU5ErkJggg==>
[image3]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAaCAYAAABVX2cEAAABaUlEQVR4Xu3UvyuFURzH8a/8iPwqPzZKsigbJWUwyWRAUZTBoAwGkcgqKcqPwWwzMMlgQUYmg0UJ5R8wGP14fzr34Tyn+zw912LgU6+6fc+55z7nnO99zP58qtCHftTmavVojCZkSSlWcI9ZzOAaG7hE+/fU9JRgDweo9Op6oitcmHviTOnBEzrCAbKMnbCYllU8oykcIAsYDItp2ccHllAcjLWhLqilZszcYvKGM0yhxp+UNbrJNXMLRYvKjeXfeqZoi2qBdbyYW3DaGy+y+G3Hoi/rTDQpzADesejVdOsnltAmrdg112dhOvGKca82Zyltois/Rnk4QCZwh2a0YBsP5hpYi5ZFE6Oov/Tr3UFdW9fhD3u1Bpxbwt9K+z7CPG5zn9UOW3jEqMXPshenqPZqX6kw9wSKWqMLI+beGPm2rVtNPK9Cogs6xJC5M5yMDxcWbVfb3zT3morecz+OFtR55evJ//xmPgHIbDZ3AARizwAAAABJRU5ErkJggg==>
[image4]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAAC50lEQVR4Xu3cMahWZRgH8DekIVDSFCMwpGxpEcOspTaNWiKKcEhwijaJFkl0bAgSBMEpinRosC2KmrpQQ9EchuCghNHiVI3l8+ecc7/Xg0Tglet3/f3gzz3veb9773O3h+c957YGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABsNFsr22f39lReq2yqPDB+XU+pMXUAANyX/q681a2vdNdHKv906/VwoA01xtXKr93ersr3lS3dvbNt/RtMAIA1k6nVL5Ud4/rJyonFdnus8lm3Xg+ftqHGWKn8tthq77ehiUudkb/n+cU2AMDyerDybBsmVGmIpuPG/ZXL04fKQ5W93Xqt5fem2ZrST8ZS30uVn9tQY3xV+Wv1E61daLc2bKnf0SkAsPSeqxwerz9sQ5PTu1T5d8y7s73bOVW59h95ZPHRVWmq/qy8XXmncqwNTdfUbP3RfS4N2lRjpn2pK16c7b/a/l+9AAD3tIcrP45fI5Or6Ti092blu3brcWmmcmvl0JjYXPm425tqjPmR7fE2NGyZ/J0Z72X9SuWTys7xXpi0AQBLKQ1P/xJBf7x4svJCt44cR26rPFp5erY3ScPVH2vOM38BIPdyjDnJz82UbZIaD47XmZz1e3k5Ig3aubZoOrN+rw1NXC/3AACWzsuVG+P17jY0TnmrMk3T723xrFiarDzQPzVFfdN0p9JYfdOt+5cIIjWmUUt9eZ7u8TbUGJmkpUHrp31Z9xO6yEQuE0IAgKWTRuejNjwL9mUbnjHLc185Pvy8DY3RB5WfKl+M3xP99VrYVzlfuVh5ZraXGn9oQ31vVL5ui2fT0sil5t71ylOze5kUfju7BwCwoaX5yf9ke2K+cY9Kg/l65eh8AwBgo1ppw1HlssgLCafb4kgXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7pKbm7Zh1Wz8zgkAAAAASUVORK5CYII=>
[image5]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAABmElEQVR4Xu2UPyjFURTHv0L5VyIpRb0iJUXIaGO0KBO7ksEikRgoMqFkkTKI1SbEK4PBYFcGFpPRYMH3OL9b55337qtXz6B+n/oM757f757zu+e8C6T8B1roPf02vtEh2k4fXeyJdv++CUy72DmtT2JRFqEPz/kAWYbGJnyA9NEbmnHrUaagm0lCT0gkz1gq6Aodc+tFGYduJptaMvQZhYvopdu02q0XZZR+0WOzJhVv0B3kJ6qim9BkJSHN/0BuIlnbgvZGEtnYCF2CFlMSIdE1dHLkOHZpl4mFRA30gHYkv0uijb7QLHQjafB8EvNFyFDMJDFPHa30i5aQ6AH6FUe0NYn10HdoEZ10H1qMp5FeQAuL0gRNIsnW6KSJhSLu6Cri49wPfUYugShSYRY6eWe01sRCIhmIU+SPcw1doJf0FdrbaP/k7KUHn9CJsoQipE/FjmUP8d7lIFN1CP2PWEKidcTHWY5erqJhHyiEVBs730Ha7BcNMjC3iL9fNmTkT6D9m4VO4J8gk3gFvSsHXKzshBslJSXODwQIUi023gwZAAAAAElFTkSuQmCC>
[image6]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAbCAYAAABIpm7EAAAAuUlEQVR4XmNgGAWDFbACsTgQS6JhAWRFIMANxBOA+C8Q/8eC90DVgAE/VOA8ENsCsTwQLwXit0BsyQCxgQemmJEBYvIVIBaDCQKBMRB/AmJPJDEw0GSAmJSDJm4DxL+B2BdNnHQNIIFvQGyKJp7OADEIZCAKAGl4yADxGAxwAvEOIJ4DxCxI4mCgA8Q3oDQIgAKhDIgvMkBCCwOAFGQB8RkgngXEB4B4JhALIanBCjgYIM4C0aNgkAMAUTIgX1iegXkAAAAASUVORK5CYII=>
[image7]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAaCAYAAACO5M0mAAAA10lEQVR4Xu3RMQsBYRzH8b8wUTaKQQxKySuQkQwMXoGJxS4vQMpokngHXoKBzaSsShlksyoZ+P49d3pcjBb51ae7+91zPf+7E/nnG/EjixJCCCL9soJksEIfTSyxRs9elMQWXficroEbas61BDDBASm3JB2cxIzyiJ5oMRPzkEaPer1A2OmkKmaLlluQBPYYWt1zYcXqCriINZ8mJ2Zrt4xg7nTP+TT6lm1sMBXzWY7imc+OllHEsRPPfO+i811R995wo78rhgHOKMuHrfMYYWwpvqz4ldwBPwAkC14X0V8AAAAASUVORK5CYII=>
[image8]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAaCAYAAACD+r1hAAAAvUlEQVR4XmNgGAWDDXAAsQMQuwIxJ6oUWA6E4cAJiF8D8X8o3gPE/FA5ViCeCMQqUD6DNhDfB+JWINYH4gggfgTE5VB5SyBuAWJGKJ+hD4iDYRwoABmyAYiFgbgLiHWQJQWBmBlZgAFiGsgZINtAGlhQpbGDUiA+wwBxElEgCIhPMCA8TxD4AnEOuiA+UAPENuiCuAAoILYBsSa6BC5gDMS7GSAaiQLRQDwJXRAfKARiD3RBfAAUkfCkMNwAABlBFUJo3zKLAAAAAElFTkSuQmCC>
[image9]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAaCAYAAAD/nKG4AAAEEElEQVR4Xu2YW6hNaxTHh1wijnsuObJdIpFLUhR5kSi3yOnIfpPkzV2kLA8eFOVWLokoSTx4ESF2US7npJRbpNBBHrwo6iSX8TPmZ8095m2x17btWr/6N9f65lxzjm984/LNJVKjxu9GF1VHP9iKaKvqoWrjT1Sbcaqjqm7+RAyMGaqarxoZfcewYdHnlgZb1qhWRZ+bhT9VV1Wj/IkYOOee6rhqieqQqkG1V7WrfFmL017MxoX+RDVgBZhsyY3HGa96qprhxutVn1Vz3fivggX8RzXVjY9W3VANdONNhhs/io5ptFOdVO2TZGgTkf+KGd0SLFe9leTzg80lN95kNqjOSXZhH6J6rdrkTyj9VcfEGsOvhoU7IRZZFHUPpSLrXIKuqllSDkWOs8UmGMBBOCrNEYEJqveqx2KFPE4n1Rg3Vik0EtJ6uNjEsQubi6Am9RWrr2QETYnf+gUbq3qumujGE/DDg6qtqv9Ue1T7xaLogWpwdB0P4YZzou9pMKmbqi+RXqp2iznRp2UlMNn1Yvdk9Q+rLqqeiEVxESzOAbHfYA9Hmo2vm2FuC9x4gpli+Yx334l5n3C8LI1znAm/UU2JvmfBtuKhlB0WtDJ+UQXgXByFY+qisV6qO2Kd1UdHHln1KsC9GsQCJJcVYo7Cqx/FnIGhpCGODBGBs15Ex0ogyhaJbTNw1n1V70ZX5BNSuhQbG6B6Jhb9lVJUryA4a5sbzwQD8iZU5Kw+qp5+UKxWXRAL83gNLAJ7wuIF+Py/JNMoD+bDvPIcHJxFmhfyh+qa2Apk1ZYiZ22W7BSlC+atrCcY7xePNMlLpzQo2h/Eal4WFach8HCMILezoKC+EuuaHh5G8ZzuT4jVmduSbkj3SJ5gPHWzczTGfuhMNB6vVzQBoppjGsyJWkypgcWqeeXT32ARWczVbjyVeL3KIoRzmkPD/ormEDead8CNqkuSfI8cIdYw6MCh4waIbja2DVJ2DK8knySZTjvFamLJjQe4PkT1ILFO721hw8xbR9piJyip7opFQRZMAGd4Y4Eawi6Ydz86IYVyqeqWWDT0K1/6HQzkmbwCpW1HOE9p4PdnxaITZ/l6RZflHkRhWockAFjIU2L34r4eriEQaCCFsOFMe5DnbzGjfe0hMkK6sAlk8vzbUCfZNTCwTNJTG4hMUgzbSJGsekXUs1DBBg/z4z5Z/3aUxBabVK8aRN51sS1FNcC4HZJMQ0+oV0QazcgzWWxP9jMwpyuSfMGuCqTBabEtQVPB6dslP/poAHQ0mgvP9RFC1B2R/L+M8qgX2+VnNYgmwcTWRcqbZBFEy1+SLLZxOoilH68oQVukcckgLafFvv8IvL+eFyv8zQarsFY1yZ9oRZDONKK0gl+jRo0arZ6vFpi0foDfQfQAAAAASUVORK5CYII=>
[image10]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAAFcElEQVR4Xu3cXag1VRkH8BUVVJqZhR+plCKF5RdIF0KFRH5BhuhFSAWBFyJI5IWJXgXSRZFBmfSBGl3kleCFCoJSWwoU9FIpkiDEioIIgrpQNNffNeNe73rPPu/xnHnPebHfDx7OzJr9MbPnwP7zrJldCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH5521rqt1R60Thm0AAOXd48A+SUh5xzj4f2pV695aZ9f666Gb3nBQ5+ig3hcAGPxsHCgtTF1T69zSQlXWl5bXvWX6e7QkcOQ4PjYtH6vOr/XR0j7vMbB9oGx9jvbDr0p7fwDgAF1WDv9C/lOty7v117rlJTzTLZ9X66lufUl/rPWpaTmh8D/dtmPVA7VOGsZWZX2OckxfnpYvrPXYtJztCVdLhN9f1/ratPzeWg932wCAfXZxrReGsUzJ5Tqq3i+H9b1IoOgDW3yl1geHsb36TK1rh7EfDuvHknTXvlfrtLIOS5FzlK5bvKfW6d22TKHmOGfj8e7W30r7P5hluX8fAOAoyEXsZ07LX+zGb6z12249EhDSxTmnG7ugW94k042fq3VcrTOGbbNTSut4/aLW8d14OkWf7taXcHVpnaK+W9Uf01IyVZxAleOeQ06C1aWldadmGZvls8pnkXCWOrHWQ7X+N9VL64e+cY7ePy1/pKw7aAm4Cb59gJuD3W6dXFpw/E2ts7rxHNsY4gGABV1Z2pf+v0sLSk+U9Rf7qtZ3puXe78s6PKSO5O+lTdPN044JfZtkX8ZgkfB22zAWCVgvblPXrx+6pX+U9TG8PGxbQsLN7aUd99drfam0QJrPNCE0+xiXTI+JfP6fLe25/5y29eG1l/HVODhJV3In5+at+sJUo1XZvJ8AwB7dVFp4eKW0aa102ObwsCpbB6XINVHptCQUfHjY1suX+NOlPT6v+3zZ/Phsz3VW4/RnXmOr4LiEdLKeK+045i7X+9abD9F3vfpK52m86eJdpU1Jzp2ofI6frPWDNx/RwtuHSpvqjBx/9mWWEH1Vtz7aLrAlfOecHsl4LH1tJZ20fjp0lm7eeN4AgAVlSm2rC9JX5fDANl6rlOvXtvuizvPnjkw6a+mgbZJpz/+Og2Vzhy0haQwZfW3q+CRM9aFjDj4ZT3gdp4F3I1OufYfrz91y79nSumiR5/RTv+mwjd3G3naBLZ3MB8fBPcpUdj8d21uVzZ83ALCAXIw+BrH4Ua1Hu/WEnHRuZrfXerxb/0RpX+j99U2Zcp2nQHPt29xdu6scPmWX90unJtOB/Z2pCQpbTcPtVo41+z5LJ+zn03L2YbtQuVPnlRa4IscyL49u7pbznFSC87dK+xyOJPvbX/8Wef7cMV1SPpfccJDj+Uk3nvfLfgAAR9G3S5uaG+Waq36KLut3lzY9eUNpHZxTu+0JVvmJj3SKZrmA/neldeL+VdZdvG+W9ti+K5OAkUAwdoYy3l88v1e3lvYeCUXfKC1s5ML5yI0IS9zgkONMGLuv1pNlcyesD1V5Tjpuq7Lz31XLOemnmBOYXy3ra/Pu6bbt1cdr/aHWI7Uu6sbTYc1+AAAHIAHizunvTn2/HNph643TnfNU4HYSJBOi9kumhhMyx9+e24uEsq1+UiOdx+2mk3ci5+bH09+DsJv/EQBgYemazT8uuxPfLYd/eWfKLq/zl2k50lm7/81HbPbVWj8dB4+iTPEu+RMVuVEhNxVcMW4o7U7OJeTmj7dyjpaUO3WfGAcBgP33+XFgn+RmiAS9t6vxztK9OKhzlGsUAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADZ7HTMzropOCGt6AAAAAElFTkSuQmCC>
[image11]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAaCAYAAABozQZiAAAAvklEQVR4XmNgGLnACYjvAvEjIrELRBsDAyMQTwHilUCsAOWDwBwg/gfEHlA+MxDbA/EDIDaFijGIA/EqIBaDCQCBIBCfZoAolEYS5wHixUAsAxMAOaEQLg0B+kD8CYjXADELkjjI0ElAzAsTCAViNbg0BEQD8X8gLkcTFwbiNAaE17ACkH9/A7ENugQhgMu/RAFjIP7KgOlfogAu/xIEoICYzzAk/AuK43NA/I4B4lcY/gLE1xkgBo6CUUAeAAAc6iv7Yi1TmwAAAABJRU5ErkJggg==>
[image12]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAaCAYAAABRqrc5AAABTklEQVR4Xu2SvyuFYRTHj6QoEcmPwSIlFkkpZTIxMJiUzGw2io0MlJLBYJFkliwGw8XAJAZ/gIXJYlPC59tzXu+P697rpkz3U596n3Oe97znPc9j9s+s4jvOZRPl0IQXOJJN/EQf7uAiDmGtx/vxClt8XZAe3LXw8gl+4pLnZvASN3EL2z2exwSu+3O1hWINvlZ3e1iDU7iPVZ5L0Yu32J2JZ+ehrnJYH21IUodneI6NibiKX2Obr9WVLMgsfuCaxe1qqMcWvqxCms2A51I04xGe4h2+4bDnNIcNXMBDHPd4ig68sXCs+vq0hZNZTuxRXLPRwPNQcttCkWgOmsELHkSbStGFzxbfB6HOHi3dSVH0f/r/5HVW4SecTMSKoiKvOJiI6eUHi4+0JJ0WXhjzte6Krvz8945fMor3FgaZwxULx1o2OrpWK3CVK1T4K1+gOTJ9R/E+RAAAAABJRU5ErkJggg==>
[image13]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAZCAYAAADTyxWqAAABN0lEQVR4Xu2TPShGYRTHj6TIVxlIsUiJVZRVBgbvbLHLZqGYJDMZDBYZjIoIg0Emk1DKajJZbBb8/p3neXuu9+K+m/L+61f3ec65556va/ZvNQRbsAij0Jg1F9cAbJsHOYIPWErs+tAurMI4dCS2Ck3DeniuNw/aFs4lOIAWGIFH6Au2XA3CDfR/uW+HK5gM5wk4g+ayR46a4BwuzANEDcM99ISzSo8V/KhZeIc1qAt3CnZoXqI+eGpe9rx9k52auQ/HcAtvMBZsCrIDC7ABJ+aDmgn2jLrh2nwdlI2cNMnl1Am1mg9GPrkZybBpHiz2SYN4gb3oVFQa77Nl90mZPlllZr9qCl7Nmxyl5j5AV3JXSL3mL8Yd0rS0/XNljyqlX+POvEeXsAINqUO10pQ6zdegppr+rD4BjEss1Oew2XkAAAAASUVORK5CYII=>
[image14]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAA/CAYAAABdEJRVAAAHeklEQVR4Xu3deahtYxjH8UcoMg8ZMl3DP3IliSLkD0JCuabcTCkkZEhyow6S8ZrnzBJxy1+km7LlhviL4spQl0RuuUpRlwzvr3e9nXc/993T2Wutvc893089nbXftdpn7bVPrec877DMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgClzkW/ASLYMsb1vBAAAqMM5IV4I8b1rx3B2tHj9fguxe/cuAACAepGwjedFI2EDAAANI2EbT9MJ2yYhzvCNDdHv+ivEfyGeDrF5iJ2q8O4KsZVvBAAAzSBhG0/TCduSEI/4xoacEOLqEItDrLOYKKrrvKTN8wIAYMEjYRtPkwnbLhaTqLbskG1vY7HKdm3W5rV9fgAALFgkbONpMmG73WK35KQ8HmJf3+h8bOUuUwAAUKMffcMYLguxaYhVtnBu4krY9vSNNVEyNEn9qmvJjE3+PAEAmBOtzfVwiLMsDuSW5SFODLFHOqhySrWv5EFrb8B5HQ6ufv4U4vh8R/BUiCNcWxv2CbHCN7ZMyWv6OxjWaSGO9o0t2i/ESt9YoG7UT230zwcAwMTdEuKCEH9Y7C47NMRuIb6yDW/C71kcC1SiG/3bvnEe0I1+6+z1ZiEur7aVzL4c4oce0cTMw2NCbOcbW7I2RMe6r8cwlPDv7BsrZ4dY5BtrpoTxdd/YwzPW+1wBAJhautmmioMSts+rbd0AlbwkJ4XYNXstb4XYInutROPc7PW0u9PiorJ5EvpOtn2FzSZvSy0uIZGcmm2PQ9U9X+Fb7V6X6LtoImHs2GgJm6pbP/tGi8nucRb/ETise1fRfSEO8Y1DesDibNFh6FrruwQAYF5RIqZlD0QJW8diAnddiIurdnk+205KY8om3aU3LHU/7h3i/BCHZ+3fZNuP2Wy3sCozf2f76rrpL7OY9OTUbTeIvqtREqthdWy091UVNr8u3rAJm5bdGOa4kvwfi0GUFN7hGwEAmGZKzL4M8W+IV7N2zfZT9SYf66MuwORCi8sorLdYods/21dK4pqk7ltVvhRp3FmeTGls3T82e4yiX2VKA/NLfrd6k9G9LFaGdO11LS/N9t1ocamKfppO2FRh1XW722LV9EMrd9XqXPUZemkjYRuFKpMd3wgAwLQ6KMQv1baSLt1Y+/FLami2YalS4Y8bV55o5ZGoCpYo6TzS4gxQJZtK1lJ3p5KBk6vtXpSoKAHx9F6qIvkxfZ4erK5EqhS9kqtSd6K6W33VTfL3U6XogOx1r/cXf+38Ncx1bPa9dC0OrLZ1TqVuYB3T72+nX8KWf55nLU50Sa9LM3f9+Q8Tnj5bxzcCADCtlGytqbZ10x1UGfOJWGnslfjjmpaPodMsVS3voOqVEo1fs31KtkoJR65XwqbZhWtsw1mz3g0Wq2WluCQ7LtG5axygp/P047lUFczf7xWL3dT93n8uOtadsCl5kiYStvzzaJLLm9lrPU6qCSRsAIB55V2b7f7rhLhydleRlr9IUqKhJOIe617bKz8ud2afOD07bhw6D3XxipKLPHnUZIJBY520X2PVPHWxlhK5cSnhVdVPM0Ovytr1+/wED29QVW2uOtZewpZrq0tUXc0d3wgAwLTSjTbNiNQg917LdSR5IqZlEb6otq+x7rFu32XbbVNyoeqMLLZYtRGd32fV9iClipcqWYO6Q+dCXbfq+nzC4iSIZJl1Vw5L2kjYdB4pGe+VsOkzlLoekz+te1JHL20lbLpupe8YAICpNihRS2ZswwqVxmzltH/GtdVBS0ScZ3GsnZ5O0Iu6eZVA5HSDHpT85FKS15bS9V/tGwqaSthGpSSrX4VtWG0lbPodmgENAMBG637rvUq82m+tftbtpRDbWnxvVXN6dU+utPEXRVVFSRMyJkWD7SfxlIW5UpK+wgbPam3D+xa/v35Jt6qGpQkdAABsNFQFWeIbKxqHpRt3E5SkpaqIutg0/q5EFbY6EkYtaVFawqJpOveZ6ud8ovGPaTbppChxTOfxkduXpLGXo1RcAQDAHCh5W+4b0ZfG4Om6aV215KZse1zqss7X8WtSmuWpBO16iwm8ujnTciCPWkzuS5M2tJ5cPnMYAAA0QFWvr617gD76U7XuE4vLhDxncckTJTt1d/u2NdkkPQpLjxQ7KsRtFv8u9JluDvGaxQS1VKVUBbg0CxgAANToA9+AgXz3n7qNSwsej0td4ot8YwN07qoU6skbx1o5MSvR0zu0kPKwxwMAgBFpEL4WihWNUXoo24fRaGC+ugaboPFhvcY41kXVtTTpRN2b6v6818qzbRNVZFf5RgAAUK9vQ6y1WFVZZ81UiBaKJ608vqsO6qZ8wzfWbL3Nromn5US0Npy6RftVzjSJRN2nAAAA84LWspvP8rXntCbfuMu4AAAATBVVofTYKwAAAEwpLXuhJTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGjQ/z2MJKiztWoOAAAAAElFTkSuQmCC>
[image15]: <data:image/png;base64,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>
[image16]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAZCAYAAADe1WXtAAABI0lEQVR4Xu3SvUtCURjH8Sd6ISijJpGcHALBRodeBtcgW1oCabWGhnAJWsKgpSUIgtaGtginJgnBBvEvaBGrxa3dqb5P58S99zjUvS4i9wcfuD7nUc89zxGJMwqZxC4esY91TAU6Qka/fIMjTOAUD7YeOWtoI2k/X6PiLUfLCe7F7HIeT9gMdDiZxgbmnPoiZu2z/qjS5NASs/s9WxvIDvri/fMKXvCFO1tbFXOGh7hFHZfI2vWBXOANaWzjDHk0seW1/Uw/YZ/17WZ8a4Ho+TTwjCuUxJzbUMmgh0984BVVLPibwub31Zd9Nb0uHTHXp4Cyb+3P6LT1wN1LrMNpiNntuZhp/zspvOPAqRfRRQ3HEvKMtXlJzFTd6AD1nsaJM3b5Bq84J7DqWU/LAAAAAElFTkSuQmCC>
[image17]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAaCAYAAABVX2cEAAABN0lEQVR4Xu3TL0gEQRTH8ScqCIp/OEFEQTQIwlWTFwwGhdNitCuYxGIUBbNiPhBNFy5YbHKcaDBptYh/ikGwm/T7fLPs7Jx6t1s03A8+sDPzwvx5K/KHyaGKV0wFa5lSwAUGwoUs2cRhOJklHajgAMdYRWeiIkUGcYNltOEES4mKFPHvK9rlVqIiRdZQct+6y1vMxctx9Owz6A7m+9HlvvXyo53M4ww9bpyInv1d7CiaSVzhQ+yyNWM4wjrKGHXzddnDo1hBEduYxiUW4rKvE/R547roVmti3b2PFbHXypQJvOANz7jDDnr9omYT3tc4rp1fj/Rdovsa8eb0l7nHEGbFur1htBXOxRpQGzGKvmBN7Ki7yHtrP2YYT2IN6WcRDzjFhjT5IFqkv0d7uCD2ytq0rbTyb/IJEiktz+Q5o/0AAAAASUVORK5CYII=>
[image18]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHYAAAAaCAYAAABikagwAAAEvklEQVR4Xu2aW6gVVRjH/5FKV6wUozLSbhAVBdKVLAgLQ4soK8HooSALekorKKoj1UM9dK8Hu4hIiBpRRBcy8IA+dHsJKkMKThKFT1EYBIH1/fhmddb+nNlnz9nus+fQ/ODP9qw1M3tmfdc1W6ml6RxmOsN0lmlmmGuZxlxjutG00fSR6ajO6cFxjOmIONiFusf/37nTtDQO1uW4Qr1yoWmDaXacMI403W160XSL6WR5WjnN9Hbx2XRWmq6Lg1PMeaZd6mO9ZpgeNP1p+lAeWd2Yb9phOjdOFDwqX5g75Nf8yXRSMbfY9K7KHaIpnGn6xfRwnDjE3CuPSJz+EtOTGl8nxq40fWPaqj5r7EOmA6YlcSKDL3zeNBLGEzjFZtPpxd/c6Pny84DPl+Xf1URw8tdN/8hr26BgnQiiq+Upl0B4Re7wrNEDcsNfbvrZdIGfNjkI/d/kC58MEeGY74vPKkjBT6j6GpeZdpsWxokGQMNClPwtLxsYug7nmF41ra8QUQg4/o/yVHuR6fBiHFif7XIjzy2OWZTN14ba+LFpzHRK59R/EGkfqHsTRHr5y3RznCg43vSlaVWcGDJE0Ra5cSkfo8XYILjB9J68fj4iX9P0XazxU8W/MewnGs+Ak2a1PA2RGiIYkxuYqPacYPpcXqeoV2WQ7t5SdVQPg3vkKZDygWFxPpywDO57gXw7QjTlEdcLGC6VI84nQjHeXab7szmi9wXVzxwHwcV/ldfJeLH0wNeH8QQPu9z0nelNeb0uuw5w4ztNx8aJIXGq3NHmyCNnVJ1NX85Vpm9NL5lWyEvPs+rdSY+WN5AYDWhG3zE9I6+lfCf1ndr7mvroinMwArUF48bwx7P2ma4I4wmK/dfyG6E+fCYv/Nx4BOeoWrgcOuy9NUQpIWPUAYM8LU+PkAxbtgbc937TbdkY/QYpHIP1CsfmjkDXOyv7mzmyRd1MUAkdMSmUdExazsGwLF5ZIWfrg9HzFI4ns90pO54FKlu4YcBWg8hIW4rk3PHe58kbKxw2da+UGrYjVf1EI1gmr4202GNy76ehSnQzLDVjTJ1NF+m2W8T+oT7b+EMAz/e+3JGj4taPFxaME7Gsw6emdaruIxrBTXIjXKzxvSZbn3xbU2VY0goPGbcHGHtU5Z1lr6mYczmmVxFVddIXKfUxHVwfcUqMmPcTqbGs6jEaB0b9XZ3phC0LHpu/SCBtkqbjq7ZkWAp+ItWp+7KxHLY67ONOjBMBXm7QoPQq7i3PMt0gk5BGcYZIMiwdaoJ1iuk5EWvm0MGYbMZ5pZjfWNprpnoCc+XdYKy9wELkqZu9IOeXLRqwZZpoPzxIMMQbpsfjRMGtcsPmjr1QntXy/TdrRtPFW6NeHWrgnC1veEZU/i5yRB616RcGHmKDvCmKsFA0IKPyyN0lfwtTRmpO8kWbKnCkTeqspc9l82SIPWGe8kPfAWSEH+TPiGN8ZVqj8vUbGhO11IwTcXmNpOv9QtUbd46dqM7h+bs1vo+bbmBESsgcNSz99gMPQzT28zvh7fJ9X6O8vMVryjZNrqbgGPyqQffd0jBIP7xTRXVSEceOyGtSnfNaphDS6FrTpXGiC9fK/2dFa9SWlpaWlpaWace/zi/i36Ejq24AAAAASUVORK5CYII=>
[image19]: <data:image/png;base64,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>
[image20]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFUAAAAaCAYAAADG+xDjAAADnUlEQVR4Xu2YS6hNURjHP3lEXomSVyITFEmYoEhi4JEMFCYkBkoxkKJuZCAxkBJ5TSQSE1yk3FBECYVCeeQRwgQD8vj/+/Z37trrrLXPvuecugbrV//Oaa+99ln7v771rW8dkUQikQgyEJoLLYXGQl3zzYmydIFmQnegVmh5pivQM2hK+60VekBHoJ/QX0fvoG/Z97fQeqh71sdlm7TfZ/oCzXFv6kT6QRugQ6JjHZJvLoYvvAt6IdXmsY0P/QpN8toMRvNn6ITo5BiM8K2iZm1yrrv0gdpE+/M5/wsjoQfQGqgnNB96Ck11b4pB0w6IRkisg5m2X/KmGQtFjVvrN4DJ0HdR42igz2jovcTba8GJ6+tfbJBu0GHoTPbd2Aldgno514Ksg/5knzEGQHehR9Agr43wx2gcDfQxw09KfoCGtfMZ9cAX3AOdhyZIeNI7ik30Zu/6Eom/Z4UxovnvCTTYa3MxU19JdV6x5RsyvD90HfoETfTaDJpJU7m8GoGb627oBjRbGttcmdMZaL6pC0THyr0mSouUixKbuZCp1nYWGiraTs2A7mXXh1fuzmMTwv58TjPg5sI8fhtaJPWZa+bFTPWvV7AX4ozU2m1t5hgFfv6y5csqgRuaicuR97OiiC1Jm5CrUG+vrVH4PO7cj6FVUiIPOtC0kHk1TWU0MfLK7Lr7RB/W4l0nsXxKI7eI9uMOGqIon7JcC5VhHYVm0tSOmLtRwuaVNjW0pF1GiNapzIvjvbaifEps549FYiyfckPjxsaXaAacnJXQc2ix1xYiZl7segWaQDOKTHWjLVRnxupTY55o2rggWuu5FOVTbpq3pPYKqoVF6f3ss0yUkunQL6k2z0xlFRDEooGd+ZAQrFtZ9DNHhpZiUX3K+09Dv0Xv84nVp6uhc9AH6JhomdRRLJ9ys1om4bEXMQx6KZr2XPieNdMlT0g0jYP3f5hlyUdor8RnmD/6Q6pPYSxvGL0W4aEoLsqnHLz/QmVoVlnF8e4QnRSWhcSCJFZv56AhzDU889t5vxV6KDow3xAu4+OSP7PzNPY6E7/zvwDu/uO0S47topNlfak30KysnQPmSSa6xAIwffG0dw2aJtVjrgeaeRE6JZrzj0I3JV4eVsEZZUjzXynmDdabzRhYPTDX05zCJebAlzwozTtNudAXbrb0hZ/1Rn6nw/x+WbQeXgGNyjcn6oFlW5to1cHqIdEkmLf9EiyRSCQSiUQj/AOy69RrnzpceQAAAABJRU5ErkJggg==>
[image21]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAbCAYAAACjkdXHAAAA7UlEQVR4Xu2SvQtBURiHXwNRiokUJhOjldFosbLblcgqkzJZDTKYjHab1T9gIGU0WUj8Xue659xzv2TUfeqpc9+Pc+55O0R/TQYOYFJP+BGCQ3iCWS3nSxleDXn9NTG4gEd4hxVr2psGHMM+fMK6Ne1OCq5gDvZINDctFR50YctY84nczJv4UiJx17jx/WkemRUuhOEUVpUYD4oHNldijtTgg8RJumsYlaV2fm5OwCUsaHF+nge4ITkHGx3Y1oMkm/cwreXe75eHsoN5LcfwH21JbMAbmfBULyTvdYZFJT+BNyXP6xmMKDUBAc68ALggNDDdcTA1AAAAAElFTkSuQmCC>
[image22]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAaCAYAAACD+r1hAAAAwklEQVR4Xu3RPQ4BURSG4StIJMRPJUJDodKqKLQSbEBPawdCopbYgV5hASKTUFiCRvw0Or2K95o5cXJXMMV8yZPM/e4pbs4YEyVsSaKFtNPnkXK6XwZ4ox2c6zjig7UM6SxwQwU9TNHEAd3/mJ8MPOyxxBAxPeCmhideeOCMGbJ6SMd9fxWnQE6GdOT9ZdWtcEERHYzkwq5xhw0SUhp/M57xnzVHQy5KuGMsRZA+rthiYtQS7EcBcSlU7Pbsj4sSsnwBsHQdFUNifaAAAAAASUVORK5CYII=>
[image23]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAaCAYAAADBuc72AAACJklEQVR4Xu2WzytnYRTGjwzFsBAlY2aDNJOFJLOaLCYpi7GajfwBrFnI7tvULKYmi2lKSclCTM2eZKFsplHS1GSBUKIIJbMZmfE8vfd8vffcH+5O6n7qk7rn3vs9vfd5z0skJyeWWtgL38NXsDRcflhKYDf8CRfhYOAy3IZdd7cWeQpX4P8MzgbPKC1wM6ipN/CbuPfGUgY/wT2JNsTaFLyAHaamdMI/8Dt8YmrP4Q/4xVxX+sU1mVQvwkYm4Tl8bWoKP/8Z/Cpu5S1cef7YiC0EDMExezHgo7hn2XAqw/Bf8DeJGrgOf8M6UyPT8Bq+8a6Vi1sEwkbfeTWlCq7CY9gULoVhTo7gFqw3NR9t9AA2JNR2JfyOD+IiQd7CZq+msDk2yZwn5pIUxC07lz8NfWFco+3wUsL5fAmXJH71fTSfqb+vy87P3hMuRWCd963BalPTfF6J+zq6g+ckPs8+ms8+W/DhynCFuEm4WdLgjuQLC+Y6G5mRcD45cz+Ly2UamfOpjcZ9Tp8X4uboKWwzNc3nPmz0rvv5TCJzPpkf7uK0Rrli4+JWc9TUSNL8ZDzuO80y5ZPwxfMSHSs+nKsc9Bz4Omp87pufaWSen4QnDRthzmwjHCkncAJWmBqJy2dWMufTh0fmjrgzXs93nvW/xDVrdy7ztCBul+sO5xl9CAe8++JohRvwr9w9y/dwQlR69yXCPHHn878lniDPJNpgTk5OTs4j5BZYGYJJm1S/eQAAAABJRU5ErkJggg==>
[image24]: <data:image/png;base64,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>
[image25]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAAE+ElEQVR4Xu3cX6hlZRkH4DdUUDI1jUwSbCAVs1RQLCFTSyMJQRQ0KG8s/BMoqBfiRTgVkpA3afgnhApRIxxJTA0SnKHAKC8K0iAIU+pCJb0xbwT1+/WtNWfN7uxzhjn7bEfneeDlrL3WnrO+s/bA/vF+61tVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwL7n2aFGB7Q6evL66lZvTF4DALBk97a6cnbnRMLbH2Z3AgCwuT402X6q1a2tLmz1+VaPDPsPqh7kbmv1xLBv0T5Qu47ll62+Vf18x1UPk/kZ+7V6uNVlrfZvdUarB1pd2mrb8J7N8qNWJ1Ufz9h9vLj6WK4Z3wQAsEgJYaOHqgegP7X6WKufDvsfrx7aHm11/rBv0c5tdd2wfXj1c8WprT48bN9YPZz9cXh9+/AzY/tI9fceNuzbiFyDa1v9ZFI3tzq41VeGnwmYX68+niOrj+X6/OP3sfzNO1ptaXV/9esCACzByZPtC4afd1XvqCWwnTXZn27Xl4btRcuXf4LPKCExAWEMjen4fap6aEt9uvr07Ndafa/6ePP+zRrfaDZIZiyRsUzHv7dLyP119W7h7rql1VHDdrqhv6t+zQGATTJ2iz432ZfQlC7Roa2+2eoX1b/Y76vecfpNqx/sfPfiHFE9oB0/2be1+jhyzkgn6/utPlO9E3h3qydbnVB9Gvdn1UPbnk5LHtjq7OpjWUumjSPdtYSVjOeqWulO7omc89jqncX8zV/e9fD/zvOdWglLCYanV+8mZpp4Xmg6rdUXWn1w9sAg5/1hqy9Wn2Zez2O1Mob8/9leK91PAGCB8iX+r1qZ0ot/tzpx5zv2zDOtXlyjVnNR9bFEOmjx11YfHbaXIcEjYTFBKX48OTZPQspvJ68z3nQiNyJdzITkfC5ZnftK9c8kHbzxGmVsfxm2f97qnFZ/rl27pJEw99Kwnd/338mxeRLqnqse0Od5of4/sI2vAYAF+lv1TlVkKjHyRbzW6tDN8mqrb1Tvbn18si/3sy1aFlJ8dXZn9e7YW8N2xpHFDOv5ZKt7Jq9vqP7Ik42ElwS2ceo5MqZMQeYxK+lsRQLuGL4S2Oadb3utrOZNYJs+qmUtCWsJbZfPHhj8owQ2AFiK3HeU+48ylTguOMiz1TLluBHpMuXLe17Nyhd+blxPoJgGtHSTtkxeL8ohtXr3KGFm++zOd8FsYEswy/X5T/Xp1lkJbLmGq0nYG69p7rXb3TCeoJZa7TpFxjEb2KYrewGABUmX5LOtvlt9Ki2hYJwOXLaEpU+0uqP6IzJ2VJ8OXKat1VfFjsZFDsuWsHZT9QCbz2OcGj6v1ZvD/txzdufw/gS2eYsc0qVMUDum1d+rr6CdJ78zIT73sa0nncUs8ogzW708OQYALFDutUpAeb76KsFLdj28VHluWqbgMpaEhtwgv2wJRw9WX3Dxq1p51tuyJbDl/Flk8c/qj1UZ5TPL9cl9c6dUD2Fvt3q9Vl9QkM7p71s93eq1mr8oIffEZRHFvOOryWd1RfXP7dszxwCABRsXHOwNxgUH+7LZKdFFyGec6W4A4D1qdmXhuyU3+s+b2ttXZFoyixZS6z1WZHflum6rvgI42wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANC8AzBhrevw6qplAAAAAElFTkSuQmCC>
[image26]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAAFMElEQVR4Xu3cX4htZRkG8C/UsP9ZlkZ2I2bYRRqRIaT0jwgtkQq0EikV6yrRyKAyUBAEM6vjjSV6uohStJu0MsOOFRgYlVEWqCgihhd510VG1vu01ndmndWe8ZyZvZ3Rfj94mLXX3nP2njUD5+H9vr1bAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGBj76+8dn5y9JL5CQCAVXlf5aXzk/zXxyuvm58cuWYAwKYd0vad/ry5clnlPZNzV1feOx7/sHJk5crKwXsfsVwvGBMfqtxcOb/yycqxlZvGr/Hp8f63jbdPqnyxcuZ4vCqvqXyj8pbKdytHtX0LW67ZrsoHxtsKGwCwaZ8Y091aeXvlL+PtF1WObkMpemHl15WPVT7fVrfMl+frZfC8NhSwV1UeqnyuDcXt+jaUoBSilLsrxsd/sPL9ykWVb47nVuHsysmVj1S+MKYXtre2odTm53jn+Pj/l8J2YeXStv6kEQDYhOsqh43HmVJlUpTy0QtQ7G5DcYprKj+tvGLvvcv35cnx6yu/HI9vbEM5u7MN07PvTB6TctTl/v4zrVKuU/yqDdeuF7Y/jOc/3NaK5/OpsB00P9GGnzMluv+8+ftJ2QcAluCrk+OUjhSLH1VOr7yhct9431mVQ8fzv6i8sa1uSfRbk+NMqPJ6UtSy/Bl3tWHpthe2TNmyZPuuyuVtWKLM408Z71+FXIssD0cviL2w7WnD81873h/Ph8L2jsrP2+IilsL818ntlNW+TA0AbEImJFnWzCTrS5WftLWJ2cvaUDamy50pJ9H3lc33vS1LimBK2O7K7ysntGEZNs83lefu5/rr6Lfz+rfy+rKMmuc+pw2Ts/X0a5SvfUl5uoft+Mo943FspbD9sfJoG4rgBeNx/31lr2Fe63GVH7dhKfiJyvfaMD3tpXIqrzvn87rvaMMy93pyLVPWs9S5kew1/Pfsdp9AAgCb8FjlmDb8J99L2J8qh+99xLMre76eHI9TDrPEmelMP7dsZ1ROm58sn2lDOYmUoB9M7ltPrmMvY9PCliXk6d7ArRS2fO+e8WtkwpVpViaOT4/noi8d59ptVJb+NTn+e1ubXE6l1P22/W9ZXs/FTWEDgKU5og1LdSlqWbbqsqE/922HvJ4/j8fZQ5el1nx8yLRYLFP2WuVdnHM/qzzehjdcfLQtXvrbyIvb4v1d0YvxZswLW/yzDdPIFLYsIffERoUthXi6dJnCtmjp8lNt37+PZ2LCBgBLlKla/jON/i7KfATGiePxVmUyk+lVCs+i5Pnnbm9r+9EyqYkHKpeMx8+WPqHq9ne6tGrzwpbl0BTc7N2bTtj6MmkKWP8dz2UCmMIa2XfX38ixnnMr97dnXmLOVPSRye1M7Rb9rgGA/fT1yr2V31S+3Yap1nZKYUhpvKXycBuK00YlYlVSeO5uw6Qqm+v7Z71ttxS1B9uw1/C2yt/a2vVJkcrtFN68M/NrlafaMO3KtHCRvIs1/9bv2uLp2iKZHOb3ctX8jolcv3xG3g1t4/1/AMB+yvTo3fOT2yxLcHkTxHZ6Zdt5nyE2nbBl2Xo++csy54G85nx/vieTtgPZt5iSmA8KzmfirScFMFlvaRgAOAA7cblqV9u+fXQ72ZvaMA3L12VI8Tq18o/Kq8fbAMAOlE39O81X2uo+2+25bP6mgq16eVv79z7bXHMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABglf4DZ9yo1UNAW+QAAAAASUVORK5CYII=>
[image27]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAaCAYAAADBuc72AAACOElEQVR4Xu2VS6hOURiGX7mUW3InItcykkQpE5EzcCkmbhkwMJLCQCFyK9eS+2FgJJcSExEGSFEMpFwmMlEyMDT3vH1r2ev8yfmPcv4/7bee9tprrb339631fmtLtWr9Vv1hcGNnO2pDIqsfbIFLMAN2w31YCgvgJpyHcfkBNBFOwVnogAmp34uwGe7AKtgPF2B0Gu+RDsLc4n4OLIdrcBEGpvGvsDrNOQE7UtvznyoCd8AfYWEaWw9TYRK8gZXwCZak8aY1HDrTNWsMzITnqhLwB24pVtu47T63r8PWNG8WvICxacw7YjmwhzAMRkLf1N+0ZsPJxk5FgI9UJXAEdqX2FEUSvo6HtzAvjZUJlfKzfkeP5YymwWE4rsiylD17FfrAEMVq5O3ymFdxPmyEJ4qAPfecwhKbFFt+Q+HtewrbeM42GKVu5ADXwXvYqyiSPfBA8XFvjXUZ1qb2ZHimqkDc7wAOKBI8pHjXsdTvBLcrgrkN++AunFGs7GJ1I1egi+MxjFBXf7pgHOxpRdaDVPnI9wNSO2uouvrMx5vfn9t+xiqPvnLOH7UTvsD0dG/j25/5pc72nZrYln8pV+EHhYdyYPZMeX56a3x0eG7L5Cr+oeoctMrz0958qfBpY8X2qrzN32FFurfHvIL5+FkD3xSV3FJ5lVxIrkgb2oEfVdjAv7zPsOzX7BbLVeeqfqU4F18rtvuK4tfXdvKKOuBF+otfWW+q0Z9tq+zPtpf/2eUxVatWrf9NPwFodFY20cy7zAAAAABJRU5ErkJggg==>
[image28]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAaCAYAAABhJqYYAAAAzklEQVR4Xu3RLQ/BURTH8WPDBA/FZoKiSQJRsQkEgq4yVREUgjegeQ+aKti8BlGw2SSBZorv/T/Y9XdMs9n8tk+4597dc+6uyD/fTBIN5Lx1BGU0kfEPmcQxxwQHdLBEFyOcUfMP19FDEReskPL2sthj6K2lL+7BNm6o+hukgBMGVs3JDDt5nrGFKypWTRLYYIGwVdcuUNuZh6/l9YLHvHY77QInY2yRtmrqvCYxcdvamYoyr5a382rJ4yjuZ32M+VXzzaXghpYQosHiL+YOVq0i+LqbEd0AAAAASUVORK5CYII=>
[image29]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAAZCAYAAABggz2wAAACcklEQVR4Xu2WzatNURjGH6GICKVukXNRmEjh+gdQBqRQYihlYGTAkJJ85A6IlI8BJUoZkQHlhBFjBj4SiTIwIynx/HrXZp999r53d865TPZTvzp7nbXXXs9633etJTVq1KhRaKs5VWys0HSzy1w0Z816M7mjR2iZYkz60Z/3/qta5o25Wmgv02xz2+w1S8xh89PcS/9l2mZemFVmpjlq7hf6dGlaYiI01Vw2v1TP6H5zzcxKz5MUJnj/UGpbaF6Z3ekZzTHPFO9XarFiNU6aeYX/+tUOM2o+qJ5R+mDqQK5trflmHpgZCoNfzepcHxbkumkrIlwpOq5UpMgVM9z5d09qKepnuXmnekap5edmQ64NQxhrK0xQt0WjiPE/KQJXSxT5rQS/exEpe9qsM0Oqb7RMRJAokxmIcaqMlrWPK6JKdB8pJkzU64rNgvTjnX6Msrnw/ZdmkSKibZUb6tloJiZ6XvUNM6FL+rsD9mqU7xw0r82K1EaNUqtlhvo2itikLpgnitqr0hRzQrEgmXo1SlY8Vvf3qgxVtdcSkzxnHqpeNOcrJvc+x0dFjf1Iz/v+9K4WJtkY56ZnUpZa5Rg8pnJDGGV3X1BoH1PDivS7o9iNxzM4lqoiym2npe4zfERxVOQPf1L3jGIeWxSXCG5MmRjjbqI4XpcYZNBHDGKFWWkmn1+wPYpI31CkPMIQNflZnVnxRRFJRBk9NUfSM1qq+MbOXFuX+DhpSXqSpkRgEGLjuKlIWQwBE85Sd7P5rthwsgUg6lnfIvmb0BrzVvHudsWtiAsPx1qlNpnjGvytaKLFQm5UXDK4FjZq1KhRo0b/Ur8B7huHcVWEW+EAAAAASUVORK5CYII=>
[image30]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAAZCAYAAABggz2wAAACY0lEQVR4Xu2WQUhUQRjH/5GBYdFBMILCrUt0KJWoSCI8SRGFFOnFDuFFvAZJpzY6RJciyzqkdEyqY9GhII0gCFEPnYQOSXWrmyIF6f/PzLjz3tvddp7rqfnBD3ZnPmbft/PNNw+IRCL/O5tpLy2kxquxiR6jI/QhPWnHfC7STrrNzrXQftruB200W+lZeo9+p4v0cCKiMltokX6iB+kBOkVvoJRsA31GV1I+pztsTFkarfVCiZ6mXfQ6whI9T3/TU97YCfqTHvfGxuhnukBf0DMw1VOVffQNvU2bU3PrZRi1J6o/+xWy8bvoV5hSdtxHbWtmUFkcoq/pON2bnM5NSKI6b5PIxrtENacYkTtRn/0w9S71eT3UM9EvdKcde0Dv0hn6jX6kHXYuGO2qdvc9TBdMd75aCElUFOkSPeKN6Yz+gUlWSYsn9BpK51Id9xc9ar/nQouPIl/CoYm20nmYJPQ76sJqPOqqfqLbkWw+u2F29ilMV86NmtQj+gFhd2JooqJA38JcTbP0ErJnNE258g5CC+g8vEP4boo8iabRXarrxXXdy/QvHVyLKCXq73pN6Hw+pi9hunFogo5qiar0Ckje4W10jvZ4Y+dgzq27R7WmStlP1JXuJCrv+hpKpt5XjB4q3VwcAzAP7J8rvVFprGi/601HZaxj42KUsKpM59fRR5fpBW8sgxJUWao8tUDQ1pehiU7AdEH/Fe0HvePFKSk93FWUKkY7M01vwvwR+qwOqzUdir0C87yKUUmrtIfsXEX0unYL9X8ryotKuQumfPckpxJoTjHd+Mc7biQSiUQiG8Aqku59xiAEx7cAAAAASUVORK5CYII=>
[image31]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAAAZCAYAAABuKkPfAAADUElEQVR4Xu2XWagOYRjH/0KRtewhH1kSiYSUC0S4EIkQOUWWC7KVLRdKkiVF1iIHZUlIkSxF3JAS5UJKoVwg3CAly/9/nnn6Zuab+ZbznXMuTvOvX3Nm3vd7Z57nfZb3AJkyNQe1J91Jy/hASBrrEH/YXDSHPCLbyFXSMzpcJzlgC9kQH2gqtSDjyCFylMwj7SIzktWGLCHDg79lSB+yguSCOX3JczIhuF9MvpIdZBDpRaaTO+Q2LGKaXK3IQbILZoA+7CF5SfqF5iWpM3lC/sXYT1oHc2aQd7C1pQHkAOlP5pJFZDYsQkYFcxpU2k3/mDQNJV9gO+G7oA+TMYqMYtL8mzCHvSG1sIhSZLk2w5ygHZd0PYPojq9EI6TBEHKZnCfdYmNxae5H8oJ0CZ4ph+WEkz4pRTJEBrmBSdJaYScoIo7D0kcaRs6hgdLA8/o+OQzLxXKlsPaP0Dr6/V+YAcVUjhPk5FdkTHCv/F8f/N0WFj1Vp4GK0WRY9d2H/G7WR3LANFjhCud1muSES7AcV0p8INeRL4qS1twIqzNLyQ3k64NSoKo0kPGzyGOynXSMDlesKeQ9+UROkK7R4UTJCaro82HGip3kNQqL6kAyE/nv1O5fRD4CFTF7yUKUdn7dhAXkGVmL8lpZJZJzd5NvMMcUk4zW4SZcCBX2P2EtME0yXA7wNBgL6zK6X07WIbpmgSaRt2QVLKcaQ26IQlynvEo0mvwg95C+QeE0UIu+AGvRkmxS4ewd3KcqHA2bUF0qjIC1Ql1dKnSq6DJGRqVpD/lNpoaeuRMeILnix9OgB6y9qpW61qD4eyPyuvAU9S+Kqu5qh7q63BCdH3SOkPSuHPKtTdJv/iDqBI+i0ygM6XgaSO7wsBNWk/Gh+7Kkl4XbY7GWFZde/hnWulw62soxx2DhKi0Lnil0/ZmiUed9N1bXrbB6ojyPSymgg1FYqinqbO4EraHO5M6vWFpAYX2LnIIdTUupE8ww5bAMVTv7DtsxjblU2X/B0s+NVloeIVdIDTkLix4dlePS7tciuY7JAddg66mLqCYkzatYcoBajhYtJRmVg53hRSUHLf12MOz/gImIpotLz+SstG+Rwaovioi7ZGR0OFOmTJkyZcpUQv8BVGWO1pNbL+cAAAAASUVORK5CYII=>
[image32]: <data:image/png;base64,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>
[image33]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAaCAYAAAC6nQw6AAABLklEQVR4Xu2Tr0tDURTHj+BEEBnKwDyDIA6LaDKJwaJhKwMHCwaLdYLFP0KLQTCJIMKyyR9lg61azKIo2NQm+jncN7z3+OS9t2LZBz7lfB/33nPevSID0rCBb/jleY8z3jeT2PbyT6x7ecCOuI8ucNhkSgm7uIJDJguYxid8wKLJ8niOS6Yei57iTNypNr36GB5jxaslUpWwvRwe4a4ktGPRlrS1V5zDBu6LWzATuuuJuFPd4KH0sUgPvQ660K24IccxIik20EHrQts2iOj9lHUbWA7wAxdtEDGFLZy1gc8EdvAOCyZTtrCJz+JmOR/GPyzgO57K379bW9ZTx7KKjxK+t5eo7qPz0TtWNvXMaLtXkjCfNCzjJY5jTX6/ydTobb/GPVwLo+yMRg74L74BeCo1dgIPpJsAAAAASUVORK5CYII=>
[image34]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAAAaCAYAAAD8K6+QAAAB4klEQVR4Xu2WPShGURjHH8WA8p2PYjAqZfCVspBBGZTEwPBuDErZ1btaUEwsWEhiYJDVYjQxK5tksVA+/n/nHM49Xu49982b4fzqV7fz3Pf2/s85z7lXJBAIFJIBeAfftKew1KpXwDOrTg9huXVPIeB/ysAqZ/xXiuAGfIZPsC9a/mAMHkg09F9TAyfhFnyAN7DJviGOargN50WtyLqosDYLcMoZ+2sYbBR2wT1JEawDroj60TW8ha1WvRhu6vuS0AJL3EELbuNadzAGTrx3MK7EjL7Oilq1uc+qSJ2oB3NlkzANVyV3uEZ4BHvcQgypgi3DTn3dLmo/X8BKPdYP1/R1EriNOTH8jR0ubSjiHcz0F1eFcNvtwlc4rMe4mr795YbLJxTxDmb6yz4sGIjBGJCnoE9/2Zhw+/BY0oci3sHs/jJwC3IrcksOil9/ufAguYQ7krvnkuIVjDPKrdLtFkQdADxEruCSU0tKs6gXfq+ol6vbcz54BXP7y6ZB1NHPcL79RUwos/04iRlJH84rGLcZP4/K3IImC+9hmzMeB0OdiHqx2uQTjsH4fuWzf2QIPsrXtx8/o0Yidyh49HPWfftrUb6HMjDcOJxwCzmoh+cS/a8vogLOWvcFAoFAIBD4r7wDEudeiCzF9wAAAAAASUVORK5CYII=>
[image35]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAaCAYAAABozQZiAAAAZ0lEQVR4XmNgGJmgHIj/k4gPADEPAxKogkr4IgtCgTEQfwXiw0DMiyYHBjAX4NN8gAHNRhgY1YwKhrtmEI0OCGqGJU+SNGcA8SsG1IT/DIhdgFgXiK8A8V8kuS9AvAKIuUGaR8HwBwBse0VGGvGD+AAAAABJRU5ErkJggg==>
[image36]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAAaCAYAAADi4p8jAAACG0lEQVR4Xu2WzUtVQRjGH7Eg7QtJkBYtWkUguogQoU3RIjFIokVgKzciBIabNi1sGYKCKWIh4ULciFsjAhctahW6SP8BdyGBuNGgeh7fOd05U9c593r9WMwPfng879zDzLzzzgyQSCQS/+c2/U5/O9/TBi9+gX7w4nKRnvXanHjq6Bu6S3doZz68x0O6gPzgj4V6ej58GaGJztJBWIYmYIP2GaK9wbsjRTPbR1fooyAWo52O0ct0nW7Qq178FH3r2u2L1nMXveL+19/7sA9Xi775gn6hPfR0PlwIZabfPQ/Dsvj0bxRohmVYmS7LOTpNX8JmaJxO0ed0DfkZK8IlOkI/0TuwpVkto/SGe26lP2ATdtG9u0Vfu+ey3IPNktK8Rd/BZuQj3aTXS033RdlWjSzTDvxbK5WS1Z+yJLQc5+kvWJ+F+h2tvwHY4LQb/YTNijqnJaoPxTqqDM/QJdqGePuiZPXnf0/90QA1UNV2ofrL0NL8htKMFUFnjjKubF8LYgfFr78MLU0tUS1VLf9o/WVo+1bNzKG6DPhZrMXy1O9VWzfDAHkC22y0P7wKYmVRnanewhmrFNXhJP2Mg20wYf35tMCODA0yWn8Zfv3VAv+IeIDKB6rJ0bWrMQw4hlHZBrj3g1XYFl9LVKO6hXyljxE/C+/SbZTulrqededaGDoydDctVH/iDOw8PCw0MA3wWRhIJBKJRCJxePwBG1BYgb4zfRQAAAAASUVORK5CYII=>
[image37]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAaCAYAAAC+aNwHAAAA/UlEQVR4Xu3SP0tCURjH8V9E0J+h0pbARocmEWlqts1FAgfBVYi2CMzVydEWoa2huTExqKEX4FuIoNGm9vo+nMv1nnO7oNB4f/Dh3nuew/l7pTx50jnCFe6i56Ffzs4GhnjHvdwAM3yivOiWnRZ6WE+02fs+1hJtmblEA0W5Ze/55Tjb8ieJc4pv/EQesen1kHYxQS1oVwljFMJCkArecBAWumiGjYnYSq4xxQdGcrcVxwa4kX9YWxjIv4Fbub6p2N6e8CU3g5mjo8WgdhsvOIm+U7GTtcM5xxl2/LKO8ao/9r9s2niQ++Eu5Fa9Uup4Rh/VoLZ0bFu2gjz/nV9hAR+u8WEzfgAAAABJRU5ErkJggg==>
[image38]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAZCAYAAADuWXTMAAAA5ElEQVR4Xu2SPQ5BURCFR6EQhUKCjkapkrABCo2CRrANErENlSVoFCIiUaosQSm2oCF+zjHvRt7kIlp8ydfMeXfm3rwR+U1ScAuv8Ah3gXt4DurOAyzqsQdd0ZBNsiYjcTiAJ1gzmUThWLTBDMbC8Z0IHMGODQgnuuv3RT+2cCpv4KUq+k6+rWwykoNtW3RwGqdy+gYmw/F7EnAl2uDpFV/RgkvRRh9RgVOYsQEowKEtOkpwLf5/TeqwZ4uEB3iQDXxwFyawYQO+bQ6bps4DaZgXXaKLmA3j6i0kvMPP9O72n+/mBsCiMazwKLp4AAAAAElFTkSuQmCC>
[image39]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAAAaCAYAAAD8K6+QAAACB0lEQVR4Xu2WvyuFURjHH6H8KBKFoliUUga/UgYkKZskxXAHPxZSRqWkDBY/YiIDi5IYUJI/wCgDk80mWSwM+H6d83Lu8eqeM9BV51OfvJ3nvfd6zvM873lFAoHAX9IB7+Gb9hTmGvECeGbE6QHMN+5JWzLgBnyBz7A1OfxBH9yX5KTTniK4DadEVWRdVLIm03DIWkt76uEyLIc38A5WG/EsuKnvc6ESZtuLBmzjYnvxN2AlxvX1nKiqTXxGRUpEVZSVdWEYrkh8cmXwEDbbgRgyYRts0NfckG5RG2x3VCxLoj5M6uAjvICFeo1fvqavXeCPcmP4GTM5n6TIJJyH13BL1IyPwis4aNwXSzRfrAph2+3CV9ij11hN3/myk/NNqhSuivq/zuEtrBI16+yoqMN+JJovs7RMiIkxQT4FfebLJEpuDx6Je1KkEY7AClEzv6DXa+CYOBw35nxFsAXZimzJTvGbLxs+SC7hjsTPXCq6RB1D/OsMd5St0mQHRD0AWHL296IVc4W7zQO/BSbk+8y5MCOqYvwuZ+z5MmGP89HP5Hzni0RJRe3HTUyIX3I58ETLa2fYZnw9yrMDmjn4AGut9VQwqWNRc2Lim5w9Xylhvz7J17sfX6N6k+5Q8NHPXfedr1n5nlQEk+uHA3YgBr7acc7brfV/Dw9lbqrTYRwIBAKBwH/mHUpXWGWMf2YcAAAAAElFTkSuQmCC>
[image40]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAACDUlEQVR4Xu3cQatNURQH8CUUUYiUiaQMjCmZUwyUekNlJh/BhJExiUyUgQnyBZh5pSQmKBMGIhEDI2Os3d7vnf1uPXWfevfW+f3qX3vvcz7Aau2zTgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+d55mXmfeZ1W+9Z8QYAADOzKbOjrT9kDmd2DY8BAJiFzZkzmd1RC7Ylr6IWa1u7MwCA0fiS2T5x9mdivx7ux9BRu5TZ1talUDvX1gAAo/QpZl+wHe/WpYv2JLOh7S+Gq1AAYOTWq2A7kDkb9dpz0uXMlsy+zELmTTu/k/mYuZs53c4AAEZnmoLtSubzP1I6Y5OOZn7E0DF7EbVou7X8RsS1zM6oBdvVzIPuGQDA6E1TsK3FYtQibcm9zMnM7e7sQrcuQwffuj0AwOhNU7CV90oXbLXsHV5d9jtzotuXgYIbMXTcynBB/2+1m5l33R4AYPTKlGgptnqrFWxr8TNzqq03Zr5njmQOtbPybdqxti4DB49jmBYFABi10g17FrU4+5V5mLketcAqZ19jZWdsrc5HLQrLVWj5n9r+zNPMo/a8DBwsRh0seBv+tQYAMFcORh04AABgTpXr0H7gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAObcX2sBQ+ic0/ZnAAAAAElFTkSuQmCC>
[image41]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAZCAYAAADqrKTxAAABEElEQVR4Xu3SsUtCURTH8RMamBoKReAQQrY5OJkU0dDW0KJzLuKfUFG0NQeBi4MNLQn9Azo4+C/Y2h/g0Bbt9T3v1vO+8zJcA3/wWc693nvfOYr826ziCKfYMGu/popntNDEBPXIDpMynpDzapcYIePVwqyhi11Tv8YYWVMPso8LU9ODhrjHilkLcoNDpFBAHg1x31T09oXR93ZwgCk+v31gz9sXyQ7uxD1Bb9Cb0rhFH8nZ1llO0LZFcXPSm/XQWK7EzcdG5/OOil3QU3SY66a+jVdxT4x1Tp/2hppX01b3MJDooMNoq8/EDfARD3jBubgfx/LT6k0ksCXuDxp7jh+/1QtnXqv/zDFKtrjMAvkCasYizmLdJLUAAAAASUVORK5CYII=>
[image42]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACUAAAAaCAYAAAAwspV7AAAB30lEQVR4Xu2WzysFURTHj2Ihys9IrGSjlOTHip0FSYoFklIWrClrkiWJPwArFjY2FjZeKSkbFpYKibJgw0p+fL/vztR15s2dmXpj9T716b3uuXPuefPOnTsiBf6HCliiBx1wLq9JjSG4LsmL2oSjOuBiEr7CH8t3OG9PAh3wFNapcZ9SOAMr1TipgcewRwdcFMEd+A37VYxwwSM4ocar4TjchW/wHjbYEywG4Aks14EwquAlvIONf0NZmPAa1qtxFjUCu+CBuItiX51L8IeF0gk/4CEsVjH/Lm6pcc2euIsia5J7jZxMiemlBR0AtfBGohs1TlGDYuY06YDGvxOfsFfFCO/ig/fpIk5RzPEIu3VA4/fTrQR7hgzDZ9isA4o4RTHGOczpxNVPhAmiFiNJimK7OHH1E0mjqLC1skT1E0mjKOffF/V8Iiz2CbbrgCJOUexL5uIuDCWqn0irmES5nvQ2LIo7y7Xdueu4oZgzwLSYhezz7gXO2pM8eCxk4JwaJzwHz8SclX6eLzHF6bOTsH8zkuCocbEs7rsZB167LyZXXmiBV7BNBxLAHBfeZ95YhBtidmxSeM0KXPK+5w2+rG3DMR2IQZ+Yvz+VN9AyuCrRR44NHzV8O0iloAIFwvgFVSNlQfTeedAAAAAASUVORK5CYII=>
[image43]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAACe0lEQVR4Xu3cy6tNURwH8CWUVx6lMFKYmJEi5R8wYEAyQEYylaHMZKhEGSjJxCtzibgm8hopKZIyMZAhJQN+v/Y+nXW2e49bzqXO+Xzq215r7X3qDH+tVykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADPck8jzyNjLVtlfXHwAA8P8siKxo2+/a56r2CQBAZWNkc2R+ZFP7nCsLI3vadhZsPS/a5+JqDABg4m2NvK/6hyN7q/6oXS/9GbUHkaVtO2fVDrXt2prIj8iuamxe5FJkeTUGADC2PkVOVf11kWVVf5R2Vu2cRTtfmuIrHS8zL4V+L/3Zt3S7agMAjL2vpdns3zOXy5GnI4tKUxTuj6xvxy9HPkSutP2up6WZZUvbS/93AAATYUvkTeRnmxODr6f1cUhelmYPXNeGyLnIytIUbGfK4P61YbaVprB8XJqCDQBgIh0oTcH2uhpbUrX/1u7Isaqfhw6yiJuNXKKdihzsjPeWUwEAxlYuUdaulcG9YrmvbDo5QzZT8pBAngLtulgG71a70OkPk6dXv5TfC7STnT4AwFjJ2a08cNArrvIaj/ulf4Izi6TZFlSz8Tmyo23nPrm71bs/uRr51hnL//aoMwYAMFby6o4bpdm/djbyLLK2ep9XbHRntP7GkdIsa+bBgldl9ocbbpb+/rps964ByWs+7vU+AgCYNHm9xsPS3NE2CjmbN8riL92J7Isc7b4AAJgEOft1q4xuj1geOBi1vMMtT532lnABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgH/oF4+tG8C8i3+oAAAAASUVORK5CYII=>
[image44]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAaCAYAAABsONZfAAAA1klEQVR4Xu3RPQtBURzH8b9QTAaPhWQw2I2KTAxmyguw2AwGXonIaKAMSl6CzAZlsFgtyqb4nnvO5eAVKL/61O3/O/ec+yDyz68mjjrKCLxX30lgjgWa6OOEgumTyJlrJxnsMITfzHyYYo0gBiiazilHonfNukOTHi6oYoKQW+RxFv1oagM7LVxF39CwC/XSd7TtoYnbLUU/4jM1U6gFn1GzG0qfRRoHdKyZBxXs5bVhChFrjbPgiBnG2KCLKFbYiv6K6iu/xYsYwqJPsufqBPdX/PMjeQBZzSE9o6921QAAAABJRU5ErkJggg==>
[image45]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAaCAYAAADxNd/XAAABgElEQVR4Xu2WzSsFURTAj4WifOSjZCGRyIqSnawsKF9h42Mvf4GyItlY2oiNLPwJVhRlo9hYYC9lZSM2LPidue+OcWfmZRbem3J/9WveO3Onzrn33Dsj4vF48kg1LuA+bmPPz9v5ph5PcBNrsB/vcDY6KM+s4hU2RGKLeI8tkVgu0aQ1+UMnPoivOOnEQ+pwDNsK//U6jq3hiNLQi88SL2AA33DLiQdon+3hBj7iDu6KWUrtvY7voX+OTTStADceMIrL2IcveCBmKU/FzIbOShrz+JDBa+wKnkxmAj8lnmjRAlbEJD+DHziEFWJaSIvT36VC2zhzARZtnVtsdm+UkLRE0+IhtXiBR5JtxqvEbPTfqsdgZfBkMp34JPFEbQFrTjzE7n7dC1lox7kMTmFj8GQyeqCc47GYybGM4Hvhmki0/8vNkpgNb08/7Qh9K1+KeUsnso432OTEy4G2mB7jZzgtJnndm/pJkYouly5fXtBZ7xbTdsNSfN94PB6P5x/zBWOHTUQOBpmKAAAAAElFTkSuQmCC>
[image46]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAZCAYAAABdEVzWAAACr0lEQVR4Xu2WS6iOURSGX6EIKeQSIteMDNxS6ChkgAEGomSgpMhl4F5KGbiVjCQlSkpkJMLgHAbIxASFknJJMlEGJngf69vn7H+f7z9OShn8bz3937f3/vZee6211/6llv4P9TUjzeCyo9Ag079s/Fcaam6YE+aq2Wj6NIwITVGMG1d2lBpo1ptz5qiafzDdHFeMW6PwTq695prpZ4abh+a+WW7GmKnmgPlkVlXfNNVo02FOKwxaYd6YBdkYdr3JvDQLzQRz2VxSbAoRmnuKhZP2m0WmzWww68wOc0ZhfFPRed68MKOydrz2SBEaNMt8M9s6R0iTzAfFYgiPvFV4LYnnldk7uUeIm0WkUzPMF9OuxmRlsh9mSfXODn9W7UmMbzc3zQAzxDxQo2F71DUHXj+mXoQQJU+0q7thGJIWuVi91xmGl/AWOqUIMUYQ4rNmbNW3VJEuPYYw6U+GEWbUk2Ffzcyqjdx7Yg4qjNhdtRO669Vvr0QOkUuEgFAkkWMYgkGIPOJ9deeIyLGPio2xwSQ8hXcwFs/hIYxMIUwVgNM9uWqrFcf+s5lXvU9UHIbcMDbAibugKIosuE+Rh6VhpTAohZBvKTWHFflNRPByrViEEvFakS8Uvi1qzDE0TJE/7xVlY6uiRuU5VqoM4XzzSuFtRH3bVT33ShiWn8o6jTDP1HUqS5UhRORcvhEMJm1qtV2RY8mleJCQ5XWMCTrMzqofET4SP9WxUnkIk4hAbhi/J9X9Bvkt8ui7ws1oriKp852m03tFjblyR/WXNRuhkFJQc3F4csOY90hXd6Mw4LnZbA6Zd+p++bL4LYUxXC0YdFdxnZVKIeRklhpvnprFivlZr25cp7iOqFHLFHdenfDSbLPWTFP9vwbUpliwWf8c89jcVuQX87bUUkst/Y1+AYewhDh9ueBgAAAAAElFTkSuQmCC>
[image47]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAaCAYAAAC3g3x9AAABRklEQVR4Xu3UvyvFURjH8UcoP6NIGaRQBgMLIhlsBgszi/wJiGwWixKLgYGBslIMhruZKZM/wGATM++nx835PrnnnLvqfuq1POd07vk+3+d7RWoJ0ohZLKDLrVWdcVxhFSt4xGJhRxUZwQU6gtom7tEa1LLSjGMMufo2Smhz9WSmsOFq+iN3OECdW0tmBzNoQi86sSTWw/5gX1a0P0eYxiu+fnxgItiXnQHsiz2W3kxv2IJdXKLhd2te5rHmi2JzqDfWHyynR2yctDUVsyU2fz46f+8YxTCucSOJt17uX7dfIId4luKa3rokkQP1cfTLaHf1PryI9TEcmeSB2r83TAY1nb8T3Erxq9EkD9T5WxbbdIZTPGFd7GCf6IFh/+rF3qD+u8S+iuiB4fzlJnpgpfn7Kzrwe3jAJ84xVthB5jDoi7X8o3wDJocxjfBu4o8AAAAASUVORK5CYII=>
[image48]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAaCAYAAABctMd+AAABXUlEQVR4Xu2TvyuFURjHH/kRoZTyY1IGpUgyKLEZTBY2mS0GWZRBFhnEwIqSxGC1GWS0yB9gNslEGfz4fJ33vPfpfe97c1fupz7d9zzn3HOe85xzzGrU+H/04XAmVo/tmVjVtOENnmGdi2/iA3a6WNX04xOuulgrXlt+QY+S6ki+m6xg3Cy+46SLxQWXXCzSg4e4kniKB9jsB0X28RG7XazcgqLFwmTjLraAa66dEut9iQ0uvmX5BcUI3mOvi83gtGunDOIzrruYX1DfO9iV9GnyFzzHCSsoRUTb/7KQaWQOPywsOIS7Vjos7U51frPwv1fLly5F9f60kM02XuAGHuGdhQxH09El9AYG8BZPMn0/ZLevOupXKFPd79gWY5avrQ5SCeaI9fb3uxK6Fcuu3YjHOOViKUXXrRzayR5eWUhm3sLEi0lfDg367fPWBHq1sVwqYcWbooE6mBp/gG8yFDeY8PsmCwAAAABJRU5ErkJggg==>
[image49]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAaCAYAAACzdqxAAAABSElEQVR4Xu2UTysFYRTGH6G4VxE3srplp+xkQXwACztW7N2wUmQrX4BiQUoW8hUsLCwslZRiyQewsbCReB5nZt4/3VnMtdFtfvVrmjNnZs57zjsDlJS0J910hI5GDvhJRajSffpFv5t4neQUoh924z2do3V6Qd/oNKzivizbodXVaCftSszogFX6SIe9+CR9p/NeLEUP3KQHdJmewwoZ95N0oso2/CCZpZ90IYqLBt2BFSXG6CWiVenGDzrlB8kq7IVBFbA+q23+C9WqLe/8FyW8wi6m9NIreoqob3APVuuW6FB42TFBn5Oj0PK26QNsiM2YoU9wO2YXri0ZCqzRO3pCb+gxHfRy8lC1e/QF4YoDemAXdcxDw1lHWJ12zy1s27WMpn8Gm0HKCnJaUQQNWkM9pIuwLXeEFr7ImArsK1NL/vT/KPnH/AAAPDE8lPwzEwAAAABJRU5ErkJggg==>
[image50]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKQAAAAcCAYAAADvLBRZAAAFsUlEQVR4Xu2bechlcxjHv7JkZ5CxTcM0lOwZZH+tkVCWkKUZe9lClhmkISVbWRNiJIlM/GOZqBlDtj9ETZSoISUJpSjJ8nw85/ee5d5z7zn3nvvec/X71Lf33LPcc8/5Pb9nO+eV/j+sb7rO9KDpXNOzpudNu2d3ikRmistNN5vWST7PM71g2nR6j0hkhtjE9Lbp5My67U03ZD5HIjNGMMg1pjNNW+c3D8VJpouT5fVMi+TGHon05BDTF6Z/Ei1VGr4HheOfkeekQBqw2jR7eg/PXTn3HNOOcqONNMjZSgcV/WX6LhHLrHt8eu/2gXe807RWqSc73rRzsgxHmJ4o0WNKC6FZprdM+yefTzG9pNTotjDdLjfQY+TnnWS4X7fJ7wMTen5+c082N10jP5bvaCSKBI/wrek407qZbQeafpGHRgaiLVC0XKG8N8SA3jNtY9rQ9LDyXq0qfA8GiWHCXaab0s06LRFsK/eUkwrjy7UebtrH9Jrc+Vyv/pFmrukz0yXy+32i6Uv5dw4FIecD0x6F9XiMr+Un4eRtgjDKJNoos+48+Qxn1t5qet90jvITrAqXmV6Rh2UmIeH6BHlFz42fMv1qetl0qB8yMjZT/d9fFe7dq6YLlZ6DSPOx6TelEaIbRIun5Pcgm64wed9Uflxqc6zplsI6DBBD/NG0oLCtDVBZc+GPmM6Qt34elRc6wDVdmSzXIUSLr+RFzX3y89xjOiqzH4Z6vukduUduEn7DQaaVpvs15OD2gPD6jXxy4R0DS+Rekv5uGTiE75WPHEDk6GfMfSHXyjaSudmEaEL10O53RGwsn9WEbm7slvnNusp0mGmnwvp+YFyEfa4bb8E5MJBg6AwcuSjgvR5Qc6kM5zrV9KHcwzfZNegGEYAHCiuUz/0wMgyyaGxZmPB/q3MfHAXHhoJwaJiNuGIKmdML2yaJxfLchtyoDsX8sQjtpVAA4YGnclsHA8OgqPxEXiAE4x8HhF/CMOM/ld+UIxhemUEW1w8EN4ZqiS+8Uf2T2n5QdVEoVRWhcav/jhwfGPAdKr/2DeSDRjFDPjkMTH7yt0+Tv6MKzXUgVSDkYgfYQxllXrQxg2QAMELccNYYufn7Kv/jGJReP3bUcMFtVRXwgHhCPCKecdh7yVgR3gm7VVQ2kUKq9pz6e2nyS663aHiNGCQXdJbcTRdnxjx5+yRUUvzlWXH2cV2kHovkBSMFQBMVNPnz3ersrZaJ9kwRxpw+MzlxFU9dZnhl62tBrogxdus1kifRBgnMlreIqrxNEwqOqiIMNjFAk0DWS16gakYwKoIxLlZ6/xlfit0yKBj/VKfhBYMMfdrahMb3atN2hW1z5RXnnsnni+T9uR/krZG9k/Vl7CVvyVQVM3ecAzMOQh75ucZT0BAdaYJfmywHcEJZo8JR7aB0H3rXa00PhR0SOO4nVXNYHWBwhA7EcoAZc6T8GXGxyckJiz8iMjzFlg/N/VGDcS00/S5/RJwtMH+We0EgP+WJzB+mg5N1HMsjU35viKrYDY9YSemyzfJKMBPfUGdSXlS2uRxaAgO740hfMMyjTe+a7tVoe5GkSTTGi2OOaHpTPwCp1+vqdFwYIutflEe3p+URtW7vd2BoGq/UgO44Ugs8ECkRRca4W2G9YALRuyXl4u+M1gC48BXyJxQ8N94lv7n14BHCWy1BV2uA8BJpB7x8sUpejfGywSSxwPSR6VL5c/s18mfR8zP7kMaQOxMm52TWR1oMTdWyxmpbIdcJr1gBnnJV8jdAbrREXkwsVPq2eCTSOAfI3/Mj1QByHZJvcuIAoZv9YDd1vooXiTQGue+T8kIBLZX33rLQY6XVsUxukJHIyKCPymtWvGFOUYM37Pb8mKdQ/Mfick1eWhKZQMgluxkiHnS/ZJkeGi/lxso7MjYoZqi8d02WY/4YGSuEZzwnL3d086CREfMvkngtnQIwVSoAAAAASUVORK5CYII=>
[image51]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEoAAAAaCAYAAAAQXsqGAAADHUlEQVR4Xu2XS6hNURjH//KIvBKFkEhJhCImREImHqHIo8TARIlb5FUkA0R5DJCSJBPFgBDlhCImTCiPREpSlGLm8f/37XXP2uvu5zlHl9q/+nXOXmufe9f+1rfW+jZQUVFR8X+xkv72/Ek/ROq72k613925DKV76Bm6j46Jd2cylh6C/XYV7RXvzqYLPUff03m0q9c3jX6ld2h/r72z0Hhu05l0Er0Om8Q22HNksYw+p5NpH7of9rcKP9cw+pCOD9rH0Tf0JR0Z9BVBAe8bNjaBZv8qXY/6ZA6kj+l3OiVqS2IEfUVXe20D6BO6yWvLZC7dFbQpMArQZzo16CuKHuwIvUYnIn/G89CSe0e/wbLJsROWVVu9thAFKAymxnOR1mAZlst8WPY4lIpaalpySvVm0awfpvfpHMSXdhm602P0Fixoju2wQOkzjePoGChxnn6ko4P2XJQFZ2EbuNZ0K+lHd9NHdDEaD5hPN3oZNt7Z8a4YCkhaoJLaM9GM6TTQ7GxD80sljd50M2xj1X5T6uQJmA57UI1b409Cy6qG5ICUDpSCouD8ij5dkDRjOiX8QfQIrhtFAVKgGg2Y2yIuwIKfhvp0X1JASgVKQVkBS99wZrR2T8ACJvR5iS5sv6M59L/W0td0SdCXhX6nuu4oigU4LSBp7YloL1KQkmolHZ0bvevBsFLC3/wbwWXT0+izyMM6XJB2oL7PaTw6lNI4gOSAKFAqqocH7R1wBeU9OiToU4nwgE6IrjfQK/QTrEDVkV8Wtz9pQ9fbQNklrOxvo1ui7w5N5lLvWhOq09axCJYMKoUcPWEFq9T3VFytFBaUGvws+oLeRHy2NSAdtWVpRYmgwKyjP2BZoDcJ5xc6I7pPrymaTN0zKmrT/1dhuje6Fnr10T2asFQ0szcQf79L0q9a3VHsz1weqndO0ruwE6qZk9QVnOEYpV8LaRk9Q8etRIXzW9hhtRxWlR9E+azOZRDsgYvuTxrwabSmKm8VShDtZTo49FrzV1BqqyrW+9sa1NO6IkAvzTXYabMg3lURotMh84SoqKioqKj4p/gDt8WgD7mWsfUAAAAASUVORK5CYII=>
[image52]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAaCAYAAABIIVmfAAAEQElEQVR4Xu2YW6hPWRzHf2LK/Z5LyHFPIcJ44W0SaSSXKEqRGQ+iCFE6Sp68oXmQS5JrytRMZh5kanjwJMqTkksumcLLeJLL7+O3V9Z/nb32Xvuc/5Gj/alv5+y1//v33+u31u+y/iI1NTVGP1X3cLCJdLb9Ls8B1chwsIl0tv0uT5GDxqmeqj56+k/1RPV/dn1fNcw9kEOR/a9Bf9U21THVPkl/F6J2ieq3TCtVvRo+ITJdtUps/t1UfVULVGv9D5VR5KClqneqPao+3vgA1TXVG9WP3ngeRfargp2jqj9VA4N7eYxV3VVtUvVULRbbMGXv/IPqiKpVNUHs+beqe2I2HWukcXMiNudM7zOlxBzEip5UbQ7GWYgzqveqFcG9PGL2q0AknlD9pZoh9m5l9FAdV13O/nccVP0tbXezzyKxDTbKG1sn5mBsOns/qx5kuiUWaURcJWIOGiq2AOx2BzuDUOZFdkiaI2L2y8A2zsbpOHFK4+1SxqteqHYH48vFdvPsYNyHZ5yzHaPF0jHOHp6NsQCh/crEHNSiWuZd45Bdqg/Z3xTnQ8x+DOzOU/0jlm7GNN5O5iexdw0dhNNwblGenqO6o/rFG2MOjzO5+XTqAvjglNViaYcIIBJSSbEPrujdUB1SDWm8XRnn6NBBsfEy5ovVwyti9QSwdVospREZD8XqBXNJJsVB5HqcT170U1IKKfZhp+q5lBfIVFwaCR3dngVgw52Stk0Htq6rBmfXE8XSVJUMUeogvpAvDp0/SaxOOFj13t61o8y+T3uKbYztku/o9iwAG5B6QlrzIRJcNADve1ZsEZhLEkUOYkVp29BYb5wugDaNQuegJWOXhBTZj8Hnyf/UAepBexYi5ujYeAw24G3V3PBGBFIS9ml5k4g5CIfj+DDsYJrYF/mrT8fwq3ftiNlPgTpAPaAuUB+q5FaXs0NHuwWgGyqDed8U24jAxqMd5QwyWfVM9Yc0trRuAfieJPIc5B+0wrBzreiW7JpDBzufEOVl1mfjjjz7VeHsQY/NTiTSUpoAevhHqsPBOJvklWqqNzZc2hZ9NiDtrx/5pFzm3k+sjaWd9RfApaCyNreB0EHOweFBC+MtqnNiEyAKHEyGdOHXBEdovyMw0Q2q3+VL4YvB+/LdHJBc7WJul1Tn5cthivPFS2nM2yNU/6pei51snfgZhud5Fps4u8Ue+Qz/0w1V6hRDB20VC6EihSdJempeJi9Xh/a/JjjpquqiWE7mYElK4VDl4H9+rvCbDNdB5YmTtIPzAlG5V8xvj1QXpGKn2AwHEeZ5+R+aYb8jUDdIB/yYxt8qdSQF0uNCsUNri+RvwkI66qBBYr0wXcIssWLpk2qfgs7nUkS+rjzRb5VUB8UgFRHi+8V67zD3pdonRZA7U9Qq9tPvd0Gqg4ogrOkM8miG/e+ajdK2BWsmnW2/pqampqamS/EJNWviqEMR7SIAAAAASUVORK5CYII=>
[image53]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAaCAYAAAC3g3x9AAABRElEQVR4Xu2UTytFQRiHX/kTSTaKshA7kSxkpWyQzd2wYGFnYWOt5FuwkZSljbJEKZLyEZSykY0sfAJ/nl8zc5ozTud27k1Z3Keezp15z3nvvDPvOWYt/oJ1/I78xFevfmvuMLu7Dm14gi+4iO1RbBY/8Br7o/lShvEBJ5L5cXzGJxxJYqUs4F4ypwRK9I4zSawuS+ZWE1BpKlGlquSm6MFjcwexmsQq04lH5k50x9xhNYweVpIvfw3JOnDa3J8FupLxL/TwmrkytcL45jE8MJdY6HqKteyOArRXSlbUa9u4FY0HzbVYfIg5QuPe4VASU+vc46Qfb+I5vpl7Eab8fEbotbRxVfI8PuKluZMPaLX70TijFy8s//4WqZID2r8zXInmmmIAb6xk/6oyh1fYhxs4mg9XRx+PW9zF5Xyocbq9Lf47P5VlO3tA2P8mAAAAAElFTkSuQmCC>
[image54]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAZCAYAAABHLbxYAAACDklEQVR4Xu2WQUgVQRjH/6GCoJJkJoIeiqBjiRDkSYIiiEDU8KCX8FB07SAJwqO6B+VJBOlUVNAp6RD0sEMHQQkKL3YwIulQ3SKCsv+/b/bt7D7f7rx3CIX9wQ8e38zOfG92vpkFCgr2L530Mj2QbtiFXjpHF+gN2p9sronGH6LtsHmO0Cl6yu+UhyZ/ABski0t0mZ6k3fQ2/UlH/U670Ewf052UT+hBr18uIYm20uf0Ox1wsWN0m76nh12sFov0Hf1In9KLtCnRI4DQRJ/R33TYxfroJ/qB9rhYLe7TwXSwXkISFUq2C/FevkD/wFZLrzeL/5qoj1Zzha6633nM07t0DfYW3iDeQlW00UewfeL7mf6CDZBuu/XvyRhV62tYX02qwgo5LZboTcT7UhX/jZ6u9AigkRUVZ+kPWqItyaYqOpAsnmh/P0T+tqnQaKKaXKurAjuXastDc24hrBArhCSq865EJ5F81XpOZ+KMF0tzBVZ017xYlKjU7yBCEtVhr4T8gdW/7OJXXUxohXQ6ROhPqI+faPTqy8ieN0FIojpatPnvIN6Px2GFuIG48k/QL7AkjrrYGVjV+/t4AnarjXmxCofoC1RXdlbV614Xet3XYbfQLGwLrNNNJO9rJfyWvkR8PepZfRe8otP0Hv0KGy/kxGgITX6ejsNWuZ5rUB8wI7Dn67rjCwoKCvYAfwFg4GywFCCwtgAAAABJRU5ErkJggg==>
[image55]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAAFIUlEQVR4Xu3dWaitYxgA4FeGyDyTec6UJMMRkqEoJEPIdOGCJIlkiJRSUsqUZMhQFIlcKFOsKykXUqa4cMgQN6K4M3xv379a3/6dvdY67H3O2qvnqbfz/d//n73Xt9bFenu/YUcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApHtKnFRig/6NObZhifNL3N6/sQLlWB4vsVX/BgAwH84ucUTUpO2d3r15NijxZIl9S5y58NaKskXUsaQfYmWPBQBYxOYlNi7xUInXe/fm2eEl9ipxcInTevdmwYElLojpqp45lpQJ2yyOBQBYApmwXRjTJQfz5oV+x3q2WYlnmuvVTXucY0ps1+8EAOZDJmk5LXpQidt692bFw/2OJZDVtftK7Fri6N695ZDv8TSy0rl1c/1J015MjmXPEpfHuhkLALDMMkEZRlbWTi7xdxfLvQB/76i/s3Vq1GnZcaZN2PJnnxv19/QNp30zcpH+qzEa9+7Nc8tl2oTtjqivb6eo6+tyY0Qrx9F+hpvGaCzfxboZCwCwTH4vcVeJs0r8EvXLPhOD/+rbMfFG89xQTvUd2+8sfi5xY7+zZ1LCllXCm6JOC6ZVJc6ImvBsVOLZEvt391ImOevaNAlbvtZMuPL1bVPixBL7dPey6vZR1/d81DHmZwgAzIlDS1zTXGcCM6mqtdQywciEJF1a4pyu/XT8e2fj9rGwivRU77qfcB0VNSEdymdyB+i13fVjJXYosXP8u8K3XPqVsCt612tKlvP1tWPL5/K9yoT0gahVt5Tj7b9nAMAKN4jRhoJMXD4d3RprXFLXJh/9GCYWrbZKlrsZV3XtPD+sn4D1TaqwDWLhmPKoiz9LnN5dr+2GimmfXZukaZoK23W969zxmZH/94+m/+qou1unsabEsJVjte4NAGbAIzFKQj6LOn04jVzvtVTyZ+XU5xMl/irxbtQpvkyuJpmUsN1S4vOunYfH5vODqJW6HWPhmrbjm/ZiJk3RDg36HWNMk7B9GaPP6aKoa9LSYSW+6Np5/+OuPUmer5cJ+iRZcQUA1rNcF/Vy1C/mrAoNk4L898qoa78yodql608PRk1Ipk1eJsnfkQvjf42aMGb7/QVPLG5SwpaVwJxazWrd2yUOKfFT184x/hY1+cn1ctmXsnL1StSNFnnm2dDeJb6OOu5Nmv41GfQ7xpgmYbu7xGtRx/FijDYQ5Bhyejf7B7Hw+I7dSjwXdbPFeTH6bLNq+VbUz3GPrm8xEjYAmGE5dblf1DVRuVZq0Nw7ocSWzfX6lBslllomOi917aw65lTu0JtNO5OkNh5t7g2a9iSTpjCHGw7W1gExWuf2YSxMDLOKOZSvuz+WIQkbAMy4TNZy2iwrVO0UaK6TmmeZsH3QtfMvPLTr6KadCh70O/6H/oaDaWWilp9d+ioWJobvNe1xJGwAMMOGX/bXlzil6/s+6pqw3JWYZ4K1h7jOkxzjN1GnDPN9yPVumbhk+/6YPO6cWs6dqTfHf0u0lkom3HmUyg0x2iSyOmrFMKd/j+z6FpM7cH8scW/Uo0QAgBmzbdQv9XbqM6flLo6auAzXQ82jnA7No0XaYz6GVcVZmQqeRk6HZtLd7ui9LGrFbtwuXwBghbikxFUxOh8tHRfznailrCRlonNr05fHX6yrM9qWUv6B+Dt7fe3mEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1uwf09+hAsKjinEAAAAASUVORK5CYII=>
[image56]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAA9CAYAAAAQ2DVeAAADDElEQVR4Xu3dT8jURRgH8AkLtF4wNAwRirwJ+e8QHpLoYNDFDtHN7l0iwovgSYJAvaUX8SIiHaygg2VK4Puilw6CBglBJ0UIAi+CZ3ueZpad/YlK+r7v7vvy+cCXnd/M7rLHh5nZmVIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgCX3QmRzl02TwwAATFMWa/9EHna5P/EOAACm5oPIg9aeiyyMh/7z0uD5ebw87AAA4OluRw639rbIvW7s9da3WD4bdgAA8HRZsO1v7c8jf3VjP0bORHZE1pVacH3cxvIz70aOtrFcVn0v8lXk/fae45GfIrtK/Z4LrR8AgP8hi6nrke8juwdjP3Tti6UWZll0bYi8EfmojJdMb0b2Rg6U+rlbrX2qjWc7izoAgBXvtci1Mt78nzNgS21N5NVhZ5jv2qNl0/xtaUtka2unP0r97SdKnYn7sxtL2Q8AsKpksTZtC5EPW/tc5MvIpVKP/DjY+kdy+TOXPW+UWrh9Ueqs2slSZ+bOl0c/AwCwYuXM1cKwc4ZtLLWwW1/qMikAwKq3L/L1sHOG5T62Iy0AAKve2sjPZXJ/WO4xG51htpjnogEA8AyyUPs78krXl8uNuZH/xTI+gmOx9LcbLGcAAFasXAodFjTfllqs9eeiDW2PfPKE5MZ/AACeQ/7zsj/O407Xfqu9pz8XDQCAGTQ/7AAAYLYslPG5aAAAzKD8BykAADwiC8XNXQAAmBFvlnom3M7I1VLvC1WwAQDMkAtlfBxInv92vxt7ktEhvwAALLG7XftE5Fb3/Dg5G5fHkwAAsAx+b695HdYvpV7unj6NHG7P70TmWn/Kwi5vZQAAYBkci5yO/FYmb0rYEvmutfeWyX1tV0ot4gAAmKIs0i6XOrOWrzkDt6eN5TVa+TyajQMAYAoORW5HvokciGyMnG1jv5a6XAoAwBRdj7w96HMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADPmX0SbecaDttSQAAAAAElFTkSuQmCC>
[image57]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAaCAYAAABLlle3AAABm0lEQVR4Xu2UvytGURjHv/I75Gd+ZJBBEVlECasYlGQjKYOSwUQGf4CJLCJFSkrKYqCEzUBJGZQFm8FisEh8vz33eq/73veN602p+6lP9/acc889z3nOOUBERMR/JJNW0CqfRd5OqSKPLtI3+h7gkdMnZRTCBr2kXbSGbtEn2g7LNP+zdwpIg2V4Tcs98Rb6THs9sZ+SBStXHA2wjCZ98U76Svt88e+SQbeR4HsFX2irLz4Om4wmFQZtxjMk+F4/vYfVzSWXHtA12IxdqukmHaX9dIcOwErkZYzu0Ue6Tpu/NgNN9MZ5Cg0wTa9gG8pFcQ3WTe9oGy2g+7D6+9FKLfmDLhpsgl7QVXpKV2iJp49QxnV0CDZ7fVdMzxFfN/Xdha1CUnJgS6xnIvQj/VBZCGV4i/i6ldGTgHgolNkxYptuhi7DMuugI867dv4hbPmHaa3TPxTK7IFu0Cm6gNgtNQ+rtVarEVamWdrjtIfGrWc2gq9EZaWjIlSmZKX6Fumw4+K/RFxKYW3eI/ZrtEMH6RyCN0g9rfQHI/6cD3P3PZSv9hsFAAAAAElFTkSuQmCC>
[image58]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAaCAYAAAC3g3x9AAABDklEQVR4XmNgGAXUBo5A/BqI/yPhX0C8G4iFkdSRDOYA8T8g9kCXIAcIAvFpIH4AxNKoUuQBTSB+C8RrgJgFTY4sEM0ACbtydAlywSQg/g3ENugS5ABY+N0FYnE0ObIAofBjA2JWPHwMAAu/InQJIGAE4iYg1oHyQRYuB2JfuAosAJT+cIWfChDPBWJOKB8UJMcZIL7CCvClP5C3ZjFAfAACyUC8HohfAvF8INaDiqMAYyD+yoAZfpIMEMMeAbEikng6AyRFYAAXIH7GgMi7f4H4CRSD2DBxUHjBLALRIIuDoHyKgQgQ72fAE36kAlCk7QRiXiCOYUANCrKANhAfAOJKBiqVSCDAAcWjYLADAJKyMrpiFpCOAAAAAElFTkSuQmCC>
[image59]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAAZCAYAAABggz2wAAACdklEQVR4Xu2WT4hOURjGH6EookaGkGGUFY38ayZLaSSlkI3NJJIVpmhsrCQhZQzToFkqlI2yICSxk2Q7C1JKYYOU/Hme3ntmzj3fvfe79+uOzZxf/er7zj3f+c57znvee4BIJDJVmU530KuJe+jsVI9yTKNH6eGgfS/toXNgfRbS/bTL7zTZzKSD9DTtpAfpd/qWLvf6lWEz7LcnvbYZ9Db9G3iHzvP6NTArsS566SO6xGvTamsyN2ATLYMm/Rj2Oz9QoXG0cO/pXVj2KIsKWUkf0nO0LXjWCpqUC8qxlH6gY7Tda89D6XicnkfjjgplzPqgrRQaeC19QG/SFenHldhAX9NDXtti+i5Rn5uhlL1Au1FzoD6rYfku9bkOttBf9B6aHxOl7AjtgAWTFegVeom+gmXKS7ou1aMC2lXt7jPYCmvXW0HFaZR+pZuCZyH6j2OwqiryAtV4A5g4l6oBX9B8/EKUakNoPeDd9CPdGj7IQBO9CFsckRfoXKSLj6sBt1C+2GWiInWNPoelVFk0caXXxvBBBnonDiM9fl6gIa4GlC12DWgAnYcnqL6bClILsyr5rpVWis0f75FmDX0De2U4P8Eq+Lfku7Kij/5B+hJRtdiNo/N5nd6HVeMqAQpdDPR+8y8IC2BFRmknlHodKC5OWTvqXl9+oC51n8IyoxAFU8crZhHsPKs4hLujG407QwdgEy46V0r5H/SU16ZXjrLMnWOxj/6E1YNcFKDSUumpASptfQZuxbM84/XbCZvcCTRmjO6uL+hvTPxWC6XUVd9+2Hy1WJfpZ3okeZbLdnoW9dyK/ifL6C66DU3uuJFIJBKJTAL/ACuCglvRvz7JAAAAAElFTkSuQmCC>
[image60]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAAZCAYAAABggz2wAAACaklEQVR4Xu2WP2gUQRTGv6ABRYOFYBAMRJsgQlBEQ0KQXFBBRBEkamEjFoqFhUJCqthISOO/JIqoWCoq2CgWCgki2EgqO7FQ1E4LQVEE9ft4O7nZuWO92SQQcH7wI3c7L3vzdt68HSCRSPzvLKGHaHtw/V800S56hV6lO7JrjgHaQ1dm19fQo3SzF7PgLKf76GX6kX6jW3MRxaygt+lDupFuoq/ormx8Kb1H/wTep6uymLosy5wvlOge2kdHEJeoVucSfY7qpM/AEhlyQeQmfU3f0wd0L6x6CtlAn9IxujoYmyuaXEyiilP8Ce/aOnoeNk/HOBq/Zw49yU76hN6i6/PDpYlN9Bz9RXth+28t6ldb6UR9OmD1LvV5LsQkqoQew+JH6XVY2b6hp5BvRhP0Ip2hH+hLusUbj0KrqtXVflEH9H+oUWIS1QpOw/aj9qCajqjQr3R/9l2oWQ2jui/Vcb/Q7bMRJVD5TKJcwmUS/U13etf1++9gq+3KuAX55qN9rJW9g+oDKoWa1DX6AnHvxJhE9Vp5htp4l6jU53q4mLe0NRhrCN1A+2EK8aspYhIVKtkwPkz0GGzVTxbENIz25w36CNaNYxN0FCWq0mtHvqseoT9hXdcRlq7uqX3sJ+pKdxq2BQpRMvP9itGkvtNt4QA5Dpuwv690lNNBQP/nqCDfjLphVdY8GwEcpj/oQe9aDUpQZany1A2ilr4O2mt3YV3QP6J9ohe8OB0TNblB5CumH7Y6esWczj6f9WL0V981Xz0snYc/o/YVVIOOa7qpGs5iQQ9rNz1A24Ixh65rXHGFZ9xEIpFIJBaAv5T1fSRJ3DF7AAAAAElFTkSuQmCC>
[image61]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAAAZCAYAAABuKkPfAAADW0lEQVR4Xu2XWahNURzGP6HIWDKFXDIkEgmRMpTiwRDKGCLDg1mZ8qDkwZAHURK5KGNIkbGIFyJ5IBLlPsiD8ERK4vvuf6/22uucvc85zkXd9le/ztl7rb3OXv9xHSBXrsag1qQTaRoOeNJYm/BmY9EM8oBsI5dIl+RwvWSALWRDOPAv1Z/sIUfIAtIqOVxULchCMij6ro10J8tJTTSnB3lGxkTXWvsz2UH6kq5kErlFbsIi5r9oJjlPBsA2sYY8Jj39SUXUnjwivwL2kebRnMmkDrau1JvsJ73ILDKfTIdFyNBoToNK3nQvk6bO5Abil5SakIMwb2VJXrtGnpO3pJaMhD3vtBlmBHlc0ucJJD2+An8hDRTaF8hp0jEYCzWMvIKFpi+9/NHgXihtRBtyGywm1QPfCDL2YVj6SAPJKTRQGsj68sJdmBeVi+VI4fmBvCFjo3vtYDk61U1KUTlGkENk5OHRtfJ/ffS9JSx6qk4DFaMJsOq7l3RIDpeUjLcJcT4fJ1fIxmgsSzLCOViOKyXew56t8eZoDa11nywhVxGnnlKgqjTQ5qeRh2Q7aZscrkhaaxdiQ3wkE1GeEVTRZ8Pmip3kNQqLah8yBfF7yvtnEaeB605zUbqO1U+YQ56StSivlWVJL74KFv6jYZ6UIX7C2l+W9KwON76xFPbfkF1UtXEZwKXBCFiX0fUysg4lHDCevCMrYTlVreRxhbKrIYqKeeQreQnrHpVIhVbP3kG6g/w0aEbOwCJR0p5UOLtF16nyo0H5XE0qqAMcCG/CCpgONdpUmnaTHzBDOjkj3EPxih+mgYys9qpu5LQa2b+bkKsLOtj8SVGUVN2dF3ypeD2BHaAk/VYN4tYm6VmljW8Elw4qsGFIh2kgqbPUIWkEpeco77os6cf89pjVskKpDb5AYUVfDDtnKFylpbBaodB19xSNOu+7zepzK/kCy/NQSgEdjHyppqizOSNoDZ04nfErlhYYTK6TY7CjaSnJw0qpT7Auo2OsDlqq+v4fHVX277C5btNKy0PkIllETsLW0VE5lLxfi+J1TAa4DFtPXUQ1odi8iiUDqOVo0XKkVNJGdZ7vh8JQTpPmab6eG4dkujjpnoyV9i7asOqLIuI2GZIczpUrV65cuXKV0G9RaY9SjmE8+AAAAABJRU5ErkJggg==>
[image62]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAaCAYAAAA5WTUBAAABwUlEQVR4Xu2VzSsGURSHj7CQj5V8FJGilJ0koixsLMhO1hak/AfKAgslSVYkUVJsKWSh7JUiK4VEkmxQvv1+75nLzDHzvmJhM089zTTnzp25Z849IxITE84YfILvPvdhlRdvhHe+2Cvs92IOjjmT4By858I75z3r8jVnKIXwGB5555Y0uAgHYLqJ+ZmCD7DOXK+Ur/lLTOyTJvgMl0QfaMmFK7DCBnzkwB14CPODoQQLollptwFHr+gAHsOohquiD4qCL3gJ5+X7QtwLPsKGYEjhDczAPaw1MUcHHLYXDRwTtZAu0bqYhhkmloCpYwqj0kgmRR+SjFHRlbbBYs8yOASvYLckqScWEYspLI2EqVyWn9XDteg8M55zoosbgXlucBip6oEP50v8th7KRXfGHiwIhhRXD2HbyvHXeiBuZ/BTfcOlkW8a1R8mYIu5bonqDyQLbsA32GpiCbLhNtwV7QWWejgLM23AR6r+0CxasFuS5JP2wRtYY65zVWuiFZ4M9hDebxsddwLTfwsPJMU8XOW46OBB2AM3RZtTkW+chSs8leA/5Vz0H8LjCzwRbfX8JD+iFHaKttWw+oiJiYn5Fz4ACwpoLjyIdt4AAAAASUVORK5CYII=>
[image63]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAaCAYAAABPY4eKAAABk0lEQVR4Xu2UTSuFQRTHj7yUkhKRKC+hlJ0siIWycBdsJR/AysbGUokvICuRLGRjq8TGXlmRleImkmSD8u5/nOfwzHnuzGNl4/nVr3ubM3PvzDlnhijjvzMIb+BHzGe4B6tj8+L0wTy5a+7hZfT9DW7DDl2Qxgp8h8M2EGARPsIeM94OT+EJbDSxBFXwAJ7BBjfkpQLuw2NY44a+WCfJwogNWDrhLdyCJSbmoxVewTVYZGK6sSfY64aSTJDscsYGAoySrJm0ATBGUvcl+sVhuHYvsN8GAiyQnCwH6yOb4Cy8huOw+Hu2B603N0idifnQtPIt4bQvR66S9MA8rNTJIdLqXQZLzVio3s0kBzmEtW4oidZ72gZIfngOdpnxUL0Z7XQuSRC+3756t5GkstyM++43w3N3SN6MIRNzCN1vTjXXkTMTJ+1+D5A04i7JXC/d8IGS9ebO5T/mJ7QlNs5oj2yQW2/ubE7zHTwi6fyCcDr0Hda3+CKSv+v4Jv1sik90XmANb5A/X0kyOEXJMmVkZGT8DZ+y72Qc3i0f6QAAAABJRU5ErkJggg==>
[image64]: <data:image/png;base64,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>
[image65]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAAHE0lEQVR4Xu3dW6h1Ux/H8b8ccszhlUPIIRLvW8jpBjcoEolyyOFGDpdexyjZJRciSkqJHErORXLhEKsI5cYNSvQ85BCFGxRyGF9zjtZ/jz3Xep6991raz+P7qdFec8z1rLnmWKvGz3/MuURIkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkrRiW7cdc/ZPH29zsGPbsZEca0mS1rj/l7Zt29k4I/75Sf3x0nZuOzdBN5e2S9s5B0eW9nDbuZE2l7GWJP0LPFjaaW1nj0DzUmn7pr6fSnslbc/DmaX92fQ9Hd0EW51S2h+lnZP6cF1pn5Z2UNOf8W8+K23vdkfyQmkXtJ3RjdcJafvbmD4eBL5bS7s8urHbv+/nL32M8cd9H7Yr7cW0vVrnlvZcaSeW9mssHa/qw+jCz22x+hBzdHTnyl/w/fooxuGXz3aL/vHr/d/st/T4itJ+j/HzM/oW0vakseZ8OC8qcZwnY4JZj7UkSXNxWGnflbZVu6NHMMmTJ9aX9kXTN2uvlfZ1//iQ0naPLsRd0/fxfu+PLnQ92veBCfnk6CbrSa4v7apYHChaBIF7284YHq/3YukYZTdGF/JA8CVM7hnj8wMhKr8XwmYOhavBeXJMEHjbIIyLYnx8zn0UK19m5FzujO44fGbgs6qvt0MsPveb0mMwxs+m7WNL+zm670CLsc1V0kljzWdU8Xo/pu1ZjrUkSXPBRD00gVdMgFSxKiZznn916psHJvRR//ie0o6IrvpDwz6lPVbam9GFu+rS0q6M6ef0VHThgZA1aaImHAxVos6Kpa/9S2nvNn3ZMdFViUBFr1b28rgSbPgsKkINS4qzQHWtLhkSbtv3D8JpDoyM/7QK5TQcb7/ojlPP6ezx7r9fd5S2qYZljDGfYbUQw+8ZVH+zSWNNyK5q9a+a5VhLkjQXVB5q9WXI+tLWlfZA31jWYhlpQ24p7fMpbbfxUwcxQbPUSCVvqNJFkCRQUU2p1T4mXaoqH8TiEJedGuPqWA4ULQLUUGAZRRfQ6nh8FYuXi6dhuY7gdFy/nUMIx+OcslGzPQssLd7Q9BFeR7E4sE2rPk7z3xhX0ji/29O+ij5C2ZD6Xt6O7rvGa/wnP6HxTdvRmzbWbWDDKFZeUZQkae6oPOTlJ9TrjEAVKgeJ46OroNTQtmXaNytMnLnCU0PVTjE+HktsLJ3x3urkW6s4bA8FBTyRHjOJ53PbJj0mQA1d38Z4vRrdPtqGblrIuG4qLwVuKLDlZbyKz6Yeu217pOcN2T+6alX+fEGFiYC7ocDGNX3PN32t+9Jjvjt5uRo1kA2FYeQlZ6q5LJcSsis+/+3TNuF/yLSxHgpsjPWuTZ8kSWtGuyx4cIzDGGGE6lWuIHE9EBMqEyvq9UItJuY2UOQ2LehRfRkKXPkGgDv6v7x3zqEuaTHJT1rOOyAWV2uYtGug4LzeSfsmBTaWMbnZYbmo9DC2uDi6uyfzdW8cr12CHTXbq8FF9+f3j0/KO3qMdw5o7ecOlqXrOQzhGHk/y5GjtI2hJeWMamoeF8J6/i7wfcvLpUOBbWisc3gbCmyjsMImSVqjmLiortUlQpb48gRIMMnX9lCZYUmt3kFIaHtjvHsmhiowVFg+Sdssp3LhOAhVBIP6nghwQ2HvwlgaHr5MfYSAvPTKubXB7H/RXTM1rap2WSyu4mGv0r6P8XIwd7sy5vluzNpXTbrpYSUWolvG5dgEsfV9/6H9NgiyC/1jws7Q3bHTMO7vN30EYD6bbBSLw1PrhxhXfBkPxqVWHvk8+L7lmw9Y/s4mjTXB7cD+OYx7vuZwlmMtSdJMce0VlQ4CGJM2f9muYeOSfpufgWDi4/k857x+P6h+PJ62V4slPY7Hcbl+jeMy+bLNRA6CBdu0J6MLePXCc/5N3beu70M9FxqTPUuA3HhQ+7jejpCQK1y8bq7k8JMY9fm8p0molLXXBBI46r+l1UBJWON9nF7aW31fxfLcpOu8lotqUj5+XWqlgpav9WPMuL6N/e2y6TR5bA7v++5OfQ+V9kiMv3O0GhQrXoNwV/fXc2dpk9DFZ0jll+8bAat6OD3GpLHmfN6IbtmckFdvXsEsx1qSpDWH38+i0sWPy27qXo7uGrlahQEVI6pqy3VX27FMBBKqQDmYqAtWfN+OSn0E+Em/IbgxHGtJ0maP6tBCLK8as1aNYunvgV0bXaVoObg2L18kvxIsSebKlzpU2BZi/Ft81TOxcXcuD3GsJUnaDLB8OnQDwzytpmL0b8R/MPB/t1gJx1qSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEnalP0FGaZZ27gGL8IAAAAASUVORK5CYII=>
[image66]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD8AAAAaCAYAAAAAPoRaAAADJElEQVR4Xu2XS8hNURTHl1CeGRB51CcpeWWAJCSFUkgRSkwUBkqRV0rfxMQIE4pCEklKEgODWySkTIiUwoBSEiGPPP4/++zuts4+93ulDM6v/nW/vfY9d+211l7rfGY1NTX/Ab2kUdIyaYU0tlj75yyQXku/Er2Rvkk/pLvSSql3sX+j9NHtvyQNLOwwR/qS2J9IYxJ7hGeulp5Kp6W1Fp7/QLptIQg5cj7w9ympX3Nb5zkhfZfmJms4t8lCEHZZMxuDpIb0SZperHlmS/elCd5QMEQ6L92z8iGx3ZAeWz5oQLDZ806a4mxdYrB0U3omjXC2kdKLjI1M+WBF+krHpEXeUMDhrlnIeJuzRRZKP6Wt3lAwzkLFNiwko9tMlN5KF6U+zjZT+iw9koYl6wcslBv31MOhD1n5WUD1YKOaqoID8XBXLV/KMTj40SOWWzjIZm8Q7RZsO9z69mJ9t1sfaqEHjHfrkXnSV+m61N/ZUjrKbAw+vveII1YuYUqXxkJF7Cz+TiHj/DjfjZDVPdKWZC2FSjhn4XvrnM2DL/jUsPLhqQQqguAQpG4TmxfdnQ7LZxoNDh61kMkc0bmz1myEk6UzVnY2Mlp6biGgXLVWUIX4kD4/EquChpdOmS6Tu+/82F5rfS+nSR+smRkq47g0K9njYTIwIXz/8MTMcnjGn6ej+z7AmqO5JfG+c4dToqOMwBx+CqyR9ls5SynxmQ2rrg6I+x5Kw50N4n1f4g0W+shhK0+tLLn7DtxJfqAqunE8Un7zpQuWdzQlVlnDqg9PBZ20UHW5Zhbne9V9p6EetNZJ+EOr+U5Qct08EkuTMUivyDnqYb7fsWrHgbdJkpG+VKW0mgI8/4qVE5mFNyPekPx85zOZTA+/z8JdS+FFhz1kyk+DKggSWaWnpIfj8wbpvbTNqu9svKa+InmTJBkElyBUQmlwX3lIFO/zafbIAE4ShPUWmpmfywTmlVXP9BwccqmFoF+25vs8/0PckiY1t/4F4xMfo79Mp5cWfj89R3uxv8dwFZjndHx/cGiTpvrFTkKlzJBWSYuteqTW1NTU1NTU1HSK3ySOwSkxVh52AAAAAElFTkSuQmCC>
[image67]: <data:image/png;base64,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>
[image68]: <data:image/png;base64,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>
[image69]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAaCAYAAAD1wA/qAAACkElEQVR4Xu2WS6hOURiGX7lElEQuIX+YuAxIjiIlGTgDEmbEkCkDt6QjGRicwalTJuKgSCkj0TE5KAampFwKiVIoYUAu7+vb65y1v7332nuG2k899bfW/tda31rfugAtLX+FSXQd3U4X0dFZ+UQ6J/v9T7OY3qef6RW6n16gg3QJvUE30Gn0Lv0V+S2rC4yjl903B6J6oXZu0x/It/M6KntIt9FR2X+SjKVHYY0cohPy1VhLP8E6iFdkE6yz81FZjFbyEj0C66OKblg7J125xqHxfIdNQjIYdXAa9rEiL2M8vZ6p34EV9Au9SsdE5YFV9CKKE+NRoD+RX9GABt9PP9Klri7HXthsHEY6Ys26OoyZT9/SIdi+itHgB+hyV+4Jk6R21F4ZYeX3+IrAQvqGPqVzXZ3nDIozNoM+py/pLFe3kx5HenLEbPoCxdWOCYH4iRymB+W5WcZkFPNcqzBE38NOt0CHXqPTo7IqNDlKq9QYdOhonAd9hQiDqMrNJmhfaH98pSuzMq3AKbo5fFSDBpcag9o7Bwtkh6v7g1JBKZHKzSYo5dSJll/ohNPm9KtXRpP9oSP6EYqrPkwIpCy/PToQVvvCDM2oAtHyK/10d2jvNaHJ/tgIWzFNWNnJiCn0AeoDmUrPojrft2JknymYypOlhLr9ofTXZfyEznN1OXphDSnqMpSfGlzV/SLCYB7D7gx/DKdI3R9KzR76gXblq4ooSkV7h850dboHjtF9SB+h4VKUugCbovZvorg/1JeeSkq3Z3RZVJekQ+/BTp4Bupv2wd5S65EOQoS9dgL13wql6i3Yc0gpKd/RV7AnkMr1vtqF+hdBAQ2gQ7dkLsDIi7cOpcAa2Mu4paWlpaXlv+I3H/SMDhBtP9gAAAAASUVORK5CYII=>
[image70]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFUAAAAZCAYAAABAb2JNAAAD5UlEQVR4Xu2YW6iOaRiGb9lEtiESImcolOwKBzbhwBCKksiQSHaJsjmSEApJ0WQyk900NtPYlrIZoZQQDkQ44UCc0ZQynmue7/V93vX9v9/y/2tZm7vu1vrfb3+/z3O/z/tIxdHdeM340tgnM97PeNl4zDjXuNs4y7jVONp40/jU+I9xSHJNJ+NfxsfG34wdknGwwng3OXbVeEV+j33Gjulp9QfjjKeMLY1NjPPkYvVOT1Fz+Tlrk98DjS+UChow0zgxGgvYbPxV/gzA3zXyiWgVTqovWCf/YDBKHoH908OfsVI+AaCbXNTJ6eH/JwHRm2XGAlobLxkXReNkx0P5JNV5kN5EDWl9wzjJ2EaeltuURlMW4+UpDtrKozmIxPmkeO/kdwzEe2QcnDOO9fD8Oo3pxhNyEUcan8g/jg9+qzQaiwGrOCuPcsB9uG8hINoDY+donGe9Vu1HajvjcuMB40Z5JpYMIu22Ut/jo87L05NUfqUvF6wAjveIxg4lbC8Xt5gvxn6aHb8lv0dtoZfxnnGhPFgIABbUodmTioFovK9UoKyfIjB+GosHfjIOj8a4Fp9cpuKRFmwl9tOe8ujl3qWiqdx6ygX8/xfjn8n/AWhyQcUD5TMQ9bT8Q7ngnPyjlsj98I58Bc+CyOV4HGWI9K9xfc6xLPL8lDLqiHGpil8bg3feaTxjHKBvuzYPvBvZGaqagGnGd6q6BuQCMfENVvId8nKGWpEaFAyS15PM1AzjauMCeUkVgzQhdcPilYef5faC+Iflz8bPiQIWy+qCZ26XL5Zj5BFcHZCdH1VVVKzwP+PsaLwoSCFehJnGL7NgvIvcrPGYQkDo+NqaBgvMBvnkTtG3ixvEKyRqPN6gwOSyemMz81WiF8pFyxOvUdQMEBNRSxV3lfLFK1lUTvrRWQ5gS3Pk9ffU6FiMQuIVGm9wCFHKQltKlAI2LR9UVbwgKlVAg0TwUxYrqpi8SqUQ6NI9N+6JxikX3xj7RuMVxWLje6Ubh7wqotIoR1nFe2/Sl7s6JuUP41HlN4cqBkotWoGhTzBC3gugDq40KPX2yvu9w/T9GwDEZBN0XF57HzReV/7OsqLggcxu6BOwisYpVAnw3P0qz24qC6Kc3RMbHv5WJ+qrBYwfrzkp342xU2JXRPvwmXx/j7gtkvMb8RWQIqQ33S18hgZE8FPaeaRijZp6fQB7eHyHaMVP/1baWKYsuajydo8aBOibhoZ07KdYQk34ab0DolIQAywAPx2bECugSKY/Ojc5pxElYILxd3lDmlYhrbstxq7GXfIeJ92i2uzg10lkW32s8GHnQmmDn5azxPmh8QkmtdQEkXwKOgAAAABJRU5ErkJggg==>
[image71]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAaCAYAAADbhS54AAACIUlEQVR4Xu2VO2hUQRSG/6ARJD4aiQqKQVAI2IUQFAULGwtFbMTKwkIR0qQWUkSLQCIqqRQRBQkklgoqgoK9IChW4gNRREIaFTQv/z/nDjtz9s7dRYNbuB987O6cmdl5nJkB2rT5N4zSX3Qp8gXdXcT30W9RbIGeK2IB1fmAtA+1+VR8V5v7qPXZNJvpG/q6+O7poLfpIF3lYjFX6Q/a78p3odb/NherZD+do3dgg/Csp1N0pw9ErKNP6Su6KQ0tcwu2ekd8oIozsEb6LKOXTsP+PIcG/ZneRP3kwqB/0r1pKI860Up9p30uFjhKR3yhQ3VykzsBy7MJutrFsmjZtfy5LRCXYX9cxUXYihymWwt30GH6hZ5EdX7WoURVwpZtgdA2TKK5/PoK6+da4Q3YhC/QDaFyszTKLw1IA/vT/OqBncjntDsqV72u6HdCyK+yIx742/wS4URqmwM6BLrbSicctkAzyt1fl+hBV+7J3V9iLX1AF+mhqHwI1q4ULeVj+gx2V3kG6HXa6QMRje6vA7BD8QhWt4deoW9h7TTANUXdhLN0hu5x5Zr9PdjJqkJ3nNr7y1knUFs3S18i7UcTeAJrm0WrMQbr4Dw9TR/CLtQtUT2PVuI90jf0I+zN1Oc8fQd7xrSdMXpp9B9lu1THdnoM9mSU5dtKokOSza9WoZv/Lj0OW4xTabh1KA/1kozDUmdjGm4tGpzyy1/Gbdr83/wGusFzLXh3e7UAAAAASUVORK5CYII=>
[image72]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHsAAAAaCAYAAACXbyOAAAAFZ0lEQVR4Xu2ZeahtUxzHf0Lm4UVk6l0i4ZlCeoZExozxDGUqCkkpMkvvHylDIZmLl0SSkkTIO4ZkylCmlAyJECJkyPD9tPavvc46a+2z7z7neNL+1Ld77lr77Lv2+o1rX7Oenp6enp6pspK0qXSkdIw0V431TJn9pS+lvyN9Lf0u/Sm9Ih0nrVxdf6b0U3L9I9Ja1TzsLf0azX8gbR7NO9zzBOlDaZl0koX7vym9ZMHoOXJr4Pd7pdXry/4z4Lh7SjdLt0rH2/B+NZF+93CrbdGZu6U/pH2iMW56lgWjX2x1tK0tDaSfpd2qsZTF0uvStulExXrSg9KrNmpU5p6R3re8kwCbxTXfS4uSua7g+C9bcLpVk7murCLdJF1t4Vm2kZ6X3pEWRtflYL/Z9+ekLaUNpPulO22C9a0jvSB9JG2czG0ifZqZIxJT53BYyO3SQelEBcZ8wkJElx74QOkv6bx0omIrCxlpYMH5psUa0hnSWxYySNsILLGd9K30lNXrPNlCNiJamyCQvrLhPea5sceh0di88AU9bMETY/aQfpHelTaMxvFUFkydTcHIN9rovQBvZY5sUXIGcGM+bvnU7M7AOmYBDkuEk52ulNYdnm4NmQ2DvW0hMuFYC3tHNm2CZ8OwBJzjgXmPdexrjrLwx89OJ8RSC3MXJuMXVOOXJOM8EDV862Tc2Vf6TXrSQhSVGBe57mysfZZQyg6wsMHXWW2w+bC+1c+AgW6x4KgYvQQOjqOnxvYS+pq0IBpvDekkTcl4NmmMiL+o+j2GiE5TEQ9yqXRONBZDpD9g4XuksiZYC2sa2KixfSNwBpzi38AbpeUWjBUboC3c4xDpO+l6G93TGDdqydjpeCv8y3TfdMB8pjHCILdZ2ZPdGDQMnk52kO6zUeM4m0mfWHAgSkcTZBnWEN/f8ainQZu0ps6X1aSrpM+tnL1yUHY+s3DSucOGS2IO75VSo05k7Fy9ZnMvs+a6urP0o9WRh5feZcH7S9Bw0MGn9T/FIxdjUzdTxtXrNW34eJL+3oW4cTvfujsZ67jGwimC5yhBM0xTnBp1ImN7vaYGx7hhSk1E2qWfaMHj0yiM8XsOrBz94NdxPNkomQOv14elExaMwlHHTw50/vQHpSPiODAqxn3Dpnck86a39HxQMmppvBW5eg1+PChFj3eFpNP9pIesvHDHs8jAysZmM+k0ySq55svP16V6TQN4rdVORwZinU2ZJAfdN1045+6jrXtm2MnCHvPT8UBpek9BliXbpkZ1Y/NM2KA1TedrFpjrth1PtXgotT5nmBSijM0rGQp4W4fzxS9xYpq6dO7/mAXHZX00lpxvqZVE+xb1pUXYWBowGrG9rLuRnWUW9pGfjmeuuHfh78zZ8DGTvU/7G5yWMjjujD7CIgu1I67XwGciNTb2FTZaY/xBiMS26Q2nIGrpCWJj8vk06QcLabO0yV520ozDeRbnw5kwusOm5I6UOY6QHrUQhTlH6wL7940NvwQ5xeoG2Pedkw9jnFZ8jAaQRjDuW8hcNHmLo7FG+ALpgZu7uEEcnUQYRsHop1povtJzMQ/yhc2vK2UT2VScjI319+G8g39R2r6+dAiOc6zR18vpgYjl78fPsbS6HjiHPmuhRq4ocDwMSOnhOXlfwXt8XhXHTslRlv8npBmNs/jHFl5bny69J52bXDMVSO0sgo48NTQslHZMB1tCJthdWiIdbOUj3iSQ/pbb/Ov1tMEwcxb+o4falJMY9gY7oFns0/8CmkzO6TgW0RBHUg6uw8Gp2+NEM1oqNT0rADLS09Ll0q7JXI5dLPxHqY1usA5Hn57ZwlGtbfPY09PT09PT09PTM1v+ARAWOTjYoQuAAAAAAElFTkSuQmCC>
[image73]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAZCAYAAABdEVzWAAACiElEQVR4Xu2WS6iNURiGX6EISUoJKZdkRLml0FHIhISBKMnERAlFbrll4BpGkolkImKiiMGJgTAxMjEQKZkYKGbiffrWstde/n3OppTBfuvprH/9a/3rW99tH6mn/0NDzQQzun5RaZQZXk/+K401d81Zc8tsNUPaVoRmKNZNrl/UmmiOmKvmuGJjk2aZM4p1m83I9tfab26bYWa8eWaemNWKM2aag+aTWZv2dNRC88gsNXPMffPD7FX7bTeY12auIkwnFfvwEiI0jxUHZx0wy0yf2WI2mV3mssL4juLG98x2RW4gbvrCfDXz0twU80bx8axx5qXZmZ7xyDuF17IYrymeyT1C3FUI+dgXhbeyuDVe25OeMag0FOHNm6Zf4cEx5qnaDdtnVqQx60+rixAiquKSeagwMouPY1g+BNfXhqHr5qOZlp7PK4zFCKJxxUxK71aaixokhAOJjSTwd0VuIAzoZFg5P1UR3kMKI7LHCd2d9PevtUhxGJWHRwlTf5obzDCEp/AOqYHnuChG5hDynoqmuqenuUFFhVFZNxRVhnK11QagJsNqYVAOIRflwkfNbHNN4eUBxSZy4oJ+70+dDOg0n1WHcLGiunNO0t92p3GjslH0ndw2uNGqND6lZgMw7IOac6cOISLn6AK50NjHtxtFHtBMsZxx1g6zPo35OMWQSx+NUDRjYFyrDGEWVV4axt9zajnjlzBkm/mmuPn7gs9mSVqXm+6x9Iz42WIP3bwWnqCR0lBLcdHSMCJwovW6pdxg6Vk1ZX9C881bRdPcqGgLNMz6P4QcQiqzFr8gr8xyhVMOq3ndH4sKJe/WKQ5pUp/iwDItSi0wz80DRX7VF+upp5566lY/AbYpfnazqs5IAAAAAElFTkSuQmCC>
[image74]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAaCAYAAADfcP5FAAAB6klEQVR4Xu2VzytmURjHn4lRxJRMJFOYUMpiJAtioSxG09giC0srGxtLEv+ArCaShaTspDRT2Csr06zUmESSbFAzfn6/nnu493ndHyZRup/69L7vec7pveec7zlXJCXlaWmDh/Da5z/4Axb5+vlphn8kOOYE7nnfL+EyrHED/ocpeAU/20IEE/AMNpr2argNf8EPppaIQrgBf8OyYCmUfLgOf8L3wdIts6Kr9dUWklALj+AizDa1MD7CfTgD35iae9i/sClYSkav6GyGbCGCTtEx/bYAukRzNCnJJxiAWTiHLbYQwbjoCnTAUs9yOAwPYA/Muuv9CFx+GMISUwvDbQlPJ7fsm+e0aKbG4DvX+bHE5ScHvjVtUfmpEJ3cJiz2tXO18ny/Q3H5GbQF0T8bhXWmPSo/xJ0wbqejW3QCsfD+CctPleg25Jr2sPuHsO+K6J3W7mvn/4RN4I6o+4fbxFxwBf3E3T+tomH/Ltr3k+jKcIuXYN9910wa4Klk5ocnhg/D10Olr524zM1JMD/MCLfoGG6JnjgHx6zJwxO4hUvp3juUd8auJ7+79nm5f1DOfOeBMXxofl6IrvSAZG4xV9lO4EVh5mLz81wwp6uiB6AefgmWnx9u3wIcEb1a7J32IjDwBbYxJeXVcgNUO28azMORmgAAAABJRU5ErkJggg==>
[image75]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAaCAYAAAC+aNwHAAABC0lEQVR4XmNgGAXowBGInwPxfyT8Coh/AfFfID4JxMFAzAzTgAvMAeLfQGyDJAbSlMYAMagMiBmR5FAALxAfBuK7QCyOJicJxA9xyMGBJhC/BeI1QMyCJmcKxN+A+CoQi6DJwYEfA8Tv6egSQNDAAJErRhNHAZMYMP3PCsTJDBCXlUL5WAEPEB9ggIT6MSj7OgPE1ulALAxTiAtg8z8otCsZIKHvChXDCWD+L0ITNwbirwyQ6MULsPkfBKIZIAa3oomjAHzxDzIYZEA5mjgK0AHi9wyY8Q9ir2JANaAaiF1gCmwZIKkLPf2DwgMGQOkfFIggg2KBeDYQcyLJEwVA3vJlgMQEyZpHwfAGAGlHPJOLUE8QAAAAAElFTkSuQmCC>
[image76]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEMAAAAaCAYAAADsS+FMAAAC6klEQVR4Xu2XS6hOURiGX6HcIlHuuSRldAZERBlIGTCQTBiclEyUMJBTZ6CTqVxLklySSFEuEQPFQEwYSIlCShQDoZDD+55vL//aa1/+vfeJf7Keevv/1tpr77Xe/X3rWxuIRCKRZkyheqnj1F5qbrq7lLHUdthY3UP36gSjqHPUD+p3Iv3/kPx/Tx2lprsBeSyiblPLqS7qOmzwLmqId10eM6kn1BZqBLWaeg67Z6fQGj5Tl6hhXrtMeACbn+adYSR1hdpMDU3aJlAPqa/UgqQtDz3oBLIP3UfdhN27E6yDvcydYQfZjeK+gZB+DXNSjjp6UDIoYQ71DvYAH02mnZFiBjU8bPQYDXsxdTlE/aSWBe2KcqWR1rU16BtAkzlI3UI6152D4UJ9VlL9yF6zBjZ2Y9Aesok6gHxDJlOXUT/dxlB3qafUxHQXZlNvqWdos2/4KOQV+r+oFemuFG7RRWaE7SF6U9uow0gb0tQIMZ/6iGzqyog7sPSf57W3ZTEszFUd8t6aoyh6qpohQkMGY4RYC3v2C1iE3KO+wdJZkez2xUqMgzl4FpazZWg/yVt0HTOEM+QidRXNjRB5+8Us6iXs3pU3db2ZY9R+VBtUtOii9jK0mT6mzqA8Gstw+4UWPindhdPIFolCnBF70Aol5d+qv1dkkft6C+GinRmqKlXQZqZSrPTsRnYPqUrRfjGeegRLFVXAUhSmOmDtSP47VH78BSmFpqJ1zTTqFSw0fTROk9Lk2uGMcKmhe3ejmSFF5wtnko4Qpadj93BtMio7bzx9Qiv3VO910vxOLUnaNLYPdqqTUUILUO6fR/rt5CEjrlELg/YmhmjMEWT3C7EUNm9nhiqLskAn5hTu0CVHQ/lhpXy8gexRViao/QLsKH6Suo9qdVzfMaERDi1uPbUh7AjQgk5RX9Cat75HVJH0zSRcUVCF1NlHc1Rl+Sdoj9FpU5PXb63y9Z9QhMl4zbHOR2gkEolEIpHIoPkDn0mg5WPjjk0AAAAASUVORK5CYII=>
[image77]: <data:image/png;base64,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>
[image78]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAAAZCAYAAAB6v90+AAAC8ElEQVR4Xu2WS8gOURjHH6Eol/TJXSKXZENCCivJJRIWbqWUS6JcClkpWZAslEs2PkksxAaFhduGbFi4pCQSUaxYsOH/+86c5syZOd75XEqZf/1633Nm3nmf/3nO88wxa9Tod9VLDBBd4wuBuNY7nvyXtUTcFXvERTGoeLlDmNottscXUuopVoqT4qAYV7xcS5PEnHgyUxcxTRwRx8QCK2ZluHgoZmTj1eKT2CvGiMFirrgurpnLbEv1FTfEPnM/mCieiKXhTQkR7E7xQHwXu4qXO4Qp7rktRoo2cdbcInbP7pknXolh2XiUOGzu/mVilVhsLpMsYC0RDIH1C+Z40FMxMJirEsYWivnii1UbmyzeW54NROAYIQuI3zEmM4jP01bMzAbrxBbEDKZ4SKgp4rNYFM2nRPApY/utGDSi+KmnU+YySn2F95C5E6JHNp4gzljNLYjGi49WNuYDJag6ShkjsCtWNkaAtyzfKdT0M3MLisjktuw79d9undiCyAeUMhbPp5Qy5g2kjPl5srZD3BFrxWXL643tV3sLelEfFH1s4E8ZI2iCb2XMa7S5mPpkY7J03vItSGbp2issbzyVohv9TWM0nxdWNpAyFop7MOW34FRxPxuvE1vNZbpSKQOp+ZRSxlIGUvOhwi3YTZyzvOapO5rL0GxcEm33nZUN+EA5BdRRyhgBXbCyAW+Mzlh1PIq3oM98+Pwt5v63Uv4P6Fy+taLZ4lv26cWLfIhVpz9lDDFH56UDe/UXj82dRGLFWxD5Wg2fv1lMD8YlcXx5be4tjwicU8g9c2ZQm3gkvlr1w36WYRrCG7E8mJspPlj1s9h+vIxD+feeN0aMh6y4WCXRXY6Lm+aOLZhiNTlaebGKV8VzMSKY3yjemmtAHs54BMEp3YsX8EuxXqwxd2TbZOXsk6V2czUUC1OXzMXLYlFjVfcVxB+MNXcum2UtWukviqzTyoHvsSiFo+aCrhImDphbNM624cI3atSoUaP/Tz8A4G2fJm4Aft4AAAAASUVORK5CYII=>
[image79]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAAZCAYAAABggz2wAAACVklEQVR4Xu2WT4iNURjGH6GIslD+1MilhizYCJnNWFAs2DCzGJKysZD8KUpTZiNhKCLCNM1miNXU1AyKkmxYiVhYKRSxQjbieeY93/i+89253XPuvSvnqV/d7/z7znPf93vPAZKSkv53TSfdpOK116tp5DA54HdQK8k5coPsIXOK3a3XbLKdXCIfyA+ytjCifm2AzT/hte8kd8kq0kYOkedkaX6Qr1mOZklGt5FN5BTijc4jj8gfFI0uJOMwg5kU+SukL9dW0nLykJwl872+RqUNxhjVxo+S8yhHVGu9Je25NkljbnltJWnhNWSMDJBlxe5oxRpVyvaTjSgbVWA+kXek07Up+g/IjmxQPdJHfs+h340oxqg2rQJTgc3zjSoox2EpLQbJCDnm+oKlqCq6T2D/cMwioUb1jiOkyz1XMyqpmp/GP7NfyBbE7XFSi8lVxBkONbqeXCAz3XM1o3r/QViqdsCiKbO/yd7cuCipSF0jTxF2JoYYnUuuo7h+NaOK3CuyxD0ruj2wcW9gVTlYiqbK9mOER1MKMbqavCTvc3yGReu7e94Mq6yX3Zy8tpJvqO9dk9L3eZOMwqpxqMFMtYwqEhXUPsOrRXQI9n360rn6AnaJqCmZafYRow3+JOv8Dmo/LFq3yQyvL5Pmaf7JXJuOkNcoprj2vo8MY+q1JgYpLZWeSlOlayPSnfMOLI2yqig+kou5cbom/oIdFX7GLCDPYAUmm680VuoqEzTnK+klu2FH4X2ySJOnkq5rZ9D8W1Grpf3qz9pFVqD8ZyUlJSUlJbVSfwHrW3na9pLD/gAAAABJRU5ErkJggg==>
[image80]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAAAZCAYAAAB5CNMWAAADcklEQVR4Xu2XSciNURjHH6HIlISEzMkGZUqhjxALQ0iiJBsWSnxFpkxZGEMWEguJZIiNEOWLhanExsbCkCEbC8VO/H897/Gde773Xve7303K+69f9973nHvuOc95pmtWqFCh2tRe9BFd04FEXUTH9OH/pB7imjgoLomVol3JDNdw83kD0oG/LW52qRicPA/qJ3aIU2K3+car1UhxwPy7y0Xn0mHbLK6IDqKXeCjuiznmvztCbBWfxfzsO39dbHqeOCY+iG9iXMkM10RxR0wVY8QN8VM0Wr4HxFosXoqx5iG213wtvAkRVnfNjRG0RUwTDWKFWCbWi+PmBq27qoltjDXXfFM7Ld9YzLkuVpt7H+L2n1j+/FgDxSvzAwf1FE/FuuwznvPW3LuCeM8lBpHLCM+6hx8uf1lcEL2TsUpig3mHD4f5au5VQXgC3rUxepYKI6Vr4onnRZO5p3UTD6zUWJvEzOw98/dbHcOPBSeJe+KE+Y22VuWMhXcSprfNDRfEfIwVHzIVYZO35lnxSQzNPh82NyDnwJNPiv7Z2Cxx1OoQfoTFDPOboZIQHrWqnLHyxMZJyD/MQ7icMEremunzQeahuc3cMMFbCbur2WvNwkgLxCOxXXQvHa5JrTEWXsxcqlu5vEiINVn+mqmxEB6FFxHqeBgXguFC+DFOJaWqDsueVRQbozI8M68OJPF6qVpjUcWoXues8u+HKpe3Zp6xUmGkEH6cm4uhCI0Sp829saKmizdirbXsVdqqaozFpsknR6y63y9nlHLPg9Lwm2xeVUOOo//akL2vqNi7qBr1CEH0J2MFQ9EDhRaCW579e0ZL7bP8NTHWe8vPRWn4IXIYFTkUGL7H2lUr5C0SY1uTO6pkLHJIo/lt8j5ojVgUfe5rpfvgwBSB0AagTuZNLfA+VRx+QewtNhavh6z50qpW2jbE5b01YkPfxYTkOeuvMh/DG95FfBFTsnn0d/wdYc6Q7BmGo3ndlX1G/E1iDtGRCo+h+aQJjcWFxMbiQvc0D7deHGq0uCnOWPOGK4kkfNH80PRMgY/meQmFpjQeD8S9Egd9YZ7Uw18ZNF68Nk8ZS8wjgSYzraIh/KiIqegbn5vnbM5JB5A3ryZhKEost/gviEshty208g1zg7kR4jCPhcc/FrfM81Vq7EKFChUqVKhQoVr0CxiJrgbgPN9JAAAAAElFTkSuQmCC>
[image81]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAbCAYAAAB1NA+iAAABQklEQVR4Xu2TvyuFURjHv0Jx/UrJTVlMMiEZlGym+w8og1FKKQYlw7salCwWMhmUzSpkMVhJKYNSshosBr7f+zzXe86578b4fupT93nOueec5znnBUpKclrpFJ2j7clYX0EuQoN7dIve0N1gbJS+04Ug18Q83aa99Jqewk4kFuknnfa4kBU6Tmdgk8Pd9ukDHfBYC1fy4ZiMvtIRj/vpHT2hLZ7T4sf+u4kMf1igG1b/GW3znMr6oMsei8Mk/mWIvtDNIKcGftFZOgHb+Y2e06VgXp0qfUa+gO7+AnEDx+hVEEeoxg1YD45gtX/Ddm3UrxOF/YjocNULnUZXqvrTKy2sf5g+wo7cBWviAb2ngz5HN3IJe1CTtOb5Ouq2nusq7M9r9AnWuAadsBea0XUk34YC1X/r7sCedYpeYU+aLPkHfgDShjXKLMofvgAAAABJRU5ErkJggg==>
[image82]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAaCAYAAAC6nQw6AAABKElEQVR4Xu3TzStEURjH8UdSGCXS1CxsJikWJFlamKLmb5AtZSUlhSVL5WUtWWApG4lCWSplYWXF3kZZWfD93XPui9vcqZGazf3VZ3Huczovz5kxy5Pnf9OKccyggBYMYhodiXl1040TLGMdT9jBFg5whvZodka08yYm/biEV5xiBO+4Q5evZ6YXGxbvOIoPzKINcxj2tTCd5lpRN1rg01y/akVtuLTsehBd8xAP6EnVwujE9+hLF7TDPubNFZ/NLaZFFb2aarr6Cq7whl30+zlBqvjGNqbwhVVf0yZHGPBjZQ8LiXGUMh5xjHMs4cXcs1+gEk8NrnuDicS3X9Gxixa/RHocZgi3VqM/jUYvqpPrZ7Fo7up/ihp/jTWMpWoNR/9DnShPs/ID3zcnxrOyMrkAAAAASUVORK5CYII=>
[image83]: <data:image/png;base64,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>
[image84]: <data:image/png;base64,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>
[image85]: <data:image/png;base64,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>
[image86]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAaCAYAAABRqrc5AAAA+UlEQVR4Xu2TvQ4BURCFR0KhUPgJ8VNLVAqJSqnR6kRUElGj8hgaiUioVN5CqfIAEhqFQkUlwZnctWZvXHtLxX7Jl3Wd2Ukcu0QBfrTgBT6FV9iXQzaE4AI+YF3LrInDLTzAvDeypwJvcA3DWmZNm1QXAz2w5d3HHda0zJp3H3uY0TJrTH1EYQ/OnCufjXzrg5eNYZHUzUu4ghEx42LqIwuP9FnM2QmW3QmB6fng5VWYcs78APKSkjshMPUh4e/ncOp8dumQ2izflzPsyiGHJpyQT7G/aMAhqULTMOGN/eFO+B/KkSp6BAueCR+ScEfen7uBMTkU8K+8AP1rMFzvt+gmAAAAAElFTkSuQmCC>
[image87]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAaCAYAAABIIVmfAAADG0lEQVR4Xu2YTagOURjHH6F8lq/cJJFEkpAsRLFgIV8bRIqywIakkFKUhZKPsCCJKEWx0q2bhLIkxcJGSvKxsxBKCv//fc65zpx3Zt6Zc82Z+2p+9e/OPWdm3pnnf87znDkiDQ0NnckoaBLUBQ31+urEPtdEaLDX919xCLoPnYOmJbtqZRV0GXooakQpePHvAvoGLTTX1AUNWOs3GjjyNkm9xhyXAAMs16Gf0FKvnS+2E/oIzfb6YuMbMNz8zxnxQeofJMEGjIWeQm9E86vPBKhbAm/+D0kzgDN4OXRUOtiAedAX6A40xLQNgkaaYxpwUbTY1IlvgAv7OtaAraJ5fr/TxnRzStSIMdBGc1wnVRswBVpj/pLxojOsaFCDDbgiyfzPvH8C2tV3xsCgKgM4sPaJzvJt0CvoJHQDOgi9hmb1nZ1NkAE2/3MGsNB+NcffoUXOeWXYAr0roWfQjN4r86nKgJXQEfk7w7kgeS+aBVj7it43yADemD/g5n/+8APR3O/CmTHCa4tJVQbshWaaY9Y9vruNxxJovSQ/sLLiEGSAzf98AYub/102Q9e8tphUZYDLdOiTJOPhkxWH0gYwwLyRv/7nZ75dAbmwVhSpC8NEH6SouqTY1kIMA9ZBP6T1e8glKw6lDWi3/rfMFzWKI+MetD3Z3cJUaEMJcYqP670yn1ADaC73adJM5iDcAV0SXWafh95Ck00/rzsrmnLaxaG0AWn5PwumpUfSWhdi0s6ArIXDadE0e8xrJxx4HIAvRFc6z6HHombQnAOiadqSF4fCBqwQXfHwoaw+Qz2SPRL5EDeltS7ExDeAafKW6LO778J3O+OcxyXmL9Hi6n9MclZw1cMRzfffI5oVOCjvQofNOZa8OBQ2IAROzbS8FxPfgDJwxF6Q9NrGVQ1TDWtX2v8ueXGozADWCm61cnovgFYnu6PRHwMWi35U9Yd2cajMAG563RbNodyuSCtmMQg1gGnnKjTH7yhJuzhUZgDhtBztN0Ym1AAWzmV+YyB5cajUgIHAbtGti5fQXK+vTuxzPRGtHQ0NDQ0NDan8ASO4s5YM63d0AAAAAElFTkSuQmCC>
[image88]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAaCAYAAACO5M0mAAAA5klEQVR4Xu3RMQtBURQH8CMpSpSBDBYpsSqlTCYGZovdJ6AYZZfBbvABZDEYjCYx+AAWJovNgv955z3OvVaDwb9+5d3zf8999xH9VPIwhg4UIWiOJVmYkBTm8ICu0XBTh6H7209yQ+Q9ficHW8jYAzshWMIKotbsIy24wwB81sxJDGawgB3coGQ0kCRsSI6En9IkeeOeLvFgRFL09sUvdYGpV+Kk4UzmefE/HMl6Yo1kP2W1xjefoKHWnOIVCmqNCwdIqDVKuYtV95rPkj9f+9VQqcCeZPNr6ENAF3T428YhbA/++V6edoEhw7l7aiQAAAAASUVORK5CYII=>
[image89]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAZCAYAAABD2GxlAAABq0lEQVR4Xu2WPyhFURzHfy8UUZJSikhKlCgyyCijFIPYDRKDoizkz8Aok4nBMymLiWRmM1gMUspoYRTfb797vXPvO+d0PV6Pup/69O4759x7v/fc8+eKpFiphDNwELbAKdgVaVFiauAV/IAvcBFWmA1KDQNuBr/fphVOxgsNOuAO3Bd9NVXR6kQw2BYcgmOwMVqdTyechZfwHR5Gq78Yh3ewV/QmG/Ac1pqNEsBzs6JjsB6ewoFIixgMyCfhCU9iD9gM7+G0UVYHb+Bc8L8czov2rs1Vsb/WFXgser4XdvWj2AMy2BvsM8oy8Eh0wNtu7KIHnkju1S5Lwmv4Au5KfkDCts+wLVbuYxgeSG5o8Np7og/sxReQZa6AtnIfnFjropNxAV7ApkgLB66A4bplC1JIQFIGGwJ5nAhXwGrRp7QFKTRgQbgCElcQV3lR8AXkwmoLwrZcmhKNoZ/iCzgquohzBoZw0z8L5HHRCQNybcvE6rjiX8M1o6xdtPd8W+OvwF7hjdhD/MKgr/AWdhvt+uEDXIITorvItvyxLxHO6BHRrZHbX0pKSsp/5hMZe1fDzG2zpQAAAABJRU5ErkJggg==>
[image90]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAA+CAYAAACWTEfwAAADMElEQVR4Xu3dwYtVVRwH8CMlFJmYSoMLwQQJkVaSobUQsU1gSLRTdNlCceFGhMBVm9zYoBFSRCsh2oi0bvaBC3ElRAZB/0C5MCp/P+69vPMO6DAzT991+nzgy9xzzuM9Zvfj3Ps7txQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBQ2Rz6PbGsXAACYv5cii5GLkaXIpqlVAADmLgu2W+0kAADjcjZyoJ0EAGAcsli7HfmvXQAAYP7ejxzsr3+M7KjWAACYsxcjP5RJk8F3kVcnywAAzNtC5JdqfK+6BgBgBHKH7evIhn58s1oDAGBEthQH5gIA/0P5PNi/1Th3sR5E3q7mPohsrcbrwf7SNS/k2W4AAKP2WZk+JiMLtr8j7/XjjZGrk+V140TkQjsJADBGn5RJwfZy5Kt+fKyfuxZ5vb9eL16L/Fy6ZgYAgNHLwmwo2I5HzvXjYfcpd6LWm7wd+lfpmhkAAEZvKF5SFmwpC7bsyvy0Hw/y4NpZv3A9f+tZpPZt6W77Di5F9hTHhQAAI7U78kfkSjWXBdz3kX3V3BeRXyPnq7nBC6V788DjMusib63y/8uDeAfv9n8XiyYEAGCEsqD6LXK5msuCJjtHh3PP0vbIT9W49krk4ydk7+Sjo5A7bm3Dwc7I3WYOAGAUcvdrqZm7HznZzGVzQu5APS+y2PyydLuE2ema8gy3P0tXsD0sXRGacv1If+25NgBgdLJAGV6qPnindB2jtbyF+FHkdDM/K7vaiTU6HDlUuuJs6Hh9nDOl22l8s0zvKgIAPFeykHkaL1zP26VZWP3eLqzR0eKtCAAAMzM8RzdLWWC23aEAAKzSrAu2XaVrkMjvfGN6CQCA1ViuYGuPCanTPnOWz97dKN2zeXlbtO0GBQBgFZYr2K4/IVuqz6V8B+qH/XUeCpyNEgAArNFyBdtK5MG++X0pd9fablcAAFYob1v+U7oGgTwj7a3p5RXLW6R3It9ETjVrAACMRB6Eu9BOAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwLP3CJFpc5qUq5aoAAAAAElFTkSuQmCC>
[image91]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAaCAYAAAC+aNwHAAABBUlEQVR4XmNgGAWjADsQB2JfILYDYlY0ObxADYhPAPFyII4A4mogXg3EnMiKcAF9IH4NxJVAzAgVEwXirQwQF+EFIBs2A/ETIFaEioE0TQXiUgaEgSDABcTMSHwwANn+CYh/AfEjBog3uoDYmAFVMz8Q74CKowAXIP4HxOXoEmgAZNFhIBZBlzAF4m8MkJBHByAnczNAvLKLAeLCiUAsi6wI5DSQs1uRBRkgBoNiRBjKnwTE6QhpVGAAxBcYINE2iwFiYD8DxHYQEATifQwQQ3ECUOiKQTF6SGsC8X4GLP4nFkQD8VIGSMrMYoB4myTgCsS7gbgKiA3R5IgGoPAgKW+MAiIBAIAlIgdy/jqwAAAAAElFTkSuQmCC>
[image92]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAZCAYAAADaILXQAAABiElEQVR4Xu2UPyhFURzHf/InL2QgpSzK4k9KYZLBIIpFDMJCoYwG2ZQMBgYpZTFaSCZZvFcWmZkolJQUk0nh++13znPuue+67xr1PvXp3ne+95x3zr2/c0QK/IEa2A9HYTMsDsbJKYK98BKewAnjGbyFXT+PJqMUrsM7CQ/CbBe+wQ4vi4Wdd+Ar7PYyS5vo4NuiK8ybefhprlHUwwd4DWu9LJIm+ASvYJ2XudjBKe/zYgV+metvNMBHSTB4JczAD9gTjEIw53PnsMrLcpJkqUuSe4VlogURwg4eN5tqeAFfYKvTXgL34bDTlsWGLDGWGmewKrphNsxvliZz1rk7wxl4BJ/hHmx3sizcFLbzOBwz7VNwAd7DTZgy7S5zcMtv9OGOvBH9YGuiW/5YdAV9opuGZ4s7c676AI44bZGwY6foQcV3OCv6R5Yhk1u4kdKih1pipuE7XBRd+qEEXw1L81S0ECZho5PFMihaepTfxD9zWDUZuAwHglE8FaIdOesWL7OUGwv8d74BEjhGFyrh+tEAAAAASUVORK5CYII=>
[image93]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAADGklEQVR4Xu3cO6gdVRQG4CU+8IkEJShBjGhjZxAFwSoQwUIJsbSwVMTKUizEXhBBC1EkhYVgH1J5wCKitjaijYiFkE5txMf+ndl3xnHuDXrPibf4PliceXL2nOpn7T2nCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4Oh4otU1y4MAAOzOw61+bnXd7Nhnrd6a7cejrb4dt79udWJ2DgCAHXqv1a/j9g2trm/1fKun9q4Y/NLqsXH7j/rneQCAI+dkq7Otrl0c35Y7Wt0z20+Q2oUvauqcvV5Dx+10q/v3rqi6vYauWz4j4e2R6TQAwNHz02z7o1a3zPaXHmj1ZavvDqilm2q4r3e1jtUQrHaxdizdsjxPPj+s9e94udWPNYz1hxrGkjHNHV/sAwD8bxJM0m1KsEmoemY8vhZ0/qt01tJR612tB1tdHs/dXOtdvVx/9z61dn1kzJkOfbyGazIVOj/Xna9hDJFr+/q23JPxRL4nct9BAbZ7stVDy4MAANuwtr4r0oVak1CTkLcMUfNa83QN4SiyzuyrcfvTVneO24eV6c+Pa3rhYB7s+vPc2mpTU4BLiOzTsxlXD3n9OXLf8oWFNZta/x0BAA7tvlbPjtsJMW/XMIX5yd4V25Hg00PQbzUFo3kX7DAy9g9qCoVzCYTz53mphlB3V00dxcg1PTz2seZY78YdZFMCGwCwQ9+0er+GtWm923RxOr0V97a6UMMaufztRrphWTe2jcX+mbLs69YSBr//++m/Qtz8eTItm05cnnc+VTpf89YDW+67bdx+d1HvjMdjUwIbAHAVpft0rtVzyxOH8Pv4earVC+N2OnsJiC+O+7uScHal5+nhMeOLBLb+O8zfbt3PpgQ2AOAqSpfpjZr+9mIb8ibmyVaf19TFO9PqlZpC0q68WVd+nkwDv1bTWrcEtv47vDoe209eOEjX8FKrGxfnAADYkf1engAA4IgQ2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgX/gTCZdfb57BDUgAAAAASUVORK5CYII=>
[image94]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAAxCAYAAABnGvUlAAAEL0lEQVR4Xu3cS6h9UxwH8CWPvOURKQYeEwNJov4yEkoykYlHJgbEUBJR/5SBMkCZSGEg5DVAecUtA2GKgUchUSYmDFBYv//aq/M7+9577jn3npP+//P51K+919r73zn3/CfffmvtXQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAq/NdrY+H40atr6auAgDwvzonnb89HK9IcwAAK3NNrRtrHT6+wJQTh+MRtR4Zzk8fjgAAS3dkrUdrXZrmnq71WxqvyvVlEnjCRbV+SOPs2rL53g/SeBX+qPVqGp9V68k0jq7auWm8DGeW9hvEMfuwCIUAsLb+qXXnaK6HhtNG88t0XGmBqweeY4fjw8NxLJYeczg6obTvvkp/l8lS51HDMUJmOLpMlkPn1f/tLNG1i6B4SZq7I50DAGvm/FpflM2dm+26PMsU4euX0oJbhJ+nhvlYkt3K96Xd1+2r9V4aL9vJpT1QcMYw7kHyvFrH13qh1p+1nhjm5zFPYAv/lul7X0/nAMAaidCxUbbeLB9z0V2KLtYsD9X6cUadMrl1k1jejGDy63B8YPryJnHPRq1PSwuZF05dXb5bav1e6+fSPjsC2l4tEtj673F1aV03AGANzeqi3VdaaOj6cuAybZTWYQuxT+yqyaUDXbcsOmv93hBLho+ncRZ73Zbh2TIJs4+V3S1Lxv7A+H173TYab/dwR/z2z9c6tdaLaX67+wGAQ1TsT/uyTJb8stgbds9wHtc/Sdey6NLlADKuWQEjQkl/iCA6eYela3ljf4gwlx84iPD2eRqfVCZ7vm4ejtGV2qmj9Watt0r7O8YiFPbOVny//Lf0/XaL2un7dD+V9vc9V+uYNP9MOgcA1sTFpT0RetMwvru0vWI5JERnaRyg9ioCUoSurZ6wjCB5QRrHd3mnTN/bl0f31bq3tL1ssZfs7NIC2+213iitS7aTCFHjwBbhMZaEtxJPp8YLc3dj3sC2UdrfmENs/CYfpTEAsEa+KS2cxJ6tb2tdWVo3KZbzQixX3jCcL0OEnb9KCyRxvHz68oFlyL53LpYE+72xn6x3ub4ubTn3lWGcA2XvsM27hDkObC+V9lnxmdHp6mE2xNJshMPYnxfhcFHzBrZYDv1sNBf/P8vYRwcAHORiGTCWQl8rky7bu6UFqPx2/1WJJcgIiIuEoXHnKQJbdOnm7UaNA9tO4n1o+Z11i8idw0X1z71ufAEAWC/RVYq6LM1t1Lo/jVcplgDjYYIHxxdm6J2nu0rbyxaBLbp0ETRvTfdtZ9HAFkFwle+n287LtfaXSecTAOCgkp8q7Uui+Z1ty/R+2fkVJAAAzNAD26pEONTlAgDYg92+cgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgEPHf6Rklz6kTtbOAAAAAElFTkSuQmCC>
[image95]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAaCAYAAAAe97TpAAACC0lEQVR4Xu2WzytmURjHH6H8TCKy8AcoNSQ7lEJpMpvZiGShZsqSEDY2/gAWFEopC7KbzTRqTCnyB8jKgo1SsmJhEt9vzz113ue9bve+oUvvtz713ue59/Z+z7nne45IXnnlFaVucAWePK7BA3gEJ+A7KHQPpFkb4D/o8Gr84z9EzUyDAq+XOlWCQ3AO6k2vAVy80EuVmsAN2ANFptcO7sEpqDW9VOmb6Fr4aRvQgmhv0tRTp2XJXg/FYEx0hqaC69SqAvwTTaOj4PeZ6Oivghp3Y4T6QJ0tvqfC1gNTaFY0lXqDmi+mVpl3PSQaAL7YzyWW+Uw/aLGNKLn1MGHqbeBONHqtBsGmd21NVIHfou9IojnwSzQNB0wvUmHrgRoWNbdo6hSN+SFgTXwRjexc0sx93rFNRO0PNEcTM16NU8wZ4A7PERsN6s5EiWgI/AGXYAk0BvfEVWITzeBWsvcH/t6VTBPzoEd0DR1I5ijbmeAAhMV1HMU20Sn63dnzEteHE89LXNg0MwLWQanoZ7YtmUcQ30Q1+Cu6STp1gbUXWBEdGKfYJuKKnxhfxoSiASpslH0TYTOVRK9uwsof5VbwNaj7JtxMcXMcF02qJHpzE5yNHdGjCCPZ7eK+Cc7avmhc0mgScX/YEo32Y9GQYFi8urgZMdV82YVdLik/poTJmviQ+hQmcj0n5fXh9QwzAmmOAnjltAAAAABJRU5ErkJggg==>
[image96]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAAA9CAYAAAAQ2DVeAAADE0lEQVR4Xu3dP6iWVRwH8JOVlFEtVy1MxXAJJQXNNotwilzCIcitrbmMW4ItQbiIRSjtcge3WqSgIBouujppDkmLCAkOLWH2+/E8J8/7dBXh3vcPvp8PfHnPn+fCHQ/Pec75lQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzJTDkYPDQQAAZsNy5L3IZ5GnB3MAAExRLs6+j+yIbI58E3msn1tfHwIAYHr2RG5HrkdORvY1c0tNGwCAKXkt8tdg7PHIB5EbkVcHcwAATMHeyPnSfcd2qh97IvLuf08AAMypu33uRP7of8+OPDE5m0r3Zq1aiLzS9AEA5s6WyK6mfy1ypelPW/5vi8NBAIB5cqhpb4/cjOxvxmbBU8MBAIB5lNdq5FYoAAAz6Fjkn9Lde5Yf+efH/1X7PRkAAFOQi7R8s/Zt33858nXffr6M3oe2WvVww7gCAPDIORC5FXmhGfs1sjvyceSHyOlmrnoycuQBef3eowAArEaeBm1PhL4RuVDu1fD8qpkDAGDG5N1nPw8HJyC3Yd9cIR9FTkTeWmGuBgBgrrwfORf5cDgxZkcH/fzG7mLp/p/Mb6UrYwUAQHhmODBmuTj7tOmfifzZ9FN+X5ff3eWzAABMWB5WyAMNaWfkculKVbVejPze/wIAMEG5UMtTqdXnfYZeKl29Uws2AIAxyvqg2wdj30U29u0HvUX7pIzeubahaQMAsAaei1yNLA3GcyFW1QXbs81YtVy6eqcpT5TmNSQAAKyxw5G/I+v6fhagb9+UZXmsXNDlAYPcKs2TofmbF/1mRYZsZ0H43EK9Htna/RkAAGvp7cjx0pW++nIwV+WbuFzY1es88g62rG1aDyb8VFzxAQAwNrmdeal0W5r3q1eaC7P9pXsj90U/9k4/lvJi34W+DQDAGPwS2TYcvI9a4D23Rasfy+i9bQAArLEsNfWwFsv/a5vmxb51exQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAm7F/WaG24lFAeOgAAAABJRU5ErkJggg==>
[image97]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAaCAYAAABByvnlAAADZElEQVR4Xu2YW6hMURjHP7lEboVc8mBIJJfkGhHJEYpELuW8SV5EPJBLefIgpVweJMKDKOTBA0qcUDzIk1JKJJSEJ/Li8v+dtbezZp2ZObNn9pzp1PrVr5lZa+/Zs9e31re/NWaRSCQSieTHFvnX87f8kMh72s7+P7pn01tukoWgvRxj5E25OnkfOig5brrcKEfKXkn7Yrk16a8aTr4o38sWcz84ZZ78Lu/LoV57T2OAXCNPyo/yh5xddER5OI7j/QnruyM5LpzUyJjOTPqrZqx8KqcG7VPkG/lajgv68qavHBg25ggBWSWXyiOWLSAE8rk8F9hmxROV4xgvfCZ3yyFJXyaWy0NBGwEgEF/knKAvT/jBh83dwMKgr1Hst2wB2WNujHwIwnUrnsQEhO+umxXmVkMKFyPypCpSViMg956RD80Fwk+TjSZrQGbJYd5nUvwxucFrg9wC4sPSPm/uQR5eMA/Gywvyjpxh7ua6m6wBCVlrrsAhzfoQkMvyrrm09VZutzomGxcgN/Iw2mf5DtZkc0sced9M6gnIcPlALgg7zAWEvnQ1TTRXpdY0lpzAiX+S1/QL+pirEvzZ0C/4XA6+g1XAamBVsDrqheuOss6lZykpP0vNznoCQiX1Uo4IO0T/xBTu/4q5oGS6d07cbC5NsUL8wZ4gT5sLDPB61dxs6AoGhGfEJXMDlAdMjrDaKSclbqH9rGJqDQjpnHR0wzrGoytIYWQcKryq4VlBMErtNXZaR50NzE5KZL8IqIS/SvhxzU5XUGtAuOev5u4jZJK5/c1tc4FLSQNSzQRuJ934PZKjgz5K3ydyWvJ5m7wlP5vbSDLQWSAYzK5mPtChUkBIcQUrTj0pzHIG92jYYR2bRz8gacoqd61OpHsN9Dd+pKwl8pW5JepHnNVyyvtcC37JO9+6PzAE5KecG3aYm3QMOmk5TEvcO32cH0JmYfALXhvvqbbCx0BJ2BUzU7lAJUlZKfxAZvh6r60eqFiOy8dymZV+AOcF93tNfrPi+/skT3jHkVp+WenKaK+VDwiwgX4hD8pd8p25a4aPgdygsmBWV/v8qBYGi78Z8gp0o+B3tljlv0M4ho32OnMrJAxqriyS9+Rg2WoZS7lI/vC/TZs8IFcWd0WaRbj5iUQikUgkEomk/AMxCLPISkvnFwAAAABJRU5ErkJggg==>
[image98]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAaCAYAAAAnkAWyAAACHklEQVR4Xu2WwStlcRTHz8TUGDTDU1IsCDUshmTBVoqkJkMpC1GynVEYZSFS1ExTZDMNVpqmbKQGTSH+AStZSoyVhbUF39OZ6537e+++d3lHz+J96lOv837v97739zv33h9RhufBa5jlFtMIZ+FMoZiBJW4xAWXwA2ygp7lonnfRLQYRNnwOnILfYTdcgH/gGzXGAvPwL+A8/Khq/JtT2K5qFpiHb4WTJBfhUQrPYaeqWWAaPht+heVOvQ1ewFqnniqm4StIJnsJI7AYFsG/cIz8u2GBaXju6SE4B2+VKyS7Yo1p+AnYSLLyvOq8+lXwBDapcVaYhc8lmYjbRJMH9+GsqvHYQfhW1eKR7CVkFp77/RvF9nU+PIQ/SXbiF9yExxQ8l0cvXHWLCrPwXr+7vINXcFjV+E+PKHguD75g/TsXs/DcFl1uEYzDf7BS1ZKFryNZ8UuSXer3f32PSXju4R24THKzejSTvJzci0oWnuEd26PYe0hjEp77fYlklffhD7gBD2BNdNg9YcL3wTWKvYc0JuF1v/PThce8in4dQ5jwfJhL1O+MSfgJkud7WOKFr4afSE6kBXCXZM562KHGaVIOH/R8jwfvxijcgjdwnaInzQF4TRKIL+A3ybF6hPz3kSbl8Pyi6aHEvRmWFvj+/2d+QfE7IhEPCj8Nz5TbsNA34vHw6n4muWeCiJAc8nSGL74RaYLP/HwOypAhndwBx9VgxjDe1ewAAAAASUVORK5CYII=>
[image99]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFQAAAAaCAYAAAApOXvdAAADQUlEQVR4Xu2YWahNURzGPxkic2SIcklJ5ohISIhkCEWGF4qSUgoZSp7lBQ9IiZKUeKBQ4oaMZcqUKORFHpR48GD4vv57uXuvs4+zp/CwfvXr3rPXPufs9a31X3vtAwQCgUCgSpbRnzG/0/eR+l/HDv4++/+gCXbdWehEj9IVtD/t69ktOq+JLo/OaU3b01F0bfR/JlrBvuwdnQn7IMd4+olepl1jx/8VQ+l6egU20MeSzXVRaG+RnDRx90XnTaLfvLavdG7Unol+9BYd5h3Xxb+mL+kAry0LGpjO/sGS6JoWwjqu6ska6Fh6nx72PEefoqV/Ou8ZfUEf0t2wwcjFDLrDO6YvUJAf6TivLSsd6F56no6EVUJVuBmXNdB5dJ13rC1sGVNVOhTo/tjrQsyCjbxDpa0SV6mr5MvSg+6h1+l0JJeUouQNVP1TJcZRwJuRHOhKAo2jWXUEtj4t9trK0oXupLfpApQLNm+gPmPoGdTeFxToWXqcvoLdV3bBcsmNSkDrihbiLai2RON0pBtha9VqFLvYMoGqn6eQvkNQoHfo4Oi1qusuLBe9LzMKTyH+iP66MNvQ0Uh+WDvvdVEUpAItEmyZQCfSN0gucw71SwMeZzvszq/3ZULhLYWVuT8Sg2BrioIV+nsStshXgb5rFay8dPfOStFA1dcD9B7t7rXVYyusajf5DfXQWqkw0/aaG5C8O/aGbbHSRjcPbnZqa5J3doqigfaEbZOaYZv9OCpvBf2E9oodd4Hqb0Pcxv0a7eO1aet0gw6PXq+BLdgfYA8C2grlxa2fujFpDSu6dDQKVAOvgHz01POZnkZL1TncZ/qBquQV6PzYsVTcXtPfuKuTU+lzehHJ2aPZ6p4q8lD11sl1/gRqb55DYIOujf9Ar017bt0n0gZCAWtfOiF2TBWryZZWvQk0Uy6g9hHMVyXv0BdqZBfFjjVCHdeadRV2oX7n86JA4r8vyC/0MR0RnaPn8EdID2EO7D1pgQpNLA26Bl8V+YDehH1m5Wj9UTBZ109dxCFU/5RUBlXfNPz5cVLnTKFLYH0tW011mUwvwZ7PV6K2nAI50Y8nzXQbnZ1sChRFvwlm/l0wEAgEAoHAX+YXb9KrtouHDd4AAAAASUVORK5CYII=>
[image100]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGcAAAAaCAYAAACq/ULmAAAC20lEQVR4Xu2YS+gNURzHf5LyyiOvFAsWZOMRthZCWXgU5bmyURaUkjx2/5QsiEIpCwtZWFggolA2SsnGo2QjZSEbYWGB76cz0517ZubO487ce9P51Cc5Z/73N3N+5/zOmTELBAKBQCDwPzFJ7pXX5Dm5tLs70BTj5Tb5Qu7y+rKYLh/LMTlVrpRv5Y7kRYH+mCB3y1fyiJzS3Z3LcflSzky07ZPv5LxEW6AGJIFkMNsPmCtRZSEhJOaG175W/pBbvfZCyOYWuc7cbBkEw4hZxDR52lz52m717muZ/Gbp5KyWP+UZrz2XJeZu5Ja55XtK3rZqM6Uqw4hZxHx5Xj6X683tMXWJk5CXHL89kxXyqzwhx0Vtc+R9a68uDiNmEQvkB3lRTvb66kA1+GvpJJRODrP0rvwsF0VtDM5lecw6AwfccNFMWiM/ysN+R4KmY2bBb1DbN/kdBdTd+LPYbH0mhxn8Xf6Wn8yVGc7i/EBykDgSPozae3HU3A3dkRO9vpimY/rwTnFPvjF3WqpD8sjM3sMeVJW8JOS1p9gg/1jxQzCg1OHZfocHq4KNvdfDNB0zDx6+KEYRJIm9h/tgL2JPKsti+cXSSYiTc9JrT8HS/2WuPvpQUljWlJpH5mY59Xhh8qIaDCpmE8mJYUUvlw/kdeuU417w0vnM3D6arCJMTqoG//aE0sHS9Y91DCCnqFnR/y/Jg53uvhhUzCaTk4TPL5wq9/gdGew3N8HiZJJkvhbw/IxDIXxSeG0uIN9/+MML1tkMeZl6Ym7wmqJOTMol12Z5xdx7RZK2klMFDhhX5VNz70skhr2Q5y8NtXVupH864qH58bq1P4+2Y45CcoDVwjvdTmvhRZtvQTfN/eghK7kc+6SJmFWTwyBSUtn0y5h3Gh0oG819WeV0scrra4t+YlIy2LQ5Kb2XZ+WMriuy4Rqu9UtmnqyCkYC9oNHlWIJhxAwEAoFAIBAYHf4BFVGbfoRekUEAAAAASUVORK5CYII=>
[image101]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFUAAAAaCAYAAADG+xDjAAADx0lEQVR4Xu2YW6gOURTH/0K5yz1FDpFIIbdXCZFcQhEPwgNJFEVETuEBb3hCJFHy4EGkiBMvQi4PKJdEIgovSMll/Vuz+/bMt/eePc58xzn6fvXP+fbMrJm1Zq211wDq1GlrdBe1zy6WSK3tt0p2iwZmF0uk1vZbJSGnh4reiH5b+iB6LfqS/H4q6m8ucBCy3xLw3jtFR0SbRYPTh4OMFO2HXrtM1Dl92E/I6XmiH6Jtoq7Wek/RVdFn0WRr3UXIvs1K0XXRNJTXLuaKLonGivpBn+W7aKF9kodFoseicaJu0GuvQH3Pxed0O9Fx0drMOoN7SvQTeuM8fPZd9BDtEN0SLRB1TB8uRCfRReiLH5+sDRO9Ez0S9U3WXDCbn4mWW2u9RHdE6601Lz6neVMG1X4zdJKlwLJnKTHwefjsh+CL2yh6IFqFAmVnwaCeh778qcnaIGg7eyEakKy5YDC/iiZYa/T1tKgJmrlBfE43QLPFQKNbRL+Sf2MCSnz2Y+BLXCq6Bw2y3YJiYGD7oPKss6DPf0zUwZzk4CCqg0pOQjOdGR8kxmk+1BLoW2emFinLGPt5sMfOh7YFtge2iaIwS29AS5h/h2DwfEF1rVcR4zR7JwPKzSmqUVvE2I+FwaRjDG7vzDEfnExuQsueGc9NK1RlLO0muINXWlC5u7PZZwM6Aulmz2zqYv025NmPwWxgd6HtoEil2HCy+CZqhN8GWwx9dQWvlKAOh86h1BBrnf3oENK9hc6esH4bQvbzYD88AM20MkYtft3RFqtuRuaYjS94vvUqfE4ziAymaxYdA70BNwIDm/8a67fBZz8Ezz8MnVunIFyuPlhVjdCd3L6ez83pZau1lmUv3MHjtWwjeT3Z6bQ93E/PHDNjlZnXOBwzQ7krXhCtSNYNLvs++AVzLtFo/F0wDRz8GbxXqNzf9Euu2wnA8YpVYeBHD7PZ9t3MvZSdTE6yTpugZYd7OtggOiP6CM1WwyhoVrkG6qx9HxyZjkI/jcuAWfZJtAeV/sl29lb0BJVs44t8D81Ac28G+DY00w28luewzeWSdXoD0t/6Ll1GeiBniXEwdmVW1n5LwWdZB/162g59xvui59DqMjC4D1G9EU8UvYTO5Iuho9g++De4FGU4zWHZ1U9JGfabAwM1ExoYZm+RzY6TAK/lR1CR/4hpttP8Jr4mmgT9xp6TPhxtn32K58WI5emqilZDrNM+2AbOQvvPJlSXR6z92dBeHqNdiPj+/pfEOh2CJcUZ0EUZ9tscq5EeJ8qm1vbr1KlTp85/yB/EOr+AzEVh0wAAAABJRU5ErkJggg==>
[image102]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGYAAAAaCAYAAABFPynYAAAEn0lEQVR4Xu2YWahVVRjH/5FBpTSjjXSLSJLAIAcEUZAseiiCBiIThB4qCQXDgkoQQtQmRItoIioaSXtooFDo0ouioCZmkAYqUhTYQ5RQ0fD9+vbHXWedvc/Z+9zyPrh/8EfvWvusvdb6prW21NLS0tJyAnKKaZLp1Kx9gmmi6eSsfdScZrrL9KLpCdPkzu7aXGp6Rz7J483pprdMf5r+LsT/t5jOLZ65wnQk67++6OsFY7MufvOX6bqifYrpQNH+lem8ov0/4UzTZtPjcstfY9pnujV9qAZ406umQ6YLsr7jyVTTz6aP1e3ZePQjpk1yI53U2d2XVXLDXpy0nW3aYXpTzcfrycPygXlBsMD0tTxs63K76Q+NvWGYOx7MulLGm14wLdFgKQcjY+zc4FeZjpruTdpGTVj7tax9uukX081ZexVD8jT4keob5hJ5lFXBRkYKasJ6daYbGDJtlK9rUC43fS+PmhQcAYecnbWPirB2bphrTb+qexJlsLlPm2bIx6lrmLtN61RunPNNH8jHbEI42kHTRUXbPNPb8jH7wVymmW6RO04Khv69+DcFR8jrCxHJHs5R9/ooHXlbF2GAKsPk7WVQi5bJ82sTw/D8A6YN6pzooEaBcLT35QX7Ibk335k+VALvX27aL091PP+5aX7yDE5KxBA5QTgC7xtXtDEWDkct+0LutAGHqh/Ufz66SZ6PcwPUNQynsJfkXgBNDAO5cUZjFIj68qT8IPKcPK19Kj95lsGGPis3ylDSzrxwOGhSXzDmY6YzTMOmdzVS05jfMdVIqTdqcMOwkU+ZZiZtTQ0DYZz3TB9qcKMAaYX17JFvGg6zzfSbaVbyXAobybF5ZfE367rB9Jnc8aBJfblffjLkfRghjY6ytFdKlQGq2lM4GEQKCwYxDJDPd5teV438W0GkFd6f1geMjrGIinSuwcvy/h9N38o9/B65xwdN6kuwUn60vqz4u9GxOjwhN0AYhjxZxTOmw5mYPIv8Tn4h42TVD+4EpBoib5G6a05d4v6S5ntgY9igdJOCSFH9nKlufQm4Dw6rsy/mV+tYHQPkubPMQ0gLF6q3tZtGTBgl0hdjL9Jgxon6EnUhYEyihT6iJ+/Dg4fle5FCXeAAkdaXs0xr5BGS1heK+qP+s39h/exDepcqS3s94diKt4c3MVm+ApCbo6hzn/hSvXN1LBLPTG/GVfAM9x6OpymDGIffUOyrFk4bfdvVfTeiBhDhaTRwUFhtWii/ZJPiiJqr5QcKooD6zH4w9mJ11pL4TRiGfdyi6rRXCot/Xn485PyOURiATzMB3vSJ6RuNFMSU+0w/aeQbFNHWL5WtULdRAjb6NtMdeUcG4/MeLsPpu4nCc4pnmFuk2Ojn3QHrX2vaJb8kU2NwyrlFP3PhyHtA7kgR3XwP3Cnftzc04sTAbx6UO+kr8pTHu3GeXhmnCx6+Ur4ZZZeiEwGcjxSUpzRgf4i0vI99wkD5fpH+EM8TPWQZ6kvf+0vL/wcpmu+MpC4imrRHRtqrsfnq3lLA6YvbPQcNjLJUXgLS0tAyBpDWqC9bC1G/0jtRS0tLS0tLS0sn/wBsBAoxsdJYKwAAAABJRU5ErkJggg==>
[image103]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAZCAYAAADe1WXtAAABKElEQVR4Xu3Tvy9DURjG8UeKtEHEj0okEptEEJPJYNJNYpFI/AFiazqaLBKTgYQQA4ONychmw2oxMxgsNoPwfb0ncXoj2rTHIn2Sz3DPe+7tue99K7XyS7pQwhjaMrWGMoprbOEJ81GtGycYjtbqyob8oZv4wDI6sYOFsD6HY/R/3VEjfbjFEQYxjVyoWUsqeMY2BsJ6zYzjRX5znHas4QIHOJefuBBvyiYv79MK3uWvbNfWQ4t9rAkU5T0dwRQ6Qv3HzOIQd3jDabieiTfJT7yk7x+rK3YK66n1Nkl65F/9TH6iJLE+PWI9W2gmq3iVj1Gy2HDfy+czSWywr/RH/bQWNBUb6DIusSh/6GTVjgYyhAf5XO5hXwle3U5q/+cb7KK3utzKv80nN6MqzQa3w14AAAAASUVORK5CYII=>
[image104]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmwAAABCCAYAAADqrIpKAAAEoklEQVR4Xu3dS8imYxgH8FtDkfMhUmqQZBYWcqjJcWFBMmkWypaEkkRDpEzJwsKGJiViFCJlIWVh8ZWllYWsLIiUshELyeH+97xP7zP3vOfDl8PvV1c9z/W8p+9bXV33qRQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1nN3jcvb5JIO1DihTQIAsL7Ha5zUJleQz3irTQIAsJ69peuMbdIXbQIAgNWcWOOVspnu2tD7xdAoAMBGfFTj3TY58EuNa0fXl9S4d/BslhSAKdo2XQgCAPzv/FzjtjY58HWNC0bXt5dx8baIfPb+NgkAwOLSMTvcJgf21XhsdH1ajZ3SDaEu6qUa37VJAAAWk/llb5Rx92ySFFwXja6zKOG3GreM7vfUeHMU01xa44diLhsAwErOq/Flmd0x2yld0RWflq5b9nzp5qUdKV0hNmuI9NTSve/s9gEAwLZk+4vPanw/un+6xjM1bq5xWf+iDUghlO7WHaXrZM1yZo0r2+QCDtb4q0020lE7t8b5o/uTS/d7nqzxYf+iOR6ocWebBADYlsOlK9j6Qiedpm9qvN2/YANSrD1Ruvll+dxXj318nKdqXN0mF5Bi8/c22ciCg0lShB0dXadgnCULFVLgAQDsip/KeBJ+5Binl2tcU7rhv03IkGOKwDi9dAXitDlgKZZS0K1SsO2U8fdMkrlt97XJgfymdNzmyZBqhkU39f8BAJjo+tIVRhkiTNdr1rytde2UcSHVr8y8sH84cF2NF0vXvVqlYMt37LTJLchvH24NAgCwVZ+X5SbQ31Pj2xkxad5bCqlFCrYUkBeXf37Blr8h3zXpbwAA2Khsb5F5X9u2U+YXbFeV8QkC0wq218rsRQXzCra8d5mYRsEGAOyaG8ryqx0zxyuFyrSYdGzTB+X4gi1z2YYeLOMuXY6O+rF0c92GzqhxU5MbmlewbYqCDQDYNdlmI3uXbVuGSfvTAW4sXTHWm9TJStdvlTl12YMtQ7zblkItxWRbdAIAbFSGQ3f7iKV087IH2rakk5dTCFaRIdlHS7cYI12+WTJcm4Ua01a6AgCsLYXJXWX3C7Ztyxy3X9vkgjJvLis/c0pCCr9ZMpSc7iQAwNY8VLq91uZtEPtvc2uNP9vkktI9m7a5bi9DtinaAABYUn8w+zwZyszQbL9Qoj+mKt6psX9w3+o7cP0B8gAALOlwjX1tciBHZN1fuoLrjxqPlK54y+rWZ0tXwGUD32nSgVt12BUAgNIN86YDlk5YK1uSfDK4z4rSo4P7RXxcjv0MAABWkPNRJ3XZ0j0bbhSc1+XQ92XkPdkzDgCANWTF51dtsjqlxsOj6wx7Hhw8W0TODlWsAQBsyIHSnUva2lPjrDZZHalxqHQb9maO2yTzVo8CALCEzGHLYfKLyukL6Z5lL7dJK0CzKOG5NgkAwHr2ltkrPof6xQdZtHDF8EHpPsNCAwCALcmK0RRu87xeug7bCzXOaZ69VyZ33QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP+evwFrpKLKMGxX5QAAAABJRU5ErkJggg==>
[image105]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAaCAYAAAC3g3x9AAABIUlEQVR4Xu3UL0hDURTH8SMaBF1SFJNgk2kQjRajxaDFNrCsGxVEEFkYGiwmoyhi1GZQMNkstmWTmA3++f68u3i47wl3beH94MP2znvj3HN3N7MqVfov05hPaoOoJbWsjOIe5xhw9X08Y8zVsjKDV2y72gjurNgkK2v4wLKrxSZNV8vOCTqYdLWyJlmJ+3eNIVc/tGKTrMziDTuu5pvofRsT3XtT2MUNGlayvxrt28KKYjbwaaHJHI4sfHAJDxaOmK4PrGQC7d8X3tHCJfZwhidcYAHDuMUVNnGMLUtWmI6mcfSq6EGdv3itey9Y6V6XJu6fP3//ZRyPWHS1uv01/E2vR2MdpxZG1r6uWjKyVtbrT0t7qecL366iov4AqvRJfgB2BS5djbtgLgAAAABJRU5ErkJggg==>
[image106]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAaCAYAAABYQRdDAAABcUlEQVR4Xu2VPyhGURjGH6GIEPKnRNkUpUQpNotBKYuSWcmMZBelZCOLgQzKKhm+waikDAaLks1isEg8T++5fd8997u5f77B4Fe/7vedc+97zn3Pec8F/gjVdI4e0g3aFO7OxhKdgAXfpDe0LXRHShppgR65//30lU4HN2RliPa53wOwoFPF7vys0SvYG1SEMXpKW/2OrAzTLdoAC9oR7o5SSztpt2eL61c+d2iva1+ko64vgkbdo1/0u4zXsK1z4bW/0B6UoRn20B2dhM3mhL7RcdiMUi1GFWyGDwjnZoS+I+Me1F7TjFa8dlXNJ53x2hOhhz4QTbbKUYNp0NQo6DMsbwH19BJWjjUl7YkZpI/uKpTjVXqPYjmmRkGW6S3sOCvQA8RXit5inp7TXdjOiaUOlgJd41AA7VOdp2IWGXdHKVq8J1gV6TD5daZJOKbbsJRVjH3YDAO6kGMxAxTgjC7Agq/DFi43+ja1w06zf6L8APW2OQRECmXvAAAAAElFTkSuQmCC>
[image107]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFUAAAAaCAYAAADG+xDjAAADtElEQVR4Xu2YW6hNURSGhw7lfsktRc4RScklt/IkISWXUMSDBw8kURQRdQoPeMMTIokS8eBaxIkXUeQB5ZJIROEFKbn8f2PN9lxzz7n2XGevrbO1v/pz1lh7jbXGWGOOORaRBg3qjV5Qk2sskFr775Dsgoa4xgKptf8OSVbQLdBb6I+lj9Ab6Gty/AwaZC7wkOX/X8B774QOQ5uhYenTmYyG9oleuwLqlj4dJivoBdBPaBvUw7L3gW5AX6Cplt1Hlv9aMx+6Ao2HBoo+yw9osf2jAEugJ9AEqKfotddFY69IKOhO0DForWNnck9Cv0RvXImQfxf67eIaq6ArdFn0xU9MbCOg99BjaEBi88Fqfg6ttGz9oPvQessWJBQ0b8qk2m+GQXMpcNlzKTHxlQj5d5kO3YE2SnpVtBcm9YLoy5+R2IaKtrOX0ODE5oPJ/AZNsmyM9RTUJlq5mYSCboYWWcd0ugX6nfwbk1AS8u+jCZopmtz9UP/06dwwsfRhnnWu6PMfhTqbH3k4IOVJJSdEK50Vn0lM0HyoZaJvnZWaZ5nG+Hfh/aZBt6BDkv96H6zS26JLmH9nweSFkuqzlxETNHsnE8rNKapRW8T4D8HkjoOuQudEd+O8cDJh5XPZPxDdtLJWGZd2m/iTV1hSubuz2bsJHSXpZs+l2906NlTyHwsTysSeT/5uD2wt36FWCa829nPG6kteIUkdKTqHUsMtO/vRQUn3luXQcevYkOU/Ly2iSeUG1Nc5FwO/7li1XHWznXM2oeSF7GWEgmYSmUzfLDpW9AbcCAxs/musY0PIfx5YmWcTxVYpV1Wr6E5uL3c+N6eXrZbNZY/4k8dr2UYq9WRv0PZwP8s5Z8YqM69xOGaFcle8CK1K7Aaf/xhMP70EHRGt0jxw8GfyXkvp/qZf0m4XAMcre9LgRw+r2Y7dzL2UXUxe3KBN0tzhnkE2Q6ehT6LVahgjulP7BmrXfyXYm4sYq1hln6HdUuqfbGfvoKdSqjZW/gfRCjQvjve8J1rpBl7L37DNVcQNeoOkv/V9uibp72AuMQ7Gvl3V9R+CyVwI3YV2QL3Tp3PDZ1kn+vW0XfQZH0IvRFeXgcl9JOUb8WTolehMvlR0FNsr4Q0uRWzQWXBY9vVTEuufn8NFfU3ZMFFzRBPD6uXLi4XPwmv5EZTnP2Kigw7Bb+Kb0BTRb+x56dNV+69Lqg2abeCMaP/ZJOXLo1r/dUkRQXNJcQb0UYT/umO1tH+HjaHW/hs0aNCgwX/IX6xUt15Ofo8bAAAAAElFTkSuQmCC>
[image108]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAAZCAYAAABggz2wAAACpklEQVR4Xu2WS+hNURTGP6EIUeQRcj1KBl55RZKBRCJ5ZMJAon+mioxcSTJSnuWRoUKZkAHlT0pJGBgpAyKlMCJR+L7W3veuc5xzzz238x85X/3q3r3X3fesvb+19gFq1ar1v2ow2UDOB7aR4YmIfA0iy8hp2G9XhTGv7WQFGRnmxpOdZIEPGmgNJWfIETKT7CXfyCsyzcVlSb9tkqdkLplDHpKjaCc7hFwnf1LcIKNDTKaGBarSOnKfTHZj2m09zGXYg+ZpC/kJWyNqJflMlrsxraONe0duwtwjF3XUDHKPnCRjU3O96BDaSUVNIe/JGzLBjXtps+/ATn+RG59E3sKsHCXH+JiuJVvMI3fJFTI9OV1Ki8lLss+NxYcV+pwl1Vs/8hPVnGKknhP1mg3zu9DnKiT7/SK3kF8mRYl6N5wlp8hzmFOekIVhrrR0qjrdR7AumO583UoN5ir5Spam5tJqku9kiRuLm+TdoPUOo12X6gFfULx+R2nxc+g94a3kI1mTnsiQuvJrWBL6H22Sal017xMdhWTziT3gGjo3u0KpSV0gj0kjOdVR2mHZy59QkRqwrv2BvCC78G+NppVl71LSAqqHByh/mkpSGzMrfNdOy2JjWhHdSXeprpfYdXeT36SvFdFds8uU6vMSuQ3rxmUSlGRB3W/+BWEcuQiznSTrNZBsTvNhHXuzG9sEq9t4j8bryycarduP/FNvSclUccVMhNWzmoMu9Mgn2BtNrKE9sAf2dbUxjDXDd73pyMYqmxijhOUy1W/UDvID1g9ypQRlS9lTC5Q6+gzFHc/iuItTUnq4g2g7RifzjByDbYQ+q8OOCPOSYg/AnlcxsrSsvT/M5Wo9OYFq3oqqkKy8GmbfqcmphDSnmLUoeMetVatWrVq1BkB/Acccie03BNBCAAAAAElFTkSuQmCC>
[image109]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAAZCAYAAABggz2wAAACXUlEQVR4Xu2WT4hNURzHfzKKaBQKRTPUkJQFTWLDgjKKDUlIysJGiqmxUWwmsZgaEWGSDWIspqb8jUl2LImFLRZiRzbi++nc+9555743zbn3sXG+9al3z++8e+/3/H7nd65ZUlLS/67pYo/oDsZbabG4L7Znv0Pm1KfaSnFeXBUHxGwv9k80S+wQw+Kj+C7WNcxoLeYx/3cLjmTzdom7YpVYIo6JV6IrizfVzIx2CaN9YrM4bXFGWaDX5rLkMyGeirlioXhozmCuaeKiOOONFbRcPBHnxPwgVlUnLc7ocbElGMPcPbE6u+Ze70VPbYYTz7oejBXEiqwRD8SIWNYYLq1Yo2vFPO+a9yIBlGouEvNZfBCbsjEW47HYmU+aitjkrCDwu4pijYbixa+IGd4Y5gesvm9viDHRn8WiRVbJ7gux3srdpIpRttEzsSEMmOvmg1Y3+0VstXLvWBNt/ZKVM1zF6F7xRiwIxnn+UXOlutFcNjH7Sxz05pUSq3tZvLSpn4morFE6N511VHQEMTLHAizNrsnuPnPPeWeuK0eLbNK2n1t8NlFZo5yPX8XNMGCus14IB6Vt4ptFPov9eU2Mm+vGsQZzTWaUTHRb8zOcc5hyZB+Gwnyzcc5VzmAWaVJhpt1HDEZ/iN4wIB02Z+a2FcuTLyBi/D8UnfitNW4h3v2QuGXFe9XEJMqS8qRMKdcq4pvzjrkyyrsifBJD3jy+gn6aOyrCijlhrY1SCfyH0j4l9ps7Ch+JRd68giiTs9b+r6IqYrFoOp1hwBPvy2LtFiusuFhJSUlJSUl/U38Azmlz9oKc/hgAAAAASUVORK5CYII=>
[image110]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEEAAAAZCAYAAABuKkPfAAADNElEQVR4Xu2XWahNURjHP6HIVGYh15yUSEhJEcWDCA+mJxKFTIWkKHkwFhEJXWQoxQMlQznxQkpSJEPhRXnwRjyI/++utTrr7LvPPfvcc1C3/a9f19prn7XXN67FLFeutqCuoq9on5yIxFy35MO2ooXikdgprov+pdNNwgE7xJbkxL/WaHFAnBaLrOWoBXUWy8z95piYZaW/Gyyei2l+vEJ8FXvESDFAzBF3xR1zGfNf1FHsEk/FRDFM3BRr45dS1MNcZFeL4WK3+CVu+zk0V3wUg/yYtY+IoWKxWC4WmFtngn+nrupizsBKwti3osGP2fhvcT68UEbrxQXR3Y/bib3mfrvdP+MvTiDiiL+sG0d8jf2FMiCtr4nLok9iLinSFQeQykFEkcyYEj1LE8ZgcGzAJPFd3DcXBPpB7AQy4pTo5MdjxUWrUxkQBTb9QBw3Z1wWLTFnCGnJxths1g2Rxi/F7OgZ5fRNFMytQ0Bem3MOov43+3/TTxqtDmVAE5pprvseFL1KpyuKDMAJpDERWWeuke23bKWUFM5kvcN+THC2iodipbhlxf5ABtVUBhg/Xzw2l7qhLqtVSOmCFTOArv3Z3OarEWWEsW/EkMTcCDHPivsk+let+M1wMi21DM7nBVL4mdhoru5qUXACzSmIjRXMpXrv6HlLIuLbxDsxJjGXFOvjgFAGk8UTP+ak2WRuvbKaIT6Y6+jUVK06Y84JRCkoOIHapsaziHsFJdmQeJ6muAw6iCtinx9jE41zoB+XVZwNeL+1pYDYTK1OwAHcDXr6Mb8PjTapZBn0E++teKSiDZbtu00KfYFLTmuaIpoqfprbdFBaOfCtBmtuGKl8yYqXI0Q5HLXmKZ0sA8RpxBEaO4H7B/uqSsnjMZzJWUT6cTukLEhNlNYYV5nLGFI3vIex9IAv4lME1+KQ3rHIurj3IP7DRBkFJ2DLIavcV8qKBcaZS82z5q6mWcQHX4lz5ox9IU5aaZemXH6YK78Q4dBU04gzCxH9RkvvYzjghrnvcYrQE9Leq1o4gCOHRbOIDUw3d58fZc1TuRZRQies/F4wmHsJGXFPjC+dzpUrV65cuXJV0B9vnZSXwQLs0gAAAABJRU5ErkJggg==>
@@ -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?
Alpacas 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, youll 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 Firms [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.
@@ -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.
@@ -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.
Alpacas 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 OShaughnessy (“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.
@@ -0,0 +1,58 @@
# About Connect API
Develop applications on Alpacas platform using OAuth2. Let 10M+ users with an Alpaca brokerage account connect to your app.
Alpacas OAuth allows you to seamlessly integrate financial markets into your application and expand your audience to millions of brokerage accounts on Alpacas 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)
<br />
# Non-Registered and Fintech Partners
Alpaca Connect allows non-registered firms, developers and fintech companies to build trading apps on Alpacas 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.
<br />
# Terms of Access and Use
* You must read the terms and register in order to connect and use Alpacas 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)
<br />
> ❗️ 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-users 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 Alpacas 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: Im developing an app/service targeting non-US users. Can we integrate with Alpacas OAuth API?
A: Alpacas platform supports brokerage accounts for international users. When you build an app on OAuth, all users on Alpacas platform will be able to use your service, including international users.
<br />
@@ -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
<Callout icon="️">
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.
</Callout>
## 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.
<Callout icon="📘" theme="info">
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).
</Callout>
# 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}"
```
<Callout icon="🚧" theme="warn">
Note: Access tokens are valid for 15 minutes. Do not request a new token for each API call.
</Callout>
For the WebSocket stream authentication, kindly refer to the [WebSocket Stream documentation](https://docs.alpaca.markets/docs/streaming-market-data#authentication).
@@ -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\<number> | 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\<number> | For `partially_filled` orders, the quantity of shares that are left to be filled. |
| `price` | string\<number> | The per-share price that the trade was executed at. |
| `qty` | string\<number> | 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\<timestamp> | The time at which the execution occurred. |
| `order_id` | string\<uuid> | 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\<timestamp> | The date on which the activity occurred or on which the transaction associated with the activity settled. |
| `net_amount` | string\<number> | 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\<number> | For dividend activities, the number of shares that contributed to the payment. Not present for other activity types. |
| `per_share_amount` | string\<number> | 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 |
@@ -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, youll be able to buy and sell stocks in your brokerage account, and youll 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, its 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.
<Callout icon="👀" theme="default">
### 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.
</Callout>
# 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 accounts 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 FINRAs 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
<Table align={["left","left","left"]}>
<thead>
<tr>
<th>
Attribute
</th>
<th>
Type
</th>
<th>
Description
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`id`
</td>
<td>
string`<uuid>`
</td>
<td>
Account ID.
</td>
</tr>
<tr>
<td>
`account_number`
</td>
<td>
string
</td>
<td>
Account number.
</td>
</tr>
<tr>
<td>
`status`
</td>
<td>
string\<account\_status>
</td>
<td>
See detailed account statuses below
</td>
</tr>
<tr>
<td>
`crypto_status`
</td>
<td>
string\<account\_status>
</td>
<td>
The current status of the crypto enablement. See detailed crypto statuses below.
</td>
</tr>
<tr>
<td>
`currency`
</td>
<td>
string
</td>
<td>
"USD"
</td>
</tr>
<tr>
<td>
`cash`
</td>
<td>
string`<number>`
</td>
<td>
Cash balance
</td>
</tr>
<tr>
<td>
`portfolio_value`
</td>
<td>
string`<number>`
</td>
<td>
* *lpaca Broker*\* Total value of cash + holding positions (Equivalent to the equity field)
</td>
</tr>
<tr>
<td>
`non_marginable_buying_power`
</td>
<td>
string`<number>`
</td>
<td>
Current available non-margin dollar buying power
</td>
</tr>
<tr>
<td>
`accrued_fees`
</td>
<td>
string`<number>`
</td>
<td>
The fees collected.
</td>
</tr>
<tr>
<td>
`pending_transfer_in`
</td>
<td>
string`<number>`
</td>
<td>
Cash pending transfer in.
</td>
</tr>
<tr>
<td>
`pending_transfer_out`
</td>
<td>
string`<number>`
</td>
<td>
Cash pending transfer out
</td>
</tr>
<tr>
<td>
`pattern_day_trader`
</td>
<td>
boolean
</td>
<td>
Whether or not the account has been flagged as a pattern day trader
</td>
</tr>
<tr>
<td>
`trade_suspended_by_user`
</td>
<td>
boolean
</td>
<td>
User setting. If `true`, the account is not allowed to place orders.
</td>
</tr>
<tr>
<td>
`trading_blocked`
</td>
<td>
boolean
</td>
<td>
If `true`, the account is not allowed to place orders.
</td>
</tr>
<tr>
<td>
`transfers_blocked`
</td>
<td>
boolean
</td>
<td>
If `true`, the account is not allowed to request money transfers.
</td>
</tr>
<tr>
<td>
`account_blocked`
</td>
<td>
boolean
</td>
<td>
If `true`, the account activity by user is prohibited.
</td>
</tr>
<tr>
<td>
`created_at`
</td>
<td>
string`<timestamp>`
</td>
<td>
Timestamp this account was created at
</td>
</tr>
<tr>
<td>
`shorting_enabled`
</td>
<td>
boolean
</td>
<td>
Flag to denote whether or not the account is permitted to short
</td>
</tr>
<tr>
<td>
`long_market_value`
</td>
<td>
string`<number>`
</td>
<td>
Real-time MtM value of all long positions held in the account
</td>
</tr>
<tr>
<td>
`short_market_value`
</td>
<td>
string`<number>`
</td>
<td>
Real-time MtM value of all short positions held in the account
</td>
</tr>
<tr>
<td>
`equity`
</td>
<td>
string`<number>`
</td>
<td>
`cash` + `long_market_value` + `short_market_value`
</td>
</tr>
<tr>
<td>
`last_equity`
</td>
<td>
string`<number>`
</td>
<td>
Equity as of previous trading day at 16:00:00 ET
</td>
</tr>
<tr>
<td>
`multiplier`
</td>
<td>
string`<number>`
</td>
<td>
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)
</td>
</tr>
<tr>
<td>
`buying_power`
</td>
<td>
string`<number>`
</td>
<td>
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
</td>
</tr>
<tr>
<td>
`initial_margin`
</td>
<td>
string`<number>`
</td>
<td>
Reg T initial margin requirement (continuously updated value)
</td>
</tr>
<tr>
<td>
`maintenance_margin`
</td>
<td>
string`<number>`
</td>
<td>
Maintenance margin requirement (continuously updated value)
</td>
</tr>
<tr>
<td>
`sma`
</td>
<td>
string`<number>`
</td>
<td>
Value of special memorandum account (will be used at a later date to provide additional buying\_power)
</td>
</tr>
<tr>
<td>
`daytrade_count`
</td>
<td>
int
</td>
<td>
The current number of daytrades that have been made in the last 5 trading days (inclusive of today)
</td>
</tr>
<tr>
<td>
`last_maintenance_margin`
</td>
<td>
string`<number>`
</td>
<td>
Your maintenance margin requirement on the previous trading day
</td>
</tr>
<tr>
<td>
`daytrading_buying_power`
</td>
<td>
string`<number>`
</td>
<td>
Your buying power for day trades (continuously updated value)
</td>
</tr>
<tr>
<td>
`regt_buying_power`
</td>
<td>
string`<number>`
</td>
<td>
Your buying power under Regulation T (your excess equity - equity minus margin value - times your margin multiplier)
</td>
</tr>
</tbody>
</Table>
# 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. |
@@ -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).
@@ -0,0 +1,52 @@
# Alpaca API Platform
# Why API?
Alpacas 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
Alpacas 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.
Weve 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
@@ -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) Gatewa&#x79;*\** 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 DASHs 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/<your_order_id> \
--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
Were 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/<your_order_id> \
--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/<your_order_id> \
--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
<Table align={["left","left","left","left"]}>
<thead>
<tr>
<th>
Parameter
</th>
<th>
Required
</th>
<th>
Description
</th>
<th>
Values
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`algorithm`
</td>
<td>
**mandatory**
</td>
<td>
Must be set to "VWAP" for Volume-Weighted Average Price Orders
</td>
<td>
`"VWAP"`
</td>
</tr>
<tr>
<td>
`start_time`
</td>
<td>
optional
</td>
<td>
When the algorithm is to start executing
</td>
<td>
`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.
</td>
</tr>
<tr>
<td>
`end_time`
</td>
<td>
optional
</td>
<td>
When the algorithm is to be done executing
</td>
<td>
`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.
</td>
</tr>
<tr>
<td>
`max_percentage`
</td>
<td>
optional
</td>
<td>
Maximum percentage of the ticker's period volume this\
order might participate in
</td>
<td>
Decimal number, must be 0 \< `max_percentage` \< 1, with up to 3 decimal points precision.
</td>
</tr>
</tbody>
</Table>
## 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/<your_order_id> \
--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/<your_order_id> \
--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
<Table align={["left","left","left","left"]}>
<thead>
<tr>
<th>
Parameter
</th>
<th>
Required
</th>
<th>
Description
</th>
<th>
Values
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`algorithm`
</td>
<td>
**mandatory**
</td>
<td>
Must be set to "TWAP" for Time-Weighted Average Price Orders
</td>
<td>
`"TWAP"`
</td>
</tr>
<tr>
<td>
`start_time`
</td>
<td>
optional
</td>
<td>
When the algorithm is to start executing
</td>
<td>
`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.
</td>
</tr>
<tr>
<td>
`end_time`
</td>
<td>
optional
</td>
<td>
When the algorithm is to be done executing
</td>
<td>
`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.
</td>
</tr>
<tr>
<td>
`max_percentage`
</td>
<td>
optional
</td>
<td>
Maximum percentage of the ticker's period volume this\
order might participate in
</td>
<td>
Decimal number, must be 0 \< `max_percentage` \< 1, with up to 3 decimal points precision.
</td>
</tr>
</tbody>
</Table>
## 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 DASHs 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.*
@@ -0,0 +1,154 @@
# Alpaca's MCP Server
Turn your words into action with Alpacas MCP Server
Alpacas 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.
Alpacas 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.
## Alpacas MCP Server Overview
Alpacas MCP Server brings this same bridge-like concept to trading by exposing capabilities powered by Alpacas 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
Alpacas MCP Server gives your AI model structured access to real time market data, news context, portfolio details, and order actions powered by Alpacas 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
Alpacas 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
Alpacas MCP Server lets traders begin with natural language prompts and move into vibe coding or full code whenever they want to optimize strategies.
<br />
## Supported MCP Clients and Connection Types
Alpacas MCP Server can be configured on the following MCP clients. Each client has its own setup requirements. For more details, visit [Alpacas MCP Server GitHub](https://github.com/alpacahq/alpaca-mcp-server) . The connection type indicates how you can set up Alpacas MCP Server.
Note: Remote hosting for Alpacas 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 Alpacas remote MCP Server, visit our learn article “[How to Deploy Alpacas 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 Alpacas 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 Alpacas 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
Alpacas 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 Alpacas MCP Server
Using Alpacas 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.
<br />
## 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..*
@@ -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
<Callout icon="🚧" theme="warn">
The Client Credentials authentication flow is not yet available for Trading API.
</Callout>
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}"
```
<br />
@@ -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)
@@ -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: <KEY>' \
--header 'Apca-Api-Secret-Key: <SECRET>' \
--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.
@@ -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: <KEY>' \
--header 'Apca-Api-Secret-Key: <SECRET>'
```
```
{
"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": "<KEY>", "secret": "<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"}]
< ...
```
@@ -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 youd 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: <KEY>' \
--header 'Apca-Api-Secret-Key: <SECRET>'
```
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: <KEY>' \
--header 'Apca-Api-Secret-Key: <SECRET>' \
--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":"<YOUR API KEY>","secret":"<YOUR API 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.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
# Fractional Trading
Fractional shares are fractions of a whole share, meaning that you dont 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 Alpacas 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 securitys 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.
@@ -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 Alpacas software development kit (SDK), create a free alpaca account, locate your API keys, and how to request both historical and real-time data.
# Installing Alpacas Client SDK
In this guide, well 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. Its 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 well 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 clients built-in method, get\_crypto\_bars. Additionally, well 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
<
...
```
@@ -0,0 +1,27 @@
# Getting Started with Trading API
This section outlines how to install Alpacas 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
<
...
```
@@ -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 Alpacas 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!
@@ -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}
```
@@ -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.
@@ -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.
@@ -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, theyre just indicative derivatives. The trades are also derivatives and theyre 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. |
@@ -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.
<Table align={["left","left"]}>
<thead>
<tr>
<th>
Source
</th>
<th>
Description
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
**iex**
</td>
<td>
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.
</td>
</tr>
<tr>
<td>
**sip**
</td>
<td>
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.
</td>
</tr>
<tr>
<td>
**boats**
</td>
<td>
[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.
</td>
</tr>
<tr>
<td>
**overnight**
</td>
<td>
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.
</td>
</tr>
</tbody>
</Table>
<br />
@@ -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 accounts 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 accounts 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 stocks 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 stocks HTB short $ market value _ that stocks 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 Firms <Anchor label="Margin Disclosure Statement" target="_blank" href="https://files.alpaca.markets/disclosures/library/MarginDiscStmt.pdf">Margin Disclosure Statement</Anchor> before investing.
File diff suppressed because it is too large Load Diff
@@ -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.
@@ -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.
<Callout icon="🎉" theme="default">
### Multi-leg options trading is now available for live trading!
</Callout>
## 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 orders 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
<Image align="center" className="border" border={true} src="https://files.readme.io/444efc02ec4fecfc528035df0c4dabacf34a9082a46b08af4de56859ef498e02-long_call_spread.png" />
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
<Image align="center" className="border" border={true} src="https://files.readme.io/a807ae88d756237e9fc54b29b2c0bcfa4d24c0013becbfbda70244f2144082f8-long_put_spread.png" />
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
<Image align="center" src="https://files.readme.io/f86f771f0b68ce6af0706ba6e7a6d308af0678bbf3d71c23027722c600d67854-iron_condor.png" />
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 Options Payoff**\
Each option is represented by a piecewise linear payoff function (PnL) based on the underlying price (p).
<Image align="center" src="https://files.readme.io/32286a51ed97659a53b176cec7edbe0df282c76a42831ac7f81dc7bf80c4d26d-Screenshot_2025-01-20_at_19.01.45.png" />
3. **Combine Positions**\
To determine total payoff, sum the piecewise functions for all open positions:
<Image align="center" width="400px" src="https://files.readme.io/995133508c4fd0176faf1c370ba26aa1444b74719ecbb3e3d6ec4cb2d3b8f77a-Screenshot_2025-01-20_at_19.05.08.png" />
4. **Find Theoretical Maximum Loss**\
Maintenance margin is based on the worst-case scenario for the portfolio:
<Image align="center" width="450px" src="https://files.readme.io/66772ec029ae5859d8c97d2e063ea2893faa9c0c794c8593ee2af5f7fe514111-Screenshot_2025-01-20_at_19.04.45.png" />
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
<Image align="center" src="https://files.readme.io/12a13471e5bdfaeb7669cac5d145b9f5ed1c7545b93c82999a38c447bde6f158-call_spread_1.png" />
With maintenance margin = 1000 since the difference between strike prices is 10 and the options 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
<Image align="center" src="https://files.readme.io/ec7e1a5000f960cb984d595becc471067bafb8b8fa597cc08f15ac986c64aa88-call_spread_2.png" />
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**
<Image align="center" src="https://files.readme.io/e30a3e59350972901aed6ff30df3409cba7f8115e4b88df66a9c33dfa8337bec-2_call_spreads.png" />
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 anothers 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 spreads maximum potential loss, if any. To\
determine the spreads 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** = (1510)=(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 orders 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 legs `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 isnt simplified, youre 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.*
@@ -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.
@@ -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.
<Callout icon="🎉" theme="default">
### 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)
</Callout>
# OpenAPI Spec
You can find our Open API docs here: [\<https://docs.alpaca.markets/reference>](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.
<Table align={["left","left","left"]}>
<thead>
<tr>
<th>
Level
</th>
<th>
Supported Trades
</th>
<th>
Validation
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
0
</td>
<td>
* Options trading is disabled
</td>
<td>
* NA
</td>
</tr>
<tr>
<td>
1
</td>
<td>
* Sell a covered call
* Sell cash-secured put
</td>
<td>
* User must own sufficient underlying shares
* User must have sufficient options buying power
</td>
</tr>
<tr>
<td>
2
</td>
<td>
* Level 1
* Buy a call
* Buy a put
</td>
<td>
* User must have sufficient options buying power
</td>
</tr>
<tr>
<td>
3
</td>
<td>
* Level 1,2
* Buy a call spread
* Buy a put spread
</td>
<td>
* User must have sufficient options buying power
</td>
</tr>
</tbody>
</Table>
# 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), youll 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)
File diff suppressed because it is too large Load Diff
@@ -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.
<Callout icon="🌍" theme="default">
### Anyone globally can create an Alpaca **Paper Only Account**! All you need to do is sign up with your email address.
</Callout>
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 wont 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).
@@ -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).
@@ -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 bars 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}]
```
@@ -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). |
@@ -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`<string>` | 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`<string>` | 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 bars 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`<string>` | original trade conditions |
| `ci` | int | corrected trade id |
| `cp` | number | corrected trade price |
| `cs` | int | corrected trade size |
| `cc` | array`<string>` | 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"}]
```
@@ -0,0 +1,37 @@
# Registering Your App
<br />
Before integrating with Alpaca, youll 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
<br />
### 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)
<br />
### 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)
<br />
### 4. Application Review
Once your application has been submitted, our team will follow up with you shortly via email to complete the remaining process.
<br />
@@ -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:
<Anchor label="https://alpaca.markets/disclosures" target="_blank" href="https://alpaca.markets/disclosures">[https://alpaca.markets/disclosures](https://alpaca.markets/disclosures)</Anchor>
## 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 currencys 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)
@@ -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)
@@ -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",
"<channel1>": ["<SYMBOL1>"],
"<channel2>": ["<SYMBOL2>","<SYMBOL3>"],
"<channel3>": ["*"]
}
```
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 dont have to send what youre 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 wont have to implement anything yourself.
## Errors
You may receive an error during your session. Below are the general errors you may run into.
<Table align={["left","left","left"]}>
<thead>
<tr>
<th>
Code
</th>
<th>
Message
</th>
<th>
Description
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
400
</td>
<td>
invalid syntax
</td>
<td>
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.
</td>
</tr>
<tr>
<td>
401
</td>
<td>
not authenticated
</td>
<td>
You have attempted to subscribe or unsubscribe before [authentication](https://docs.alpaca.markets/docs/streaming-market-data#authentication).
</td>
</tr>
<tr>
<td>
402
</td>
<td>
auth failed
</td>
<td>
You have provided invalid authentication credentials.
</td>
</tr>
<tr>
<td>
403
</td>
<td>
already authenticated
</td>
<td>
You have already successfully authenticated during your current session.
</td>
</tr>
<tr>
<td>
404
</td>
<td>
auth timeout
</td>
<td>
You failed to successfully authenticate after connecting. You only have a few seconds to authenticate after connecting.
</td>
</tr>
<tr>
<td>
405
</td>
<td>
symbol limit exceeded
</td>
<td>
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.
</td>
</tr>
<tr>
<td>
406
</td>
<td>
connection limit exceeded
</td>
<td>
You already have the number of sessions allowed by your subscription.
</td>
</tr>
<tr>
<td>
407
</td>
<td>
slow client
</td>
<td>
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.
</td>
</tr>
<tr>
<td>
409
</td>
<td>
insufficient subscription
</td>
<td>
You have attempted to access a data source not available in your subscription package.
</td>
</tr>
<tr>
<td>
410
</td>
<td>
invalid subscribe action for this feed
</td>
<td>
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).
</td>
</tr>
<tr>
<td>
500
</td>
<td>
internal error
</td>
<td>
An unexpected error occurred on our end. Please let us know if this happens.
</td>
</tr>
</tbody>
</Table>
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":"<YOUR API KEY>","secret":"<YOUR API 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}]
```
@@ -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`<string>` | 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"}]
```
@@ -0,0 +1,63 @@
# The Intraday Margin Rule
<br />
### **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 accounts 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 Firms [Margin Disclosure Statement](https://files.alpaca.markets/disclosures/library/MarginDiscStmt.pdf) before investing.*
@@ -0,0 +1,29 @@
# About Trading API
Trade stocks & crypto with Alpacas 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 youd 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 dont 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).
@@ -0,0 +1,82 @@
# Understanding FINRAs New Intraday Margin Rule and the End of PDT
<br />
### **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 mans speculation is another mans 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 accounts 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 accounts 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 days 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 accounts 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 Firms [Margin Disclosure Statement](https://files.alpaca.markets/disclosures/library/MarginDiscStmt.pdf) before investing.*
<br />
@@ -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 Firms [Margin Disclosure Statement](https://files.alpaca.markets/disclosures/library/MarginDiscStmt.pdf) before investing.*
@@ -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)
## Alpacas 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 days 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.
<br />
## 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.
<br />
## 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 Alpacas 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 positions 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 Alpacas 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 [FINRAs 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 [FINRAs 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).
@@ -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 Alpacas 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, youll 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
```
Heres 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.
<Image align="center" width="50% " src="https://files.readme.io/06f62610cde297ce4ce76d38c3570190006ea4a6e4612eade17a658d03025b6b-Screenshot_2025-02-14_at_12.00.23_PM.png" />
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.
<Image align="center" width="51% " src="https://files.readme.io/9b92fd7ba5d76739d84d77054f4c37f453eb5f86a9fcfa66a57342d1bdef8dfb-Screenshot_2025-02-14_at_11.57.09_AM.png" />
## 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 dont 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 shouldnt 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"
}
```
@@ -0,0 +1,369 @@
# Websocket Streaming
Learn how to stream market data using Websockets.
Alpacas 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`. Alpacas 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 messages 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 orders 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 hasnt 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"
}
}
```
@@ -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)
}
```
@@ -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, youll 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.")
}
}
```
@@ -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 theyre 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 youd 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.
@@ -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, youll 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.
@@ -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));
}
}
@@ -0,0 +1,8 @@
public async Task<decimal> GetAvailableEquityAsync()
{
// Recupera le informazioni del conto
IAccount account = await _tradingClient.GetAccountAsync();
// Ritorna il valore totale del portafoglio o la liquidità disponibile
return account.Equity;
}
@@ -0,0 +1,13 @@
public async Task<List<IBar>> 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<IBar> barsPage = await _dataClient.ListHistoricalBarsAsync(request);
// Restituisce la lista di candele per il simbolo specificato
return barsPage.Items[symbol].ToList();
}
@@ -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}");
}
@@ -0,0 +1,13 @@
public async Task<bool> 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;
}
}
+611
View File
@@ -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
{
/// <summary>
/// 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)
/// </summary>
public class AutomatedBotEngine
{
private readonly ITradingService _tradingService;
/// <summary>Lock per serializzare le operazioni di apertura/chiusura ordini.</summary>
private readonly SemaphoreSlim _orderExecutionLock = new SemaphoreSlim(1, 1);
/// <summary>Asset class per il controllo orario mercato (settato dal viewmodel).</summary>
public string AssetClass { get; set; } = "us_equity";
// ?? eventi esposti al viewmodel ???????????????????????????????????????
public event EventHandler<BotLogEntry> LogGenerated;
public event EventHandler<TradingSignal> SignalGenerated;
public event EventHandler<decimal> EquityUpdated;
public event EventHandler<bool> 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<IBar> 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<System.Collections.Generic.List<Alpaca.Markets.IBar>> 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<IBar> 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<IBar> 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<IBar> 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<IBar> 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<decimal>();
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<IBar> 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<IBar> 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<IPosition> 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<decimal> CalculateEma(List<decimal> v, int period)
{
var r = new List<decimal>();
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<decimal> 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<IBar> 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<double> _spreads = new Queue<double>();
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<double> 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));
}
}
}
+459
View File
@@ -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: 03. 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<double> _spreads = new Queue<double>(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
// ════════════════════════════════════════════════════════════════════
/// <summary>
/// Analizza le barre più recenti e restituisce il segnale di trading.
/// Richiede almeno 50 barre da 1 minuto per calcolare tutti gli indicatori.
/// </summary>
public BtcSignalResult Analyze(List<IBar> 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<decimal> CalculateEma(List<decimal> prices, int period)
{
var r = new List<decimal>();
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<decimal> 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<decimal> 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<decimal>();
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<IBar> 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;
}
/// <summary>ADX basato su True Range e Directional Movement.</summary>
private static decimal CalculateAdx(List<IBar> bars, int period)
{
if (bars.Count < period * 2) return 0m;
var dmPlus = new List<decimal>();
var dmMinus = new List<decimal>();
var trList = new List<decimal>();
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<decimal>();
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;
}
/// <summary>Slope della EMA(period) negli ultimi n bar (normalizzato per prezzo).</summary>
private static decimal CalculateTrendSlope(List<decimal> 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 (0100)
// ════════════════════════════════════════════════════════════════════
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
// ══════════════════════════════════════════════════════════════════════
/// <summary>Regime del mercato classificato dal Regime Detector.</summary>
public enum MarketRegime { Unknown, TrendUp, TrendDown, Ranging }
/// <summary>Risultato dettagliato prodotto da BtcUsdAlgorithm.Analyze().</summary>
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; }
/// <summary>Stringa diagnostica completa per il log operativo.</summary>
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}%";
}
}
+218
View File
@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Alpaca.Markets;
using DesktopBot.Models;
using DesktopBot.Services;
namespace DesktopBot.Engine
{
// ═══════════════════════════════════════════════════════════════════════════
// POSITION RISK MANAGER
// Valuta la situazione del portafoglio live PRIMA di aprire nuove posizioni
// e gestisce le uscite in profitto / in perdita in modo proattivo.
//
// Regole applicate:
// 1. MaxOpenPositions → blocca nuove aperture se già raggiunte
// 2. MaxCapitalAllocated → blocca se troppo capitale è già impegnato
// 3. ProfitLockPercent → chiude posizioni con PnL% >= soglia (lock profit)
// 4. MaxLossPercent → chiude posizioni con PnL% <= -soglia (stop loss dinamico)
// 5. UseBreakEvenStop → quando PnL% >= ProfitLockPercent/2, annota il
// break-even e lo riporta al chiamante per aggiornare
// l'ordine stop (o come informazione di log)
// ═══════════════════════════════════════════════════════════════════════════
internal sealed class PositionRiskManager
{
private readonly ITradingService _svc;
private readonly BotConfiguration _cfg;
private readonly Action<string> _logInfo;
private readonly Action<string> _logWarn;
public PositionRiskManager(
ITradingService service,
BotConfiguration config,
Action<string> logInfo,
Action<string> logWarn)
{
_svc = service;
_cfg = config;
_logInfo = logInfo;
_logWarn = logWarn;
}
// ── Verifica se è possibile aprire un nuovo ordine ──────────────────
/// <returns>true = ok aprire, false = bloccato dal risk manager</returns>
public async Task<bool> CanOpenNewPositionAsync()
{
try
{
// Conta solo le posizioni aperte dal bot (ClientOrderId BOT_)
var positions = await _svc.GetBotPositionsAsync();
int openCount = positions?.Count ?? 0;
// 1. Limite numero posizioni globale
if (openCount >= _cfg.MaxOpenPositions)
{
_logWarn($"[RISK] Apertura bloccata: {openCount} posizioni aperte su {_cfg.MaxOpenPositions} consentite.");
return false;
}
// 2. Limite posizioni per asset
int assetCount = positions?.Count(p => string.Equals(
p.Symbol, _cfg.Symbol, StringComparison.OrdinalIgnoreCase)) ?? 0;
if (assetCount >= _cfg.MaxOpenPositionsPerAsset)
{
_logWarn($"[RISK] Apertura bloccata: {assetCount} posizioni aperte su {_cfg.Symbol} (limite per asset: {_cfg.MaxOpenPositionsPerAsset}).");
return false;
}
// 3. Limite capitale allocato
if (positions != null && positions.Count > 0)
{
decimal equity = await _svc.GetAvailableEquityAsync();
decimal totalEquity = equity + positions.Sum(p => Math.Abs(p.MarketValue ?? 0));
decimal allocated = positions.Sum(p => Math.Abs(p.MarketValue ?? 0));
decimal allocPct = totalEquity > 0 ? allocated / totalEquity : 0;
if (allocPct >= _cfg.MaxCapitalAllocatedPercent)
{
_logWarn($"[RISK] Apertura bloccata: capitale allocato {allocPct:P1} >= limite {_cfg.MaxCapitalAllocatedPercent:P1}.");
return false;
}
}
return true;
}
catch (Exception ex)
{
_logWarn($"[RISK] Errore controllo posizioni: {ex.Message} — apertura permessa per sicurezza.");
return true; // fail-open: non bloccare per errori di rete
}
}
// ── Calcola la dimensione ottimale per la nuova posizione ────────────
/// <summary>
/// Ritorna la quantità da comprare in base al capitale disponibile e
/// alla quota assegnabile a questa specifica posizione.
/// </summary>
public async Task<decimal> ComputeQuantityAsync(decimal price, bool isCrypto)
{
if (price <= 0) return 0;
decimal equity = await _svc.GetAvailableEquityAsync();
// Considera solo le posizioni aperte dal bot per stimare il capitale già allocato
IReadOnlyList<IPosition> positions;
try { positions = await _svc.GetBotPositionsAsync(); }
catch { positions = new List<IPosition>(); }
decimal allocated = positions?.Sum(p => Math.Abs(p.MarketValue ?? 0)) ?? 0m;
decimal totalEquity = equity + allocated;
// Budget massimo assoluto per questa posizione
decimal maxBudget = totalEquity * _cfg.MaxPositionSizePercent;
// Non superare la quota di capitale libero disponibile
decimal usableBudget = Math.Min(maxBudget, equity * 0.95m); // riserva 5% liquidità
decimal rawQty = usableBudget / price;
decimal qty = isCrypto
? Math.Round(rawQty, 4, MidpointRounding.ToEven)
: Math.Floor(rawQty);
if (qty <= 0)
_logWarn($"[RISK] Quantità calcolata = 0 (budget usabile: {usableBudget:F2}, prezzo: {price:F2}).");
return qty;
}
// ── Gestione proattiva delle posizioni aperte (profit lock / stop) ───
/// <summary>
/// Valuta ogni posizione aperta per il simbolo del bot. Se una posizione
/// supera la soglia di profitto o di perdita, la chiude emettendo log adeguati.
/// Chiamare a ogni ciclo PRIMA della valutazione del segnale.
/// </summary>
public async Task ManageOpenPositionsAsync()
{
try
{
var position = await _svc.GetPositionAsync(_cfg.Symbol);
if (position == null) return;
decimal entryPrice = position.AverageEntryPrice;
decimal currentPrice = position.AssetCurrentPrice ?? entryPrice;
decimal qty = position.Quantity;
decimal marketValue = position.MarketValue ?? (currentPrice * qty);
decimal costBasis = position.CostBasis != 0 ? position.CostBasis : entryPrice * qty;
if (costBasis == 0) return;
decimal unrealizedPnl = marketValue - costBasis;
decimal pnlPct = unrealizedPnl / costBasis;
_logInfo($"[RISK] Posizione {_cfg.Symbol}: ingresso={entryPrice:F2} corrente={currentPrice:F2} PnL={pnlPct:P2} ({unrealizedPnl:+F2;-F2})");
// ── Profit Lock: chiudi se in profitto sopra soglia ──────────
if (_cfg.ProfitLockPercent > 0 && pnlPct >= _cfg.ProfitLockPercent)
{
_logInfo($"[RISK] PROFIT LOCK: PnL {pnlPct:P2} >= {_cfg.ProfitLockPercent:P2}. Chiudo posizione.");
await _svc.ClosePositionAsync(_cfg.Symbol);
return;
}
// ── Break-even alert (solo log, SL fisico già piazzato) ──────
if (_cfg.UseBreakEvenStop && _cfg.ProfitLockPercent > 0)
{
decimal breakEvenTrigger = _cfg.ProfitLockPercent / 2m;
if (pnlPct >= breakEvenTrigger)
_logInfo($"[RISK] Break-even zone raggiunta (PnL {pnlPct:P2}). Lo stop-loss dovrebbe essere al prezzo di ingresso ({entryPrice:F2}).");
}
// ── Stop Loss dinamico: chiudi se in perdita oltre soglia ────
if (_cfg.MaxLossPercent > 0 && pnlPct <= -_cfg.MaxLossPercent)
{
_logWarn($"[RISK] STOP LOSS DINAMICO: PnL {pnlPct:P2} <= -{_cfg.MaxLossPercent:P2}. Chiudo posizione per limitare le perdite.");
await _svc.ClosePositionAsync(_cfg.Symbol);
}
}
catch (Exception ex)
{
// La posizione potrebbe non esistere (già chiusa automaticamente dall'ordine SL/TP)
if (!ex.Message.Contains("position does not exist", StringComparison.OrdinalIgnoreCase) &&
!ex.Message.Contains("404", StringComparison.OrdinalIgnoreCase))
_logWarn($"[RISK] Errore gestione posizione: {ex.Message}");
}
}
// ── Report sintetico della situazione del portafoglio ────────────────
public async Task LogPortfolioStatusAsync()
{
try
{
var positions = await _svc.GetAllPositionsAsync();
if (positions == null || positions.Count == 0)
{
_logInfo("[RISK] Nessuna posizione aperta.");
return;
}
decimal totalUnrealized = positions.Sum(p => p.UnrealizedProfitLoss.GetValueOrDefault());
_logInfo($"[RISK] Portafoglio: {positions.Count} posizioni aperte | PnL totale non realizzato: {totalUnrealized:+F2;-F2}");
foreach (var p in positions)
{
decimal cb = p.CostBasis != 0 ? p.CostBasis : 1m;
decimal pnlPct = p.UnrealizedProfitLoss.GetValueOrDefault() / cb;
_logInfo($" [{p.Symbol}] qty={p.Quantity} entry={p.AverageEntryPrice:F2} mktVal={p.MarketValue:F2} PnL={pnlPct:P2}");
}
}
catch (Exception ex)
{
_logWarn($"[RISK] Errore lettura portafoglio: {ex.Message}");
}
}
}
}
+248
View File
@@ -0,0 +1,248 @@
using System;
using System.Collections.Generic;
using DesktopBot.Models;
namespace DesktopBot.Engine
{
/// <summary>
/// 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
/// </summary>
public static class StrategyAdvisor
{
// ── Profili per asset class ─────────────────────────────────────────
private static readonly Dictionary<string, StrategyProfile[]> _profiles
= new Dictionary<string, StrategyProfile[]>(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"];
}
/// <summary>Restituisce tutti i profili strategia disponibili per la classe dell'asset.</summary>
public static StrategyProfile[] GetAvailableProfiles(string assetClass)
=> GetProfiles(assetClass);
/// <summary>Restituisce il profilo raccomandato (primo marcato IsRecommended).</summary>
public static StrategyProfile GetRecommended(string assetClass)
{
var profiles = GetProfiles(assetClass);
foreach (var p in profiles)
if (p.IsRecommended) return p;
return profiles[0];
}
/// <summary>
/// Applica i parametri ottimali del profilo raccomandato alla configurazione.
/// Mantiene Symbol, Quantity e Name invariati.
/// </summary>
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 ──────────────────────────────────────────────────────
/// <summary>Descrive una strategia disponibile per una classe di asset, con i parametri pre-calibrati.</summary>
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<BotConfiguration> _applyParameters;
public StrategyProfile(
TradingStrategy strategy,
string displayName,
string description,
string icon,
string accentColor,
bool isRecommended,
Action<BotConfiguration> applyParameters)
{
Strategy = strategy;
DisplayName = displayName;
Description = description;
Icon = icon;
AccentColor = accentColor;
IsRecommended = isRecommended;
_applyParameters = applyParameters;
}
public void ApplyParameters(BotConfiguration cfg) => _applyParameters(cfg);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

+12
View File
@@ -0,0 +1,12 @@
<Window x:Class="DesktopBot.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesktopBot"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
</Grid>
</Window>
+28
View File
@@ -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
{
/// <summary>
/// Logica di interazione per MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace DesktopBot.Models
{
/// <summary>
/// Risultato semplificato della ricerca asset su Alpaca.
/// </summary>
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; }
/// <summary>True quando questo asset è selezionato nel Market Watch panel.</summary>
public bool IsSelected { get; set; }
public override string ToString() => $"{Symbol} — {Name}";
}
}
+236
View File
@@ -0,0 +1,236 @@
using System;
using Alpaca.Markets;
namespace DesktopBot.Models
{
/// <summary>
/// Modello per la configurazione del bot
/// </summary>
public class BotConfiguration
{
/// <summary>
/// Simbolo ticker da tradare (es. "AAPL", "TSLA")
/// </summary>
public string Symbol { get; set; } = "AAPL";
/// <summary>
/// Quantità di azioni da acquistare per operazione
/// </summary>
public int Quantity { get; set; } = 1;
/// <summary>
/// Intervallo in secondi tra ogni controllo del bot
/// </summary>
public int CheckIntervalSeconds { get; set; } = 60;
/// <summary>
/// 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.
/// </summary>
public BarTimeFrame AnalysisTimeFrame { get; set; } = BarTimeFrame.Day;
/// <summary>
/// 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.
/// </summary>
public int HistoricalBarCount { get; set; } = 0; // 0 = automatico
/// <summary>
/// Percentuale di Stop Loss (es. 0.02 = 2%)
/// </summary>
public decimal StopLossPercentage { get; set; } = 0.02m;
/// <summary>
/// Percentuale di Take Profit (es. 0.05 = 5%)
/// </summary>
public decimal TakeProfitPercentage { get; set; } = 0.05m;
/// <summary>
/// Percentuale massima del portfolio da utilizzare per operazione (0.1 = 10%)
/// </summary>
public decimal MaxPositionSizePercent { get; set; } = 0.10m;
// ─── EMA CROSSOVER ────────────────────────────────────────────────
/// <summary>Periodo EMA veloce (default 9)</summary>
public int FastEmaPeriod { get; set; } = 9;
/// <summary>Periodo EMA lenta (default 21)</summary>
public int SlowEmaPeriod { get; set; } = 21;
// ─── RSI ──────────────────────────────────────────────────────────
/// <summary>Periodo RSI (default 14)</summary>
public int RsiPeriod { get; set; } = 14;
/// <summary>Soglia ipervenduto (default 30)</summary>
public decimal RsiOversoldThreshold { get; set; } = 30;
/// <summary>Soglia ipercomprato (default 70)</summary>
public decimal RsiOverboughtThreshold { get; set; } = 70;
// ─── MACD ─────────────────────────────────────────────────────────
/// <summary>Periodo EMA veloce MACD (default 12)</summary>
public int MacdFastPeriod { get; set; } = 12;
/// <summary>Periodo EMA lenta MACD (default 26)</summary>
public int MacdSlowPeriod { get; set; } = 26;
/// <summary>Periodo Signal Line MACD (default 9)</summary>
public int MacdSignalPeriod { get; set; } = 9;
// ─── VOLATILITY BREAKOUT (Keltner + RVOL + CVD) ──────────────────
/// <summary>Periodo ATR e canale Keltner (default 20)</summary>
public int KeltnerPeriod { get; set; } = 20;
/// <summary>
/// Moltiplicatore ATR per larghezza canale Keltner (default 2.0)
/// Bande = EMA ± KeltnerMultiplier × ATR
/// </summary>
public decimal KeltnerMultiplier { get; set; } = 2.0m;
/// <summary>
/// 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.
/// </summary>
public decimal RvolMinThreshold { get; set; } = 2.0m;
/// <summary>
/// Moltiplicatore ATR per lo Stop Loss nei breakout (default 1.0).
/// SL = punto di breakout ± AtrStopMultiplier × ATR
/// </summary>
public decimal AtrStopMultiplier { get; set; } = 1.0m;
// ─── KALMAN MEAN REVERSION ────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
public double KalmanDelta { get; set; } = 1e-5;
/// <summary>
/// Varianza del rumore di osservazione Kalman (default 1.0).
/// Modella l'incertezza della misura di prezzo rispetto al fair value stimato.
/// </summary>
public double KalmanObservationVariance { get; set; } = 1.0;
/// <summary>
/// Soglia Z-Score per entrata Long (default -2.0): il prezzo è sufficientemente sotto il fair value.
/// </summary>
public double KalmanEntryZScore { get; set; } = 2.0;
/// <summary>
/// Soglia Z-Score per uscita / take profit (default 0.25): spread converge alla media.
/// </summary>
public double KalmanExitZScore { get; set; } = 0.25;
// ─── RISK MANAGEMENT ─────────────────────────────────────────────────
/// <summary>
/// Numero massimo di posizioni aperte contemporaneamente (default 3).
/// Se il numero di posizioni aperte raggiunge questo limite, non verranno aperti nuovi ordini.
/// </summary>
public int MaxOpenPositions { get; set; } = 3;
/// <summary>
/// Numero massimo di posizioni aperte per lo stesso asset (default 10).
/// Impedisce di accumulare troppe posizioni sullo stesso simbolo.
/// </summary>
public int MaxOpenPositionsPerAsset { get; set; } = 10;
/// <summary>
/// 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.
/// </summary>
public decimal MaxCapitalAllocatedPercent { get; set; } = 0.30m;
/// <summary>
/// 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.
/// </summary>
public decimal ProfitLockPercent { get; set; } = 0.03m;
/// <summary>
/// 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.
/// </summary>
public decimal MaxLossPercent { get; set; } = 0.02m;
/// <summary>
/// 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.
/// </summary>
public bool UseBreakEvenStop { get; set; } = true;
// ─── ASSET-STRATEGY OPTIMIZATION ─────────────────────────────────────
/// <summary>
/// Strategia raccomandata dal StrategyAdvisor per la classe d'asset corrente.
/// Viene aggiornata automaticamente quando si associa un asset al bot.
/// </summary>
public TradingStrategy? RecommendedStrategy { get; set; } = null;
/// <summary>
/// True quando la strategia attiva corrisponde a quella raccomandata per l'asset.
/// Usato dalla UI per mostrare il badge "Ottimizzato".
/// </summary>
public bool IsOptimizedForAsset
=> RecommendedStrategy.HasValue && Strategy == RecommendedStrategy.Value;
/// <summary>
/// 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.
/// </summary>
public bool IsLocked { get; set; } = false;
/// <summary>
/// Timestamp del momento in cui la configurazione è stata bloccata.
/// Null se la configurazione non è ancora stata bloccata.
/// </summary>
public DateTime? LockedAt { get; set; } = null;
// ─── STRATEGIA ATTIVA ─────────────────────────────────────────────
/// <summary>Strategia di trading da utilizzare</summary>
public TradingStrategy Strategy { get; set; } = TradingStrategy.EMA_CROSSOVER;
/// <summary>
/// Blocca la configurazione corrente, impedendo modifiche successive alla strategia.
/// Questo viene chiamato quando un bot viene associato a un asset.
/// </summary>
public void Lock()
{
if (!IsLocked)
{
IsLocked = true;
LockedAt = DateTime.Now;
}
}
// ─── LOGGING CONFIGURATION ──────────────────────────────────────────
/// <summary>Configurazione dei limiti per i log e i dati storici.</summary>
public LoggingConfiguration LoggingConfig { get; set; } = new LoggingConfiguration();
}
/// <summary>
/// Tipo di strategia di trading
/// </summary>
public enum TradingStrategy
{
/// <summary>Crossover di medie mobili esponenziali (EMA veloce vs EMA lenta)</summary>
EMA_CROSSOVER,
/// <summary>RSI (Relative Strength Index) ipervenduto/ipercomprato</summary>
RSI,
/// <summary>MACD divergenza tra EMA veloce e lenta con signal line</summary>
MACD,
/// <summary>Volatility Breakout Keltner Channel con filtri RVOL e CVD (Cap.3 del report)</summary>
VOLATILITY_BREAKOUT,
/// <summary>Mean Reversion con Filtro di Kalman stima dinamica del fair value (Cap.4 del report)</summary>
KALMAN_MEAN_REVERSION
}
}
+74
View File
@@ -0,0 +1,74 @@
using System;
namespace DesktopBot.Models
{
/// <summary>
/// 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.
/// </summary>
public class BotInstance
{
/// <summary>ID univoco del bot (6 hex chars, stabile nel tempo)</summary>
public string BotId { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 6);
/// <summary>Nome leggibile assegnato dall'utente (es. "EMA AAPL #1")</summary>
public string Name { get; set; } = "Nuovo Bot";
/// <summary>Simbolo ticker associato (es. "AAPL", "MSFT")</summary>
public string Symbol { get; set; } = "";
/// <summary>Nome dell'asset completo (es. "Apple Inc.")</summary>
public string AssetName { get; set; } = "";
/// <summary>Classe dell'asset: "us_equity", "crypto", ecc.</summary>
public string AssetClass { get; set; } = "us_equity";
/// <summary>
/// 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.
/// </summary>
public bool IsAssetLocked { get; set; } = false;
/// <summary>
/// Timestamp del momento in cui il bot è stato associato all'asset e bloccato.
/// Null se il bot non è ancora stato associato a un asset.
/// </summary>
public DateTime? LockedAt { get; set; } = null;
/// <summary>Bot attivo o sospeso</summary>
public bool IsEnabled { get; set; } = true;
/// <summary>Configurazione della strategia</summary>
public BotConfiguration Config { get; set; } = new BotConfiguration();
/// <summary>Data e ora di creazione del bot</summary>
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>Note libere dell'utente</summary>
public string Notes { get; set; } = "";
/// <summary>Colore badge nella UI (hex, es. "#00E676")</summary>
public string BadgeColor { get; set; } = "#00E676";
/// <summary>
/// Blocca il bot associandolo definitivamente all'asset corrente.
/// Dopo questa operazione, Symbol, AssetClass e Strategy diventano immutabili.
/// </summary>
public void LockToAsset()
{
if (!IsAssetLocked && !string.IsNullOrWhiteSpace(Symbol))
{
IsAssetLocked = true;
LockedAt = DateTime.Now;
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Newtonsoft.Json;
namespace DesktopBot.Models
{
/// <summary>
/// Gestisce la persistenza delle istanze di bot in un file JSON locale.
/// Percorso: %AppData%\TradingBot\bots.json
/// </summary>
public static class BotInstanceStore
{
private static readonly string StorePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"TradingBot",
"bots.json"
);
/// <summary>Carica tutti i bot salvati dal disco. Ritorna lista vuota se il file non esiste.</summary>
public static List<BotInstance> Load()
{
try
{
if (!File.Exists(StorePath))
return new List<BotInstance>();
var json = File.ReadAllText(StorePath, Encoding.UTF8);
return JsonConvert.DeserializeObject<List<BotInstance>>(json) ?? new List<BotInstance>();
}
catch
{
return new List<BotInstance>();
}
}
/// <summary>Salva tutti i bot su disco.</summary>
public static void Save(IEnumerable<BotInstance> bots)
{
try
{
var dir = Path.GetDirectoryName(StorePath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var json = JsonConvert.SerializeObject(new List<BotInstance>(bots), Formatting.Indented);
File.WriteAllText(StorePath, json, Encoding.UTF8);
}
catch
{
// Logging silenzioso: non bloccare la UI per errori di I/O
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
using System;
namespace DesktopBot.Models
{
/// <summary>
/// Modello per un log del bot
/// </summary>
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;
}
}
/// <summary>
/// Livello di log
/// </summary>
public enum LogLevel
{
Info,
Success,
Warning,
Error
}
}
+47
View File
@@ -0,0 +1,47 @@
using System;
namespace DesktopBot.Models
{
/// <summary>
/// Record di un trade (aperto o chiuso) effettuato dal bot.
/// </summary>
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; }
/// <summary>PnL percentuale sull'investimento iniziale.</summary>
public decimal PnLPercent =>
EntryPrice > 0 && Quantity > 0
? PnL / (EntryPrice * Quantity) * 100m
: 0m;
/// <summary>
/// Badge testuale: APERTA / +X.XX% / -X.XX%
/// </summary>
public string PnLBadge =>
IsOpen
? "APERTA"
: $"{PnL:+0.00;-0.00;0.00} ({PnLPercent:+0.0;-0.0;0.0}%)";
/// <summary>Stringa di stato colorabile via DataTrigger.</summary>
public string Status => IsOpen ? "APERTA" : (PnL >= 0 ? "CHIUSA +" : "CHIUSA -");
/// <summary>
/// Categoria PnL per i DataTrigger XAML ("open" / "profit" / "loss").
/// </summary>
public string PnLCategory =>
IsOpen ? "open" :
PnL > 0 ? "profit" : "loss";
}
}
+56
View File
@@ -0,0 +1,56 @@
using System;
namespace DesktopBot.Models
{
/// <summary>
/// Configurazione globale per i limiti di memorizzazione dei log e dati storici.
/// I valori sono conservativamente alti per mantenere il massimo di informazioni possibili.
/// </summary>
public class LoggingConfiguration
{
/// <summary>
/// Numero massimo di elementi nel log del bot (BotLog).
/// Default: 5000 (mantiene ~8-10 ore di trading con CheckIntervalSeconds=60).
/// </summary>
public int MaxBotLogEntries { get; set; } = 5000;
/// <summary>
/// Numero massimo di elementi nello storico trade (TradeHistory).
/// Default: 2000 (mantiene mesi di operazioni).
/// </summary>
public int MaxTradeHistoryEntries { get; set; } = 2000;
/// <summary>
/// Numero massimo di elementi nel log attività della dashboard (ActivityLog).
/// Default: 5000 (cronologia completa della sessione di trading).
/// </summary>
public int MaxActivityLogEntries { get; set; } = 5000;
/// <summary>
/// Numero massimo di elementi nel log live globale (LiveLog).
/// Default: 10000 (log dettagliato completo di tutte le operazioni).
/// </summary>
public int MaxLiveLogEntries { get; set; } = 10000;
/// <summary>
/// Numero massimo di punti dati nel grafico dei prezzi (PriceData).
/// Default: 3000 (mantiene ore di dati a 1min, giorni a 15min).
/// </summary>
public int MaxPriceDataPoints { get; set; } = 3000;
/// <summary>
/// Clona la configurazione corrente.
/// </summary>
public LoggingConfiguration Clone()
{
return new LoggingConfiguration
{
MaxBotLogEntries = this.MaxBotLogEntries,
MaxTradeHistoryEntries = this.MaxTradeHistoryEntries,
MaxActivityLogEntries = this.MaxActivityLogEntries,
MaxLiveLogEntries = this.MaxLiveLogEntries,
MaxPriceDataPoints = this.MaxPriceDataPoints
};
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using System;
using Alpaca.Markets;
namespace DesktopBot.Models
{
/// <summary>
/// Segnale di trading generato da una strategia.
/// </summary>
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; }
/// <summary>Confidenza del segnale (0100).</summary>
public int Confidence { get; set; }
/// <summary>
/// Livello di Stop Loss calcolato dalla strategia (0 = non impostato,
/// viene usato il default percentuale di BotConfiguration).
/// </summary>
public decimal StopLoss { get; set; }
/// <summary>
/// Livello di Take Profit calcolato dalla strategia (0 = non impostato).
/// </summary>
public decimal TakeProfit { get; set; }
/// <summary>Lato dell'ordine Alpaca derivato dal tipo di segnale.</summary>
public OrderSide Side => Type == SignalType.Sell ? OrderSide.Sell : OrderSide.Buy;
}
public enum SignalType
{
None,
Buy,
Sell,
Hold
}
}
+52
View File
@@ -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
//<UICulture>CultureYouAreCodingWith</UICulture> nel file .csproj
//all'interno di un <PropertyGroup>. Ad esempio, se si utilizza l'inglese (Stati Uniti)
//nei file di origine, impostare <UICulture> 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")]
+71
View File
@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace DesktopBot.Properties
{
/// <summary>
/// Classe di risorse fortemente tipizzata per la ricerca di stringhe localizzate e così via.
/// </summary>
// 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()
{
}
/// <summary>
/// Restituisce l'istanza di ResourceManager memorizzata nella cache e usata da questa classe.
/// </summary>
[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;
}
}
/// <summary>
/// Esegue l'override della proprietà CurrentUICulture del thread corrente per tutte
/// le ricerche di risorse che utilizzano questa classe di risorse fortemente tipizzata.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}
+117
View File
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+30
View File
@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
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;
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>
+101
View File
@@ -0,0 +1,101 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopBot.Services
{
/// <summary>
/// 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.
/// </summary>
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<PingResult> 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
{
/// <summary>Latenza in millisecondi. -1 se non raggiungibile.</summary>
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 }
}
+541
View File
@@ -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
{
/// <summary>
/// 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)
/// </summary>
/// <summary>
/// Implementazione locale di IBar per dati crypto deserializzati dall'API REST.
/// </summary>
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";
/// <summary>Contatore e rate-limiter per le chiamate API Alpaca.</summary>
public ApiCallCounterService ApiCounter { get; } = new ApiCallCounterService();
/// <summary>
/// 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).
/// </summary>
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;
}
/// <summary>
/// Recupera l'equity disponibile del conto.
/// </summary>
public async Task<decimal> GetAvailableEquityAsync()
{
EnsureInitialized();
await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetAvailableEquity").ConfigureAwait(false);
IAccount account = await _tradingClient.GetAccountAsync();
return account.Equity ?? 0m;
}
/// <summary>
/// Recupera le informazioni complete del conto.
/// </summary>
public async Task<IAccount> GetAccountAsync()
{
EnsureInitialized();
await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetAccount").ConfigureAwait(false);
return await _tradingClient.GetAccountAsync();
}
/// <summary>
/// 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.
/// </summary>
public async Task<List<IBar>> 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<DateTime>(startTime, endTime);
var request = new HistoricalBarsRequest(symbol, timeframe, interval)
{
Feed = MarketDataFeed.Iex
};
IPage<IBar> page = await _dataClient.ListHistoricalBarsAsync(request).ConfigureAwait(false);
return page.Items.ToList();
}
/// <summary>
/// Recupera barre storiche crypto tramite chiamata diretta REST
/// GET /v1beta3/crypto/us/bars con paginazione automatica.
/// </summary>
private async Task<List<IBar>> 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<IBar>();
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<string>();
JToken barsToken = root["bars"]?[apiSymbol];
if (barsToken != null)
{
foreach (JToken b in barsToken)
{
allBars.Add(new SimpleBar
{
Symbol = apiSymbol,
TimeUtc = b["t"].Value<DateTime>().ToUniversalTime(),
Open = b["o"].Value<decimal>(),
High = b["h"].Value<decimal>(),
Low = b["l"].Value<decimal>(),
Close = b["c"].Value<decimal>(),
Volume = b["v"].Value<decimal>(),
Vwap = b["vw"] != null ? b["vw"].Value<decimal>() : 0m,
TradeCount = b["n"] != null ? (ulong)b["n"].Value<long>() : 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);
}
/// <summary>
/// Piazza un ordine bracket con Take Profit e Stop Loss.
/// Conforme alla documentazione Alpaca: MarketOrder + TakeProfit + StopLoss.
/// </summary>
public async Task<IOrder> 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<IOrder> 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<IOrder> 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<IOrder> 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);
}
/// <summary>
/// Verifica se esiste una posizione aperta per un simbolo.
/// Il codice 40410000 indica assenza di posizione (non è un errore reale).
/// </summary>
public async Task<bool> 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;
}
}
/// <summary>
/// Recupera la posizione aperta per un simbolo specifico.
/// Ritorna null se non esiste (codice 40410000 = no position).
/// </summary>
public async Task<IPosition> 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;
}
}
/// <summary>
/// Recupera tutte le posizioni aperte.
/// </summary>
public async Task<IReadOnlyList<IPosition>> GetAllPositionsAsync()
{
EnsureInitialized();
await ApiCounter.ThrottleAsync(ApiCategory.Trading, "GetAllPositions").ConfigureAwait(false);
return await _tradingClient.ListPositionsAsync();
}
/// <summary>
/// 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.
/// </summary>
public async Task<decimal> 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;
}
/// <summary>
/// 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.
/// </summary>
public async Task<IReadOnlyList<IPosition>> 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<IPosition>();
// 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<IOrder> 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<string>(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<IPosition>();
foreach (var p in allPositions)
{
if (botSymbols.Contains(p.Symbol))
botPositions.Add(p);
}
return botPositions;
}
/// <summary>
/// Chiude tutte le posizioni aperte.
/// </summary>
public async Task CloseAllPositionsAsync()
{
EnsureInitialized();
await ApiCounter.ThrottleAsync(ApiCategory.Trading, "CloseAllPositions").ConfigureAwait(false);
await _tradingClient.DeleteAllPositionsAsync();
}
public async Task<IReadOnlyList<IOrder>> 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);
}
/// <summary>
/// Chiude una singola posizione per simbolo (DELETE /v2/positions/{symbol}).
/// Ignora silenziosamente se la posizione non esiste già (già chiusa).
/// </summary>
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
}
}
/// <summary>
/// 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.
/// </summary>
public async Task<List<AssetSearchResult>> SearchAssetsAsync(string query, int maxResults = 20)
{
EnsureInitialized();
if (string.IsNullOrWhiteSpace(query))
return new List<AssetSearchResult>();
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();
}
/// <summary>
/// Verifica che il servizio sia stato inizializzato
/// </summary>
private void EnsureInitialized()
{
if (!_isInitialized)
{
throw new InvalidOperationException("Il servizio non è stato inizializzato. Chiamare Initialize() prima di usarlo.");
}
}
}
}
@@ -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
{
/// <summary>
/// Categoria di chiamata API verso Alpaca.
/// </summary>
public enum ApiCategory
{
/// <summary>Trading API: ordini, posizioni, conto (paper-api / api.alpaca.markets)</summary>
Trading,
/// <summary>Market Data API: barre storiche, prezzi real-time (data.alpaca.markets)</summary>
MarketData
}
/// <summary>
/// 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
/// </summary>
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<DateTime> _marketDataTimestamps = new ConcurrentQueue<DateTime>();
private readonly ConcurrentQueue<DateTime> _tradingTimestamps = new ConcurrentQueue<DateTime>();
// ── 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;
/// <summary>Chiamate Market Data API negli ultimi 60 secondi.</summary>
public int MarketDataRpm
{
get => _marketDataRpm;
private set { if (_marketDataRpm != value) { _marketDataRpm = value; OnPropertyChanged(); OnPropertyChanged(nameof(MarketDataUsagePercent)); } }
}
private int _tradingRpm;
/// <summary>Chiamate Trading API negli ultimi 60 secondi.</summary>
public int TradingRpm
{
get => _tradingRpm;
private set { if (_tradingRpm != value) { _tradingRpm = value; OnPropertyChanged(); OnPropertyChanged(nameof(TradingUsagePercent)); } }
}
/// <summary>Totale cumulativo chiamate Market Data dalla creazione.</summary>
public long TotalMarketDataCalls => Interlocked.Read(ref _totalMarketData);
/// <summary>Totale cumulativo chiamate Trading dalla creazione.</summary>
public long TotalTradingCalls => Interlocked.Read(ref _totalTrading);
/// <summary>Totale chiamate rallentate per rate-limit.</summary>
public long ThrottledCalls => Interlocked.Read(ref _throttledCalls);
/// <summary>Percentuale utilizzo finestra 60s per Market Data (0100).</summary>
public double MarketDataUsagePercent => MarketDataRpm * 100.0 / MarketDataRpmLimit;
/// <summary>Percentuale utilizzo finestra 60s per Trading (0100).</summary>
public double TradingUsagePercent => TradingRpm * 100.0 / TradingRpmLimit;
/// <summary>True se Market Data è sopra il 90% del limite.</summary>
public bool IsMarketDataNearLimit => MarketDataRpm >= MarketDataRpmLimit * 0.9;
/// <summary>True se Trading è sopra il 90% del limite.</summary>
public bool IsTradingNearLimit => TradingRpm >= TradingRpmLimit * 0.9;
// ── Statistiche per categoria ───────────────────────────────────────────
private readonly ConcurrentDictionary<string, long> _callsByEndpoint =
new ConcurrentDictionary<string, long>(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 ────────────────────────────────────────────────────────
/// <summary>
/// Attende se necessario rispettando il rate limit, poi registra la chiamata.
/// Deve essere chiamato PRIMA di ogni richiesta HTTP verso Alpaca.
/// </summary>
/// <param name="category">Categoria della chiamata.</param>
/// <param name="endpoint">Nome endpoint per statistiche (es. "GetAccount").</param>
/// <param name="cancellationToken">Token di cancellazione.</param>
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();
}
/// <summary>
/// Restituisce le statistiche per endpoint (nome → conteggio totale).
/// </summary>
public IReadOnlyDictionary<string, long> GetEndpointStats()
=> new Dictionary<string, long>(_callsByEndpoint);
/// <summary>
/// Azzera i contatori cumulativi (non il rate-limiter sliding).
/// </summary>
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<DateTime> 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));
}
}
+99
View File
@@ -0,0 +1,99 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace DesktopBot.Services
{
/// <summary>
/// Gestisce il salvataggio e il caricamento sicuro delle credenziali Alpaca.
/// Cifra con AES-256 usando una chiave derivata da MachineName + UserName.
/// </summary>
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);
}
}
/// <summary>Modello credenziali Alpaca</summary>
public class AlpacaCredentials
{
public string ApiKey { get; set; }
public string ApiSecret { get; set; }
public bool IsPaper { get; set; }
}
}
+119
View File
@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Alpaca.Markets;
using DesktopBot.Models;
namespace DesktopBot.Services
{
/// <summary>
/// Interfaccia per il servizio di trading - permette disaccoppiamento e testing
/// </summary>
public interface ITradingService
{
/// <summary>
/// Inizializza il client di trading con le credenziali API
/// </summary>
/// <param name="apiKey">Chiave API Alpaca</param>
/// <param name="apiSecret">Secret API Alpaca</param>
/// <param name="isPaper">True per Paper Trading, False per Live</param>
void Initialize(string apiKey, string apiSecret, bool isPaper);
/// <summary>
/// Recupera il saldo disponibile del conto
/// </summary>
Task<decimal> GetAvailableEquityAsync();
/// <summary>
/// Recupera le informazioni complete del conto (saldo, P&amp;L, buying power, ecc.)
/// </summary>
Task<IAccount> GetAccountAsync();
/// <summary>
/// Recupera le barre storiche (candele) per un simbolo
/// </summary>
/// <param name="symbol">Simbolo ticker (es. "AAPL")</param>
/// <param name="timeframe">Timeframe delle candele</param>
/// <param name="barCount">Numero di barre da recuperare (convertito in intervallo temporale appropriato)</param>
Task<List<IBar>> GetHistoricalBarsAsync(string symbol, BarTimeFrame timeframe, int barCount);
/// <summary>
/// Piazza un ordine bracket (con Take Profit e Stop Loss automatici).
/// NON supportato per crypto su Alpaca: usare PlaceMarketOrderAsync + SL/TP separati.
/// </summary>
Task<IOrder> PlaceBracketOrderAsync(string symbol, decimal quantity, decimal entryPrice,
decimal takeProfitPrice, decimal stopLossPrice, OrderSide side);
/// <summary>
/// Piazza un ordine market semplice (buy o sell). Usare per crypto.
/// </summary>
Task<IOrder> PlaceMarketOrderAsync(string symbol, decimal quantity, OrderSide side);
/// <summary>
/// Piazza un ordine stop-loss separato su una posizione aperta (per crypto).
/// </summary>
Task<IOrder> PlaceStopOrderAsync(string symbol, decimal quantity, decimal stopPrice, OrderSide side);
/// <summary>
/// Piazza un ordine limit (take-profit) separato su una posizione aperta (per crypto).
/// </summary>
Task<IOrder> PlaceLimitOrderAsync(string symbol, decimal quantity, decimal limitPrice, OrderSide side);
/// <summary>
/// Verifica se esiste una posizione aperta per un simbolo
/// </summary>
Task<bool> HasOpenPositionAsync(string symbol);
/// <summary>
/// Recupera tutte le posizioni aperte
/// </summary>
Task<IReadOnlyList<IPosition>> GetAllPositionsAsync();
/// <summary>
/// Recupera la posizione aperta per un simbolo specifico.
/// Ritorna null se non esiste nessuna posizione aperta per quel simbolo.
/// </summary>
Task<IPosition> GetPositionAsync(string symbol);
/// <summary>
/// Recupera l'ultimo prezzo di un simbolo
/// </summary>
Task<decimal> GetLatestPriceAsync(string symbol);
/// <summary>
/// Chiude tutte le posizioni aperte
/// </summary>
Task CloseAllPositionsAsync();
/// <summary>
/// Recupera gli ordini (aperti, chiusi o tutti)
/// </summary>
/// <param name="statusFilter">Filtro stato: Open, Closed, All</param>
/// <param name="limit">Numero massimo di ordini da recuperare</param>
Task<IReadOnlyList<IOrder>> GetOrdersAsync(OrderStatusFilter statusFilter = OrderStatusFilter.Open, int limit = 50);
/// <summary>
/// Cancella un ordine specifico per ID
/// </summary>
Task CancelOrderAsync(Guid orderId);
/// <summary>
/// Chiude una singola posizione aperta per simbolo
/// </summary>
Task ClosePositionAsync(string symbol);
/// <summary>
/// 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_".
/// </summary>
Task<IReadOnlyList<IPosition>> GetBotPositionsAsync();
/// <summary>
/// Cerca asset disponibili su Alpaca per simbolo o nome.
/// Ritorna al massimo <paramref name="maxResults"/> risultati tradabili.
/// </summary>
Task<List<AssetSearchResult>> SearchAssetsAsync(string query, int maxResults = 20);
}
}
+113
View File
@@ -0,0 +1,113 @@
using System;
namespace DesktopBot.Services
{
/// <summary>
/// Helper per determinare se un mercato è aperto in base all'asset class e all'orario locale.
///
/// Regole approssimative (senza chiamata API aggiuntiva):
/// - US Equity : LunVen 09:3016: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.
/// </summary>
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");
/// <summary>
/// Ritorna true se il mercato relativo all'asset class indicata è
/// presumibilmente aperto adesso.
/// </summary>
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: LunVen, 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;
}
/// <summary>
/// Testo descrittivo dello stato mercato per la UI.
/// </summary>
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";
}
/// <summary>
/// Colore suggerito per il badge stato mercato.
/// </summary>
public static string GetMarketStatusColor(string assetClass)
=> IsMarketOpen(assetClass) ? "#00E676" : "#FFC107";
/// <summary>
/// Secondi da attendere prima della prossima apertura (utile per sleep nel loop).
/// Ritorna 0 se il mercato è già aperto.
/// </summary>
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;
}
}
}
+414
View File
@@ -0,0 +1,414 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:DesktopBot.Converters">
<!-- ================================== -->
<!-- TRADING BOT - DARK THEME -->
<!-- Antracite / Smeraldo / Neon -->
<!-- ================================== -->
<!-- Colori Principali -->
<Color x:Key="BackgroundDarkColor">#FF121214</Color> <!-- Nero lavagna -->
<Color x:Key="BackgroundPanelColor">#FF1E1E24</Color> <!-- Antracite scuro -->
<Color x:Key="BackgroundCardColor">#FF27272F</Color> <!-- Card leggermente più chiaro -->
<Color x:Key="AccentGreenColor">#FF00E676</Color> <!-- Verde Smeraldo -->
<Color x:Key="AccentRedColor">#FFFF1744</Color> <!-- Rosso Neon -->
<Color x:Key="TextPrimaryColor">#FFEAEAEA</Color> <!-- Testo principale -->
<Color x:Key="TextSecondaryColor">#FF8888A0</Color> <!-- Testo secondario -->
<Color x:Key="BorderColor">#FF3A3A4A</Color> <!-- Bordi sottili -->
<Color x:Key="NavActiveColor">#FF2D2D38</Color> <!-- Nav item attivo -->
<!-- Brush -->
<SolidColorBrush x:Key="BackgroundDarkBrush" Color="{StaticResource BackgroundDarkColor}"/>
<SolidColorBrush x:Key="BackgroundPanelBrush" Color="{StaticResource BackgroundPanelColor}"/>
<SolidColorBrush x:Key="BackgroundCardBrush" Color="{StaticResource BackgroundCardColor}"/>
<SolidColorBrush x:Key="AccentGreenBrush" Color="{StaticResource AccentGreenColor}"/>
<SolidColorBrush x:Key="AccentRedBrush" Color="{StaticResource AccentRedColor}"/>
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimaryColor}"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondaryColor}"/>
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
<SolidColorBrush x:Key="NavActiveBrush" Color="{StaticResource NavActiveColor}"/>
<SolidColorBrush x:Key="TransparentBrush" Color="Transparent"/>
<!-- Converters -->
<conv:BoolVisibilityConverter x:Key="BoolVisibilityConverter"/>
<conv:InvertedBoolVisibilityConverter x:Key="InvertedBoolVisibilityConverter"/>
<conv:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
<conv:NotNullToVisibilityConverter x:Key="NotNullToVisibilityConverter"/>
<conv:EmptyStringToVisibilityConverter x:Key="EmptyStringToVisibilityConverter"/>
<conv:ZeroToCollapsedConverter x:Key="ZeroToCollapsedConverter"/>
<conv:InverseBoolConverter x:Key="InverseBoolConverter"/>
<!-- ================================== -->
<!-- STILI BASE -->
<!-- ================================== -->
<!-- Stile testo base -->
<Style x:Key="TextBase" TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="13"/>
</Style>
<!-- Titolo sezione -->
<Style x:Key="SectionTitle" TargetType="TextBlock" BasedOn="{StaticResource TextBase}">
<Setter Property="FontSize" Value="20"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,0,20"/>
</Style>
<!-- Label secondaria -->
<Style x:Key="LabelSecondary" TargetType="TextBlock" BasedOn="{StaticResource TextBase}">
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
<Setter Property="FontSize" Value="11"/>
</Style>
<!-- ================================== -->
<!-- STILI INPUT -->
<!-- ================================== -->
<Style x:Key="DarkTextBox" TargetType="TextBox">
<Setter Property="Background" Value="{StaticResource BackgroundDarkBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="CaretBrush" Value="{StaticResource AccentGreenBrush}"/>
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource AccentGreenBrush}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="DarkComboBox" TargetType="ComboBox">
<Setter Property="Background" Value="{StaticResource BackgroundDarkBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="{StaticResource BackgroundCardBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="Padding" Value="10,7"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter Property="Background" Value="{StaticResource NavActiveBrush}"/>
<Setter Property="Foreground" Value="{StaticResource AccentGreenBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource NavActiveBrush}"/>
<Setter Property="Foreground" Value="{StaticResource AccentGreenBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="28"/>
</Grid.ColumnDefinitions>
<!-- Testo selezionato -->
<ContentPresenter x:Name="ContentSite"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
Margin="10,8,0,8"
VerticalAlignment="Center"
HorizontalAlignment="Left"
IsHitTestVisible="False"/>
<!-- Freccia dropdown -->
<Path Grid.Column="1"
Data="M0,0 L4,5 L8,0 Z"
Fill="{StaticResource TextSecondaryBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<!-- Pulsante toggle invisibile sopra tutto -->
<ToggleButton Grid.ColumnSpan="2"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
Background="Transparent"
BorderThickness="0"
Focusable="False"/>
</Grid>
</Border>
<!-- Popup dropdown -->
<Popup x:Name="Popup"
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Slide">
<Grid MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border Background="{StaticResource BackgroundCardBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1"
CornerRadius="4">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsPresenter/>
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="{StaticResource AccentGreenBrush}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="{StaticResource AccentGreenBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ================================== -->
<!-- PULSANTI -->
<!-- ================================== -->
<!-- Pulsante principale Verde -->
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentGreenBrush}"/>
<Setter Property="Foreground" Value="#FF121214"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="20,10"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FF00FF88"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#FF00C060"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{StaticResource BorderBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Pulsante Pericolo Rosso -->
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Background" Value="{StaticResource AccentRedBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#FFFF4466"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- Pulsante Outline -->
<Style x:Key="OutlineButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="16,8"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource NavActiveBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource AccentGreenBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ================================== -->
<!-- CARD / PANEL -->
<!-- ================================== -->
<Style x:Key="Card" TargetType="Border">
<Setter Property="Background" Value="{StaticResource BackgroundCardBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="20"/>
</Style>
<!-- ================================== -->
<!-- NAV ITEM LATERALE -->
<!-- ================================== -->
<Style x:Key="NavItem" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="16,14"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Width" Value="200"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="6"
Margin="8,2"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource NavActiveBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Nav Item Attivo -->
<Style x:Key="NavItemActive" TargetType="Button" BasedOn="{StaticResource NavItem}">
<Setter Property="Background" Value="{StaticResource NavActiveBrush}"/>
<Setter Property="Foreground" Value="{StaticResource AccentGreenBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<!-- ================================== -->
<!-- LOG ITEM -->
<!-- ================================== -->
<Style x:Key="LogListView" TargetType="ListView">
<Setter Property="Background" Value="{StaticResource BackgroundDarkBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="FontSize" Value="12"/>
</Style>
<!-- ================================== -->
<!-- SEPARATORE -->
<!-- ================================== -->
<Style x:Key="DarkSeparator" TargetType="Separator">
<Setter Property="Background" Value="{StaticResource BorderBrush}"/>
<Setter Property="Margin" Value="16,8"/>
</Style>
<!-- ================================== -->
<!-- STRATEGY TILE -->
<!-- Tile cliccabile per selezione -->
<!-- strategia ottimizzata per asset -->
<!-- ================================== -->
<Style x:Key="StrategyTile" TargetType="Button">
<Setter Property="Background" Value="{StaticResource BackgroundCardBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
Padding="14,12">
<ContentPresenter HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource AccentGreenBrush}"/>
<Setter Property="Background" Value="#FF2E2E3A"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{StaticResource NavActiveBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Tile strategia ATTIVA (selezionata) -->
<Style x:Key="StrategyTileActive" TargetType="Button" BasedOn="{StaticResource StrategyTile}">
<Setter Property="BorderBrush" Value="{StaticResource AccentGreenBrush}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="Background" Value="#1A3A2A"/>
</Style>
<!-- Badge "RACCOMANDATO" inline -->
<Style x:Key="RecommendedBadge" TargetType="Border">
<Setter Property="Background" Value="#1A3A2A"/>
<Setter Property="CornerRadius" Value="3"/>
<Setter Property="Padding" Value="5,2"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!-- Badge "OTTIMIZZATO" verde neon -->
<Style x:Key="OptimizedBadge" TargetType="Border">
<Setter Property="Background" Value="#1A4A1A"/>
<Setter Property="BorderBrush" Value="{StaticResource AccentGreenBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="8,3"/>
</Style>
</ResourceDictionary>
+260
View File
@@ -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<BalanceRowViewModel> BalanceRows { get; }
= new ObservableCollection<BalanceRowViewModel>();
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<PositionViewModel> Positions { get; }
= new ObservableCollection<PositionViewModel>();
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<OrderViewModel> Orders { get; }
= new ObservableCollection<OrderViewModel>();
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);
}
}
}
}
+82
View File
@@ -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
{
/// <summary>
/// Base ViewModel con implementazione INotifyPropertyChanged
/// </summary>
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
/// <summary>
/// Implementazione semplice di ICommand
/// </summary>
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<object> execute, Func<object, bool> 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);
}
/// <summary>
/// Command senza parametri
/// </summary>
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<T> execute, Func<T, bool> 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);
}
}
@@ -0,0 +1,85 @@
using System;
using System.Windows.Input;
using DesktopBot.Engine;
using DesktopBot.Models;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
/// <summary>
/// ViewModel per la configurazione del bot
/// </summary>
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";
}
}
}
@@ -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<BotLogEntry> BotLog { get; } = new ObservableCollection<BotLogEntry>();
public ObservableCollection<BotTradeRecord> OpenPositions { get; } = new ObservableCollection<BotTradeRecord>();
public ObservableCollection<BotTradeRecord> TradeHistory { get; } = new ObservableCollection<BotTradeRecord>();
// 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;
/// <summary>
/// Indica se il bot è bloccato a un asset specifico (immutabile).
/// </summary>
public bool IsAssetLocked => Model.IsAssetLocked;
/// <summary>
/// Timestamp del blocco dell'asset.
/// </summary>
public string LockedAtLabel => Model.LockedAt?.ToString("dd/MM/yyyy HH:mm") ?? "---";
/// <summary>
/// Indica se la configurazione della strategia è bloccata.
/// </summary>
public bool IsConfigLocked => Model.Config.IsLocked;
/// <summary>
/// Aggiorna le proprietà relative al lock status.
/// Chiamato dopo l'associazione dell'asset.
/// </summary>
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<StrategyProfileViewModel> StrategyProfiles { get; }
= new ObservableCollection<StrategyProfileViewModel>();
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();
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
/// Polling periodico: aggiorna le posizioni ogni 30 secondi durante l'esecuzione del bot.
/// </summary>
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;
}
}
@@ -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
{
/// <summary>
/// ViewModel del pannello Bot Manager.
/// Contiene un unico bot BTC/USD fisso, precaricato all'avvio.
/// L'utente può soltanto avviarlo e fermarlo.
/// </summary>
public class BotsManagerViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
// ── Collezione (sempre 1 elemento: il bot BTC/USD fisso) ─────────────
public ObservableCollection<BotInstanceViewModel> Bots { get; }
= new ObservableCollection<BotInstanceViewModel>();
/// <summary>Shortcut diretto al bot fisso BTC/USD.</summary>
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.";
}
}
}
+379
View File
@@ -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
{
/// <summary>Voce del log attività mostrata nella dashboard.</summary>
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<BalanceRowViewModel> BalanceRows { get; } = new ObservableCollection<BalanceRowViewModel>();
public ObservableCollection<PositionViewModel> Positions { get; } = new ObservableCollection<PositionViewModel>();
public ObservableCollection<OrderViewModel> Orders { get; } = new ObservableCollection<OrderViewModel>();
public ObservableCollection<ActivityLogEntry> ActivityLog { get; } = new ObservableCollection<ActivityLogEntry>();
/// <summary>
/// Statistiche e rate-limiter per le chiamate API Alpaca.
/// Disponibile per il binding in dashboard (null se il servizio non è ancora inizializzato).
/// </summary>
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}");
}
/// <summary>Aggiunge una voce al log attività (thread-safe via Dispatcher, limiti parametrizzati).</summary>
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;
});
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More