feat: add Tushare Qlib data bridge
This commit is contained in:
+180
-1
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user