Files
quant-os/tools/bundle_platforms.py
T

459 lines
16 KiB
Python

"""Inline one portable core into JoinQuant and QMT single-file artifacts."""
from __future__ import annotations
import argparse
import ast
import hashlib
import json
import pprint
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
START = "# __PORTABLE_CORE_BUNDLE_START__"
END = "# __PORTABLE_CORE_BUNDLE_END__"
CONFIG_START = "# __BASELINE_CONFIG_START__"
CONFIG_END = "# __BASELINE_CONFIG_END__"
TARGET_START = "# __TARGET_PACKAGE_TAPE_START__"
TARGET_END = "# __TARGET_PACKAGE_TAPE_END__"
MOMENTUM_SMOKE_MODE = "portable_momentum_smoke"
TARGET_PACKAGE_MODE = "target_package"
JOINQUANT_EVIDENCE_PREFIX = "QUANT60_JOINQUANT_EVIDENCE_V1 "
JOINQUANT_EVIDENCE_CHUNK_CHARACTERS = 1800
JOINQUANT_TARGET_PRICE_SOURCE = "CURRENT_DATA_DAY_OPEN"
JOINQUANT_EVIDENCE_EVENTS = [
"INIT",
"TARGET_PACKAGE_NOOP",
"TARGET_PACKAGE_HIT",
"PLATFORM_BINDING_INPUT",
"TARGET_PACKAGE_PLAN",
"SKIPPED_ORDER",
"ORDER_REQUEST",
"ORDER_RETURN",
"EOD_STATUS",
]
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def _portable_body(source: str) -> str:
"""Remove the core module docstring and future imports before inlining."""
tree = ast.parse(source)
lines = source.splitlines()
first_line = 0
if (
tree.body
and isinstance(tree.body[0], ast.Expr)
and isinstance(tree.body[0].value, ast.Constant)
and isinstance(tree.body[0].value.value, str)
):
first_line = int(tree.body[0].end_lineno or 0)
body = lines[first_line:]
body = [
line
for line in body
if not line.lstrip().startswith("from __future__ import ")
]
return "\n".join(body).strip() + "\n"
def inline_core(wrapper_source: str, core_source: str) -> str:
if wrapper_source.count(START) != 1 or wrapper_source.count(END) != 1:
raise ValueError("wrapper must contain exactly one portable-core marker pair")
start = wrapper_source.index(START)
end = wrapper_source.index(END, start) + len(END)
replacement = (
START
+ "\n# Inlined by tools/bundle_platforms.py; do not edit this region.\n"
+ _portable_body(core_source)
+ END
)
return wrapper_source[:start] + replacement + wrapper_source[end:]
def _canonical_target_tape_bytes(
target_packages: List[Dict[str, Any]],
) -> bytes:
return json.dumps(
target_packages,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def inline_target_packages(
wrapper_source: str,
target_packages: List[Dict[str, Any]],
) -> str:
"""Inline a pre-validated, quantity-free decision tape."""
if (
wrapper_source.count(TARGET_START) != 1
or wrapper_source.count(TARGET_END) != 1
):
raise ValueError(
"wrapper must contain exactly one target-package marker pair"
)
start = wrapper_source.index(TARGET_START)
end = wrapper_source.index(TARGET_END, start) + len(TARGET_END)
body = pprint.pformat(
target_packages,
width=88,
sort_dicts=False,
)
body = body.encode("ascii", "backslashreplace").decode("ascii")
replacement = (
TARGET_START
+ "\n# Generated by tools/bundle_platforms.py; do not edit this region.\n"
+ "TARGET_PACKAGES = "
+ body
+ "\n"
+ TARGET_END
)
return wrapper_source[:start] + replacement + wrapper_source[end:]
def load_verified_target_packages(
project_root: Path,
path: Path,
) -> List[Dict[str, Any]]:
"""Load one package or a multi-decision tape through the canonical verifier."""
root = project_root.expanduser().resolve()
source = path.expanduser().resolve()
src_path = str(root / "src")
inserted = src_path not in sys.path
if inserted:
sys.path.insert(0, src_path)
try:
from quant60.target_package import ( # pylint: disable=import-outside-toplevel
load_target_package_tape,
)
return list(load_target_package_tape(source))
finally:
if inserted:
sys.path.remove(src_path)
def _qmt_symbol(symbol: str) -> str:
suffixes = {
".XSHG": ".SH",
".XSHE": ".SZ",
".XBSE": ".BJ",
}
for suffix, replacement in suffixes.items():
if symbol.endswith(suffix):
return symbol[: -len(suffix)] + replacement
raise ValueError(f"unsupported canonical symbol in baseline config: {symbol}")
def effective_platform_config(
baseline: Dict[str, Any],
platform: str,
*,
decision_mode: str = MOMENTUM_SMOKE_MODE,
target_packages: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
packages = list(target_packages or [])
if decision_mode not in {MOMENTUM_SMOKE_MODE, TARGET_PACKAGE_MODE}:
raise ValueError(f"unsupported decision_mode: {decision_mode}")
if decision_mode == TARGET_PACKAGE_MODE and not packages:
raise ValueError("target_package mode requires a non-empty package tape")
if decision_mode == MOMENTUM_SMOKE_MODE and packages:
raise ValueError("momentum smoke mode cannot embed target packages")
strategy = baseline["strategy"]
execution = baseline["execution"]
fees = baseline["fees"]
universe_config = baseline["universe"]
symbols = (
sorted(
{
str(symbol)
for package in packages
for symbol in package["universe"]
}
)
if packages
else [str(symbol) for symbol in baseline["symbols"]]
)
if platform == "qmt_builtin":
universe = [_qmt_symbol(symbol) for symbol in symbols]
elif platform == "joinquant":
universe = symbols
else:
raise ValueError(f"unsupported platform config: {platform}")
tape_sha256 = (
sha256_bytes(_canonical_target_tape_bytes(packages))
if packages
else None
)
common: Dict[str, Any] = {
"decision_mode": decision_mode,
"target_package_count": len(packages),
"target_package_tape_sha256": tape_sha256,
"universe_mode": (
"fixed"
if decision_mode == TARGET_PACKAGE_MODE
else str(universe_config["mode"])
),
"index_symbol": str(universe_config["index_symbol"]),
"qmt_sector_name": str(universe_config["qmt_sector_name"]),
"dedicated_account_required": bool(
universe_config["dedicated_account_required"]
),
"exclude_st": bool(universe_config["exclude_st"]),
"universe": universe,
"lookback": int(strategy["lookback"]),
"skip": int(strategy["skip"]),
"top_n": int(strategy["top_n"]),
"max_weight": float(strategy["max_weight"]),
"gross_target": float(strategy["gross_target"]),
"cash_buffer": float(strategy["cash_buffer"]),
"lot_size": int(execution["lot_size"]),
"participation_rate": float(execution["participation_rate"]),
"slippage_bps": float(execution["slippage_bps"]),
"commission_rate": float(fees["commission_rate"]),
"minimum_commission": float(fees["minimum_commission"]),
"stamp_duty_rate": float(fees["stamp_duty_rate"]),
"transfer_fee_rate": float(fees["transfer_fee_rate"]),
"rebalance_schedule": str(strategy["rebalance_schedule"]),
}
if platform == "qmt_builtin":
return {
"account_id": "test",
**common,
}
return common
def inline_config(wrapper_source: str, config: Dict[str, Any]) -> str:
if (
wrapper_source.count(CONFIG_START) != 1
or wrapper_source.count(CONFIG_END) != 1
):
raise ValueError("wrapper must contain exactly one baseline-config marker pair")
start = wrapper_source.index(CONFIG_START)
end = wrapper_source.index(CONFIG_END, start) + len(CONFIG_END)
body = pprint.pformat(config, width=88, sort_dicts=False)
# QMT's built-in strategy is shipped with a GBK coding declaration but
# kept byte-for-byte ASCII for portability. Preserve non-ASCII config
# values (for example a Chinese sector name) as Python unicode escapes.
body = body.encode("ascii", "backslashreplace").decode("ascii")
replacement = (
CONFIG_START
+ "\n# Generated from configs/baseline.json; do not edit this region.\n"
+ "CONFIG = "
+ body
+ "\n"
+ CONFIG_END
)
return wrapper_source[:start] + replacement + wrapper_source[end:]
def bundle_one(
*,
core_path: Path,
wrapper_path: Path,
output_path: Path,
effective_config: Dict[str, Any],
target_packages: Optional[List[Dict[str, Any]]] = None,
require_ascii: bool = False,
) -> Dict[str, str]:
core_bytes = core_path.read_bytes()
wrapper_bytes = wrapper_path.read_bytes()
core_source = core_bytes.decode("utf-8")
wrapper_source = wrapper_bytes.decode("utf-8")
bundled = inline_target_packages(
inline_config(
inline_core(wrapper_source, core_source),
effective_config,
),
list(target_packages or []),
)
output_bytes = bundled.encode("utf-8")
if require_ascii:
try:
bundled.encode("ascii")
except UnicodeEncodeError as exc:
raise ValueError(
"QMT #coding:gbk bundle must remain ASCII-only"
) from exc
# The built-in QMT runtime is documented as Python 3.6. Parse against
# that grammar explicitly; compiling only on the build host would miss
# accidental modern syntax.
ast.parse(bundled, filename=str(output_path), feature_version=(3, 6))
compile(bundled, str(output_path), "exec")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(output_bytes)
return {
"core": str(core_path),
"core_sha256": sha256_bytes(core_bytes),
"wrapper": str(wrapper_path),
"wrapper_sha256": sha256_bytes(wrapper_bytes),
"effective_config_sha256": sha256_bytes(
(
json.dumps(
effective_config,
ensure_ascii=True,
separators=(",", ":"),
sort_keys=True,
)
+ "\n"
).encode("ascii")
),
"output": str(output_path),
"output_sha256": sha256_bytes(output_bytes),
}
def bundle_all(
project_root: Path,
output_dir: Optional[Path] = None,
target_package_path: Optional[Path] = None,
) -> Dict[str, object]:
root = project_root.expanduser().resolve()
output = (
output_dir.expanduser().resolve()
if output_dir
else root / "dist"
)
core = root / "src" / "quant60" / "portable_core.py"
if not core.is_file():
raise FileNotFoundError(core)
config_path = root / "configs" / "baseline.json"
baseline_bytes = config_path.read_bytes()
baseline = json.loads(baseline_bytes.decode("utf-8"))
if not isinstance(baseline, dict):
raise ValueError("configs/baseline.json must contain an object")
target_packages = (
load_verified_target_packages(root, target_package_path)
if target_package_path is not None
else []
)
decision_mode = (
TARGET_PACKAGE_MODE if target_packages else MOMENTUM_SMOKE_MODE
)
entries = {
"joinquant": bundle_one(
core_path=core,
wrapper_path=root / "platforms" / "joinquant_strategy.py",
output_path=output / "joinquant_strategy.py",
effective_config=effective_platform_config(
baseline,
"joinquant",
decision_mode=decision_mode,
target_packages=target_packages,
),
target_packages=target_packages,
),
"qmt_builtin": bundle_one(
core_path=core,
wrapper_path=root / "platforms" / "qmt_builtin_strategy.py",
output_path=output / "qmt_builtin_strategy.py",
effective_config=effective_platform_config(
baseline,
"qmt_builtin",
decision_mode=decision_mode,
target_packages=target_packages,
),
target_packages=target_packages,
require_ascii=True,
),
}
# Paths in a manifest must be portable and must not expose the local
# account/home directory. Preserve a real project-relative location for
# outputs under the project (for example dist/target-package/...). A
# clean-room output outside the project keeps the committed logical names
# so its manifest remains byte-for-byte comparable with committed dist.
default_logical_outputs = {
"joinquant": "dist/joinquant_strategy.py",
"qmt_builtin": "dist/qmt_builtin_strategy.py",
}
for platform_name, entry in entries.items():
physical_output = Path(entry["output"])
entry["core"] = "src/quant60/portable_core.py"
entry["wrapper"] = (
"platforms/joinquant_strategy.py"
if "joinquant" in entry["wrapper"]
else "platforms/qmt_builtin_strategy.py"
)
try:
entry["output"] = physical_output.relative_to(root).as_posix()
except ValueError:
entry["output"] = default_logical_outputs[platform_name]
manifest = {
"schema_version": 1,
"baseline_config": "configs/baseline.json",
"baseline_config_sha256": sha256_bytes(baseline_bytes),
"portable_core_sha256": entries["joinquant"]["core_sha256"],
"decision_mode": decision_mode,
"target_package_count": len(target_packages),
"target_package_tape_sha256": (
sha256_bytes(_canonical_target_tape_bytes(target_packages))
if target_packages
else None
),
"target_package_ids": [
package["package_id"] for package in target_packages
],
"target_package_sha256": [
package["package_sha256"] for package in target_packages
],
"joinquant_observation": {
"eod_chunk_characters": JOINQUANT_EVIDENCE_CHUNK_CHARACTERS,
"eod_chunk_encoding": "base64-canonical-json",
"eod_api_policy": "best_effort_get_orders_get_trades",
"events": JOINQUANT_EVIDENCE_EVENTS,
"fail_soft": True,
"prefix": JOINQUANT_EVIDENCE_PREFIX,
"schema_version": "1.0",
"target_price_source": JOINQUANT_TARGET_PRICE_SOURCE,
},
"artifacts": entries,
}
manifest_path = output / "bundle_manifest.json"
encoded = (
json.dumps(manifest, ensure_ascii=True, indent=2, sort_keys=True) + "\n"
).encode("ascii")
manifest_path.write_bytes(encoded)
try:
manifest["manifest"] = str(manifest_path.relative_to(root))
except ValueError:
manifest["manifest"] = manifest_path.name
manifest["manifest_sha256"] = sha256_bytes(encoded)
return manifest
def _main(argv: Optional[list[str]] = None) -> int:
default_root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--project-root", type=Path, default=default_root)
parser.add_argument("--output-dir", type=Path)
parser.add_argument(
"--target-package",
type=Path,
help=(
"verified TargetPackageV1 JSON/JSONL tape; switches both hosted "
"artifacts from momentum smoke to target-package execution"
),
)
args = parser.parse_args(argv)
manifest = bundle_all(
args.project_root,
args.output_dir,
args.target_package,
)
print(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(_main())