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..3e2a295 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 notebooks use experimental feeds. 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_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"]
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()