Skip to content

Lifecycle Components

Lifecycle components let you move strategy lifecycle code out of a large inline Strategy implementation and into smaller AQE components. You can still write all of this logic directly inside on_start, init, on_teardown, and universe; the component model exists to make projects easier to segment, test, reuse, and expose in AQS.

Use lifecycle components when a block of logic has its own responsibility, configuration, failure policy, or reuse value.

AQE owns a StrategyState and runs the strategy in a predictable order:

  1. on_start(ctx) for one-time startup.
  2. universe(ctx) for static or inline symbol selection.
  3. registered universe models for modular symbol selection.
  4. asset metadata loading for the final merged universe.
  5. init(ctx, asset) once for each tradable asset.
  6. per-bar history updates, indicators, alpha models, strategy callbacks, and insight pipes.
  7. on_teardown(ctx) for shutdown cleanup.

Lifecycle logic blocks wrap the generated or inline strategy lifecycle methods. Universe models contribute symbols to the final universe.

AQE exposes one trait per lifecycle phase:

aq-engine/src/core/lifecycle/mod.rs
pub trait OnStartLogic {
fn version(&self) -> &str;
fn run(&mut self, ctx: &mut dyn StrategyContext) -> LifecycleResult;
}
pub trait OnInitLogic {
fn version(&self) -> &str;
fn run(&mut self, ctx: &mut dyn StrategyContext, asset: &Asset) -> LifecycleResult;
}
pub trait OnTeardownLogic {
fn version(&self) -> &str;
fn run(&mut self, ctx: &mut dyn StrategyContext) -> LifecycleResult;
}

Each run method returns LifecycleResult, which records success, an optional message, and component metadata. A failed component with can_fail(false) stops the run; a failed component with can_fail(true) logs the failure and lets the run continue.

Default failure policy:

  • OnStartLogic: strict by default.
  • OnInitLogic: strict by default.
  • OnTeardownLogic: tolerant by default, because cleanup should continue where possible.

Lifecycle wrappers use LifecycleTiming to choose where they run around the strategy method body:

LifecycleTiming::BeforeGenerated
LifecycleTiming::AfterGenerated

In generated AQS projects, “generated” means the method body generated from the graph. In hand-written AQE projects, it is the inline body you wrote in the Strategy implementation.

Use BeforeGenerated for validation, environment setup, state seeding, and dependencies that the strategy body expects. Use AfterGenerated for follow-up work, registration checks, and metadata that depends on the strategy body having run.

Direct AQE projects register lifecycle logic on StrategyState before starting the run. The engine calls the components automatically; user strategy code should not call lifecycle run helpers manually.

let mut state = StrategyState::new(
"My Strategy".to_string(),
"1.0".to_string(),
MyStrategy {},
broker,
StrategyMode::Backtest,
timeframe,
);
state.add_on_start_logic(
OnStartLogicBuilder::new(Box::new(SeedRuntimeState::new()))
.timing(LifecycleTiming::BeforeGenerated)
.can_fail(false)
.build(),
);
state.add_on_init_logic(
OnInitLogicBuilder::new(Box::new(InitAssetState::new()))
.timing(LifecycleTiming::AfterGenerated)
.can_fail(false)
.build(),
);
state.add_on_teardown_logic(
OnTeardownLogicBuilder::new(Box::new(FlushRuntimeLogs::new()))
.timing(LifecycleTiming::AfterGenerated)
.can_fail(true)
.build(),
);
use aq_engine::core::lifecycle::{LifecycleResult, OnStartLogic};
use aq_engine::core::strategy::StrategyContext;
use serde_json::json;
pub struct SeedRuntimeState;
impl SeedRuntimeState {
pub fn new() -> Self {
Self
}
}
impl OnStartLogic for SeedRuntimeState {
fn version(&self) -> &str {
"1.0"
}
fn run(&mut self, ctx: &mut dyn StrategyContext) -> LifecycleResult {
ctx.variables().insert(
self.name().to_string(),
json!({
"started": true,
"symbols_seen": 0
}),
);
LifecycleResult::passed(self.name().to_string())
}
}

A universe model is a modular symbol selector. It returns symbols through UniverseResult:

aq-engine/src/core/universe/mod.rs
pub trait UniverseModel {
fn version(&self) -> &str;
fn run(&mut self, ctx: &mut dyn StrategyContext) -> UniverseResult;
}

AQE merges the symbols returned by Strategy::universe(ctx) and every registered universe model. The final universe is a de-duplicated union. That means a static universe can remain inline while one or more universe models add symbols from watchlists, filters, account rules, or date-aware selection logic.

state.add_universe_model(
UniverseModelBuilder::new(Box::new(MyUniverseModel::new()))
.can_fail(false)
.build(),
);

Example:

use aq_engine::core::strategy::StrategyContext;
use aq_engine::core::universe::{UniverseModel, UniverseResult};
use std::collections::HashSet;
pub struct CryptoMajors;
impl CryptoMajors {
pub fn new() -> Self {
Self
}
}
impl UniverseModel for CryptoMajors {
fn version(&self) -> &str {
"1.0"
}
fn run(&mut self, _ctx: &mut dyn StrategyContext) -> UniverseResult {
let symbols = ["BTCUSD", "ETHUSD"]
.into_iter()
.map(str::to_string)
.collect::<HashSet<_>>();
UniverseResult::passed(symbols, self.name().to_string())
}
}

Inline lifecycle code is still valid:

  • put simple startup registration in Strategy::on_start;
  • put simple per-asset setup in Strategy::init;
  • return a fixed symbol set from Strategy::universe;
  • place one-off cleanup in Strategy::on_teardown.

Prefer lifecycle components and universe models when the code has a clear module boundary, needs to be visible in AQS, has its own constructor configuration, or should be reused across strategies.

Lifecycle components receive the same StrategyContext as strategies, alpha models, and insight pipes. Use ctx.variables() for shared runtime state that should not become AQS configuration.

Good uses for ctx.variables():

  • alpha model state seeded during startup;
  • per-symbol metadata initialized in OnInitLogic;
  • cached universe metadata;
  • counters, timestamps, and runtime-only flags;
  • diagnostics needed by teardown or result review.

Keep user-tuned inputs in constructor arguments. Keep mutable runtime bookkeeping in ctx.variables() so the AQS property panel stays focused on real configuration.