Skip to content

Commit 9dcb351

Browse files
authored
Merge pull request #978 from Lumiwealth/version/4.4.55
v4.4.55 - IBKR day lookup fixes and tearsheet metrics hooks
2 parents c962eb9 + bddbfb6 commit 9dcb351

26 files changed

+1129
-16
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## 4.4.55 - 2026-03-15
4+
5+
### Added
6+
- `BACKTESTING_PARAMETERS` environment variable support for parameter injection in backtest runs.
7+
- Machine-readable `*_tearsheet_metrics.json` artifacts (summary-first) with placeholder output on insufficient/degenerate returns.
8+
- New strategy lifecycle hook `tearsheet_custom_metrics(...)` for appending custom metrics to tearsheet HTML and JSON artifacts.
9+
- Regression coverage for multi-timeframe day-timestep stock lookup and tearsheet metrics/custom-hook passthrough.
10+
11+
### Changed
12+
- Backtest analysis and trader APIs now accept `tearsheet_metrics_file`; default output filename is `*_tearsheet_metrics.json`.
13+
- QuantStats `metrics_json` generation now runs in `summary_only` mode and forwards custom metrics to both HTML and JSON outputs.
14+
- Documentation updates for tearsheet metrics/lifecycle hooks and TradingFee guidance (`per_contract_fee` usage).
15+
16+
### Fixed
17+
- Day-timestep asset lookup regression for multi-timeframe stock/index backtests (including minute->day fallback paths where appropriate).
18+
- IBKR stale no-data cache reuse now forces refresh when requested windows extend beyond cached coverage.
19+
- ProjectX order processing race-condition and tracking hardening merged from `dev`.
20+
21+
Deploy marker: `15e8e268` ("deploy 4.4.55")
22+
323
## 4.4.54 - 2026-03-08
424

525
### Added

docsrc/backtesting.tearsheet_html.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ These metrics are accompanied by various graphs such as:
1919
- **Cumulative Returns vs Benchmark:** Shows the strategy's cumulative returns compared to a benchmark.
2020
- **Cumulative Returns (Log Scaled):** A log-scaled version of cumulative returns for better visualization of exponential growth.
2121

22+
Machine-readable tearsheet metrics
23+
----------------------------------
24+
25+
Alongside ``*_tearsheet.html``, LumiBot also writes ``*_tearsheet_metrics.json``.
26+
27+
- This JSON contains summary tearsheet metrics in a machine-readable structure.
28+
- It is intended for downstream automation (agents, dashboards, APIs).
29+
- You can append strategy-specific metrics by implementing
30+
``Strategy.tearsheet_custom_metrics(...)``.
31+
2232
.. figure:: _html/images/tearsheet_condor_martingale.png
2333
:alt: Tearsheet example 1
2434
:width: 600px

docsrc/environment_variables.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ BACKTESTING_BUDGET
4343
- When set, this value is preferred over any ``budget=`` passed in strategy code, so it can be controlled per-run via injected environment variables.
4444
- Default (when unset and no code budget is provided): ``100000``.
4545

46+
BACKTESTING_PARAMETERS
47+
^^^^^^^^^^^^^^^^^^^^^^
48+
49+
- Purpose: Override or inject strategy parameters via environment variable, without modifying strategy code.
50+
- Format: JSON string representing a dictionary. Example: ``{"symbol": "AAPL", "quantity": 10}``
51+
- Notes:
52+
- When set, the parsed dict is merged on top of the strategy's existing ``parameters`` dict with highest priority (wins over both class-level defaults and code-level overrides).
53+
- Useful for parameter sweeps: run the same strategy code with different parameter sets per backtest.
54+
- Nested dicts are supported (e.g. ``{"ALLOCATION": {"SPY": 0.50, "IWM": 0.50}}``).
55+
- Invalid JSON or non-dict values are ignored with a warning.
56+
4657
BACKTESTING_DATA_SOURCE
4758
^^^^^^^^^^^^^^^^^^^^^^^
4859

