From 852441251c5c0355df49b37150c90bf2d6af43d0 Mon Sep 17 00:00:00 2001 From: Stefan Jansen Date: Thu, 24 Sep 2026 13:04:04 -0400 Subject: [PATCH 1/2] docs: make Live workflows and book links verifiable Use the credential-free shadow example for first use, repair its shutdown, add a checked public OKX shadow path, and pin semantically reviewed companion links. Keep the book tree and documentation links under CI verification. Refs #105 --- .github/workflows/docs.yml | 13 ++ docs/book-guide/index.md | 169 +++++++++---------- docs/getting-started/quickstart.md | 151 ++++++----------- docs/index.md | 11 +- docs/user-guide/backtest-to-live.md | 23 ++- docs/user-guide/brokers.md | 16 ++ docs/user-guide/cli.md | 2 +- docs/user-guide/examples.md | 1 + docs/user-guide/feeds.md | 16 ++ docs/user-guide/operator-guide.md | 17 +- docs/user-guide/risk.md | 17 ++ examples/README.md | 2 + examples/okx_shadow_strategy.py | 23 +++ examples/shadow_mode_demo.py | 22 ++- scripts/qualification/check_book_links.py | 69 ++++++++ scripts/qualification/check_public_claims.py | 6 +- scripts/qualification/qualify_artifacts.py | 7 +- tests/unit/test_artifact_qualification.py | 6 +- tests/unit/test_public_claims.py | 2 +- 19 files changed, 361 insertions(+), 212 deletions(-) create mode 100644 examples/okx_shadow_strategy.py create mode 100644 scripts/qualification/check_book_links.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bee04db..7bc6490 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,6 +28,15 @@ jobs: with: fetch-depth: 0 ref: ${{ env.CANDIDATE_SHA }} + - name: Check out pinned public book tree + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: stefan-jansen/machine-learning-for-trading + ref: d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb + path: companion + filter: blob:none + sparse-checkout: README.md + persist-credentials: false - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: version: "0.10.9" @@ -38,6 +47,10 @@ jobs: ML4T_DOCS_VERSION: ${{ env.CANDIDATE_VERSION }} ML4T_DOCS_COMMIT: ${{ env.CANDIDATE_SHA }} run: uv run mkdocs build --strict + - name: Check pinned book files + run: uv run python scripts/qualification/check_book_links.py --companion-repo companion + - name: Check documentation links + run: uv run python scripts/qualification/check_documentation_links.py - name: Verify rendered documentation identity run: >- uv run python scripts/qualification/verify_documentation_identity.py diff --git a/docs/book-guide/index.md b/docs/book-guide/index.md index 5c1b778..d03e73b 100644 --- a/docs/book-guide/index.md +++ b/docs/book-guide/index.md @@ -1,88 +1,85 @@ # Book Guide -This guide maps `ml4t-live` to **Machine Learning for Trading, Third Edition** so you can move between -the library docs and the book materials without guessing which notebook or chapter matters. - -## How To Use This Guide - -- Start with the chapter map if you are reading the book. -- Start with the API map if you are coming from the library and want the matching notebook. -- Treat the listed code paths as the canonical references in the `ml4t/code` repository. - -## Chapter To Feature Map - -| Book material | What it teaches | `ml4t-live` connection | -| --- | --- | --- | -| Chapter 16 strategy simulation | event-driven strategies and parity-friendly design | the strategy surface you carry into `LiveEngine` | -| Chapter 18 costs | execution frictions and turnover budgets | the live costs you compare against post-deployment | -| Chapter 19 risk management | kill switches, drawdowns, limits | `LiveRiskConfig`, `SafeBroker`, staged rollout | -| Chapter 25 live trading systems | brokers, feeds, operational parity, deployment | the core `ml4t-live` library surface | -| Chapter 26 MLOps governance | shadow mode, challenger rollout, circuit breakers | operational procedures around `ml4t-live` | - -## Notebook And Script Map - -| Book path | Why it matters here | -| --- | --- | -| `code/16_strategy_simulation/06_framework_parity.py` | shows why keeping one strategy interface matters before live deployment | -| `code/25_live_trading/01_unified_framework_demo.py` | compares lifecycle traces, canonical intents, and signals across the two engines | -| `code/25_live_trading/03_ib_paper_trading_demo.py` | Interactive Brokers connectivity path | -| `code/25_live_trading/04_alpaca_paper_trading_demo.py` | Alpaca paper-trading path | -| `code/25_live_trading/05_alpaca_crypto_live_demo.py` | Alpaca crypto workflow | -| `code/25_live_trading/08_pipeline_verification.py` | parity checks between research and live workflows | -| `code/25_live_trading/09_crypto_funding_deployment_loop.py` | OKX funding-rate deployment loop | -| `code/25_live_trading/10_safety_risk_demo.py` | `SafeBroker` limits, shadow mode, and kill-switch behavior | -| `code/26_mlops_governance/03_safe_model_rollout.py` | shadow-mode and staged-promotion procedures around live deployment | -| `code/26_mlops_governance/04_circuit_breakers.py` | broader operational safety concepts that complement `SafeBroker` | - -## Case Study Link: Crypto Perpetuals Funding - -The clearest live-trading case-study bridge in the current book materials is the crypto perpetuals -workflow: - -| Case-study path | Library relevance | -| --- | --- | -| `code/case_studies/crypto_perps_funding/03_financial_features.py` | feature definitions that must stay consistent in live inference | -| `code/case_studies/crypto_perps_funding/14_backtest.py` | the validated backtest side of the strategy | -| `code/case_studies/crypto_perps_funding/17_risk_management.py` | portfolio and risk assumptions before deployment | -| `code/25_live_trading/okx_funding_rate_demo.py` | the live-style deployment bridge using exchange funding data | - -## From Book Concepts To Library APIs - -| Book concept | Library API | -| --- | --- | -| portable decision logic | `LiveEngine` plus a lifecycle-v1 `Strategy` subclass using supported broker operations | -| sync strategy calling async infrastructure | `ThreadSafeBrokerWrapper` | -| explicit deployment risk policy | `LiveRiskConfig` | -| pre-trade enforcement and kill switch | `SafeBroker` | -| paper-like live validation without routing orders | `execution_mode="shadow"` with `VirtualPortfolio` | -| broker-specific execution path | `IBBroker` or `AlpacaBroker` | -| stable-supported live data source | `OKXFundingFeed` | -| experimental opt-in data source | `AlpacaDataFeed`, `IBDataFeed`, `DataBentoFeed`, `CryptoFeed` | - -## What The Book Often Shows Manually - -The notebooks are pedagogical and frequently expose mechanics directly. The library turns those same -ideas into reusable interfaces: - -- notebook orchestration becomes `LiveEngine` -- ad hoc risk checks become `SafeBroker` -- replay/live feed adapters become `DataFeedProtocol` implementations -- deployment-stage bookkeeping becomes `RiskState` and `VirtualPortfolio` - -## Best Reading Path - -If you are learning the stack end to end, the most efficient route is: - -1. `code/16_strategy_simulation/06_framework_parity.py` -2. [Backtest to Live](../user-guide/backtest-to-live.md) -3. `code/25_live_trading/01_unified_framework_demo.py` -4. the broker page that matches your venue -5. `code/25_live_trading/10_safety_risk_demo.py` -6. [Risk Controls](../user-guide/risk.md) - -## Related Docs - -- [Home](../index.md) -- [Quickstart](../getting-started/quickstart.md) -- [Backtest to Live](../user-guide/backtest-to-live.md) -- [API Reference](../api/index.md) +The public companion files for *Machine Learning for Trading, Third Edition* show the methods and +operating decisions behind Live workflows. These links point to commit +[`d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb`](https://github.com/stefan-jansen/machine-learning-for-trading/tree/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb). +Each file below was checked in that Git tree. "Calls Live" means its code imports `ml4t.live`; +"manual" means it teaches a method without that import; "related" means another workflow informs +Live decisions. A matching topic alone does not mean a notebook runs this library. + +The Alpaca and IB feed notebooks use an experimental opt-in data source. Their broker adapters +have a separate stable paper contract. + +## Run and control a Live strategy + +- [Unified framework demo][unified] calls Live and Backtest to compare lifecycle traces and + portable strategy decisions. Follow [Backtest to Live](../user-guide/backtest-to-live.md). +- [IB paper trading demo][ib-paper] calls Live with an IB paper broker and experimental IB feed. It + requires TWS or IB Gateway. Follow [Brokers](../user-guide/brokers.md). +- [Alpaca paper trading demo][alpaca-paper] calls Live with Alpaca paper execution and an + experimental Alpaca feed. It requires credentials. Follow [Brokers](../user-guide/brokers.md). +- [Alpaca crypto live demo][alpaca-crypto] calls Live for a crypto venue workflow. Review its live + order prerequisites before attempting it. Follow [Brokers](../user-guide/brokers.md). +- [Pipeline verification][pipeline] calls Live to compare research and deployment inputs and + decisions. Follow [Backtest to Live](../user-guide/backtest-to-live.md). +- [Safety and risk demo][safety] calls Live to exercise `SafeBroker` and shadow risk behavior. + Follow [Risk Controls](../user-guide/risk.md). +- [Runtime safety showcase][runtime-safety] calls Live to show stale-data rejection, persistent + kill switch, reconciliation, and engine health. Follow the [Operator Guide](../user-guide/operator-guide.md). +- [Crypto funding deployment loop][funding-loop] calls `AlpacaBroker` only in its + operator-authorized paper path. It uses OKX public data for a separate execution rehearsal. + Follow [Data Feeds](../user-guide/feeds.md). + +The crypto funding loop trains on Binance-derived perpetual data, observes OKX funding and bars, +and maps a subset to Alpaca USD spot crypto for paper execution. The venues and instruments differ; +its paper orders do not reproduce a perpetual-futures backtest. The notebook needs its case-study +artifacts and outbound data access, and paper submission needs credentials and explicit operator +opt-in. The [synthetic shadow quickstart](../getting-started/quickstart.md) is the local first run. + +## Learn the methods before deployment + +- [Framework parity][parity] is a related Backtest workflow that compares strategy behavior before + a Live port. It does not import Live. Follow [Backtest to Live](../user-guide/backtest-to-live.md). +- [Safe model rollout][rollout] teaches staged promotion and shadow evaluation manually. It does + not import Live. Follow the [Operator Guide](../user-guide/operator-guide.md). +- [Circuit breakers][circuit] teaches operational safety manually. It does not implement + `SafeBroker`. Follow [Risk Controls](../user-guide/risk.md). +- [Crypto financial features][features] is a related feature workflow using Engineer. It does not + import Live. Follow [Backtest to Live](../user-guide/backtest-to-live.md). +- [Crypto backtest][crypto-backtest] simulates funding-aligned decisions manually. It does not + import Live. Follow [Backtest to Live](../user-guide/backtest-to-live.md). +- [Crypto risk management][crypto-risk] studies sizing and risk manually. It does not import Live. + Follow [Risk Controls](../user-guide/risk.md). + +The case-study files use research data, model artifacts, and their own simulation assumptions. +`LiveEngine` needs a feed, venue capability check, broker account state, and explicit `LiveRiskConfig`. +Do not treat a research result as a qualified paper or live run. + +## Find the public interface + +- Run lifecycle callbacks with `LiveEngine` and `ThreadSafeBrokerWrapper`: + [Backtest to Live](../user-guide/backtest-to-live.md). +- Limit and audit orders with `LiveRiskConfig`, `SafeBroker`, `RiskState`, and `VirtualPortfolio`: + [Risk Controls](../user-guide/risk.md). +- Connect `IBBroker` or `AlpacaBroker`: [Brokers](../user-guide/brokers.md). +- Choose `OKXFundingFeed`, `BarAggregator`, or an experimental provider feed: + [Data Feeds](../user-guide/feeds.md). +- Inspect or restart with `LiveEngine.runtime_status` and `SafeBroker.reconciliation_report`: + [Operator Guide](../user-guide/operator-guide.md). + +See the [API Reference](../api/index.md) for exact signatures and options. + +[unified]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/01_unified_framework_demo.ipynb +[ib-paper]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/03_ib_paper_trading_demo.ipynb +[alpaca-paper]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/04_alpaca_paper_trading_demo.ipynb +[alpaca-crypto]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/05_alpaca_crypto_live_demo.ipynb +[pipeline]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/08_pipeline_verification.ipynb +[safety]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/10_safety_risk_demo.ipynb +[runtime-safety]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/13_runtime_safety_showcase.ipynb +[funding-loop]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/09_crypto_funding_deployment_loop.ipynb +[parity]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/16_strategy_simulation/06_framework_parity.ipynb +[rollout]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/26_mlops_governance/03_safe_model_rollout.ipynb +[circuit]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/26_mlops_governance/04_circuit_breakers.ipynb +[features]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/case_studies/crypto_perps_funding/03_financial_features.ipynb +[crypto-backtest]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/case_studies/crypto_perps_funding/13_backtest.ipynb +[crypto-risk]: https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/case_studies/crypto_perps_funding/15_risk_management.ipynb diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index c5aa511..9d56150 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,110 +1,53 @@ -# Quickstart +# Quickstart: complete a shadow run -The recommended first run is shadow mode: your strategy executes through the live engine and risk -checks, but no real orders are sent to the broker. +Run a strategy through `LiveEngine` and `SafeBroker` with synthetic bars. The example needs Python +3.12-3.14 and `uv`; it needs no credentials, market-data subscription, or network service after +installation. It never sends an order to a venue. -## First Strategy +## Run the example -```python -import asyncio +Get the maintained example from the public repository, then run it with the published package: -from ml4t.backtest import Strategy -from ml4t.backtest.types import OrderSide -from ml4t.live import AlpacaBroker, AlpacaDataFeed, LiveEngine, LiveRiskConfig, SafeBroker - - -class BuyOnceStrategy(Strategy): - def on_data(self, timestamp, data, context, broker): - bar = data.get("SPY") - if bar is None: - return - - if broker.get_position("SPY") is None: - broker.submit_order("SPY", 10, side=OrderSide.BUY) - - -async def main(): - broker = AlpacaBroker(api_key="...", secret_key="...", paper=True) - feed = AlpacaDataFeed( - api_key="...", - secret_key="...", - symbols=["SPY"], - data_type="bars", - experimental=True, - ) - safe_broker = SafeBroker( - broker, - LiveRiskConfig( - execution_mode="shadow", - max_position_value=25_000, - max_order_value=5_000, - ), - ) - - engine = LiveEngine(BuyOnceStrategy(), safe_broker, feed) - await engine.connect() - - try: - await engine.run() - finally: - await engine.stop() - - -asyncio.run(main()) -``` - -## Why This Works - -- Your strategy stays synchronous, just like in `ml4t-backtest` -- `LiveEngine` runs broker/feed I/O asynchronously -- `LiveEngine` runs every lifecycle callback on one worker thread, so synchronous broker calls do - not re-enter or block the async event loop -- `SafeBroker` enforces limits before any live order can be placed - -## First-Run Checklist - -Before you move past this example, confirm that: - -1. the strategy receives bars from the feed you expect -2. orders appear in shadow mode instead of hitting the broker -3. positions and cash update through the virtual portfolio -4. you can stop and restart the engine cleanly - -## Deployment Progression - -1. Shadow mode: `execution_mode="shadow"` -2. Paper trading: `execution_mode="paper"` with paper broker credentials -3. Small live size: `execution_mode="live"` with conservative limits and low notional exposure -4. Gradual scale-up only after observing stable behavior - -## Common Variations - -### Experimental Interactive Brokers Feed - -`IBDataFeed` needs a connected IB session object and explicit experimental opt-in: - -```python -broker = IBBroker(port=7497) -await broker.connect() - -feed = IBDataFeed(broker.ib, symbols=["SPY", "QQQ"], experimental=True) -``` - -### Aggregate Ticks Into Bars - -```python -raw_feed = IBDataFeed(broker.ib, symbols=["SPY"], experimental=True) -feed = BarAggregator(raw_feed, bar_size_minutes=1, flush_timeout_seconds=2.0) +```bash +git clone https://github.com/ml4t/live.git +cd live +uv run --no-project --with ml4t-live python examples/shadow_mode_demo.py ``` -## Next Steps - -- [Installation](installation.md) -- [Backtest to Live](../user-guide/backtest-to-live.md) -- [Risk Controls](../user-guide/risk.md) -- [CLI](../user-guide/cli.md) -- [Examples](../user-guide/examples.md) -- [Operator Guide](../user-guide/operator-guide.md) -- [Brokers](../user-guide/brokers.md) -- [Data Feeds](../user-guide/feeds.md) -- [Book Guide](../book-guide/index.md) +The script defines a synthetic feed that emits one `DEMO` bar per second for 65 seconds. A moving +average strategy submits a buy or sell when its signal changes. `SafeBroker` records virtual fills +and positions under `execution_mode="shadow"`; the example broker raises if it receives a real +order. The input and full program are in +[`examples/shadow_mode_demo.py`](https://github.com/ml4t/live/blob/main/examples/shadow_mode_demo.py). + +The run prints `Starting shadow mode demo with a synthetic feed.`, timestamped prices, five-second +position heartbeats, and `Finished shadow mode demo. final_positions=...`. The final position may +be flat or long because the synthetic signal changes during the run. A completed final line means +the engine stopped cleanly. The printed positions show what the virtual portfolio held; no venue +state changed. + +To check the installed API without waiting for the full run, the [README quick start](https://github.com/ml4t/live/blob/main/README.md) +processes a single virtual fill and asserts its resulting position and cash balance. For the next +operator task, the [reconciliation example](../user-guide/examples.md) shows a restart mismatch +without a broker connection. + +## Understand the boundary + +This first run proves that the strategy callback, engine lifecycle, safety wrapper, and virtual +portfolio work with synthetic input. It does not qualify a broker, external feed, or restart on an +active account. A real paper session needs broker credentials or TWS/IB Gateway, the applicable +market-data access, a compatible feed, explicit `execution_mode="paper"`, and clean startup +reconciliation. Alpaca and IB feeds require `experimental=True` and do not have stable reconnect +continuity guarantees. + +The public OKX funding feed needs outbound HTTPS but no credentials. To try it with the CLI, see +[Run a bounded shadow session](../user-guide/cli.md#shadow). A quiet feed or a run with no trade +intent is not evidence that a provider workflow is ready for paper orders. + +## Continue + +- [Backtest to Live](../user-guide/backtest-to-live.md) explains what stays portable. +- [Risk Controls](../user-guide/risk.md) covers limits, persistence, and the kill switch. +- [Brokers](../user-guide/brokers.md) and [Data Feeds](../user-guide/feeds.md) state provider requirements. +- [API Reference](../api/index.md) gives the released signatures. +- [Book Guide](../book-guide/index.md) connects the tasks to checked companion files. diff --git a/docs/index.md b/docs/index.md index 1790f52..95bbb40 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,8 +4,7 @@ Run a lifecycle-v1 `Strategy` against real brokers and live market data, with `S controls layered on top. `ml4t-live` is the deployment layer in the ML4T stack. It sits after research and backtesting, where -the main problem is no longer "does this idea work?" but "can I run it safely against a broker without -preserving its portable decisions and controlling operational risk?" The library is built for staged rollout: +the task is to preserve portable strategy decisions while controlling broker and operational risk. The library is built for staged rollout: shadow mode first, then paper trading, then small live size under explicit limits.
@@ -44,6 +43,14 @@ shadow mode first, then paper trading, then small live size under explicit limit
+## Start with a completed run + +The [Quickstart](getting-started/quickstart.md) runs `LiveEngine` against synthetic bars with +`SafeBroker(execution_mode="shadow")`. It prints strategy prices, virtual positions, and a final +completion line without credentials or venue orders. Then use the +[task guides](user-guide/examples.md), [exact API reference](api/index.md), and +[checked book examples](book-guide/index.md) for your next task. + ## What You Can Do With It Right Now With `ml4t-live`, you can: diff --git a/docs/user-guide/backtest-to-live.md b/docs/user-guide/backtest-to-live.md index 550c93f..7185785 100644 --- a/docs/user-guide/backtest-to-live.md +++ b/docs/user-guide/backtest-to-live.md @@ -88,11 +88,18 @@ failure can therefore identify the first boundary whose trace differs. ## See It In The Book -The maintained chapter material uses both engine dispatch paths and compares lifecycle plus -canonical intent: - -- Chapter 25.1 and `code/25_live_trading/01_unified_framework_demo.py` -- Chapter 25.6 and `code/25_live_trading/08_pipeline_verification.py` -- Chapter 25.7 and `code/25_live_trading/10_safety_risk_demo.py` - -Use the [Book Guide](../book-guide/index.md) for the broader chapter map. +The [unified framework notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/01_unified_framework_demo.ipynb) +calls both Live and Backtest to compare lifecycle and intent traces. The +[pipeline verification notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/08_pipeline_verification.ipynb) +calls Live to check the research-to-deployment handoff. The +[crypto funding backtest](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/case_studies/crypto_perps_funding/13_backtest.ipynb) +is a related manual case study; its simulation is not a Live run. See the +[Book Guide](../book-guide/index.md) for input and venue differences. + +## Verify the first port + +Run the [synthetic shadow quickstart](../getting-started/quickstart.md) to check callback dispatch, +virtual positions, and orderly shutdown without a venue. Then compare the same strategy's lifecycle +and canonical intents in Backtest and Live on equivalent input. Record data cutoffs, execution +policy, starting positions, fees, and venue capabilities before interpreting outcome differences. +The [API reference](../api/index.md#engine) gives the Live engine signature. diff --git a/docs/user-guide/brokers.md b/docs/user-guide/brokers.md index 9ded5aa..00f280b 100644 --- a/docs/user-guide/brokers.md +++ b/docs/user-guide/brokers.md @@ -145,3 +145,19 @@ except RiskLimitError as exc: For the surrounding migration story, read [Backtest to Live](backtest-to-live.md) and the [Book Guide](../book-guide/index.md). + +## Book examples + +The [IB paper notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/03_ib_paper_trading_demo.ipynb) +and [Alpaca paper notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/04_alpaca_paper_trading_demo.ipynb) +call the respective Live adapters. They require configured paper accounts; the market-data feeds in +those notebooks are experimental. Follow the [credential-free quickstart](../getting-started/quickstart.md) +before connecting either provider. + +## Verify a paper connection safely + +With a configured paper account, run the [CLI preflight](cli.md#preflight) for `ib` or `alpaca` +with `--strict`. Confirm that connectivity, account state, execution identity, and reconciliation +are clean before starting a strategy. Preflight reads broker state and sends no orders. Paper order +lifecycle behavior still requires a separately authorized provider qualification. The +[API reference](../api/index.md#brokers) lists adapter signatures. diff --git a/docs/user-guide/cli.md b/docs/user-guide/cli.md index 1d12035..e2aa0c3 100644 --- a/docs/user-guide/cli.md +++ b/docs/user-guide/cli.md @@ -70,7 +70,7 @@ export IB_CLIENT_ID=1999 `SafeBroker(execution_mode="shadow")`, and runs a bounded live-engine session. ```bash -uv run ml4t-live shadow examples/shadow_mode_demo.py --feed okx --duration 60 +uv run ml4t-live shadow examples/okx_shadow_strategy.py --feed okx --duration 60 ``` Supported flags: diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index ea9b74e..4fc1d98 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -21,6 +21,7 @@ That exercises `LiveEngine`, `SafeBroker`, `VirtualPortfolio`, and shadow-mode o | Example | What it shows | Requirements | Run | | --- | --- | --- | --- | | `shadow_mode_demo.py` | shadow mode, synthetic feed, virtual fills | none | `uv run python examples/shadow_mode_demo.py` | +| `okx_shadow_strategy.py` | public OKX bars through the CLI shadow runtime, no orders | outbound HTTPS to `okx.com` | `uv run ml4t-live shadow examples/okx_shadow_strategy.py --feed okx --duration 60` | | `okx_funding_paper.py` | public OKX funding feed on 1-minute bars | outbound HTTPS to `okx.com` | `uv run python examples/okx_funding_paper.py` | | `alpaca_paper_equity.py` | Alpaca paper broker + Alpaca feed + small MA strategy | `ALPACA_API_KEY`, `ALPACA_SECRET_KEY` | `uv run python examples/alpaca_paper_equity.py` | | `ib_paper_equity.py` | IB paper broker + IB feed + small momentum strategy | TWS or IB Gateway on `127.0.0.1:7497` | `uv run python examples/ib_paper_equity.py` | diff --git a/docs/user-guide/feeds.md b/docs/user-guide/feeds.md index e0e4a1f..0311e7d 100644 --- a/docs/user-guide/feeds.md +++ b/docs/user-guide/feeds.md @@ -216,3 +216,19 @@ the engine watchdog can stop and restart the broker/feed pair after `feed_silent - Use `OKXFundingFeed` for perpetual-swap strategies that depend on funding-rate context. - Use `BarAggregator` when your upstream feed is tick-oriented but your strategy expects bars. - Pass `experimental=True` only after accepting the limitations reported by an experimental feed. + +## Book example + +The [crypto funding deployment notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/09_crypto_funding_deployment_loop.ipynb) +uses public OKX data and conditionally calls Live's `AlpacaBroker` for paper execution. It is a +research-to-deployment case study with different data and execution venues, not an example of +`OKXFundingFeed` itself. Use the [CLI shadow guide](cli.md#shadow) for the feed's public API path. + +## Verify a feed safely + +For the stable OKX feed, run `uv run ml4t-live shadow examples/okx_shadow_strategy.py --feed okx +--duration 60` with outbound HTTPS available. Observe `last_bar_age`, `health`, and any +`recent_intents`; no venue order is routed. Public data can be quiet or unavailable, so a bounded +run does not prove feed continuity or paper readiness. For Alpaca, IB, DataBento, and generic CCXT, +constructors and deterministic adapter tests check only the typed contract. Their service behavior +needs separate provider qualification. See the [API reference](../api/index.md#data-feeds-and-aggregation). diff --git a/docs/user-guide/operator-guide.md b/docs/user-guide/operator-guide.md index 6857fd5..8a11f42 100644 --- a/docs/user-guide/operator-guide.md +++ b/docs/user-guide/operator-guide.md @@ -23,7 +23,7 @@ If you only want a descriptive dump and not a pass/fail check, use `status` inst Shadow mode is the correct first deployment step because it exercises the live engine and strategy path without routing a real order. ```bash -uv run ml4t-live shadow examples/shadow_mode_demo.py --feed okx --duration 60 +uv run ml4t-live shadow examples/okx_shadow_strategy.py --feed okx --duration 60 ``` What to watch during the run: @@ -186,3 +186,18 @@ changes, recovery, reconciliation, and order events remain journaled. Exhausted `RuntimeFailureError` and leaves `runtime_state=failed`. A resource release failure raises `RuntimeCleanupError`; call `stop()` again after correcting a transient provider failure to retry release. + +## Book example + +The [runtime safety notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/13_runtime_safety_showcase.ipynb) +calls Live to show kill-switch persistence, startup reconciliation, and engine health. The +[safe rollout notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/26_mlops_governance/03_safe_model_rollout.ipynb) +teaches promotion decisions manually and does not call Live. + +## Verify restart reporting locally + +Run `uv run python examples/startup_reconciliation_demo.py`. The synthetic persisted snapshot and +broker snapshot deliberately differ; the report prints `"clean": false` and lists missing and +unexpected positions and pending orders. This checks mismatch reporting without connecting a +provider. On a real account, treat any mismatch as a stop point and reconcile before sending a new +order. See the [API reference](../api/index.md#safety-and-rollout) for `SafeBroker`. diff --git a/docs/user-guide/risk.md b/docs/user-guide/risk.md index 69f76c4..68d7323 100644 --- a/docs/user-guide/risk.md +++ b/docs/user-guide/risk.md @@ -219,3 +219,20 @@ except RiskLimitError as exc: 5. Increase size only after repeated clean starts and expected fills. For the broader chapter map, use the [Book Guide](../book-guide/index.md). + +## Book examples + +The [safety and risk notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/10_safety_risk_demo.ipynb) +calls `SafeBroker` to show shadow limits. The +[runtime safety notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/25_live_trading/13_runtime_safety_showcase.ipynb) +calls Live to show persistence and restart reconciliation. The +[circuit-breaker notebook](https://github.com/stefan-jansen/machine-learning-for-trading/blob/d2edec54b1c7a6a9d7a97d8129eb05db4491e1eb/26_mlops_governance/04_circuit_breakers.ipynb) +teaches broader controls manually; it does not call Live. + +## Verify the controls locally + +Run `uv run python examples/risk_guard_demo.py`. The script submits one order with fresh synthetic +data, rejects a stale-data order, and activates the daily-loss kill switch without reaching a +provider. Confirm the output includes `fresh_data_order: accepted`, `stale_data_block:`, and +`kill_switch_active: True`. This checks the control path, not broker execution. See +[`LiveRiskConfig` and `SafeBroker`](../api/index.md#safety-and-rollout) for exact options. diff --git a/examples/README.md b/examples/README.md index a14ecb0..687b290 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,6 +13,8 @@ session, prints progress every five seconds, and exits after about a minute. `risk_guard_demo.py` demonstrates two runtime guards without a real broker: stale-data rejection and daily-loss kill-switch activation. It accepts one fresh-data shadow order, blocks one stale-data order, then shows the kill switch activating after a simulated equity drop. +`okx_shadow_strategy.py` is a strategy module for `ml4t-live shadow`. It prints completed OKX bars without submitting orders. The CLI uses its `SYMBOLS` list and needs outbound HTTPS but no credentials. + `okx_funding_paper.py` uses the public OKX REST API to poll BTC, ETH, and SOL perpetual swaps on 1-minute candles. It needs only outbound HTTPS access to `okx.com`, prints funding snapshots and simple long/short bias labels, and exits after about 70 seconds. `alpaca_paper_equity.py` connects to Alpaca paper trading for SPY, QQQ, and IWM with a tiny moving-average crossover strategy. It requires `ALPACA_API_KEY` and `ALPACA_SECRET_KEY`, uses paper-only endpoints, prints heartbeats every five seconds, and exits after about 95 seconds. diff --git a/examples/okx_shadow_strategy.py b/examples/okx_shadow_strategy.py new file mode 100644 index 0000000..728044e --- /dev/null +++ b/examples/okx_shadow_strategy.py @@ -0,0 +1,23 @@ +"""Observe public OKX bars in a bounded, order-free CLI shadow run. + +Prerequisites: + Published ml4t-live package and outbound HTTPS to the OKX public API. +Expected Output: + At least one ``OKX bar`` line when completed candles are available, followed by + the CLI's ``Completed shadow run`` summary with ``orders=0``. +Expected Failure: + Feed or network errors may stop the run; a quiet feed may yield no bars. +Cleanup: + The CLI stops the feed and closes the shadow broker after the duration. +""" + +from ml4t.backtest import Strategy + +SYMBOLS = ["BTC-USDT-SWAP"] + + +class ObserveOKXBars(Strategy): + def on_data(self, timestamp, data, context, broker) -> None: + bar = data.get(SYMBOLS[0]) + if bar is not None and "close" in bar: + print(f"OKX bar {timestamp.isoformat()} {SYMBOLS[0]} close={bar.get('close')}") diff --git a/examples/shadow_mode_demo.py b/examples/shadow_mode_demo.py index 8e92f0c..df85b74 100644 --- a/examples/shadow_mode_demo.py +++ b/examples/shadow_mode_demo.py @@ -98,6 +98,8 @@ class SyntheticBarFeed: def __init__(self, duration_seconds: int) -> None: self.duration_seconds = duration_seconds self._running = False + self.completed = asyncio.Event() + self._stopped = asyncio.Event() self._index = 0 self._started_at = datetime.now(UTC).replace(microsecond=0) @@ -106,12 +108,17 @@ async def start(self) -> None: def stop(self) -> None: self._running = False + self._stopped.set() def __aiter__(self) -> AsyncIterator[tuple[datetime, dict, dict]]: return self async def __anext__(self) -> tuple[datetime, dict, dict]: - if not self._running or self._index >= self.duration_seconds: + if not self._running: + raise StopAsyncIteration + if self._index >= self.duration_seconds: + self.completed.set() + await self._stopped.wait() raise StopAsyncIteration timestamp = self._started_at + timedelta(seconds=self._index) @@ -212,19 +219,24 @@ async def main() -> int: state_file=str(state_file), ), ) - engine = LiveEngine( - ShadowMomentumStrategy(), safe_broker, SyntheticBarFeed(DURATION_SECONDS) - ) + feed = SyntheticBarFeed(DURATION_SECONDS) + engine = LiveEngine(ShadowMomentumStrategy(), safe_broker, feed) print("Starting shadow mode demo with a synthetic feed.") await engine.connect() heartbeat_task = asyncio.create_task(heartbeat(safe_broker)) + async def stop_after_final_bar() -> None: + await feed.completed.wait() + await engine.stop() + + stop_task = asyncio.create_task(stop_after_final_bar()) try: await engine.run() finally: heartbeat_task.cancel() - await asyncio.gather(heartbeat_task, return_exceptions=True) + stop_task.cancel() + await asyncio.gather(heartbeat_task, stop_task, return_exceptions=True) await engine.stop() positions = safe_broker.positions diff --git a/scripts/qualification/check_book_links.py b/scripts/qualification/check_book_links.py new file mode 100644 index 0000000..9bf6b6d --- /dev/null +++ b/scripts/qualification/check_book_links.py @@ -0,0 +1,69 @@ +"""Check pinned public book file links against the companion Git tree.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +BOOK_URL = re.compile( + r"https://github\.com/stefan-jansen/machine-learning-for-trading/blob/" + r"(?P[0-9a-f]{40})/(?P[^\s)]+?\.(?:ipynb|py))" +) + + +def check_book_links(companion_repo: Path) -> tuple[int, list[str]]: + """Return the number of checked file links and any revision or path failures.""" + links: list[tuple[Path, str, str]] = [] + for source in sorted((REPOSITORY_ROOT / "docs").rglob("*.md")): + for match in BOOK_URL.finditer(source.read_text(encoding="utf-8")): + links.append((source, match.group("revision"), match.group("path"))) + + failures: list[str] = [] + if not links: + return 0, ["no pinned book file links found"] + revisions = {revision for _, revision, _ in links} + if len(revisions) != 1: + failures.append(f"book file links use multiple revisions: {sorted(revisions)}") + return len(links), failures + + revision = revisions.pop() + result = subprocess.run( + ["git", "-C", str(companion_repo), "ls-tree", "-r", "--name-only", revision], + capture_output=True, + text=True, + check=False, + ) + if result.returncode: + return len(links), [ + f"companion revision {revision} is unavailable: {result.stderr.strip()}" + ] + tree = set(result.stdout.splitlines()) + for source, _, path in links: + if path not in tree: + failures.append(f"{source.relative_to(REPOSITORY_ROOT)}: missing book file {path}") + + guide = REPOSITORY_ROOT / "docs" / "book-guide" / "index.md" + guide_count = len(BOOK_URL.findall(guide.read_text(encoding="utf-8"))) + if guide_count < 14: + failures.append(f"Book Guide has {guide_count} direct file links; expected at least 14") + return len(links), failures + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--companion-repo", type=Path, required=True) + args = parser.parse_args() + count, failures = check_book_links(args.companion_repo) + print(f"checked {count} book file links") + for failure in failures: + print(f"book link failure: {failure}") + if not failures: + print("book links: PASS") + return int(bool(failures)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/qualification/check_public_claims.py b/scripts/qualification/check_public_claims.py index 2165dd2..a33833f 100644 --- a/scripts/qualification/check_public_claims.py +++ b/scripts/qualification/check_public_claims.py @@ -126,6 +126,7 @@ "ib_paper_equity.py", "live_ib_example.py", "okx_funding_paper.py", + "okx_shadow_strategy.py", } ) @@ -234,7 +235,10 @@ def check_public_claims(root: Path = REPOSITORY_ROOT) -> list[str]: for heading in ("Prerequisites:", "Expected Output:", "Expected Failure:", "Cleanup:"): if heading not in text: failures.append(f"external example {name} does not state: {heading}") - if name != "okx_funding_paper.py" and "paper" not in text.casefold(): + if ( + name not in {"okx_funding_paper.py", "okx_shadow_strategy.py"} + and "paper" not in text.casefold() + ): failures.append(f"external broker example {name} does not identify a paper account") return failures diff --git a/scripts/qualification/qualify_artifacts.py b/scripts/qualification/qualify_artifacts.py index cf08c0d..cd06916 100644 --- a/scripts/qualification/qualify_artifacts.py +++ b/scripts/qualification/qualify_artifacts.py @@ -349,7 +349,10 @@ def run_installed_examples(python: Path, root: Path) -> None: "stale_data_block:", "kill_switch_active: True", ), - "shadow_mode_demo.py": ("Starting shadow mode demo", "Finished shadow mode demo"), + "shadow_mode_demo.py": ( + "Starting shadow mode demo", + "Finished shadow mode demo. final_positions=DEMO:10", + ), "startup_reconciliation_demo.py": ("Startup reconciliation report:", '"clean": false'), } environment = { @@ -364,6 +367,8 @@ def run_installed_examples(python: Path, root: Path) -> None: missing = [marker for marker in expected_output[name] if marker not in result.stdout] if missing: raise QualificationError(f"installed example {name} omitted output markers: {missing}") + if name == "shadow_mode_demo.py" and "Runtime degraded" in result.stderr: + raise QualificationError("shadow example ended in a degraded runtime state") def install_profile(artifact: Path, python_version: str, expected_version: str, root: Path) -> None: diff --git a/tests/unit/test_artifact_qualification.py b/tests/unit/test_artifact_qualification.py index 1715023..26498c6 100644 --- a/tests/unit/test_artifact_qualification.py +++ b/tests/unit/test_artifact_qualification.py @@ -266,12 +266,14 @@ def fake_run(command, *, cwd, environment=None, expected_returncode=0): "risk_guard_demo.py": ( "fresh_data_order: accepted\nstale_data_block: rejected\nkill_switch_active: True\n" ), - "shadow_mode_demo.py": "Starting shadow mode demo\nFinished shadow mode demo\n", + "shadow_mode_demo.py": ( + "Starting shadow mode demo\nFinished shadow mode demo. final_positions=DEMO:10\n" + ), "startup_reconciliation_demo.py": ( 'Startup reconciliation report:\n{"clean": false}\n' ), }[name] - return type("Result", (), {"stdout": output})() + return type("Result", (), {"stdout": output, "stderr": ""})() monkeypatch.setattr(qualify_artifacts, "_run", fake_run) run_installed_examples(python, tmp_path / "profile") diff --git a/tests/unit/test_public_claims.py b/tests/unit/test_public_claims.py index 7c43468..3c93d35 100644 --- a/tests/unit/test_public_claims.py +++ b/tests/unit/test_public_claims.py @@ -31,7 +31,7 @@ def test_external_examples_document_safe_operation() -> None: text = (ROOT / "examples" / name).read_text() for heading in ("Prerequisites:", "Expected Output:", "Expected Failure:", "Cleanup:"): assert heading in text - if name != "okx_funding_paper.py": + if name not in {"okx_funding_paper.py", "okx_shadow_strategy.py"}: assert "paper" in text.casefold() From 588b2d595718e04ea7426e8311afcdc5fbb0a397 Mon Sep 17 00:00:00 2001 From: Stefan Jansen Date: Thu, 24 Sep 2026 13:07:31 -0400 Subject: [PATCH 2/2] docs: state feed maturity plainly in Book Guide Remove an exact-phrase assertion that forced awkward copy; the feed guide and API checks still cover the experimental boundary. --- docs/book-guide/index.md | 4 ++-- tests/unit/test_experimental_feeds.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/book-guide/index.md b/docs/book-guide/index.md index d03e73b..3e2a295 100644 --- a/docs/book-guide/index.md +++ b/docs/book-guide/index.md @@ -7,8 +7,8 @@ Each file below was checked in that Git tree. "Calls Live" means its code import "manual" means it teaches a method without that import; "related" means another workflow informs Live decisions. A matching topic alone does not mean a notebook runs this library. -The Alpaca and IB feed notebooks use an experimental opt-in data source. Their broker adapters -have a separate stable paper contract. +The Alpaca and IB notebooks use experimental feeds. Their broker adapters have a separate stable +paper contract. ## Run and control a Live strategy diff --git a/tests/unit/test_experimental_feeds.py b/tests/unit/test_experimental_feeds.py index 61aa327..52dfafc 100644 --- a/tests/unit/test_experimental_feeds.py +++ b/tests/unit/test_experimental_feeds.py @@ -40,7 +40,6 @@ def test_public_claims_separate_experimental_feeds_from_stable_support() -> None "feed guide": (ROOT / "docs/user-guide/feeds.md").read_text(), "API reference": (ROOT / "docs/api/index.md").read_text(), "docs landing": (ROOT / "docs/index.md").read_text(), - "book guide": (ROOT / "docs/book-guide/index.md").read_text(), } assert ( @@ -53,7 +52,6 @@ def test_public_claims_separate_experimental_feeds_from_stable_support() -> None in public_text["API reference"] ) assert "experimental opt-in" in public_text["docs landing"] - assert "experimental opt-in data source" in public_text["book guide"] assert "experimental=True" in public_text["README"] assert "experimental=True" in public_text["feed guide"]