Execution Model
FINSABER makes execution assumptions explicit so result differences can be traced back to timing, price adjustment, and cost settings.
One Trade From Signal To Fill
Assume the strategy sees Monday's data and decides to buy 100 shares.
| Step | What happens |
|---|---|
| Signal date | Strategy reads Monday data and submits buy("AAPL", 100). |
| Timing rule | next_open delays the order until the next available trading day. |
| Base price | The engine uses Tuesday adjusted_open. |
| Liquidity | Quantity may be capped by prior average volume. |
| Slippage | Buy price is moved upward; sell price is moved downward. |
| Commission | Cash is reduced by trade value plus commission. |
| Portfolio update | Position, cash, trade history, and equity curve are updated. |
Timing
Supported values:
next_open: signal generated on datet, order fills at the next available adjusted open.same_close: order fills on the same adjusted close.
Prefer next_open when features are date-level and exact intraday availability is unknown.
| Timing | When to use | Main risk |
|---|---|---|
next_open |
Daily bars, news, filings, LLM summaries, uncertain timestamps. | May be less optimistic because the market can move overnight. |
same_close |
Features known before close, such as intraday signals or pre-close data. | Easy to misuse with after-close or date-only information. |
Order Lifecycle
For Python-native strategies, the execution path is:
- Strategy receives
today_datafor datet. - Strategy calls
framework.buy(...)orframework.sell(...). - The framework resolves the fill date and fill price according to
execution_timing. - Liquidity cap reduces or rejects the requested quantity.
- Slippage and commission are applied.
- Cash, position, trade history, and rejection logs are updated.
For Backtrader strategies, the same assumptions are mapped into the Backtrader broker and custom sizers/observers where possible.
Adjusted OHLC
For split-adjusted simulation:
adjusted_open = open * adjusted_close / close
adjusted_high = high * adjusted_close / close
adjusted_low = low * adjusted_close / close
Raw OHLC can produce false price jumps around splits. Use adjusted OHLC for portfolio valuation and execution prices.
If only close and adjusted_close are available, the loader derives adjusted open/high/low by multiplying raw OHLC by adjusted_close / close. This keeps split adjustments internally consistent while preserving raw volume.
Example: if a split makes raw close 50 while adjusted close is 25, the adjustment factor is 0.5. A raw open of 52 becomes an adjusted open of 26.
Commission
Commission is bounded by per-share, minimum commission, and maximum transaction-rate settings.
The commission calculation is:
commission = min(
max(abs(quantity) * commission_per_share, min_commission),
abs(quantity) * price * max_commission_rate
)
Slippage
The Python engine applies:
Buy fills are worsened upward; sell fills are worsened downward.
Example with price=100, slippage_perc=0.0005, and no impact term:
Liquidity Cap
Orders are capped to:
Volume history uses prior bars only. If a cap is enabled but insufficient prior volume history exists, the Python engine rejects the order instead of silently filling it uncapped.
This cap is a participation-rate control, not a full market-impact model. For large-order simulations, combine it with nonzero slippage_impact.
Example: if the prior 20-day average volume is 1,000,000 shares and liquidity_cap_pct=0.025, the largest order is 25,000 shares. A request for 100,000 shares is reduced to 25,000.
LLM Cost
LLM costs can be recorded and included in trading cost:
Use:
from finsaber.toolkit.llm_cost_monitor import add_openai_cost_from_response
add_openai_cost_from_response(response)
The result artifact llm_costs.csv stores the cost ledger when present.
LLM cost is deliberately tracked through a small framework utility rather than by parsing every external agent implementation. External strategies should call the monitor around provider requests so model, token usage, provider, and metadata are saved with the run.
Rejected Orders
Orders can be rejected or reduced. This is expected behavior, not necessarily an engine failure.
| Reason | Meaning |
|---|---|
invalid_price |
No usable execution price was available. |
insufficient_liquidity_history |
Liquidity cap is enabled but not enough prior volume exists. |
zero_quantity |
Requested size becomes zero after holdings, cash, or liquidity checks. |
insufficient_cash |
The account cannot afford even one share after costs. |
insufficient_holdings |
A sell order exceeds current position. |
no_future_bar |
A pending next_open order has no later bar to execute on. |
Inspect rejected_orders.csv before trusting a backtest. A strategy with many rejected orders may be testing an unrealistic sizing rule.