Aller au contenu

Mise en route

AlgoQuant Engine est le runtime qui exécute votre stratégie, gère les insights, communique avec les courtiers et datafeeds, et produit soit :

  • des artefacts de backtest sur disque
  • des données de session live pour AlgoQuant Studio
  • ou les deux, selon votre mode d’exécution

Au centre de ce flux se trouve le cycle de vie de stratégie :

  • on_start
  • universe
  • init
  • on_bar
  • generate_insights
  • insight_pipeline
  • on_teardown
aq-engine/src/core/strategy/traits.rs
pub trait Strategy {
fn on_start(&mut self, ctx: &mut dyn StrategyContext);
fn init(&mut self, ctx: &mut dyn StrategyContext, asset: &Asset);
fn universe(&self, ctx: &mut dyn StrategyContext) -> HashSet<String>;
fn on_bar(&mut self, ctx: &mut dyn StrategyContext, symbol: &str, bar: &BarData);
fn generate_insights(&mut self, ctx: &mut dyn StrategyContext, symbol: &str);
fn insight_pipeline(&mut self, ctx: &mut dyn StrategyContext, insight: &Insight);
fn on_teardown(&mut self, ctx: &mut dyn StrategyContext);
}

Commencez avec la plus petite surface de stratégie possible. Elle donne les hooks runtime sans introduire encore de modèles alpha ni de pipes.

use aq_engine::core::broker::data_feeds::yahoo::YahooFinanceDataFeed;
use aq_engine::core::broker::paper_broker::PaperBroker;
use aq_engine::core::broker::UnifiedBroker;
use aq_engine::core::broker::types::{Asset, BarData};
use aq_engine::core::insight::Insight;
use aq_engine::core::strategy::{Strategy, StrategyContext, StrategyState};
use aq_engine::core::utils::timeframe::{TimeFrame, TimeFrameUnit};
use chrono::{Duration, Utc};
use std::collections::HashSet;
pub struct BlankStrategy;
impl Strategy for BlankStrategy {
fn on_start(&mut self, ctx: &mut dyn StrategyContext) {}
fn init(&mut self, ctx: &mut dyn StrategyContext, asset: &Asset) {}
fn universe(&self, ctx: &mut dyn StrategyContext) -> HashSet<String> {
HashSet::from([String::from("AAPL")])
}
fn on_bar(&mut self, ctx: &mut dyn StrategyContext, symbol: &str, bar: &BarData) {}
fn generate_insights(&mut self, ctx: &mut dyn StrategyContext, symbol: &str) {}
fn insight_pipeline(&mut self, ctx: &mut dyn StrategyContext, insight: &Insight) {}
fn on_teardown(&mut self, ctx: &mut dyn StrategyContext) {}
}
let execution = PaperBroker::new(100_000.0);
let data = YahooFinanceDataFeed::new();
let broker = UnifiedBroker::new_backtest(execution, data);
let timeframe = TimeFrame::new(1, TimeFrameUnit::Day);
let strategy = BlankStrategy;
let mut state = StrategyState::new(
"blank-strategy".to_string(),
"Blank Strategy".to_string(),
strategy,
broker,
timeframe.clone(),
);
let start = Utc::now() - Duration::days(30);
let end = Utc::now();
let results = state.run_backtest(start, end, timeframe).await?;
results.print_metrics();

Ce bloc de départ montre le plus petit chemin complet : définir une stratégie, connecter un paper broker et un datafeed, exécuter le backtest et inspecter les métriques produites. Le flux de stockage du backtest est expliqué plus loin dans cette page.

Un insight est l’objet d’intention de trading d’AQE. Il représente un trade potentiel ou actif et porte les informations nécessaires pour le gérer dans le runtime :

  • side
  • symbole
  • confiance
  • timeframe
  • type d’ordre et détails d’entrée
  • niveaux de take-profit et stop-loss
  • écart de trailing stop
  • informations de fill et de clôture
  • historique d’état
