feat: add Tushare Qlib data bridge

This commit is contained in:
2026-07-26 13:41:24 +08:00
parent 48c5f64bbd
commit e5911ca454
15 changed files with 1804 additions and 8 deletions
+180 -1
View File
@@ -16,6 +16,7 @@ import json
import math
import os
from pathlib import Path
import sys
import tempfile
from typing import Any, Dict, Iterable, Mapping, Optional, Sequence
@@ -26,6 +27,19 @@ class QlibUnavailableError(RuntimeError):
"""Raised when Qlib or its configured provider cannot be used."""
def _runner_evidence() -> Dict[str, Any]:
source = Path(__file__).resolve().read_bytes()
return {
"runner_source_sha256": hashlib.sha256(source).hexdigest(),
"python_version": (
f"{sys.version_info.major}."
f"{sys.version_info.minor}."
f"{sys.version_info.micro}"
),
"python_hash_seed": os.environ.get("PYTHONHASHSEED"),
}
def _table_audit(table: Any) -> Optional[Dict[str, Any]]:
"""Return compact deterministic evidence for a pandas Series/DataFrame."""
@@ -154,6 +168,121 @@ def _validate_provider(provider_uri: str) -> str:
return value
def _managed_provider_evidence(provider_uri: str) -> Dict[str, Any]:
"""Verify and summarize a Quant OS-managed local provider when present."""
if "://" in provider_uri:
return {
"managed": False,
"manifest_name": None,
"verification": "remote provider URI; no local manifest",
}
root = Path(provider_uri)
manifest_path = root / "quant_os_tushare_manifest.json"
if not manifest_path.is_file():
return {
"managed": False,
"manifest_name": None,
"verification": "no Quant OS provider manifest",
}
try:
from adapters.tushare_local import verify_qlib_provider
verification = verify_qlib_provider(root)
manifest_bytes = manifest_path.read_bytes()
manifest = json.loads(manifest_bytes)
except Exception as exc:
raise QlibUnavailableError(
f"managed Qlib provider verification failed: {exc}"
) from exc
if not isinstance(manifest, Mapping):
raise QlibUnavailableError(
"managed Qlib provider manifest must be a JSON object"
)
evidence: Dict[str, Any] = {
"managed": True,
"manifest_name": manifest_path.name,
"manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
"data_version": manifest.get("data_version"),
"provider_tree_sha256": manifest.get("provider_tree_sha256"),
"source": manifest.get("source", "tushare_local_mirror"),
"converter": manifest.get("converter"),
"build_parameters": manifest.get("build_parameters"),
"calendar": manifest.get("calendar"),
"market": manifest.get("market"),
"benchmark": manifest.get("benchmark"),
"price_adjustment": manifest.get("price_adjustment"),
"investment_value_claim": False,
}
if isinstance(verification, Mapping):
evidence["verified_file_count"] = verification.get(
"provider_file_count",
verification.get("file_count"),
)
return evidence
def _validate_managed_provider_request(
provider_evidence: Mapping[str, Any],
*,
market: str,
benchmark: str,
start_time: str,
end_time: str,
) -> None:
"""Fail early on managed-provider mismatches and terminal-calendar use."""
if not provider_evidence.get("managed"):
return
provider_market = provider_evidence.get("market")
if isinstance(provider_market, Mapping):
expected_market = str(provider_market.get("name") or "").strip()
if expected_market and market != expected_market:
raise ValueError(
f"managed provider market is {expected_market!r}, "
f"not {market!r}"
)
provider_benchmark = provider_evidence.get("benchmark")
if isinstance(provider_benchmark, Mapping):
expected_benchmark = str(
provider_benchmark.get("symbol") or ""
).strip()
if expected_benchmark and benchmark != expected_benchmark:
raise ValueError(
f"managed provider benchmark is {expected_benchmark!r}, "
f"not {benchmark!r}"
)
calendar = provider_evidence.get("calendar")
if not isinstance(calendar, Mapping):
raise QlibUnavailableError(
"managed provider manifest has no calendar evidence"
)
try:
calendar_start = dt.date.fromisoformat(str(calendar["start"]))
calendar_end = dt.date.fromisoformat(str(calendar["end"]))
requested_start = dt.date.fromisoformat(str(start_time)[:10])
requested_end = dt.date.fromisoformat(str(end_time)[:10])
except (KeyError, TypeError, ValueError) as exc:
raise QlibUnavailableError(
"managed provider calendar/request dates must use YYYY-MM-DD"
) from exc
if requested_start < calendar_start:
raise ValueError(
f"start_time {requested_start} precedes managed provider "
f"calendar start {calendar_start}"
)
if requested_end >= calendar_end:
raise ValueError(
f"end_time must be before managed provider terminal session "
f"{calendar_end}; Qlib's simulator consumes the next provider "
"session. Rebuild with a later calendar end or choose the "
"previous open session."
)
def _infer_columns(frame: Any) -> tuple[str, str, str]:
columns = [str(column) for column in frame.columns]
instrument = next(
@@ -315,6 +444,21 @@ def run_native_momentum_backtest(
) -> Dict[str, Any]:
"""Run the official SimulatorExecutor + TopkDropoutStrategy stack."""
provider = _validate_provider(provider_uri)
provider_evidence = _managed_provider_evidence(provider)
_validate_managed_provider_request(
provider_evidence,
market=market,
benchmark=benchmark,
start_time=start_time,
end_time=end_time,
)
resolved_feature_start = feature_start_time
if resolved_feature_start is None:
start = dt.datetime.fromisoformat(str(start_time)[:10])
padding_days = max(14, 3 * (int(lookback) + int(skip) + 2))
resolved_feature_start = (
start - dt.timedelta(days=padding_days)
).date().isoformat()
api = _load_qlib()
constant = importlib.import_module("qlib.constant")
api["qlib"].init(provider_uri=provider, region=constant.REG_CN)
@@ -323,7 +467,7 @@ def run_native_momentum_backtest(
market,
start_time=start_time,
end_time=end_time,
feature_start_time=feature_start_time,
feature_start_time=resolved_feature_start,
lookback=lookback,
skip=skip,
rebalance=rebalance,
@@ -374,6 +518,26 @@ def run_native_momentum_backtest(
"uniform_limit_threshold_approximation_not_point_in_time_"
"board_or_ST_rules"
),
"run_parameters": {
"market": market,
"benchmark": benchmark,
"start_time": start_time,
"end_time": end_time,
"feature_start_time": resolved_feature_start,
"lookback": int(lookback),
"skip": int(skip),
"rebalance": rebalance,
"topk": int(topk),
"n_drop": int(n_drop),
"account": float(account),
"deal_price": deal_price,
"open_cost": float(open_cost),
"close_cost": float(close_cost),
"min_cost": float(min_cost),
"limit_threshold": float(limit_threshold),
},
"runner_evidence": _runner_evidence(),
"provider_evidence": provider_evidence,
}
@@ -482,6 +646,14 @@ def run_alpha158_lightgbm_workflow(
) -> Dict[str, Any]:
"""Fit, record signals and run PortAnaRecord with official Qlib APIs."""
provider = _validate_provider(provider_uri)
provider_evidence = _managed_provider_evidence(provider)
_validate_managed_provider_request(
provider_evidence,
market=market,
benchmark=benchmark,
start_time=train[0],
end_time=test[1],
)
# Qlib 0.9.7 defaults to a local MLflow file store. MLflow 3.14 requires
# an explicit acknowledgement before it will open that backend. Keep the
# acknowledgement local to this process; a caller-provided tracking
@@ -556,6 +728,8 @@ def run_alpha158_lightgbm_workflow(
"recorded_metrics": recorded_metrics,
"portfolio_artifact_paths": artifact_paths,
},
"runner_evidence": _runner_evidence(),
"provider_evidence": provider_evidence,
}
@@ -682,6 +856,9 @@ def _momentum_cli_summary(result: Mapping[str, Any]) -> Dict[str, Any]:
"clock_contract": result.get("clock_contract"),
"strategy_fidelity": result.get("strategy_fidelity"),
"a_share_rule_fidelity": result.get("a_share_rule_fidelity"),
"run_parameters": result.get("run_parameters"),
"runner_evidence": result.get("runner_evidence"),
"provider_evidence": result.get("provider_evidence"),
"investment_value_claim": False,
}
@@ -712,6 +889,8 @@ def _alpha158_cli_summary(
"test": list(test),
},
"recorder_evidence": result.get("recorder_evidence"),
"runner_evidence": result.get("runner_evidence"),
"provider_evidence": result.get("provider_evidence"),
"investment_value_claim": False,
}