API Reference¶
This API documentation covers the core classes and methods of AKQuant.
Quick links:
1. High-Level API¶
akquant.run_backtest¶
The most commonly used backtest entry function, encapsulating the initialization and configuration process of the engine.
def run_backtest(
data: Optional[BacktestDataInput] = None,
strategy: Union[Type[Strategy], Strategy, Callable[[Any, Bar], None], None] = None,
strategy_source: Optional[Union[str, bytes, os.PathLike[str]]] = None,
strategy_loader: Optional[str] = None,
strategy_loader_options: Optional[Dict[str, Any]] = None,
symbols: Optional[Union[str, List[str], Tuple[str, ...], set[str]]] = None,
initial_cash: Optional[float] = None,
commission_policy: Optional[CommissionPolicy] = None,
commission_rate: Optional[float] = None,
stamp_tax_rate: Optional[float] = None,
transfer_fee_rate: Optional[float] = None,
min_commission: Optional[float] = None,
slippage: SlippageInput = None,
volume_limit_pct: Optional[float] = None,
timezone: Optional[str] = None,
t_plus_one: bool = False,
initialize: Optional[Callable[[Any], None]] = None,
on_start: Optional[Callable[[Any], None]] = None,
on_resume: Optional[Callable[[Any], None]] = None,
on_train_signal: Optional[Callable[[Any], None]] = None,
on_stop: Optional[Callable[[Any], None]] = None,
on_tick: Optional[Callable[[Any, Any], None]] = None,
on_order: Optional[Callable[[Any, Any], None]] = None,
on_trade: Optional[Callable[[Any, Any], None]] = None,
on_reject: Optional[Callable[[Any, Any], None]] = None,
on_before_trading: Optional[Callable[[Any, Any, int], None]] = None,
on_after_trading: Optional[Callable[[Any, Any, int], None]] = None,
on_cross_section: Optional[Callable[[Any, Any, int], None]] = None,
on_portfolio_update: Optional[Callable[[Any, Dict[str, Any]], None]] = None,
on_error: Optional[Callable[[Any, Exception, str, Any], None]] = None,
on_expiry: Optional[Callable[[Any, Dict[str, Any]], None]] = None,
on_pre_open: Optional[Callable[[Any, Dict[str, Any]], None]] = None,
on_timer: Optional[Callable[[Any, str], None]] = None,
context: Optional[Dict[str, Any]] = None,
history_depth: Optional[int] = None,
warmup_period: int = 0,
lot_size: Union[int, Dict[str, int], None] = None,
show_progress: Optional[bool] = None,
start_time: Optional[Union[str, Any]] = None,
end_time: Optional[Union[str, Any]] = None,
catalog_path: Optional[str] = None,
config: Optional[BacktestConfig] = None,
custom_matchers: Optional[Dict[AssetType, Any]] = None,
risk_config: Optional[Union[Dict[str, Any], RiskConfig]] = None,
strategy_runtime_config: Optional[Union[StrategyRuntimeConfig, Dict[str, Any]]] = None,
runtime_config_override: bool = True,
strategy_id: Optional[str] = None,
strategies_by_slot: Optional[Dict[str, Union[Type[Strategy], Strategy, Callable[[Any, Bar], None]]]] = None,
strategy_max_order_value: Optional[Dict[str, float]] = None,
strategy_max_order_size: Optional[Dict[str, float]] = None,
strategy_max_position_size: Optional[Dict[str, float]] = None,
strategy_max_daily_loss: Optional[Dict[str, float]] = None,
strategy_max_drawdown: Optional[Dict[str, float]] = None,
strategy_reduce_only_after_risk: Optional[Dict[str, bool]] = None,
strategy_risk_cooldown_bars: Optional[Dict[str, int]] = None,
strategy_priority: Optional[Dict[str, int]] = None,
strategy_risk_budget: Optional[Dict[str, float]] = None,
strategy_fill_policy: Optional[Dict[str, FillMode]] = None,
strategy_slippage: Optional[Dict[str, SlippageInput]] = None,
strategy_commission: Optional[Dict[str, CommissionPolicy]] = None,
portfolio_risk_budget: Optional[float] = None,
risk_budget_mode: str = "order_notional",
risk_budget_reset_daily: bool = False,
analyzer_plugins: Optional[Sequence[AnalyzerPlugin]] = None,
on_event: Optional[Callable[[BacktestStreamEvent], None]] = None,
broker_profile: Optional[str] = None,
fill_policy: Optional[FillMode] = None,
strict_strategy_params: bool = True,
**kwargs: Any,
) -> BacktestResult
Key Parameters:
data: Backtest data. Supports a single DataFrame, a{symbol: DataFrame}dictionary,List[Bar],DataFeed, or any object implementingDataFeedAdapter.load(request).strategy: Strategy class or instance. Also supports passing anon_barfunction (functional style).strategy_source/strategy_loader/strategy_loader_options: Dynamic strategy loading entry points. Whenstrategy=None, the framework can build the strategy from source, a path, or a custom loader.initialize/on_start/on_resume/on_stop: Functional-strategy lifecycle callbacks for initialization, start, resume, and stop stages.on_tick/on_order/on_trade/on_reject/on_before_trading/on_after_trading/on_cross_section/on_portfolio_update/on_error/on_expiry/on_pre_open/on_timer/on_train_signal: Functional event callbacks.on_expiry(ctx, event)fires only after the engine actually executes expiry settlement/removal.symbols: Symbol or list of symbols. Defaults toNone(not passed explicitly): whatever symbols show up in the data are treated as the ones to run ("data is subscription"), and behavior is identical to before this parameter existed. Passing it explicitly — even a value that only lists symbols already present in the data — changes the semantics to "run only these symbols": symbols outside the whitelist are filtered out up front, never enter the engine, and never participate in matching or statistics (backtest results on multi-symbol input can therefore change). Passing an explicit empty collection raises an error rather than degrading to "no filtering". The whitelist is actually the union ofsymbols∪config.instruments∪ the symbols already subscribed viaself.subscribe()during__init__; oncesymbolsis passed explicitly, callingsubscribe()on a symbol outside the whitelist insideon_startraisesValueError(see theon_startnotes in the "Strategy Lifecycle" section of the Strategy Guide). This validation only applies to backtests: live trading (run_live) never ships a whitelist, sosubscribe()is unconstrained there.- Special case for
run_from_checkpoint: the third term of the whitelist union isn't limited to thesubscribe()calls made during this call's own__init__— a warm-started strategy instance carries_subscriptionsrestored from the pickled snapshot (i.e. every subscription accumulated during the previous run, including ones added insideon_start), and those are folded into the whitelist too. In other words, if stage one's strategy subscribed to a symbol, stage two's whitelist still includes that symbol as long as stage two passessymbols— even if stage two never callssubscribe()on it again. This is part of the warm start's overall "carry over previous state" semantics, and is not fully symmetric with the "__init__-stage subscriptions" rule that feedsrun_backtest's whitelist; don't assumerun_from_checkpoint's effective filtering matches whatrun_backtest's whitelist rule would predict. - Migration note (legacy
symbols="BENCHMARK"usage): before this change, the signature default forsymbol/symbolswas literally the string"BENCHMARK"(some warm-start examples in this repo used to pass it explicitly, too). After the upgrade, any explicitly passed value is always treated as a real filter — it is no longer equivalent to "not passed" — and different data shapes now fail in opposite directions: withList[Bar]/DataFeedinput, since no symbol in the data literally equals"BENCHMARK", the whitelist admits nothing and the backtest runs empty (with just one WARNING and otherwise silent); withDataFrame/Dict[str, DataFrame]input,"BENCHMARK"instead short-circuits the existing pre-filter check and disables filtering entirely. The fix is simple: remove the explicitsymbols="BENCHMARK"(omit the parameter) rather than migrating it to some other literal.
- Special case for
initial_cash: Initial cash. If omitted, it falls back toStrategyConfig.initial_cash, whose default is100000.0.commission_policy: Run-level default commission policy. Supported modes:{"type": "percent", "value": 0.0003}: commission as a percentage of turnover.{"type": "fixed", "value": 3.0}: a fixed amount charged on each fill.{"type": "per_unit", "value": 0.01}: charged linearly by filled quantity, i.e.fill_quantity * 0.01.- When explicitly provided, it takes precedence over
commission_rate;commission_rateremains as a backward-compatible shorthand for percent mode.
- Legacy price-basis parameter: Removed.
- Legacy timer-temporal parameter: Removed.
fill_policy: AFillModeobject expressing unified fill semantics. One of five named modes:NextOpen(): fill at the next bar's open (default; no look-ahead).NextClose(): fill at the next bar's close.NextAverage(): fill at the next bar's OHLC4 average.NextHighLowMid(): fill at the next bar's HL2 (high-low) midpoint.CurrentClose(): fill at the current bar's close. PassCurrentClose(timer_fill_timing="deferred")to defer timer-triggered fills to the next event (default"immediate").- The old dict form (
{"price_basis": ..., "bar_offset": ..., "temporal": ...}) andmake_fill_policy(...)are removed and now raiseTypeError.
legacy_execution_policy_compat(via**kwargs): Removed.- Migration hint: legacy execution parameters and the
fill_policydict are no longer accepted; pass aFillModeobject. strict_strategy_params: Whether to strictly validate strategy constructor parameters (defaultTrue).- Raises immediately if unsupported constructor parameters are provided.
- Recommended to keep enabled to avoid silent parameter mismatch and distorted backtest results.
t_plus_one: Enable T+1 trading rule (Default False). If enabled, it forces usage of China Market Model.slippage: Global slippage (Default 0.0). E.g., 0.0001 means 1bp (0.01%) slippage, using percent model.volume_limit_pct: Volume limit percentage (Default 0.25). Limits single trade to not exceed this percentage of the bar's total volume.warmup_period: Strategy warmup period. Specifies the length of historical data (number of Bars) to preload for indicator calculation.start_time/end_time: Backtest start/end time. Naive strings orTimestampvalues are interpreted in the currenttimezonebefore being converted to UTC for filtering.catalog_path: Whendatais omitted, load data from this directory usingParquetDataCatalogrules.config:BacktestConfigobject for centralized configuration.risk_config: Risk configuration. Supports dict (e.g.,{"max_position_pct": 0.1}) orRiskConfigobject. Overrides fields inconfig.strategy_config.riskif both are provided.strategy_runtime_config/runtime_config_override: Runtime behavior injection and conflict-resolution controls. AcceptsStrategyRuntimeConfigordict.strategy_id: Primary strategy ownership id. Default_default.strategies_by_slot: Optional multi-strategy mapping. Keys are slot ids and values are strategy class/instance/functional callback used by slot-iterative execution.strategy_max_order_size/strategy_max_order_value/strategy_max_position_size: Optional strategy-level risk maps keyed by strategy id.strategy_max_daily_loss/strategy_max_drawdown: Optional strategy-level stop maps keyed by strategy id.strategy_reduce_only_after_risk/strategy_risk_cooldown_bars: Optional post-risk behavior maps keyed by strategy id.strategy_priority/strategy_risk_budget/portfolio_risk_budget: Optional scheduling/budget controls.strategy_fill_policy: Optional strategy-level defaultFillModemap keyed by strategy id. Resolution order at submit time: order-levelfill_mode>strategy_fill_policy[strategy_id]> run-levelfill_policy.strategy_slippage: Optional strategy-level default slippage map keyed by strategy id. Resolution order at submit time: order-levelslippage>strategy_slippage[strategy_id]> run-levelslippage.strategy_commission: Optional strategy-level default commission map keyed by strategy id. Resolution order at submit time: order-levelcommission>strategy_commission[strategy_id]> run-level commission model.commission/strategy_commissionuse the sameCommissionPolicypayload as run-levelcommission_policy:
* `percent`: percentage of turnover.
* `fixed`: fixed amount per fill.
* `per_unit`: linear by filled quantity, suitable for per-share / per-lot / per-unit fee models.
- Configuration layers (recommended mental model):
1) order-level (
buy/sell/submit_orderargs); 2) strategy-map level (strategy_*, keyed bystrategy_id/slot); 3) run-level (run_backtestargs); 4) market defaults (built-in market-model defaults). - T+1 scope note:
t_plus_oneis currently a run/market-level switch, not a per-strategy_idlayered setting. risk_budget_mode/risk_budget_reset_daily: Risk budget accounting mode and reset policy.analyzer_plugins: Optional analyzer plugin list. Plugins receiveon_start/on_bar/on_trade/on_finishcallbacks and final outputs are stored inresult.analyzer_outputs.on_event: Optional stream callback. When omitted, an internal no-op callback keeps legacy blocking return semantics; when provided, runtime events are emitted.broker_profile: Optional broker template preset for quick defaults (fees/slippage/lot size). Built-ins:cn_stock_miniqmt,cn_stock_t1_low_fee,cn_stock_sim_high_slippage.
Recommended fill_policy examples (primary path):
# Next bar close fill
result = aq.run_backtest(
data=data,
strategy=MyStrategy,
symbols="000001",
fill_policy=aq.NextClose(),
)
# Current-close price, deferring timer-triggered fills to the next event
result = aq.run_backtest(
data=data,
strategy=MyStrategy,
symbols="000001",
fill_policy=aq.CurrentClose(timer_fill_timing="deferred"),
)
Execution semantics quick map:
| Scenario | fill_policy |
|---|---|
| Next-open style fill | aq.NextOpen() |
| Current-close style fill | aq.CurrentClose() |
| Next-bar close fill | aq.NextClose() |
| Next-bar OHLC average fill | aq.NextAverage() |
| Next-bar HL2 fill | aq.NextHighLowMid() |
Notes:
* NextOpen, NextAverage, and NextHighLowMid always fill on the next bar; there is no current-bar variant.
* NextClose() fills at the next bar's close; CurrentClose() fills at the current bar's close.
* timer_fill_timing is meaningful only for CurrentClose: it controls whether an on_timer-triggered fill happens in the same cycle ("immediate", default) or defers to the next event ("deferred"). It has no effect on plain bar orders.
* Order-level fills use the fill_mode= argument on buy/sell; run-level fills use the fill_policy= argument on run_backtest.
akquant.run_grid_search¶
Grid-search entry for batch backtesting and metric-based parameter ranking.
def run_grid_search(
strategy: Type[Strategy],
param_grid: Mapping[str, Sequence[Any]],
data: Any = None,
max_workers: Optional[int] = None,
sort_by: Union[str, List[str]] = "sharpe_ratio",
ascending: Union[bool, List[bool]] = False,
return_df: bool = True,
warmup_calc: Optional[Any] = None,
constraint: Optional[Any] = None,
result_filter: Optional[Any] = None,
timeout: Optional[float] = None,
max_tasks_per_child: Optional[int] = None,
db_path: Optional[str] = None,
forward_worker_logs: bool = False,
**kwargs: Any,
) -> Union[pd.DataFrame, List[OptimizationResult]]
Key parameter notes:
forward_worker_logs: Whether to forward worker-process strategy logs to the main process during parallel optimization.False: throughput-first; worker logs may be invisible in main-process output.True: enables log aggregation for debugging.
strict_strategy_params: Passed via**kwargsintorun_backtest(defaulted toTrueinsiderun_grid_search).- Enforces strict match between
param_gridkeys and strategy constructor parameters. - Fails fast on mismatch to avoid silent fallback.
- Enforces strict match between
akquant.run_walk_forward¶
Walk-forward entry. Executes rolling "in-sample optimization + out-of-sample validation" and concatenates OOS equity curves.
def run_walk_forward(
strategy: Type[Strategy],
param_grid: Mapping[str, Sequence[Any]],
data: pd.DataFrame,
train_period: int,
test_period: int,
metric: Union[str, List[str]] = "sharpe_ratio",
ascending: Union[bool, List[bool]] = False,
initial_cash: float = 100_000.0,
warmup_period: int = 0,
warmup_calc: Optional[Any] = None,
constraint: Optional[Any] = None,
result_filter: Optional[Any] = None,
compounding: bool = False,
timeout: Optional[float] = None,
max_tasks_per_child: Optional[int] = None,
**kwargs: Any,
) -> pd.DataFrame
Key parameter notes:
**kwargsare forwarded to bothrun_grid_search(in-sample optimization) andrun_backtest(out-of-sample validation).- Therefore,
forward_worker_logscontrols worker-log forwarding during in-sample parallel optimization. strict_strategy_paramsstays effective across optimization and validation phases (strict by default).
akquant.run_from_checkpoint¶
Resume a backtest from snapshot state and continue execution.
def run_from_checkpoint(
checkpoint_path: str,
data: Optional[BacktestDataInput] = None,
show_progress: bool = True,
symbols: Optional[Union[str, List[str], Tuple[str, ...], set[str]]] = None,
commission_policy: Optional[CommissionPolicy] = None,
strategy_runtime_config: Optional[Union[StrategyRuntimeConfig, Dict[str, Any]]] = None,
runtime_config_override: bool = True,
strategy_id: Optional[str] = None,
strategies_by_slot: Optional[Dict[str, Union[Type[Strategy], Strategy, Callable[[Any, Bar], None]]]] = None,
strategy_max_order_value: Optional[Dict[str, float]] = None,
strategy_max_order_size: Optional[Dict[str, float]] = None,
strategy_max_position_size: Optional[Dict[str, float]] = None,
strategy_max_daily_loss: Optional[Dict[str, float]] = None,
strategy_max_drawdown: Optional[Dict[str, float]] = None,
strategy_reduce_only_after_risk: Optional[Dict[str, bool]] = None,
strategy_risk_cooldown_bars: Optional[Dict[str, int]] = None,
strategy_priority: Optional[Dict[str, int]] = None,
strategy_risk_budget: Optional[Dict[str, float]] = None,
strategy_fill_policy: Optional[Dict[str, FillMode]] = None,
strategy_slippage: Optional[Dict[str, SlippageInput]] = None,
strategy_commission: Optional[Dict[str, CommissionPolicy]] = None,
portfolio_risk_budget: Optional[float] = None,
risk_budget_mode: str = "order_notional",
risk_budget_reset_daily: bool = False,
on_event: Optional[Callable[[BacktestStreamEvent], None]] = None,
config: Optional[BacktestConfig] = None,
**kwargs: Any,
) -> BacktestResult
run_from_checkpoint uses the same strategy-slot, strategy-level risk, and strategy-level execution defaults as run_backtest.
For these fields, priority is:
- explicit function arguments
config.strategy_config- restored/default values
DataFeedAdapter Usage (Multi-Timeframe):
import akquant as aq
base = aq.CSVFeedAdapter(path_template="/data/{symbol}.csv")
feed_15m = base.resample(freq="15min", emit_partial=False)
feed_replay = base.replay(
freq="1h",
align="session", # session | day | global
day_mode="trading", # effective only when align='day': trading | calendar
emit_partial=False,
session_windows=[("09:30", "11:30"), ("13:00", "15:00")], # session only
)
result = aq.run_backtest(
data=feed_replay,
strategy=MyStrategy,
symbols="000001",
show_progress=False,
)
align="session": Partition by trading day, optionally withsession_windows.align="day": Partition by day withoutsession_windows;day_modesupportstrading/calendar.align="global": Aggregate on the full timeline without day partitioning.- Parameter recommendation: always use
symbols.run_backtest/run_from_checkpointno longer acceptsymbol. - Migration status:
symbolhas been removed fromrun_backtest/run_from_checkpoint; migrate all calls tosymbols.
Compatibility & Migration Notes:
- Prefer migrating realtime UI/logging/alerting to
run_backtest(..., on_event=...). - Stream use cases are unified under
run_backtest(..., on_event=...). - Legacy execution policy compatibility gate has been removed.
- Legacy execution parameters and
legacy_execution_policy_compatare no longer accepted. - Use
fill_policyfor all public execution configuration. - Since Phase 5, runtime rollback flags are removed; use release-level rollback when needed.
Phase-5 Migration FAQ:
- Is
run_backtestrenamed? No, the public entry name stays unchanged. - Can
run_backteststill be called withouton_event? Yes, and result-return semantics stay the same. - How do we roll back in production? Use release-level rollback;
_engine_moderuntime fallback is removed. - Can we still use
symbol? No. Migrate tosymbols.
akquant.merge_results¶
def merge_results(
*results: BacktestResult,
drop_expired_instruments: bool = True,
dedupe_boundary: bool = True,
) -> MergedResult
Merges multiple BacktestResult segments produced by staged
run_from_checkpoint runs into one time-ordered MergedResult, exposing the
same read-only views as BacktestResult (equity_curve / cash_curve /
margin_curve / orders_df / trades_df / executions_df / positions_df /
daily_returns / to_quantstats).
Behavior:
- Curves and order/trade/execution/position frames are concatenated by
timestamp;
dedupe_boundary=Truedrops overlapping boundary timestamps (same-ts keeps the later segment, matching the engine upsert semantics). - Segments must be time-increasing and non-overlapping (gaps allowed);
overlapping segments raise
ValueError. drop_expired_instruments=Trueremoves position rows for instruments past their snapshotexpiry_date, preventing asset blow-up over long ranges.
Metrics are a core subset: MergedResult.metrics / metrics_df only
recompute metrics unambiguously derivable from the merged equity curve + trades
(total_return_pct / max_drawdown / sharpe_ratio / sortino_ratio /
calmar_ratio / volatility / annualized_return / win_rate /
profit_factor / end_market_value, matching the single-run definitions). Fields that
depend on engine-internal state are not provided and raise AttributeError; read
the full 60-field metrics from a single-run BacktestResult.
Stream Parameters & Events (run_backtest)¶
Key Parameters:
on_event: Optional stream callback receivingBacktestStreamEvent; if omitted, an internal no-op callback is used.stream_progress_interval: Sampling interval forprogressevents (positive int).stream_equity_interval: Sampling interval forequityevents (positive int).stream_batch_size: Flush threshold for buffered events (positive int).stream_max_buffer: Maximum buffered events (positive int).stream_error_mode: Callback exception handling policy."continue": Continue backtest on callback errors and report summary in finalfinishedevent."fail_fast": Stop immediately and raise once callback throws.
stream_mode: Stream mode."observability": observability-oriented mode with sampling and non-critical dropping under backpressure."audit": audit-oriented mode with sampling disabled and blocking backpressure for non-critical events.
strategy_id(forwarded via**kwargs): Tags trading events and results with strategy ownership. Default is_default.
Event Schema (BacktestStreamEvent):
run_id: Stream run id.seq: Monotonic event sequence.ts: Event timestamp in nanoseconds.event_type: Event type.symbol: Related symbol (nullable for some events).level: Event level (e.g.,info,warn,error).payload: Event payload as string key-value map.
Common event_type values:
- Lifecycle:
started,finished - Sampling:
progress,equity - Trading:
order,trade,risk,expiry - Runtime exceptions:
error - Market data:
tick
Common trading payload fields (order/trade/risk/expiry):
owner_strategy_id: Strategy ownership id (default_default).order_id: Order id (order/trade/risk).symbol: Symbol (order/risk).status: Order status (order).filled_qty: Filled quantity (order).trade_id: Trade id (trade).price: Fill price (trade).quantity: Fill quantity (trade).reason: Risk rejection reason (risk).expiry_date: Expiry date inYYYYMMDDform (expiry).quantity_before: Position quantity before expiry settlement (expiry).quantity_closed: Quantity closed by expiry settlement (expiry).cash_flow: Cash flow generated by expiry settlement (expiry).settlement_type: Expiry settlement mode such ascash,settlement_price, orforce_close(expiry).settlement_price: Effective settlement price when available (expiry).
Common finished.payload fields:
status:completedorfailedprocessed_events: Number of processed eventstotal_trades: Number of tradescallback_error_count: Total callback errorsdropped_event_count: Total number of events dropped under backpressuredropped_event_count_by_type: Dropped count grouped by event type (event=countcomma-separated)stream_mode: Effective stream mode (observabilityoraudit)sampling_enabled: Whether sampling is enabled (true/false)backpressure_policy: Backpressure policy (drop_non_criticalorblock)last_callback_error: Latest callback error message (when present)reason: Failure reason (when present)
akquant.BacktestConfig¶
Data class for centralized backtest configuration.
@dataclass
class BacktestConfig:
strategy_config: StrategyConfig
start_time: Optional[str] = None
end_time: Optional[str] = None
instruments: Optional[List[str]] = None
instruments_config: Optional[Union[List[InstrumentConfig], Dict[str, InstrumentConfig]]] = None
china_futures: Optional[ChinaFuturesConfig] = None
china_options: Optional[ChinaOptionsConfig] = None
benchmark: Optional[str] = None
timezone: str = "Asia/Shanghai"
show_progress: bool = True
history_depth: int = 0
# Analysis & Bootstrap
bootstrap_samples: int = 1000
bootstrap_sample_size: Optional[int] = None
analysis_config: Optional[Dict[str, Any]] = None
akquant.StrategyConfig¶
Configuration at the strategy level, including capital, fees, and risk.
@dataclass
class StrategyConfig:
initial_cash: float = 100000.0
commission_rate: float = 0.0
commission_policy: Optional[Dict[str, Any]] = None
stamp_tax_rate: float = 0.0
transfer_fee_rate: float = 0.0
min_commission: float = 0.0
# Execution
enable_fractional_shares: bool = False
round_fill_price: bool = True
slippage: Union[float, Dict[str, Any], None] = 0.0
volume_limit_pct: float = 0.25
exit_on_last_bar: bool = True
indicator_mode: str = "precompute"
# Position Sizing
max_long_positions: Optional[int] = None
max_short_positions: Optional[int] = None
risk: Optional[RiskConfig] = None
# Multi-strategy topology & strategy-level controls
strategy_id: Optional[str] = None
strategies_by_slot: Optional[Dict[str, Any]] = None
strategy_source: Optional[str] = None
strategy_loader: Optional[str] = None
strategy_loader_options: Optional[Dict[str, Any]] = None
strategy_max_order_value: Optional[Dict[str, float]] = None
strategy_max_order_size: Optional[Dict[str, float]] = None
strategy_max_position_size: Optional[Dict[str, float]] = None
strategy_max_daily_loss: Optional[Dict[str, float]] = None
strategy_max_drawdown: Optional[Dict[str, float]] = None
strategy_reduce_only_after_risk: Optional[Dict[str, bool]] = None
strategy_risk_cooldown_bars: Optional[Dict[str, int]] = None
strategy_priority: Optional[Dict[str, int]] = None
strategy_risk_budget: Optional[Dict[str, float]] = None
strategy_fill_policy: Optional[Dict[str, Dict[str, Any]]] = None
strategy_slippage: Optional[Dict[str, Dict[str, Any]]] = None
strategy_commission: Optional[Dict[str, Dict[str, Any]]] = None
portfolio_risk_budget: Optional[float] = None
akquant.InstrumentConfig¶
A data class used to configure the properties of a single instrument.
@dataclass
class InstrumentConfig:
symbol: str
asset_type: Union[
Literal["STOCK", "FUTURES", "FUND", "OPTION"],
InstrumentAssetTypeEnum
] = InstrumentAssetTypeEnum.STOCK
multiplier: float = 1.0 # Contract multiplier
margin_ratio: float = 1.0 # Margin ratio (0.1 means 10% margin)
tick_size: float = 0.01 # Minimum price variation
lot_size: Optional[int] = None
# Costs & Execution (Asset Specific)
commission_rate: Optional[float] = None
min_commission: Optional[float] = None
stamp_tax_rate: Optional[float] = None
transfer_fee_rate: Optional[float] = None
slippage: Optional[Union[float, Dict[str, Any]]] = None
# Option specific
option_type: Optional[
Union[Literal["CALL", "PUT"], InstrumentOptionTypeEnum]
] = None
strike_price: Optional[float] = None
expiry_date: Optional[Union[int, date, datetime]] = None
underlying_symbol: Optional[str] = None
option_margin_model: Optional[InstrumentOptionMarginModelEnum] = None
implied_volatility: Optional[float] = None
reference_volatility: Optional[float] = None
settlement_type: Optional[
Union[
Literal["cash", "settlement_price", "force_close"],
InstrumentSettlementTypeEnum
]
] = None
settlement_price: Optional[float] = None
static_attrs: Dict[str, Union[str, int, float, bool]] = field(default_factory=dict)
Common enums (available directly from top-level akquant):
InstrumentAssetTypeEnum:STOCK/FUTURES/FUND/OPTIONInstrumentOptionMarginModelEnum:RATIO/CHINA_SINGLE_LEG/US_BROKER_SINGLE_LEG/US_BROKER_SINGLE_LEG_VOL_ADJUSTEDInstrumentOptionTypeEnum:CALL/PUTInstrumentSettlementTypeEnum:CASH/SETTLEMENT_PRICE/FORCE_CLOSE
Example:
conf = akquant.InstrumentConfig(
symbol="IF2506",
asset_type=akquant.InstrumentAssetTypeEnum.FUTURES,
settlement_type=akquant.InstrumentSettlementTypeEnum.CASH,
)
akquant.InstrumentSnapshot¶
Static instrument metadata snapshot available to strategies (injected by engine; usually accessed via Strategy.get_instrument* APIs).
@dataclass(frozen=True)
class InstrumentSnapshot:
symbol: str
asset_type: Literal["STOCK", "FUTURES", "FUND", "OPTION"]
multiplier: float
margin_ratio: float
tick_size: float
lot_size: float
option_margin_model: Optional[Literal["RATIO", "CHINA_SINGLE_LEG", "US_BROKER_SINGLE_LEG", "US_BROKER_SINGLE_LEG_VOL_ADJUSTED"]] = None
option_type: Optional[Literal["CALL", "PUT"]] = None
strike_price: Optional[float] = None
expiry_date: Optional[int] = None # YYYYMMDD
underlying_symbol: Optional[str] = None
implied_volatility: Optional[float] = None
reference_volatility: Optional[float] = None
settlement_type: Optional[Literal["CASH", "SETTLEMENT_PRICE", "FORCE_CLOSE"]] = None
settlement_price: Optional[float] = None
static_attrs: Dict[str, Union[str, int, float, bool]] = field(default_factory=dict)
Notes:
expiry_dateusesint(YYYYMMDD)semantics.- Snapshot data is available in
on_start. - Prefer
get_instrument/get_instrument_config/get_instrument_fieldin strategy code. - Field coverage differs between backtest and live. Backtest snapshots are populated from
InstrumentConfigand carry every field. Live (run_live) only acceptsInstrumentobjects, which expose justsymbol/asset_type/multiplier/margin_ratio/tick_size/lot_size/option_margin_model/implied_volatility/reference_volatilityfor read-back, sooption_type/strike_price/expiry_date/underlying_symbol/settlement_type/settlement_price/static_attrsareNone(or empty) in live snapshots. Option strategies that depend on those fields must pass them in via strategy params orcontext.
Configuration System Explained¶
AKQuant provides a flexible configuration system that allows users to set backtest parameters in multiple ways.
1. Hierarchy¶
Configuration objects are organized in a tree structure, with BacktestConfig as the top-level entry point:
BacktestConfig (Simulation Scenario)
├── StrategyConfig (Strategy & Account)
│ ├── initial_cash
│ ├── commission_policy / commission_rate (Default commission)
│ ├── slippage (Default)
│ └── RiskConfig (Risk Rules)
│ ├── safety_margin
│ └── max_position_pct
└── InstrumentConfig (Asset Properties)
├── multiplier
└── commission_rate (Asset-specific override)
2. Priority¶
Parameter resolution in run_backtest follows this priority order (highest to lowest):
- Explicit Arguments:
- Parameters passed directly to
run_backtesthave the highest priority. - Example:
run_backtest(start_time="2022-01-01")overridesconfig.start_time.
- Parameters passed directly to
- Configuration Objects:
- If explicit arguments are
None, values are read fromconfig(BacktestConfig). - Multi-strategy fields can be centralized in
config.strategy_config(strategy_id,strategies_by_slot,strategy_max_*,strategy_priority,strategy_risk_budget,portfolio_risk_budget).
- If explicit arguments are
- Defaults:
- If neither provides a value, system defaults are used.
3. Risk Config Merging¶
The risk_config parameter has special handling logic designed to support a "Baseline + Override" pattern:
- Baseline: First loads
config.strategy_config.risk(if it exists). - Override: If
risk_configparameter (dict or object) is provided, it overrides fields in the baseline configuration.- This allows you to quickly adjust risk parameters for testing without modifying the main Config object, e.g.,
run_backtest(..., risk_config={"max_position_pct": 0.5}).
- This allows you to quickly adjust risk parameters for testing without modifying the main Config object, e.g.,
4. Strategy Runtime Config Injection¶
run_backtest and run_from_checkpoint support strategy_runtime_config:
- Accepted formats:
StrategyRuntimeConfigordict. - Purpose: Inject runtime behavior switches without modifying strategy class code.
- Example:
run_backtest(..., strategy_runtime_config={"error_mode": "continue"}). - Validation: Unknown keys and invalid values fail fast with field-level errors.
- Conflict handling:
runtime_config_override=Trueapplies external config;Falsekeeps strategy-side config. - The same conflict rules apply consistently to both
run_backtestandrun_from_checkpoint. - Conflict warnings are deduplicated per strategy instance for identical conflict payloads.
- Priority rule: explicit
strategy_runtime_configparameter has higher priority than forwarded config maps. - Troubleshooting quick lookup: see Runtime Config Guide.
from akquant import StrategyRuntimeConfig, run_backtest
result = run_backtest(
data=data,
strategy=MyStrategy,
strategy_runtime_config=StrategyRuntimeConfig(
error_mode="continue",
portfolio_update_eps=1.0,
),
)
5. Best Practices¶
- Simple Scripts: Use flat parameters of
run_backtestdirectly (e.g.,initial_cash,start_time). - Production/Complex Strategies: Build a complete
BacktestConfigobject for version control and reuse. - UI-Driven Parameter Input: Declare parameter fields inline on the strategy class (e.g.
fast_period = IntParam(10, ge=2, le=200), accessed at runtime viaself.params.fast_period) and useget_strategy_param_schema/validate_strategy_paramsfor frontend-backend parameter consistency. - Parameter Tuning: When using
run_grid_search, modify the Config object or pass override parameters as needed.
Logging API¶
AKQuant stays quiet by default when imported as a library; until explicitly configured, the akquant root logger only carries a NullHandler.
akquant.LogConfig¶
Advanced logging config object used by configure_logging(...).
Core fields:
level: global fallback level.console: whether to enable the console handler.console_level/file_level: per-handler level overrides.console_format/file_format: text formatter overrides.console_show_context/file_show_context: whether human-readable output should append structured context.console_json/file_json: whether the corresponding handler should emit JSON lines.filename: target log file path.file_mode: file open mode, defaulta.file_max_bytes/file_backup_count: size-based rotation threshold and retention count.profile: preset profile, one ofresearch,optimize, orlive.reset_handlers: whether to reset AKQuant-managed handlers.propagate: whether records should propagate to parent loggers.mask_sensitive: redact sensitive fields (defaultTrue). Credential-class keys (password/token/api_key, …) are fully masked and account-class keys (user_id/account, …) keep only their last 4 chars. Masking runs at the handler layer, so a caller can never leak a secret by forgetting to mask it.order_audit_file: path to a dedicated JSON file for live order auditing. When set, every order submit/update/fill/cancel/reject underbroker_liveis additionally written as a JSON line (theakquant.audit.ordernamespace) for later reconciliation and post-mortem.order_audit_level: level for the audit file, defaultINFO.order_audit_max_bytes/order_audit_backup_count: size-based rotation threshold and retention count for the audit file (default keeps 5 backups).language: console audit message language,"en"(default) /"zh". It only re-renders the console order-audit line; files and JSON stay the english canonical, and structured fields (event/side/price, …) never change — so grep/alerting/reconciliation never fork by language.
akquant.configure_logging¶
Initializes or reconfigures the akquant logging system through a structured config.
Recommended example:
import akquant
akquant.configure_logging(
akquant.LogConfig(
profile="live",
level="INFO",
console=True,
console_json=False,
filename="logs/live.log",
file_level="DEBUG",
file_json=True,
file_max_bytes=10_000_000,
file_backup_count=5,
)
)
Behavior notes:
profileonly fills unspecified fields; explicit config values always win.profile="optimize"uses a process-aware default text format so worker output is easier to distinguish.profile="live"is the natural place to enable structured context and/or JSON output.- Rust-side runtime warnings under
akquant.*are also bridged into Pythonloggingand restored into the same structured field model whenever possible. - For example, execution-path warnings such as insufficient-margin rejects, session-close expiry, unknown cancel requests, or same-slice
same-cycledeferrals carryphase="execution"and may also includesymbol,order_id,strategy_id,slot, andevent_time_iso.
akquant.register_logger¶
def register_logger(
filename: Optional[str] = None,
console: bool = True,
level: str = "INFO",
) -> None
Compatibility helper for quickly enabling logging without exposing advanced fields. Internally this maps to configure_logging(LogConfig(...)).
akquant.get_logger¶
Fetches a logger under the akquant namespace:
get_logger()->akquantget_logger("strategy")->akquant.strategyget_logger("gateway.live")->akquant.gateway.live
akquant.set_log_level¶
Updates the current akquant root logger level.
Boundary Guidance¶
self.log(...)is the primary human-readable strategy logging path.run_backtest(..., on_event=...)is the machine-consumable event stream and is better suited for realtime UI, alerting, and audit sinks.- Inside
on_order/on_trade/on_reject,self.log(...)automatically carries structured fields such asorder_idandclient_order_id. - Rust execution/data warnings do not require manual user wiring; once an
akquantlogger handler is configured, they flow through the same text or JSON logging pipeline.
akquant.RiskConfig¶
Configuration for risk management.
@dataclass
class RiskConfig:
active: bool = True
check_cash: bool = True
safety_margin: float = 0.0001
max_order_size: Optional[float] = None
max_order_value: Optional[float] = None
max_position_size: Optional[float] = None
restricted_list: Optional[List[str]] = None
max_position_pct: Optional[float] = None
sector_concentration: Optional[Union[float, tuple]] = None
# Account Level Risk
max_account_drawdown: Optional[float] = None
max_daily_loss: Optional[float] = None
stop_loss_threshold: Optional[float] = None
account_mode: str = "cash"
enable_short_sell: bool = False
initial_margin_ratio: float = 1.0
maintenance_margin_ratio: float = 0.3
financing_rate_annual: float = 0.08
borrow_rate_annual: float = 0.10
allow_force_liquidation: bool = True
liquidation_priority: str = "short_first"
2. Strategy Development (Strategy)¶
akquant.Strategy¶
Strategy base class. Users should inherit from this class and override callback methods.
Callback Methods:
on_start(): Triggered when the strategy starts. Used for subscription (subscribe) and indicator registration.on_bar(bar: Bar): Triggered when a Bar closes.on_tick(tick: Tick): Triggered when a Tick arrives.on_order(order): Triggered when order state changes.on_trade(trade): Triggered when trade report arrives.on_reject(order): Triggered once when an order becomesRejected. In live trading (broker_live), orders explicitly rejected by the broker are also reported through this callback —buy()/sell()/order_target_*do not raise. If the order's outcome is unknown because of a timeout or disconnect, the framework instead callson_error(error, "order_submit", request)rather than faking a rejection.on_expiry(event: Dict[str, Any]): Triggered after anexpiry_datedriven settlement/removal is actually executed. Portfolio state is already updated when the callback runs. Seeexamples/49_on_expiry_demo.pyfor a runnable example.on_before_trading(trading_date, timestamp): Triggered once when the regular trading session starts each local day; on the default backtest path this session is usually exposed asContinuous. This callback follows a "previous trading day / previous snapshot only" visibility model.on_pre_open(event: Dict[str, Any]): Triggered once before the first regular event of each trading day. Use it for "pre-open decision, current open fill" workflows; default order semantics resolve toNextOpen(). Seeexamples/52_pre_open_demo.py.on_cross_section(trading_date, timestamp): Cross-sectional same-cycle rebalance hook that runs after the first complete cross-symbol bar slice of the trading day, at most once per trading day. Unlikeon_before_trading, it can see the current day's bar history and current account snapshot, and is intended for same-cycle close-style rebalances. Rebalance cadence (daily/weekly/monthly) is decided with a calendar check inside the hook.on_after_trading(trading_date, timestamp): Triggered when leaving the regular trading session, or replayed on next event after day rollover.on_portfolio_update(snapshot): Triggered when cash/equity/position snapshot changes.on_error(error, source, payload=None): Triggered when a user callback raises. Whether the original exception is re-raised afteron_errordepends onerror_mode/re_raise_on_error(re-raise by default) — this rule covers ordinary callback exceptions dispatched through the framework (on_bar/on_tick, etc.), and behaves the same in backtest and live (broker_live). However,on_errorcalls triggered by a broker communication failure underbroker_live— unknown order-submit outcome (source="order_submit"), local stop-order retry failure ("stop_trigger"), cancel failure ("order_cancel"/"order_cancel_all"), and exceptions raised by synchronously dispatchedon_order/on_reject/on_tradecallbacks (source"on_order"/"on_reject"/"on_trade") — are always swallowed after callingon_error, regardless ofre_raise_on_error, and never propagate further (implementations located inpython/akquant/gateway/order_submitter.py::_dispatch_reject_order,python/akquant/gateway/broker_execution.py::_notify_error/_notify_stop_error/_handle_cancel_failure,python/akquant/live/_runner.py::_safe_strategy_callback).on_timer(payload: str): Triggered by timer.on_stop(): Triggered when the strategy stops.on_train_signal(context): Triggered by rolling training signal (ML mode).
Recommended on_pre_open pattern:
def on_pre_open(self, event: Dict[str, Any]) -> None:
signal = self.compute_pre_open_signal()
if signal > 0:
self.buy("000001", quantity=100)
Note: if you do not pass an explicit fill_mode here, the framework defaults to NextOpen() order semantics.
Properties & Shortcuts:
self.symbol: The symbol currently being processed.self.close,self.open,self.high,self.low,self.volume: Current Bar/Tick price and volume.self.position: Position object for current symbol, withsizeandavailableproperties.self.now: Current backtest time (pd.Timestamp).self.runtime_config: Runtime behavior config object (StrategyRuntimeConfig).self.enable_precise_day_boundary_hooks: Enable boundary timer based precise day hooks (defaultFalse). This switch changes trigger precision only; it does not change the visibility window ofget_history(),get_account(), orequityinsideon_before_trading.self.portfolio_update_eps: Snapshot threshold; changes below it skipon_portfolio_update(default0.0).self.error_mode: Error handling mode,"raise"or"continue"(default"raise").self.re_raise_on_error: Whether to re-raise a user callback exception afteron_error(defaultTrue); only applies to ordinary callback exceptions dispatched through the framework (see the scoping note underon_errorabove).on_errorcalls triggered by a broker communication failure underbroker_live(order_submit/stop_trigger/order_cancel/order_cancel_all, etc.) ignore this setting and are never re-raised.
Trading Methods:
buy(symbol=None, quantity=None, price=None, trigger_price=None, ...): Buy (open long / close short).- Market order if
priceis not specified. - Limit order if
priceis specified. - Stop order (Stop Market) if
trigger_priceis specified. - Omitting
symboluses the current bar/tick symbol; callbacks without market context (e.g.on_start) must pass it explicitly. - Omitting
quantitysizes the order viaself.sizer(defaults toFixedSize(100), replaceable withset_sizer()).
- Market order if
sell(symbol=None, quantity=None, price=None, trigger_price=None, ...): Sell (close long / open short). Same parameters as above, except that omittingquantitydoes not use the sizer — it closes the whole position: total position in backtest, available position underbroker_live(China A-share T+1 freezes same-day buys, so sizing off the total gets the whole order rejected by the broker).- When the resolved quantity is
<= 0, no order is placed and an empty receipt is returned (len(receipt) == 0,receipt.primary == ""). receipt.failure(read-only,None/"rejected"/"unknown"): distinguishes the three causes of an empty receipt.None: no trade was needed (e.g. resolved quantity<= 0) — not a failure."rejected": the counterparty explicitly rejected the order; the order definitely does not exist, so retrying is safe."unknown": submission hit a timeout/disconnect; the order status is unknowable (the request may have already reached the counterparty). Callers must not assume the order doesn't exist and resubmit — if it was in fact accepted, resubmitting creates a real duplicate order. Wait for the nextsync_open_ordersreconciliation to surface the true state instead.
short(symbol, quantity, price=None, ...): Short sell.cover(symbol, quantity, price=None, ...): Buy to cover.submit_order(..., order_type="StopTrail", trail_offset=..., trail_reference_price=None): Submit a trailing stop order.trail_offsetmust be greater than 0.submit_order(..., order_type="StopTrailLimit", price=..., trail_offset=..., trail_reference_price=None): Submit a trailing stop-limit order.priceandtrail_offsetare required.submit_order(..., broker_options={...}): Optional broker extension fields passthrough (backtest currently records them onorder.broker_optionsfor debugging/audit).place_trailing_stop(symbol, quantity, trail_offset, side="Sell", trail_reference_price=None, ...) -> str: Helper for trailing stop orders, promoted to market order when triggered.place_trailing_stop_limit(symbol, quantity, price, trail_offset, side="Sell", trail_reference_price=None, ...) -> str: Helper for trailing stop-limit orders, promoted to limit order when triggered.order_target_value(target_value, symbol, price=None): Adjust position to target value.order_target_percent(target_percent, symbol, price=None): Adjust position to target account percentage.rebalance_weights(target_weights, price_map=None, liquidate_unmentioned=False, allow_leverage=False, rebalance_tolerance=0.0, ...): Rebalance a multi-asset portfolio by target weights.target_weightsis{symbol: weight}and by default requires total weight<= 1.0.liquidate_unmentioned=Truesets all existing non-mentioned positions to target0.- Orders are submitted in sell-first then buy-second order to reduce cash-lock conflicts.
rebalance_toleranceskips tiny drifts by portfolio-value ratio to reduce churn.
close_position(symbol): Close position for a specific instrument.cancel_order(order_id: str): Cancel a specific order.cancel_all_orders(symbol): Cancel all pending orders for a specific instrument. Ifsymbolis omitted, cancels all orders.place_oco(first_order_id, second_order_id, group_id=None) -> str: Create an OCO order group. Once one order is filled, the peer order is canceled automatically.place_bracket(symbol, quantity, entry_price=None, stop_trigger_price=None, take_profit_price=None, ...) -> str: Create a bracket order. The entry order is submitted first, then stop-loss/take-profit exits are submitted after entry fill; if both exits exist, they are linked as OCO automatically.
Data & Utilities:
get_history(count, symbol, field="close", freq=None) -> np.ndarray: Get history data array (a safe snapshot copy of the rolling buffer, not zero-copy). Supportsopen/high/low/close/volumeand any numeric extra fields (e.g.,adj_close,adj_factor).get_history_multi(count, symbol, fields=("open","high","low","close","volume"), freq=None) -> Dict[str, np.ndarray]: Fetch multiple fields in a single FFI crossing; identical in behavior to per-fieldget_history, and used internally byget_history_df.get_history_df(count, symbol, freq=None) -> pd.DataFrame: Get history data DataFrame (OHLCV).- The
freqparameter (supported byget_history/get_history_multi/get_history_df/get_rolling_data): takes'tick'/'bar'/None.None(default): if the symbol only has a bar series, you get bars; if it only has a tick series, you get ticks (single-stream behavior is unchanged). In a dual-stream session (the symbol has both a bar and a tick series at once),Noneresolves by the callback you are currently in: insideon_barit is equivalent tofreq='bar', insideon_ticktofreq='tick'. Note that the tick series is written unconditionally by the engine, whether or not your strategy overrideson_tick— so a strategy with onlyon_baris still in a dual-stream session once ticks are subscribed, and it relies on exactly this inference. Outside the market-data callbacks (on_timer,on_before_trading, your own threads), there is nothing to infer from, so a dual-stream session still raisesValueErrorand requires you to passfreq='bar'orfreq='tick'explicitly — it will not silently pick one for you. An unrecognized value also raises rather than falling back to'bar'.- With
freq='tick',fieldonly supportsprice/close/volume: a tick has no open/high/low, so requesting those raisesValueError(previously this silently returned a degenerate OHLC withpricestanding in forhigh— this is a breaking change).get_history_df/get_rolling_dataalways pull the full OHLCV set, so they necessarily raise underfreq='tick'; useget_history(freq='tick', field='price')instead. - To make
on_barandon_tickboth fire in a backtest:run_backtest(data=[Tick, ...], freq="1min")(freqonly takes effect whendatais a list containingTick; DataFrame input doesn't support it — passing it there raises rather than being silently ignored). The live-trading equivalent isgateway_options={"emit_ticks": True, "emit_bars": True}(supported by both the klinedata and CTP gateways;use_aggregatoris kept as a compatibility alias). Both gateways handleemit_ticks/emit_barsthe same way — falling back per-parameter (passing only one explicitly does not silently turn off the other), and raising rather than silently emitting nothing if both end upFalseafter fallback.broker="ctp"goes throughrun_live(..., gateway_options=...)→ builder forwarding; this path used to silently drop both keys (on_ticknever fired, with no error at all) and has since been fixed to forward them as-is to the underlyingCTPMarketGateway. klinedata has a prerequisite that CTP does not: klinedata has an extradriveparameter, defaulting todrive="bar", anddrive="bar"only subscribes to the K-line channel — tick frames never arrive at all. Sodrive="bar"combined with an explicitemit_ticks=Truenow raises, with an error message spelling out the dual-stream recipe you can copy (it used to be silently coerced toemit_ticks=False, which surfaced as "on_ticknever fires and I have no idea why"). To actually geton_tickto fire you must pass"drive": "tick"explicitly; settingemit_ticks=Truealone is not enough. Values derived from theuse_aggregatoralias fallback are still resolved againstdrivefor backward compatibility. CTP has nodrivelayer, soemit_ticks=Truetakes effect directly there.
get_position(symbol) -> float: Get current position size. This still returns a numeric quantity, not an object.get_available_position(symbol) -> float: Get available position size.positions -> Dict[str, float]: Get all positions by symbol (read-only property).self.position.entry_price -> float: Get the current symbol's average entry price via thePositionhelper.self.position.avg_price -> float: Alias ofentry_price.ctx.get_position_entry_price(symbol) -> float: Get the current average entry price for one symbol.ctx.get_position_entry_prices() -> Dict[str, float]: Get current average entry prices for all symbols.cash -> float: Get current available cash (read-only property).freq -> Optional[str]: The data frequency (read-only property), expressed in backtest vocabulary —"1min"/"5min"/"1d", etc.- Injected from
run_backtest(freq=)for backtests, and declared by the market-data gateway for live trading (klinedata converts its ownperiod="M1"into"1min";broker="replay"acceptsgateway_options={"freq": "1min"}). Both sides use the backtest vocabulary, so the same strategy reads the same value in a backtest and live, with no broker-specific branching. Nonewhen the frequency is unknown: a bar-only backtest that did not passfreq, tick-only gateways such as CTP, trader-only brokers (no market-data channel), and klinedata's weekly period (backtestfreqonly accepts whole minutes, so weekly has no equivalent). The framework deliberately does not infer the frequency from the data — timestamp deltas between adjacent bars are misled by suspensions, day rollovers and lunch breaks, and a wrong frequency is more dangerous than an unknown one. HandleNoneexplicitly.- Read-only; assigning to it raises
AttributeError, since the data granularity is decided by the data source and writing this attribute would not actually change it.
- Injected from
get_account() -> Dict[str, float]: Get an account snapshot. Common fields includecash,equity,market_value,notional_value,frozen_cash,margin,used_margin,free_margin,unrealized_pnl,borrowed_cash,short_market_value,maintenance_ratio,account_mode,accrued_interest, anddaily_interest.- In cash / spot-style accounts,
market_valueusually represents marked position value. - In futures margin accounts,
equityis account equity,used_marginis margin in use,notional_valueis futures notional exposure, andunrealized_pnlis marked floating PnL. Futures trades do not deduct full notional fromcashthe way spot buys do, and notional exposure is not mirrored intomarket_valueas if it were spot inventory. cashis the cash balance;free_margin(=equity - used_margin) is the amount actually available to open new positions and matches theAvailablevalue shown in the rejection log when an order is rejected. In futures margin accounts, opening a position does not deduct margin fromcash, socashis usually larger thanfree_margin; in stock cash accounts the two are equal.- Inside strategy callbacks, prefer
equitywhen you only need current total equity; it is aligned withget_account()["equity"].
- In cash / spot-style accounts,
get_order(order_id) -> Order: Get details of a specific order.get_open_orders(symbol) -> List[Order]: Get list of open orders.subscribe(instrument_id: str): Subscribe to market data. Must be called explicitly for multi-asset backtesting or live trading to receiveon_tick/on_barcallbacks.log(msg: str, level: int): Log with timestamp.schedule(trigger_time, payload): Register a one-time timer task.schedule_daily(time_str, payload): Register a daily timer task (fires every trading day).schedule_weekly(time_str, payload): Fires on the first trading day of each week (rolls forward over holidays/suspensions).schedule_monthly(time_str, payload): Fires on the first trading day of each month (rolls forward over holidays/suspensions).trading_days -> List[pd.Timestamp]: Read-only trading-day sequence, for custom cadences viaschedule.nth_trading_day_of_month(n)/nth_last_trading_day_of_month(n)/nth_trading_day_of_week(n): Calendar helpers returning the n-th (or n-th-from-last) trading day of each month/week.
Instrument Metadata APIs (Recommended):
get_instrument(symbol) -> InstrumentSnapshot: Return static metadata snapshot for one symbol.get_instruments(symbols=None) -> Dict[str, InstrumentSnapshot]: Return snapshot dict for multiple symbols; returns all whensymbols=None.get_instrument_field(symbol, field) -> Any: Return one metadata field value.get_instrument_config(symbol, fields=None) -> Union[Any, Dict[str, Any], InstrumentSnapshot]: Compatibility API for full object, single field, or multi-field access.
Notes:
- These APIs are available in
on_start(snapshots are injected before start callbacks). - Prefer these APIs for static metadata access instead of relying on
bar.extra.
Machine Learning Support:
set_rolling_window(train_window, step): Set rolling training window.get_rolling_data(length, symbol, freq=None): Get rolling training data (X, y).freqhas the same semantics and limits as theget_historyfamily above (it's built onget_history_dfinternally).prepare_features(df, mode): (Override required) Feature engineering and label generation.
akquant.Bar¶
Bar data object.
timestamp: Unix timestamp (nanoseconds).open,high,low,close,volume: OHLCV data.symbol: Instrument symbol.
akquant.Tick¶
Tick data object.
timestamp: Unix timestamp (nanoseconds).price: Latest trade price.volume: Trade volume. Per-trade volume (matching backtest semantics), not a running total.- The
Tick.volumethat live gateways (CTP, klinedata) hand toon_tick/add_tickis the volume of that single trade. The counterparty/upstream feed originally pushes the cumulative volume for the day; the gateway diffs it per symbol internally before exposing per-trade volume. - In contrast, the
BarAggregatorthe gateway uses internally forfreqaggregation (synthesizing bars from ticks) consumes the raw cumulative volume (constructed withvolume_is_cumulative=True, and the aggregator does its own diff-and-sum). This split is intentional — don't "fix" it into per-trade volume, or the aggregated volume will be cut in half. - Known trade-off: when a process starts mid-session, the first frame received for a symbol has no prior cumulative value to diff against, so the real per-trade volume can't be derived;
Tick.volumeis recorded as0in that case (meaning "unknown", not the cumulative value masquerading as per-trade). If a strategy has defensive logic like "skip ifvolume == 0", it will also skip each symbol's very first tick.
- The
symbol: Instrument symbol.
akquant.run_live (broker live semantics)¶
For live broker routing, run_live accepts broker-specific options through gateway_options.
from akquant import run_live
run_live(
strategy_cls=on_bar,
instruments=instruments,
broker="ctp",
trading_mode="broker_live",
gateway_options={"execution_semantics_mode": "strict"},
)
gateway_options.execution_semantics_mode:
| Value | Default | Behavior | Recommended |
|---|---|---|---|
strict |
Yes | Terminal states (Cancelled / Rejected / Filled) are finalized by broker order callbacks (OnRtnOrder). Error callbacks cache reject reasons and merge them into subsequent order callbacks. |
Production live trading |
compatible |
No | Allows immediate local terminal-state transitions for selected error/cancel paths to keep legacy behavior. | Migration / temporary compatibility |
Strict-mode notes:
- Cancel request sent does not imply
Cancelled; wait forOnRtnOrder(Cancelled). - Error callback received does not imply
Rejected; final status is confirmed by order callback.
3. Core Engine¶
akquant.Engine¶
The main entry point for the backtesting engine (usually used implicitly via run_backtest).
Configuration Methods:
set_timezone_name(timezone: str): Set an IANA timezone name such asAsia/Shanghai,UTC, orUS/Eastern. This is the recommended API because it preserves DST and historical timezone rules.set_timezone(offset: int): Set a fixed timezone offset in seconds. Kept only as a compatibility fallback and does not preserve DST or historical timezone rules.use_simulated_execution()/use_realtime_execution(): Set execution environment.set_fill_mode(mode, timer_timing): Set the execution fill mode, wheremodeis anakquant.ExecutionModevalue (CurrentClose,NextOpen,NextClose,NextAverage,NextHighLowMid) andtimer_timingis"same_cycle"or"next_event"(recommended).get_fill_policy(): Get the current execution policy as an internal(price_basis, bar_offset, temporal)triple.set_history_depth(depth): Set history data cache length.
Market & Fee Configuration:
use_simple_market(): Enable simple market (legacy percent-commission shorthand).use_simple_market_policy(type, value): Enable simple market with an explicit commission mode.use_china_market(): Enable China market.set_stock_fee_rules(commission, stamp_tax, transfer_fee, min_commission): Set fee rules.set_stock_fee_policy(type, value, stamp_tax, transfer_fee, min_commission): Set stock commission mode and fee rules.
akquant.DataFeed¶
DataFeed is the engine-facing event source abstraction. Use it when you want explicit control over how data enters the engine.
Constructors & factories:
DataFeed(): Create an empty historical feed.DataFeed.from_csv(path, symbol): Create a feed directly from a CSV file; useful when you want Rust-side row iteration to drive the event stream.DataFeed.create_live(): Create a live feed suitable for gateway / market data push scenarios.
Input methods:
add_bar(bar): Append oneBarto the feed.add_bars(bars): Append a batch ofBarobjects.add_tick(tick): Append oneTickto a live feed.add_arrays(timestamps, opens, highs, lows, closes, volumes, symbol): Build bars from arrays and inject them into the feed efficiently.sort(): Sort the current historical feed by event time.
When to use it:
- For normal backtests, prefer
run_backtest(data=...)with aDataFrame,List[Bar], orDataFeedAdapter. - Use
DataFeedwhen you want to reuse the same feed object, switch explicitly between historical and live modes, or wire data intoEngine.add_data(feed)yourself. - If
from_csv(...)oradd_arrays(...)encounters invalid floating-point values, Rust emits warnings that flow into AKQuant's Pythonloggingpipeline, such asakquant.data.clientandakquant.data.batch.
akquant.gateway Custom Broker Registry¶
You can plug in a custom broker by name through the registry without editing built-in factory branches.
Registry APIs:
register_broker(name, builder): Register a broker builder.unregister_broker(name): Unregister a broker.get_broker_builder(name): Resolve a broker builder.list_registered_brokers(): List currently registered brokers.
Builder signature:
def builder(
feed: DataFeed,
symbols: Sequence[str],
use_aggregator: bool,
**kwargs: Any,
) -> GatewayBundle:
...
Example:
from akquant import DataFeed
from akquant.gateway import create_gateway_bundle, register_broker
register_broker("demo", demo_builder)
bundle = create_gateway_bundle(
broker="demo",
feed=DataFeed(),
symbols=["000001.SZ"],
)
4. Trading Objects¶
akquant.Order¶
id: Order ID.symbol: Instrument symbol.side:OrderSide.Buy/OrderSide.Sell.order_type:OrderType.Market/OrderType.Limitetc.status:OrderStatus.New/Filled/Cancelledetc.quantity/filled_quantity: Order / Filled quantity.average_filled_price: Average filled price.
akquant.Instrument¶
Contract definition.
Instrument(
symbol="AAPL",
asset_type=AssetType.Stock,
multiplier=1.0,
margin_ratio=1.0,
tick_size=0.01,
option_type=None,
strike_price=None,
expiry_date=None,
lot_size=1,
underlying_symbol=None,
settlement_type=None
)
5. Portfolio & Risk¶
akquant.RiskConfig¶
Risk configuration.
@dataclass
class RiskConfig:
active: bool = True
safety_margin: float = 0.0001
max_order_size: Optional[float] = None
max_order_value: Optional[float] = None
max_position_size: Optional[float] = None
restricted_list: Optional[List[str]] = None
max_position_pct: Optional[float] = None
sector_concentration: Optional[Union[float, tuple]] = None
# Account Level Risk
max_account_drawdown: Optional[float] = None
max_daily_loss: Optional[float] = None
stop_loss_threshold: Optional[float] = None
Account-level field semantics:
max_account_drawdown: Maximum drawdown limit in 0~1 ratio. Drawdown is measured against historical peak equity; once breached, new order requests are rejected.max_daily_loss: Daily loss limit in 0~1 ratio. Loss is measured against equity at the first risk check of the trading day; once breached, new order requests are rejected.stop_loss_threshold: Equity stop-loss threshold in 0~1 ratio. If current equity falls belowbaseline_equity_at_rule_activation * threshold, new order requests are rejected.
Rejection reasons are available in orders_df.reject_reason.
6. Analysis¶
akquant.BacktestResult¶
Backtest result object.
Properties:
metrics_df: Performance metrics DataFrame. Core trade-state fields includeclosed_trade_count,execution_count, andopen_position_count.trades_df: Trade history DataFrame.orders_df: Order history DataFrame. Includesposition_effect,reduce_only, andcreated_at_iso/updated_at_iso(UTC ISO strings).executions_df: Execution fills DataFrame (prefers Rust IPC/dict fast export path). Includesposition_effectandtimestamp_iso.
Position effect (position_effect)
Values are auto / open / close / close_today / close_yesterday —
the same vocabulary accepted by order submission, so it can be filtered
directly (e.g. df[df.position_effect == "close_today"]).
With the default position_effect="auto", buy() / sell() split into
close and open legs automatically: a flip emits the close leg first, then
the open leg. These columns are where that split is visible — in the order
table at submission time, in the execution table after the fill.
positions_df: Daily position details.equity_curve: Equity curve.cash_curve: Cash curve.margin_curve: Margin curve (used margin over time).equity_curve_daily: Daily end-of-day equity curve.cash_curve_daily: Daily end-of-day cash curve.margin_curve_daily: Daily end-of-day margin curve.
Analysis Methods:
exposure_df(freq="D"): Portfolio exposure decomposition (net/gross/leverage).attribution_df(by="symbol", use_net=True, top_n=None): Grouped attribution by symbol/tag.capacity_df(freq="D"): Capacity proxy metrics (order count, fill rates, turnover).benchmark_analysis(benchmark=None, curve_freq="raw"): Return a structured benchmark analysis payload for frontends and APIs.export_benchmark_analysis(path, benchmark=None, format="json", curve_freq="raw"): Persist benchmark analysis as JSON or parquet artifacts.top_reject_reason_types(top_n=10): Aggregate reject counts by normalized reject type and include a sample detail row.orders_by_strategy(): Strategy-ownership order aggregation byowner_strategy_id.executions_by_strategy(): Strategy-ownership execution aggregation byowner_strategy_id.get_event_stats(): Unified stream summary stats (for exampleprocessed_events,dropped_event_count,callback_error_count,backpressure_policy,stream_mode).report(..., curve_freq="D" | "raw"): Generate HTML report with daily end-of-day curves by default, or switch back to raw frequency.
orders_by_strategy = result.orders_by_strategy()
executions_by_strategy = result.executions_by_strategy()
benchmark_analysis = result.benchmark_analysis(
benchmark=benchmark_returns,
curve_freq="D",
)
# Common benchmark_analysis fields:
# - schema_version, available, reason
# - benchmark.label
# - summary.total_excess / annual_excess / tracking_error
# - summary.information_ratio / beta / alpha
# - series[*].date / strategy_return / benchmark_return / excess_return
# - series[*].strategy_cum_return / benchmark_cum_return / excess_cum_return
# Common columns
# orders_by_strategy:
# - owner_strategy_id, order_count, filled_order_count,
# ordered_quantity, filled_quantity, ordered_value, filled_value,
# fill_rate_qty, fill_rate_value
#
# executions_by_strategy:
# - owner_strategy_id, execution_count, total_quantity,
# total_notional, total_commission, avg_fill_price
event_stats = result.get_event_stats()
# Common fields:
# - processed_events, dropped_event_count, callback_error_count,
# backpressure_policy, stream_mode, reason
7. Data I/O & Vectorized Compute¶
7.1 run_backtest data input types¶
run_backtest(data=...) accepts several inputs, normalized uniformly before entering the engine:
pandas.DataFrame/Dict[str, pandas.DataFrame]polars.DataFrame/polars.LazyFrame/pyarrow.Table(first-class inputs, coerced onto the pandas path at no cost)List[Bar]DataFeed(including the streamingDataFeed.from_parquet, see 7.3) /DataFeedAdapter
7.2 akquant.write_canonical_parquet¶
Normalizes any source (pandas / polars / pyarrow / path / List[Bar]) and writes a streamable (out-of-core) Parquet: a timestamp column (int64 nanoseconds UTC) + open/high/low/close/volume (float64) + symbol (str), sorted ascending by timestamp, zstd-compressed. The output can be streamed with bounded memory by DataFeed.from_parquet.
7.3 akquant.DataFeed.from_parquet¶
Creates a bounded-memory (out-of-core) streaming feed from a canonical Parquet: data is read from disk in chunk_size-row blocks (default 65536), so backtest peak memory is independent of total data size. The Parquet must be sorted ascending by timestamp; a symbol column enables multi-symbol. See "Data Guide · 2.6".
7.4 Vectorized column compute akquant.vec_*¶
Zero-copy, vectorized batch column primitives over numpy arrays (complementary to incremental per-bar indicators; suited for evaluating a whole column at once):
| Function | Description |
|---|---|
vec_sma(values, period) |
Simple moving average |
vec_ema(values, period) |
Exponential moving average |
vec_wma(values, period) |
Weighted moving average |
vec_rolling_sum/min/max(values, period) |
Rolling sum / min / max |
vec_rolling_std(values, period) |
Rolling sample std (ddof=1) |
vec_zscore(values, period) |
Rolling z-score |
vec_returns(values) / vec_log_returns(values) |
Simple / log returns |
vec_cumsum(values) |
Cumulative sum |
Semantics align with pandas (matching NaN positions, rolling_std is sample std).