aq-engine/src/core/insight/insight.rs
pub struct Insight {
pub insight_id: Uuid,
pub state: InsightState,
pub order_id: Option<String>,
pub side: OrderSide,
pub symbol: String,
pub quantity: Option<f64>,
pub order_type: OrderType,
pub order_class: OrderClass,
pub limit_price: Option<f64>,
pub stop_price: Option<f64>,
pub take_profit_levels: Option<Vec<f64>>,
pub stop_loss_levels: Option<Vec<f64>>,
pub trailing_stop_price: Option<f64>,
pub confidence: u8,
pub timeframe: TimeFrame,
pub period_unfilled: Option<u32>,
pub period_till_tp: Option<u32>,
pub filled_price: Option<f64>,
pub close_price: Option<f64>,
pub state_history: Vec<(DateTime<Utc>, InsightState, Option<String>)>,
}

Vous créez généralement un insight dans un modèle alpha ou dans generate_insights(), puis vous laissez le pipeline d’insights le dimensionner, ajouter les contrôles de risque et le soumettre.

Les insights peuvent aussi participer à une structure parent/enfant. C’est utile quand un insight principal génère des intentions de trade de suivi qui doivent rester liées à la position ou au workflow parent.

Utilisez on_start pour enregistrer la configuration runtime partagée :

  • indicateurs
  • alphas
  • pipes
  • paramètres de risque
  • barres de warm-up

universe() retourne les symboles tradés par la stratégie. AQE utilise ces symboles pour charger les métadonnées Asset depuis la stack data/broker sélectionnée.

init() s’exécute une fois par actif après le chargement de l’univers. Utilisez-le pour l’initialisation par actif, par exemple :

  • variables par symbole
  • état d’indicateur propre au symbole
  • configuration consciente de l’actif

on_bar() est appelé à chaque nouvelle barre pour un symbole. C’est ici que vous mettez à jour l’état de stratégie depuis les dernières données de marché.

Après on_bar(), AQE appelle generate_insights(). C’est ici que vous créez de nouveaux insights et les ajoutez au runtime.

Après le traitement des barres, AQE exécute le pipeline d’insights. Les pipes peuvent dimensionner, valider, soumettre, rejeter, annuler ou clôturer les insights selon leur état courant.

Voici le pattern utilisateur le plus simple : créer un insight, définir les champs clés et l’ajouter au contexte.

use aq_engine::core::broker::types::OrderSide;
use aq_engine::core::insight::{types::StrategyType, Insight};
fn generate_insights(&mut self, ctx: &mut dyn StrategyContext, symbol: &str) {
let mut insight = Insight::new(
OrderSide::Buy,
symbol.to_string(),
StrategyType::Testing,
ctx.timeframe().clone(),
80,
None,
);
insight
.set_limit_price(Some(200.0))
.set_take_profit_levels(Some(vec![206.0]))
.set_stop_loss(Some(197.5))
.set_period_unfilled(Some(5))
.set_period_till_tp(Some(12));
ctx.add_insight(insight);
}

Les modèles alpha permettent de déplacer la génération de signaux hors du corps principal de la stratégie. AQE appelle leur cycle de vie dans le même runtime :

  • start()
  • init(asset)
  • generate_insights(symbol)
aq-engine/src/core/alpha/mod.rs
pub trait AlphaModel {
fn version(&self) -> &str;
fn start(&mut self, ctx: &mut dyn StrategyContext);
fn init(&mut self, ctx: &mut dyn StrategyContext, asset: &Asset);
fn generate_insights(&mut self, ctx: &mut dyn StrategyContext, symbol: &str) -> AlphaResult;
}

Un exemple réel est EmaPriceCrossover, qui :

  • enregistre ATR et EMA dans start()
  • lit l’historique et les métadonnées de l’actif
  • construit un Insight
  • le retourne via AlphaResult

Les insight pipes s’exécutent après la création des insights. Elles sont utiles pour :

  • la conversion d’entrée marché
  • le sizing dynamique de quantité
  • la gestion stop-loss et take-profit
  • les fenêtres de trading
  • la gestion de l’expiration
  • la soumission
aq-engine/src/core/pipeline/mod.rs
pub trait InsightPipe {
fn version(&self) -> &str;
fn run(&mut self, ctx: &mut dyn StrategyContext, insight: &mut Insight) -> InsightPipeResult;
}

Une composition typique pour un nouvel insight est :

  1. créer l’insight
  2. définir l’intention d’entrée
  3. dimensionner la quantité
  4. appliquer stop loss / take profit
  5. valider le reward-to-risk ou les règles de session
  6. soumettre l’insight

AQE prend en charge les relations parent/enfant directement sur le modèle d’insight :

  • parent_id
  • children