docsrc/examples.rst

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,8 +723,8 @@ Futures Backtesting
723723
724724
if __name__ == "__main__":
725725
if IS_BACKTESTING:
726-
# Use flat fees for futures (typical: $0.50 per contract)
727-
trading_fee = TradingFee(flat_fee=0.50)
726+
# Use per-contract fees for futures (typical: $0.85 per standard contract, $0.50 for micros)
727+
trading_fee = TradingFee(per_contract_fee=0.85)
728728
729729
results = FuturesStrategy.backtest(
730730
DataBentoDataBacktesting,
@@ -733,6 +733,32 @@ Futures Backtesting
733733
sell_trading_fees=[trading_fee]
734734
)
735735
736+
Options Backtesting Fees
737+
~~~~~~~~~~~~~~~~~~~~~~~~
738+
739+
For options strategies, use ``per_contract_fee`` instead of ``flat_fee``. ``per_contract_fee`` is multiplied
740+
by the number of contracts in each order, which correctly models broker commissions like IBKR's $0.65/contract.
741+
742+
.. code-block:: python
743+
744+
from lumibot.entities import TradingFee
745+
746+
# IBKR charges $0.65 per contract per leg
747+
# For a 40-contract spread, that's $26.00 per leg
748+
trading_fee = TradingFee(per_contract_fee=0.65)
749+
750+
result = OptionsStrategy.backtest(
751+
ThetaDataBacktesting,
752+
benchmark_asset=Asset("SPY", Asset.AssetType.STOCK),
753+
buy_trading_fees=[trading_fee],
754+
sell_trading_fees=[trading_fee],
755+
)
756+
757+
.. note::
758+
Do NOT use ``flat_fee`` for options or futures commissions. ``flat_fee`` is a fixed amount per order
759+
regardless of contract count. For example, ``TradingFee(flat_fee=0.65)`` charges only $0.65 total on a
760+
40-contract order, while ``TradingFee(per_contract_fee=0.65)`` correctly charges $26.00 (40 x $0.65).
761+
736762
Multiple Futures Contracts
737763
~~~~~~~~~~~~~~~~~~~~~~~~~~
738764

docsrc/faq.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,9 @@ Only DataBento supports futures:
301301
302302
from lumibot.backtesting import DataBentoDataBacktesting
303303
304-
# Use flat fees for futures (typical: $0.50 per contract)
304+
# Use per-contract fees for futures (typical: $0.85 per standard contract, $0.50 for micros)
305305
from lumibot.entities import TradingFee
306-
trading_fee = TradingFee(flat_fee=0.50)
306+
trading_fee = TradingFee(per_contract_fee=0.85)
307307
308308
Why does my minute-level backtest fail with "no data"?
309309
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

docsrc/getting_started.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,11 @@ If you want to add trading fees to your backtesting, you can do so by setting up
221221
from lumibot.backtesting import YahooDataBacktesting
222222
from lumibot.entities import TradingFee
223223
224-
# Create two trading fees, one that is a percentage and one that is a flat fee
225-
trading_fee_1 = TradingFee(flat_fee=5) # $5 flat fee
224+
# Create trading fees: flat (per order), percent (of order value), or per-contract
225+
trading_fee_1 = TradingFee(flat_fee=5) # $5 flat fee per order
226226
trading_fee_2 = TradingFee(percent_fee=0.01) # 1% trading fee
227+
# For options/futures, use per_contract_fee instead:
228+
# trading_fee = TradingFee(per_contract_fee=0.65) # $0.65 per contract
227229
228230
backtesting_start = datetime(2020, 1, 1)
229231
backtesting_end = datetime(2020, 12, 31)

