Getting Started
What AQE does
Section titled “What AQE does”AlgoQuant Engine is the runtime that executes your strategy, manages insights, talks to brokers and datafeeds, and produces either:
- backtest artefacts on disk
- live session data for AlgoQuant Studio
- or both, depending on how you run it
At the centre of that flow is the strategy lifecycle:
on_startuniverseiniton_bargenerate_insightsinsight_pipelineon_teardown
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);}A blank strategy
Section titled “A blank strategy”Start with the smallest possible strategy surface. This gives you the runtime hooks without introducing alpha models or pipes yet.
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();This starter block shows the smallest complete path: define a strategy, connect a paper broker and datafeed, run the backtest, and inspect the resulting metrics. The backtest storage flow is explained later in this page.
What an insight is
Section titled “What an insight is”An insight is AQE’s trading intent object. It represents a potential or active trade and carries the information required to manage it through the runtime:
- side
- symbol
- confidence
- timeframe
- order type and entry details
- take-profit and stop-loss levels
- trailing stop gap
- fill and close information
- state history
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>)>,}You usually create an insight inside an alpha model or inside generate_insights(), then let the insight pipeline size it, add risk controls, and submit it.
Insights can also participate in a parent/child structure. This is useful when a primary insight spawns follow-up trade intents that should remain linked to the parent position or workflow.
How the engine works
Section titled “How the engine works”on_start
Section titled “on_start”Use on_start to register shared runtime setup:
- indicators
- alphas
- pipes
- risk settings
- warm-up bars
universe
Section titled “universe”universe() returns the symbols the strategy trades. AQE uses those symbols to load Asset metadata from the selected data/broker stack.
init() runs once per asset after universe loading. Use it for per-asset initialization such as:
- per-symbol variables
- symbol-specific indicator state
- asset-aware setup
on_bar
Section titled “on_bar”on_bar() is called each time a new bar arrives for a symbol. This is where you update strategy-level state from the latest market data.
generate_insights
Section titled “generate_insights”After on_bar(), AQE calls generate_insights(). This is where you create new insights and add them to the runtime.
insight_pipeline
Section titled “insight_pipeline”After bars are processed, AQE runs the insight pipeline. Pipes can size, validate, submit, reject, cancel, or close insights based on their current state.
Generating an insight
Section titled “Generating an insight”This is the simplest user-facing pattern: create an insight, set the key fields, and add it to the context.
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);}Putting it together with alpha models
Section titled “Putting it together with alpha models”Alpha models let you move signal generation out of the main strategy body. AQE calls their lifecycle in the same runtime:
start()init(asset)generate_insights(symbol)
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;}One real example is EmaPriceCrossover, which:
- registers ATR and EMA in
start() - reads history and asset metadata
- builds an
Insight - returns it through
AlphaResult
Putting it together with pipes
Section titled “Putting it together with pipes”Insight pipes run after insights already exist. They are useful for:
- market-entry conversion
- dynamic quantity sizing
- stop-loss and take-profit management
- trading windows
- expiry handling
- submission
pub trait InsightPipe { fn version(&self) -> &str; fn run(&mut self, ctx: &mut dyn StrategyContext, insight: &mut Insight) -> InsightPipeResult;}Typical composition for a new insight is:
- create insight
- set entry intent
- size quantity
- apply stop loss / take profit
- validate reward-to-risk or session rules
- submit the insight
Child insights
Section titled “Child insights”AQE supports parent/child insight relationships directly on the insight model:
parent_idchildren
That makes it possible to build workflows where one insight spawns another linked insight without losing lineage in state history and inspection.
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}In practice:
- the parent insight is created and managed normally
- the parent can attach child insights during strategy logic
- AQE queues those children for submission at the appropriate point in the runtime loop
- parent and child rows remain related through
parent_id
This is especially useful for:
- multi-leg follow-up logic
- staged entries
- derived trade intents spawned from an already active signal
Selecting a broker and datafeed
Section titled “Selecting a broker and datafeed”AQE currently exposes:
- execution broker:
PaperBroker
- datafeed:
YahooFinanceDataFeed
Those are combined through UnifiedBroker.
let execution = PaperBroker::new(100_000.0);let data = YahooFinanceDataFeed::new();let broker = UnifiedBroker::new_backtest(execution, data);For live mode, the same strategy-facing runtime is used, but the engine runs through run_live(...) instead of run_backtest(...).
Running in backtest mode
Section titled “Running in backtest mode”The backtest runner follows this flow:
// 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 BacktestResultsGenerated runs save artefacts to disk under:
backtests/<run_id>/backtest.db
AQE writes the SQLite artifact with:
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(())}You can inspect those results in:
- AlgoQuant Studio, through the Backtest Results views
- any SQLite reader, if you want to inspect
backtest.dbdirectly
Running live
Section titled “Running live”Live mode uses the same strategy/runtime structure but subscribes to live data and trade updates.
You can run AQE live:
- with AQS, by passing auth and enabling live sync
- without AQS, by running
run_live(None)
if let Err(e) = state.run_live(auth).await { eprintln!("Live execution failed: {:?}", e); std::process::exit(1);}When auth is provided, AQE writes live state into AQS-scoped tables such as:
insightsstrategy_accountsstrategy_equity_pointsstrategy_live_metricsstrategy_events
When auth is omitted, AQE can still run live locally without synchronizing into AQS.
Read next
Section titled “Read next”- Insights for the full lifecycle and state model.
- Brokers & Datafeeds for current integrations.
- Alpha Models for signal generation patterns.
- Insight Pipes for sizing, validation, and trade management.