Cela permet de construire des workflows où un insight en génère un autre lié sans perdre la lignée dans l’historique d’état et l’inspection.

aq-engine/src/core/insight/insight.rs
pub fn add_child_insight(
&mut self,
mut child_insight: Insight,
_ctx: &mut dyn StrategyContext,
) -> &mut Self {
child_insight.strategy_type =
StrategyType::Custom(format!("{}-CHILD", self.strategy_type.to_string()));
child_insight.parent_id = Some(self.insight_id);
if child_insight.quantity.is_none() {
child_insight.quantity = self.quantity;
}
let child_id = child_insight.insight_id;
self.children.push(child_insight);
self.update_state(
self.state.clone(),
Some(format!("Added child insight: {:?}", child_id)),
);
self
}

En pratique :

  1. l’insight parent est créé et géré normalement
  2. le parent peut attacher des insights enfants pendant la logique de stratégie
  3. AQE met ces enfants en file pour les soumettre au bon moment dans la boucle runtime
  4. les lignes parent et enfant restent liées par parent_id

C’est particulièrement utile pour :

  • logique de suivi multi-jambes
  • entrées échelonnées
  • intentions de trade dérivées depuis un signal déjà actif

AQE expose actuellement :

  • courtier d’exécution :
    • PaperBroker
  • datafeed :
    • YahooFinanceDataFeed

Ils sont combinés via UnifiedBroker.

aq-engine/src/core/broker/mod.rs
let execution = PaperBroker::new(100_000.0);
let data = YahooFinanceDataFeed::new();
let broker = UnifiedBroker::new_backtest(execution, data);

En mode live, le même runtime côté stratégie est utilisé, mais le moteur passe par run_live(...) au lieu de run_backtest(...).

Le runner de backtest suit ce flux :

aq-engine/src/core/strategy/mod.rs
// 1. strategy.on_start(ctx)
// 2. load_universe() -> strategy.init per asset
// 3. alpha.start() per alpha
// 4. alpha.init(asset) per alpha per asset
// 5. broker.load_backtest_data()
// 6. loop: broker.step() -> _on_bar() -> run_insight_pipeline()
// 7. strategy.on_teardown(ctx)
// 8. return BacktestResults

Les exécutions générées enregistrent les artefacts sur disque sous :

  • backtests/<run_id>/backtest.db

AQE écrit l’artefact SQLite avec :

aq-engine/src/core/backtest_storage/mod.rs
pub async fn write_backtest_db(
dir_path: &Path,
results: &BacktestResults,
state: &BacktestState,
) -> Result<(), String> {
std::fs::create_dir_all(dir_path).map_err(to_storage_err)?;
let conn = connect_database(dir_path).await?;
init_schema(&conn).await?;
insert_trade_log(&conn, &results.trade_log).await?;
insert_round_trips(&conn, &round_trips).await?;
insert_trade_log_rows(&conn, &round_trips, &results.trade_log).await?;
insert_account_history(&conn, &results.account_history).await?;
insert_insights(&conn, &insights).await?;
insert_bars(&conn, &state.historical_bars).await?;
Ok(())
}

Vous pouvez inspecter ces résultats dans :

  • AlgoQuant Studio, via les vues Backtest Results
  • n’importe quel lecteur SQLite, si vous voulez inspecter backtest.db directement

Le mode live utilise la même structure stratégie/runtime, mais s’abonne aux données live et aux mises à jour de trade.

Vous pouvez exécuter AQE en live :

  • avec AQS, en passant l’authentification et en activant la synchronisation live
  • sans AQS, en exécutant run_live(None)
aq-engine/src/node/codegen.rs
if let Err(e) = state.run_live(auth).await {
eprintln!("Live execution failed: {:?}", e);
std::process::exit(1);
}

Quand l’authentification est fournie, AQE écrit l’état live dans des tables scoped AQS comme :

  • insights
  • strategy_accounts
  • strategy_equity_points
  • strategy_live_metrics
  • strategy_events

Quand l’authentification est omise, AQE peut toujours fonctionner en live localement sans synchroniser dans AQS.

  1. Insights pour le cycle de vie complet et le modèle d’état.
  2. Courtiers et flux de données pour les intégrations actuelles.
  3. Modèles alpha pour les patterns de génération de signaux.
  4. Pipelines d’insights pour le sizing, la validation et la gestion des trades.