Skip to content

Getting Started

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_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);
}

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.

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
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>)>,
}

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.

Use on_start to register shared runtime setup:

  • indicators
  • alphas
  • pipes
  • risk settings
  • warm-up bars

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() is called each time a new bar arrives for a symbol. This is where you update strategy-level state from the latest market data.

After on_bar(), AQE calls generate_insights(). This is where you create new insights and add them to the runtime.

After bars are processed, AQE runs the insight pipeline. Pipes can size, validate, submit, reject, cancel, or close insights based on their current state.

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);
}

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)
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;
}

One real example is EmaPriceCrossover, which:

  • registers ATR and EMA in start()
  • reads history and asset metadata
  • builds an Insight
  • returns it through AlphaResult

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
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;
}

Typical composition for a new insight is:

  1. create insight
  2. set entry intent
  3. size quantity
  4. apply stop loss / take profit
  5. validate reward-to-risk or session rules
  6. submit the insight

AQE supports parent/child insight relationships directly on the insight model:

  • parent_id
  • children

That makes it possible to build workflows where one insight spawns another linked insight without losing lineage in state history and 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
}

In practice:

  1. the parent insight is created and managed normally
  2. the parent can attach child insights during strategy logic
  3. AQE queues those children for submission at the appropriate point in the runtime loop
  4. 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

AQE currently exposes:

  • execution broker:
    • PaperBroker
  • datafeed:
    • YahooFinanceDataFeed

Those are combined through 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);

For live mode, the same strategy-facing runtime is used, but the engine runs through run_live(...) instead of run_backtest(...).

The backtest runner follows this flow:

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

Generated runs save artefacts to disk under:

  • backtests/<run_id>/backtest.db

AQE writes the SQLite artifact with:

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(())
}

You can inspect those results in:

  • AlgoQuant Studio, through the Backtest Results views
  • any SQLite reader, if you want to inspect backtest.db directly

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)
aq-engine/src/node/codegen.rs
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:

  • insights
  • strategy_accounts
  • strategy_equity_points
  • strategy_live_metrics
  • strategy_events

When auth is omitted, AQE can still run live locally without synchronizing into AQS.

  1. Insights for the full lifecycle and state model.
  2. Brokers & Datafeeds for current integrations.
  3. Alpha Models for signal generation patterns.
  4. Insight Pipes for sizing, validation, and trade management.