docsrc/lifecycle_methods.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ When building strategies, lifecycle methods needs to be overloaded. Trading logi
2323
lifecycle_methods.on_abrupt_closing
2424
lifecycle_methods.on_bot_crash
2525
lifecycle_methods.trace_stats
26+
lifecycle_methods.tearsheet_custom_metrics
2627
lifecycle_methods.on_new_order
2728
lifecycle_methods.on_partially_filled_order
2829
lifecycle_methods.on_filled_order
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
def tearsheet_custom_metrics
2+
============================
3+
4+
``tearsheet_custom_metrics`` is a strategy lifecycle hook that runs during backtest
5+
analysis, immediately before LumiBot writes:
6+
7+
- ``*_tearsheet.html``
8+
- ``*_tearsheet_metrics.json``
9+
10+
Use this hook when you want strategy-defined metrics in both artifacts.
11+
12+
When it runs
13+
------------
14+
15+
1. Backtest trading completes.
16+
2. LumiBot computes strategy/benchmark return series and drawdown context.
17+
3. LumiBot calls ``tearsheet_custom_metrics(...)``.
18+
4. Returned metrics are appended to the tearsheet metrics table and JSON scalar metrics.
19+
20+
Method signature
21+
----------------
22+
23+
.. code-block:: python
24+
25+
def tearsheet_custom_metrics(
26+
self,
27+
stats_df: pd.DataFrame | None,
28+
strategy_returns: pd.Series,
29+
benchmark_returns: pd.Series | None,
30+
drawdown: pd.Series,
31+
drawdown_details: pd.DataFrame,
32+
risk_free_rate: float,
33+
) -> dict:
34+
...
35+
36+
Parameter structure
37+
-------------------
38+
39+
``stats_df`` (``pd.DataFrame | None``)
40+
Backtest stats dataframe (same data used for ``*_stats.csv/parquet``).
41+
42+
``strategy_returns`` (``pd.Series``)
43+
Strategy return series used for tearsheet metric calculations.
44+
45+
``benchmark_returns`` (``pd.Series | None``)
46+
Benchmark return series when a benchmark is available; otherwise ``None``.
47+
48+
``drawdown`` (``pd.Series``)
49+
Strategy drawdown series derived from cumulative returns.
50+
51+
``drawdown_details`` (``pd.DataFrame``)
52+
Drawdown periods table (columns such as start/end/valley/days/max drawdown when available).
53+
54+
``risk_free_rate`` (``float``)
55+
Effective risk-free rate used by tearsheet metrics.
56+
57+
Return format
58+
-------------
59+
60+
Return a ``dict`` mapping metric names to values.
61+
62+
Supported formats:
63+
64+
.. code-block:: python
65+
66+
{"Custom Metric A": 1.23}
67+
{"Custom Metric B": {"strategy": 1.23, "benchmark": 0.91}}
68+
69+
Behavior rules:
70+
71+
1. Return ``{}`` if no custom metrics apply.
72+
2. Returning ``None`` is treated as no custom metrics.
73+
3. Returning a non-dict is ignored (with a warning).
74+
4. Exceptions inside the hook are caught; tearsheet generation continues.
75+
76+
Example
77+
-------
78+
79+
.. code-block:: python
80+
81+
class MyStrategy(Strategy):
82+
def tearsheet_custom_metrics(
83+
self,
84+
stats_df,
85+
strategy_returns,
86+
benchmark_returns,
87+
drawdown,
88+
drawdown_details,
89+
risk_free_rate,
90+
):
91+
if strategy_returns.empty:
92+
return {}
93+
94+
p95 = float(strategy_returns.quantile(0.95))
95+
avg_dd_days = (
96+
float(drawdown_details["days"].mean())
97+
if not drawdown_details.empty and "days" in drawdown_details.columns
98+
else 0.0
99+
)
100+
101+
return {
102+
"95th Percentile Daily Return": p95,
103+
"Average Drawdown Days": avg_dd_days,
104+
}
105+
106+
API reference (source of truth)
107+
-------------------------------
108+
109+
The method docstring below is auto-loaded from the Strategy class and should be
110+
treated as the canonical API reference.
111+
112+
.. automethod:: lumibot.strategies.strategy.Strategy.tearsheet_custom_metrics

llms.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,24 @@ class MyStrategy(Strategy):
122122
self.submit_order(order)
123123
```
124124

125+
### Trading Fees
126+
```python
127+
# Stocks: use percent_fee (percentage of order value)
128+
TradingFee(percent_fee=0.001) # 0.1% of order value
129+
130+
# Options: use per_contract_fee (multiplied by number of contracts)
131+
TradingFee(per_contract_fee=0.65) # $0.65 per contract (IBKR standard)
132+
133+
# Futures: use per_contract_fee
134+
TradingFee(per_contract_fee=0.85) # $0.85 per standard contract
135+
136+
# Flat fee: fixed amount per ORDER regardless of quantity
137+
TradingFee(flat_fee=5.00) # $5 per order
138+
139+
# Pass to backtest:
140+
result = MyStrategy.backtest(datasource, buy_trading_fees=[fee], sell_trading_fees=[fee])
141+
```
142+
125143
## Backtesting Data Sources
126144
- `YahooDataBacktesting` - Free, good for stocks
127145
- `PolygonDataBacktesting` - Crypto and stocks (requires API key)

lumibot/backtesting/interactive_brokers_rest_backtesting.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class InteractiveBrokersRESTBacktesting(PandasData):
2828
MIN_TIMESTEP = "minute"
2929
ALLOW_DAILY_TIMESTEP = True
3030
SOURCE = "InteractiveBrokersREST"
31+
PREFER_NATIVE_DAY_BARS_FOR_STOCK_INDEX = True
3132

3233
def __init__(
3334
self,

0 commit comments

Comments
 (0)