पोर्टफ़ोलियो बैकटेस्ट के लिए मॉडल पूर्वानुमान QuantConnect में भेजना
सारांश
यह नोटबुक ऑफ़लाइन मशीन-लर्निंग पाइपलाइन और QuantConnect के LEAN इंजन के बीच तैनाती की सीमा दिखाती है। यह ETF केस स्टडी रजिस्ट्री से पूर्वानुमान सेट चुनती है, प्रशिक्षण और पूर्वानुमान के स्रोत-इतिहास की पुष्टि करती है और प्लेटफ़ॉर्म द्वारा उपयोग किए जा सकने वाले JSON ढाँचे में नमूने से बाहर के होल्डआउट पूर्वानुमान निर्यात करती है। प्लेटफ़ॉर्म एल्गोरिदम मॉडल अनुमान पाइपलाइन को दोहराए बिना उन स्कोर पर पोर्टफ़ोलियो नियम लागू करता है। पूर्वानुमान स्थिर करने से शोधकर्ता दोबारा प्रशिक्षण के बिना सीमाएँ, पोज़िशन लिमिट और रीबैलेंस नियम भी जाँच सकते हैं।
नोटबुक पहले से गणना किए गए स्कोर के इस तरीके की तुलना इनलाइन अनुमान से करती है। स्थिर पूर्वानुमान पुनरुत्पाद्य पोर्टफ़ोलियो-नियम प्रयोगों में मदद करते हैं, जबकि लाइव उपयोग के लिए नियमित रूप से नई फ़ाइलें बनाने वाली प्रक्रिया चाहिए; इनलाइन अनुमान मौजूदा इनपुट इस्तेमाल कर सकता है, लेकिन मॉडल और रनटाइम की अनुकूलता पर निर्भर है। उदाहरण चुने गए आर्टिफ़ैक्ट जाँचने और यह सुनिश्चित करने के लिए रजिस्ट्री तथा फ़ाइल हैश का उपयोग करता है कि निर्यात के दौरान रजिस्ट्री नहीं बदली। यह प्रदर्शन ETF पूर्वानुमानों और प्रबंधित प्लेटफ़ॉर्म कार्यप्रवाह तक सीमित है; होल्डआउट पूर्वानुमानों से बैकटेस्ट तभी अर्थपूर्ण है जब मॉडल और कॉन्फ़िगरेशन चयन के दौरान होल्डआउट अछूता रहा हो।
मुख्य विचार
- नमूने से बाहर के मॉडल स्कोर को ऐसे पोर्टेबल फ़ाइल में निर्यात करें जिसे प्लेटफ़ॉर्म उपयोग कर सके।
- मॉडल अनुमान शोध पाइपलाइन में रखते हुए पोर्टफ़ोलियो निर्माण और निष्पादन नियम प्लेटफ़ॉर्म में रखें।
- यह सत्यापित करने के लिए स्रोत-इतिहास हैश इस्तेमाल करें कि निर्यात किस प्रशिक्षण रन और पूर्वानुमान आर्टिफ़ैक्ट से बना।
- स्थिर पूर्वानुमान से पोर्टफ़ोलियो-नियम प्रयोग तेज़ होते हैं, क्योंकि नियम बदलने पर दोबारा प्रशिक्षण नहीं चाहिए।
- लाइव तैनाती के लिए ताज़ा पूर्वानुमान चाहिए, जबकि इनलाइन अनुमान मॉडल और रनटाइम पर निर्भरता जोड़ता है।
टैग
पूरा पाठ
# 06_quantconnect_case_study.py
```py
# ---
# jupyter:
# jupytext:
# cell_metadata_filter: tags,-all
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.19.3
# kernelspec:
# display_name: Python 3 (ipykernel)
# language: python
# name: python3
# ---
# %% [markdown]
# # QuantConnect Deployment: Prediction Export Bridge
#
# **Docker image**: `ml4t`
#
# **Book Reference**: Chapter 25, Section 25.4 (QuantConnect and managed platforms)
#
# A managed platform will run a backtest and route orders, and will not run this book's feature
# pipeline. Reimplementing that pipeline inside the platform's language is a second
# implementation of the thing
# [`01_unified_framework_demo`](01_unified_framework_demo.ipynb) spent its whole length arguing
# against having.
#
# The way out is to move the boundary. The model stays here and its output crosses over: a file
# of predictions, one score per symbol per date, which the platform reads and turns into a
# portfolio. What runs on the platform is then a few dozen lines of position rules with no
# inference in it, and what changes when a threshold moves is a backtest rather than a retrain.
#
# The cost is that the platform can only trade dates the file covers, so a live deployment needs
# a job that keeps writing it. This notebook builds the export and the algorithm that consumes
# it, and states where each pattern gives out.
#
# **Learning Objectives**
# - Export a model's predictions into a file an external platform can read, with the provenance
# needed to say which run produced it
# - Read a platform algorithm that holds portfolio rules and no inference
# - Say when precomputed predictions are the right boundary and when inline inference is
#
# **Prerequisites**: the ETFs case study, which produced the predictions exported here, and
# [`01_unified_framework_demo`](01_unified_framework_demo.ipynb) for the self-hosted
# alternative.
# %% [markdown]
# ## 1. Load Predictions from the ETFs Pipeline
#
# The ETFs case study's registry holds every training run, every prediction set and every
# backtest the case study produced. Exporting from it means first answering which of them to
# export, and doing that reproducibly.
# %%
"""Export ML predictions for consumption by LEAN algorithms."""
import hashlib
import json
import sqlite3
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import polars as pl
from demo_artifacts import normalize_demo_predictions
from utils.paths import display_path, get_case_study_dir, get_output_dir, registry_readonly_uri
from utils.style import COLORS, FIGSIZE, add_message_title, show_with_alt
# %% [markdown]
# ## Settings
#
# `PREDICTION_THRESHOLD` is the predicted return above which a name is held long. Zero means
# every positive forecast is a candidate, which is the widest the portfolio can be, and the two
# figures below show what moving it does to breadth.
#
# The two pins are what make this export reproducible. `EXPECTED_TRAINING_HASH` names the
# configuration the registry query below is expected to select, and `EXPECTED_PREDICTIONS_SHA256`
# is the digest of the prediction file that configuration produced. Together they say: this run
# exported exactly the bytes that run exported.
#
# The registry file itself is deliberately not pinned. It accumulates a row for every run in the
# case study, so its digest moves whenever anything is added to it, whether or not the selection
# moves. A whole-file pin would therefore fail on runs that export exactly the right predictions,
# which is the worst kind of check: one that fires when nothing is wrong. Setting either pin to
# `None` reports the observed value instead of asserting it, which is what to do when the case
# study is deliberately re-promoted.
# %% tags=["parameters"]
PREDICTION_THRESHOLD = 0.0
EXPECTED_TRAINING_HASH = "ab5300c0cda4"
EXPECTED_PREDICTIONS_SHA256 = "2efad22dbf40464939d143745116165c6447c3e4dfd154bd608dda8653b52925"
EXPORT_PATH = get_output_dir(25, "quantconnect_export") / "ml4t_qc_predictions.json"
# %% [markdown]
# The registry is opened read-only by `mode=ro`. `immutable=1` is a separate promise - that the
# database cannot change while it is open, which lets SQLite skip locking and WAL recovery - and
# an immutable read of a database with an uncheckpointed write-ahead log sees the pre-WAL main
# file: a stale configuration selected silently, or a table that appears not to exist. The
# promise holds for a downloaded artifact bundle, whose tree is left unwritable, and not for a
# live case directory a sweep may be writing. `registry_readonly_uri` decides from the directory.
# %%
case_study_dir = get_case_study_dir("etfs")
registry_path = case_study_dir / "run_log" / "registry.db"
registry_hash_before = hashlib.sha256(registry_path.read_bytes()).hexdigest()
registry_uri = registry_readonly_uri(registry_path)
with sqlite3.connect(registry_uri, uri=True) as conn:
winner = conn.execute(
"""SELECT ps.training_hash, br.backtest_hash, br.stage, bm.sharpe
FROM backtest_runs br
JOIN backtest_metrics bm ON br.backtest_hash = bm.backtest_hash
JOIN prediction_sets ps ON br.prediction_hash = ps.prediction_hash
JOIN training_runs t ON t.training_hash = ps.training_hash
WHERE br.stage IN ('signal', 'allocation', 'risk_overlay')
AND ps.split = 'validation'
AND EXISTS (
SELECT 1 FROM prediction_sets h
JOIN training_runs ht ON ht.training_hash = h.training_hash
WHERE h.split = 'holdout'
AND ht.family = t.family
AND ht.config_name = t.config_name
AND ht.label = t.label
)
ORDER BY bm.sharpe DESC LIMIT 1"""
).fetchone()
if winner is None:
raise RuntimeError("ETF registry has no eligible cross-stage backtest winner")
selected_family, selected_config, selected_label = conn.execute(
"SELECT family, config_name, label FROM training_runs WHERE training_hash = ?",
(winner[0],),
).fetchone()
holdout_rows = conn.execute(
"""SELECT h.prediction_hash FROM prediction_sets h
JOIN training_runs ht ON ht.training_hash = h.training_hash
WHERE h.split = 'holdout'
AND (ht.family, ht.config_name, ht.label) = (
SELECT t.family, t.config_name, t.label
FROM training_runs t WHERE t.training_hash = ?
)
ORDER BY h.prediction_hash""",
(winner[0],),
).fetchall()
assert EXPECTED_TRAINING_HASH in (None, winner[0]), (
f"The registry now selects a different configuration: {winner[0]}"
)
assert len(holdout_rows) == 1, f"Expected one holdout prediction set, found {len(holdout_rows)}"
prediction_hash = holdout_rows[0][0]
prediction_path = (
case_study_dir / "run_log" / "predictions" / prediction_hash / "predictions.parquet"
)
prediction_file_hash = hashlib.sha256(prediction_path.read_bytes()).hexdigest()
assert EXPECTED_PREDICTIONS_SHA256 in (None, prediction_file_hash), (
f"Holdout prediction set provenance changed: {prediction_file_hash}"
)
predictions = normalize_demo_predictions(pl.read_parquet(prediction_path), "symbol")
registry_hash_after_load = hashlib.sha256(registry_path.read_bytes()).hexdigest()
assert registry_hash_after_load == registry_hash_before
horizon_days = int(selected_label.removeprefix("fwd_ret_").removesuffix("d"))
REBALANCE_RULES = {5: ("weekly", "week_start"), 21: ("monthly", "month_start")}
if horizon_days not in REBALANCE_RULES:
raise ValueError(
f"no rebalance cadence declared for a {horizon_days}-session horizon "
f"(label {selected_label}); add one to REBALANCE_RULES"
)
rebalance_cadence, rebalance_date_rule = REBALANCE_RULES[horizon_days]
print(f"Registry SHA256: {registry_hash_before}")
print(f"Prediction parquet SHA256: {prediction_file_hash}")
print(f"Training hash: {winner[0]} | holdout prediction hash: {prediction_hash}")
print(f"Selection stage: {winner[2]} | backtest hash: {winner[1]}")
print(f"Selected configuration: {selected_family}/{selected_config} on {selected_label}")
print(
f"Horizon: {horizon_days} sessions -> {rebalance_cadence} rebalance "
f"(LEAN date_rules.{rebalance_date_rule})"
)
n_dates = predictions["timestamp"].n_unique()
n_symbols = predictions["symbol"].n_unique()
print(f"Loaded {len(predictions):,} predictions")
print(
f" Dates: {n_dates:,} ({predictions['timestamp'].min()} to {predictions['timestamp'].max()})"
)
print(f" Symbols: {n_symbols}")
predictions.head(10)
# %% [markdown]
# The exported rows are the **holdout** predictions, not the validation ones, and that choice is
# the whole point of the export. Validation predictions were used to choose this configuration
# over the others, so a backtest on them measures the selection as much as the model. The holdout
# window was not read during selection, which is what makes a deployment backtest on it worth
# running. Each row is one model's out-of-sample score for one symbol on one date.
#
# Freezing those scores into a file is what separates the two loops. Thresholds, position limits
# and rebalance cadence can be changed on the platform against an unchanged prediction file, so
# a portfolio-rule experiment costs a backtest rather than a retrain.
# %% [markdown]
# ## 2. Export as QuantConnect-Compatible JSON
#
# QuantConnect's Object Store accepts JSON. We format predictions to match the
# pattern used in the
# [illustrative QuantConnect project](https://www.quantconnect.cloud/backtest/37075c225715df9ef4477dc748b1cbf7/?theme=darkly)
# (account access may be required):
# one entry per date, each containing a symbol-to-prediction mapping.
# %%
# Group by date and create QC-compatible JSON
date_groups = (
predictions.sort("timestamp")
.group_by("timestamp")
.agg(
[
pl.col("symbol").alias("symbols"),
pl.col("prediction").alias("predictions"),
]
)
.sort("timestamp")
)
qc_predictions = []
for row in date_groups.iter_rows(named=True):
date_str = str(row["timestamp"])
prediction_by_symbol = dict(zip(row["symbols"], row["predictions"], strict=False))
qc_predictions.append(
{
"date": date_str,
"prediction_by_symbol": prediction_by_symbol,
}
)
print(f"Formatted {len(qc_predictions)} daily prediction entries")
print(
f"First date: {qc_predictions[0]['date']}, symbols: {len(qc_predictions[0]['prediction_by_symbol'])}"
)
print(
f"Last date: {qc_predictions[-1]['date']}, symbols: {len(qc_predictions[-1]['prediction_by_symbol'])}"
)
# %%
# Show sample entry
sample = qc_predictions[-1]
sample_symbols = dict(list(sample["prediction_by_symbol"].items())[:5])
print(f"\nSample entry ({sample['date']}):")
for symbol, pred in sample_symbols.items():
direction = "long" if pred > 0 else "short" if pred < 0 else "neutral"
print(f" {symbol}: {pred:+.4f} ({direction})")
# %% [markdown]
# **Finding**: Each date entry maps symbols to predictions. Positive predictions
# become long candidates; the threshold determines which make it into the portfolio.
# %%
# Write to disk
json_str = json.dumps(qc_predictions, indent=2)
EXPORT_PATH.write_text(json_str)
file_size_kb = EXPORT_PATH.stat().st_size / 1024
print(f"Exported to {display_path(EXPORT_PATH)}")
print(f" File size: {file_size_kb:.0f} KB")
print(f" Entries: {len(qc_predictions)} dates")
# %% [markdown]
# **Finding**: The full prediction history exports to a compact JSON file. On
# QuantConnect, this would be uploaded to the Object Store via
# `qb.object_store.save('research-to-backtest-factors.json', json_str)` in a
# Research Notebook, using the same filename the LEAN algorithm reads in Section 3.
#
# **Trading implication**: Small file sizes mean fast iteration. Changing the
# threshold or adding a risk filter does not require re-uploading predictions.
# %% [markdown]
# ## 3. The LEAN Algorithm
#
# The algorithm file is deliberately tiny. All feature engineering and model
# inference happened in the research step (our pipeline). The algorithm just
# reads predictions and rebalances.
#
# This is the actual pattern from the
# [illustrative QuantConnect project](https://www.quantconnect.cloud/backtest/37075c225715df9ef4477dc748b1cbf7/?theme=darkly).
# %% [markdown]
# ### Custom Universe: Reading Predictions from Object Store
#
# The `PredictionUniverse` class defines a custom data source that reads the
# JSON we exported. LEAN streams it date-by-date into the algorithm.
#
# ```python
# class PredictionUniverse(PythonData):
# def get_source(self, config, date, is_live_mode):
# return SubscriptionDataSource(
# 'research-to-backtest-factors.json',
# SubscriptionTransportMedium.OBJECT_STORE,
# FileFormat.UNFOLDING_COLLECTION
# )
#
# def reader(self, config, line, date, is_live):
# objects = []
# for obj in json.loads(line):
# end_time = datetime.strptime(obj["date"], "%Y-%m-%d")
# for ticker, prediction in obj['prediction_by_symbol'].items():
# stock = PredictionUniverse()
# stock.symbol = Symbol.create(
# ticker, SecurityType.EQUITY, Market.USA
# )
# stock.end_time = end_time
# stock.value = prediction
# objects.append(stock)
# return BaseDataCollection(
# objects[-1].end_time, config.symbol, objects
# )
# ```
#
# The `UNFOLDING_COLLECTION` format tells LEAN to stream one date at a time,
# so the algorithm only sees data available on each historical day.
# %% [markdown]
# ### Algorithm: Select and Rebalance
#
# The algorithm subscribes to assets with positive predictions and forms an
# equal-weighted portfolio. The rebalance fires on the cadence read off the
# promoted configuration's label: the signal a configuration produces is a
# forecast over a stated number of sessions, and holding a position longer than
# that means trading on a forecast that has already expired.
#
# The listing is generated from that cadence rather than written out here, so
# there is no second place for the horizon to be stated and go stale. It is the
# same reason the horizon itself is derived: this notebook previously carried a
# monthly rebalance in prose while the case study deployed a five-session
# signal, and nothing in it could fail.
# %%
ALGORITHM_TEMPLATE = """class PredictionUniverseAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_cash(100_000)
self.settings.seed_initial_prices = True
self._return_prediction_threshold = {threshold}
self.universe_settings.resolution = Resolution.DAILY
self._universe = self.add_universe(
PredictionUniverse, self._select_assets
)
# Rebalance {cadence} to match the {horizon}-session prediction horizon.
# The universe still streams daily; only the rebalance is throttled.
self.schedule.on(
self.date_rules.{date_rule}('SPY'),
self.time_rules.at(8, 0),
self._rebalance,
)
def _select_assets(self, data):
return [
stock.symbol for stock in data
if stock.value > self._return_prediction_threshold
]
def _rebalance(self):
symbols = self._universe.selected
if not symbols:
return
targets = [
PortfolioTarget(symbol, 1 / len(symbols))
for symbol in symbols
]
self.set_holdings(targets, True)
"""
algorithm_source = ALGORITHM_TEMPLATE.format(
threshold=PREDICTION_THRESHOLD,
cadence=rebalance_cadence,
horizon=horizon_days,
date_rule=rebalance_date_rule,
)
algorithm_path = EXPORT_PATH.parent / "main.py"
algorithm_path.write_text(algorithm_source)
print(algorithm_source)
print(f"Written to {display_path(algorithm_path)}")
# %% [markdown]
# The algorithm is about thirty lines, and it ships beside the predictions so the two travel
# together. Threshold, weighting and rebalance cadence are all in it, and all can change without
# touching the model.
#
# The cadence is derived rather than declared, and that is worth noticing. It comes from the
# selected configuration's own label: a five-session forecast is rebalanced weekly so a position
# is closed before the next signal forms. Had the cadence been typed here as a constant, the
# selection moving to a different horizon would leave the algorithm trading on a schedule that no
# longer matches the signal, and nothing would say so.
# %% [markdown]
# ## 4. Running on QuantConnect
#
# ### Cloud Path (No Local Setup)
#
# An illustrative project shows the precomputed-predictions pattern:
#
# **[View Backtest Results](https://www.quantconnect.cloud/backtest/37075c225715df9ef4477dc748b1cbf7/?theme=darkly)**
#
# To use it:
# 1. Create a free QuantConnect account
# 2. Open or clone the project if your account has access
# 3. Upload your prediction JSON to the Object Store as
# `research-to-backtest-factors.json` (the name the algorithm reads)
# 4. Run the backtest
#
# The Research Notebook in the project shows how to train a model on
# QuantConnect's own S&P 500 data and export predictions. Our export step
# above produces the same JSON format, so you can substitute your own
# predictions.
#
# ### Local Docker Path (Optional)
#
# QuantConnect documents the supported local workflow in the
# [LEAN CLI guide](https://www.quantconnect.com/docs/v2/lean-cli). The CLI uses
# Docker to initialize a project, run LEAN, and collect backtest results. Follow
# that guide for current installation and authentication commands; the export
# produced above remains the Object Store input.
# %% [markdown]
# ## 5. Prediction Signal Analysis
#
# Before deploying, verify the prediction distribution is sensible.
# %%
# Analyze the predictions we exported
positive = predictions.filter(pl.col("prediction") > PREDICTION_THRESHOLD)
negative = predictions.filter(pl.col("prediction") <= PREDICTION_THRESHOLD)
print("Prediction Distribution:")
print(f" Total: {len(predictions):,}")
print(f" Long: {len(positive):,} ({100 * len(positive) / len(predictions):.1f}%)")
print(f" Excluded: {len(negative):,} ({100 * len(negative) / len(predictions):.1f}%)")
print(f" Mean: {predictions['prediction'].mean():.4f}")
print(f" Std: {predictions['prediction'].std():.4f}")
# Average portfolio size per date
portfolio_sizes = (
predictions.filter(pl.col("prediction") > PREDICTION_THRESHOLD)
.group_by("timestamp")
.agg(pl.col("symbol").count().alias("n_holdings"))
)
print(f"\nPortfolio Size (equal-weight, threshold={PREDICTION_THRESHOLD}):")
print(f" Mean: {portfolio_sizes['n_holdings'].mean():.1f} holdings/day")
print(f" Min: {portfolio_sizes['n_holdings'].min()}")
print(f" Max: {portfolio_sizes['n_holdings'].max()}")
# %%
fig, ax = plt.subplots(figsize=FIGSIZE["single"])
ax.hist(
predictions["prediction"].to_numpy(),
bins=50,
color=COLORS["blue"],
edgecolor=COLORS["silver"],
linewidth=0.4,
)
ax.axvline(PREDICTION_THRESHOLD, color=COLORS["amber"], linewidth=1.5, label="Long threshold")
ax.set(xlabel=f"Predicted {horizon_days}-day return", ylabel="Prediction count")
ax.legend(frameon=False)
add_message_title(
ax,
"Most predicted returns sit close to zero",
subtitle="ETF holdout predictions at the promoted configuration's horizon; the vertical "
"line is the long threshold",
)
show_with_alt(
fig,
f"Histogram of {len(predictions):,} predicted {horizon_days}-session returns, concentrated "
f"near zero and roughly symmetric, with a vertical line at the long threshold of "
f"{PREDICTION_THRESHOLD:g}. {len(positive):,} of them fall above it.",
)
# %%
portfolio_sizes = portfolio_sizes.sort("timestamp")
fig, ax = plt.subplots(figsize=FIGSIZE["single_wide"])
ax.plot(
portfolio_sizes["timestamp"].to_list(),
portfolio_sizes["n_holdings"].to_list(),
color=COLORS["blue"],
linewidth=1.2,
)
ax.axhline(
portfolio_sizes["n_holdings"].mean(),
color=COLORS["amber"],
linewidth=1.2,
linestyle="--",
label="Mean breadth",
)
ax.set(xlabel="Holdout date", ylabel="Long positions")
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=4))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
ax.tick_params(axis="x", labelrotation=30)
for label in ax.get_xticklabels():
label.set_horizontalalignment("right")
ax.legend(frameon=False)
add_message_title(
ax,
"Breadth swings day to day, and the threshold is what decides it",
subtitle="Daily count of ETF predictions above the long threshold",
)
show_with_alt(
fig,
"Line chart of the daily count of positive predictions across the holdout window, ranging "
f"between {portfolio_sizes['n_holdings'].min()} and "
f"{portfolio_sizes['n_holdings'].max()} names against a dashed line at the mean of "
f"{portfolio_sizes['n_holdings'].mean():.1f}.",
)
# %% [markdown]
# Those two figures are one argument. The histogram is the model's output and the line chart is
# the portfolio that follows from it, and the only thing between them is the threshold. Raise it
# and the second chart drops toward a handful of names on most days; lower it and the portfolio
# approaches the whole universe.
#
# That is the separation the precomputed-prediction pattern buys. The threshold is a
# portfolio-construction decision made on the platform, in code a reader can see, against a
# prediction file that does not change when it moves. Deciding it inside the model would make
# every change to it a retrain.
# %% [markdown]
# ## 6. Two Deployment Workflows Compared
#
# The precomputed-predictions pattern is one of two ways to deploy ML strategies.
# The choice depends on how often predictions change and how fast you need to
# iterate on portfolio rules.
#
# | Aspect | Precomputed Predictions | Inline Inference |
# |--------|----------------------|------------------|
# | **Pipeline** | Train offline, export JSON, then read predictions | Train offline, serialize model, then call `predict()` |
# | **Iteration speed** | Threshold/weight changes reuse frozen scores | Threshold/weight changes stay local; model changes rerun inference |
# | **Prediction freshness** | Frozen at export time | Always current |
# | **Reproducibility** | Stable input: same JSON under versioned engine rules | Depends on model and runtime versions |
# | **Best for** | Portfolio rule experimentation, walk-forward analysis | Live trading with streaming data |
# | **Book pipeline** | Chapters 7-15, export, then Ch25 QC notebook | Model serialized and loaded in `Initialize()` |
#
# Both workflows are valid. The book's pipeline naturally produces precomputed
# predictions (one run per model, label, and fold), making the export pattern
# the lower-friction path for backtesting. For live trading, QuantConnect's
# [ML documentation](https://www.quantconnect.com/docs/v2/writing-algorithms/machine-learning/key-concepts)
# documents the inline-inference approach with serialized models.
# %% [markdown]
# ## Summary
#
# This notebook demonstrated the prediction export bridge between the book's
# ML pipeline and QuantConnect's LEAN engine:
#
# 1. **Loaded** the output-counted ETF holdout predictions from the exact registry hash printed above
# 2. **Exported** as QC-compatible JSON (Object Store format)
# 3. **Showed** the ~30-line LEAN algorithm that consumes predictions
# 4. **Linked** to an illustrative project on QuantConnect Cloud
# 5. **Compared** precomputed vs inline deployment workflows
#
# ## Key Takeaways
#
# 1. **Separation of concerns**: The ML pipeline (Chapters 7–15) produces
# predictions; the deployment platform (QuantConnect or self-hosted) handles
# portfolio construction and execution. Changing one does not require
# changing the other.
# 2. **Precomputed predictions enable fast iteration**: Testing different
# thresholds, position limits, or rebalance rules takes seconds because
# the expensive model training is already done.
# 3. **Platform choice is an infrastructure decision**: QuantConnect provides
# data, execution, and hosting; the self-hosted path (`unified_framework_demo`,
# `ml4t-backtest`) provides flexibility. The prediction format is portable
# between both.
#
# **Next**: See `unified_framework_demo` and `etfs_deployment_loop` for the
# self-hosted path, or `pipeline_verification` for systematic parity testing.
# %%
registry_hash_final = hashlib.sha256(registry_path.read_bytes()).hexdigest()
assert registry_hash_final == registry_hash_before
print(f"Registry unchanged after export and analysis: {registry_hash_final}")
```स्रोत के लाइसेंस के तहत श्रेय सहित पूरा पाठ दिखाया गया है। लाइसेंस: MIT
यह सारांश मूल स्रोत के आधार पर Stratmill के शोध एजेंट ने लिखा है; यह स्रोत की प्रति नहीं है।