feat: preserve Quant OS target-package vertical slice
This commit is contained in:
Vendored
+35
-10
@@ -2,25 +2,50 @@
|
||||
"artifacts": {
|
||||
"joinquant": {
|
||||
"core": "src/quant60/portable_core.py",
|
||||
"core_sha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
||||
"effective_config_sha256": "3ef9b9efda9ec6cc5f57a5daca57cab3b68242e0368f18fdaaf9e249bdaa7873",
|
||||
"core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||
"effective_config_sha256": "d4ae2882e9b5d8d673e7c62c0dc54def7c20221c358ac091ee6529bf0413df0b",
|
||||
"output": "dist/joinquant_strategy.py",
|
||||
"output_sha256": "22ddb0546beb3bd4e4bbedecb271452111324c3903e45be1f4ac3d39f38cce4f",
|
||||
"output_sha256": "be4ca1d5764416b7c8e6c206121c0f0bd8219e7317679d79bc72a95492286b1a",
|
||||
"wrapper": "platforms/joinquant_strategy.py",
|
||||
"wrapper_sha256": "b5a8e3b0a774a0f45f52f6bcccbe786e234913a1a4cf5b68b85e2bd989ee9294"
|
||||
"wrapper_sha256": "0c672b64665b7c1d4409ba288b00d1608e976800af3a4f9a6467767ce4def592"
|
||||
},
|
||||
"qmt_builtin": {
|
||||
"core": "src/quant60/portable_core.py",
|
||||
"core_sha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
||||
"effective_config_sha256": "11f189ea5c9d4eaddeb6394dc342abc871941cb3395d7ff79aae1275ce2ad98f",
|
||||
"core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||
"effective_config_sha256": "7e4739937e4e99e5b58154f53271d8d3bfaed47d8d1d5adc747101babde827f5",
|
||||
"output": "dist/qmt_builtin_strategy.py",
|
||||
"output_sha256": "ad503e5ebde60a5deb0ec86334c04d4be2d77ab91d9607d4fe88e2119f22d5dc",
|
||||
"output_sha256": "483877b7ea55c8e0f5e4e47b48a156b3add6cf0a8085011414c5c600913939b5",
|
||||
"wrapper": "platforms/qmt_builtin_strategy.py",
|
||||
"wrapper_sha256": "efb931bbd04d0abf0a576f2422a13403fd4d659dccf59f8170e8b13b1bafa640"
|
||||
"wrapper_sha256": "a1e53dc22ebd9bb01be43675786151606bbed273d8ab883fb933461e379d2727"
|
||||
}
|
||||
},
|
||||
"baseline_config": "configs/baseline.json",
|
||||
"baseline_config_sha256": "939a7df386b06bc82869940572c35544e9361913b94d9e03027411e51b1006e1",
|
||||
"portable_core_sha256": "af29e4e5e10705cf6aff89874394c1e80d3780f093c19d90ed53ef9d0188a934",
|
||||
"schema_version": 1
|
||||
"decision_mode": "portable_momentum_smoke",
|
||||
"joinquant_observation": {
|
||||
"eod_api_policy": "best_effort_get_orders_get_trades",
|
||||
"eod_chunk_characters": 1800,
|
||||
"eod_chunk_encoding": "base64-canonical-json",
|
||||
"events": [
|
||||
"INIT",
|
||||
"TARGET_PACKAGE_NOOP",
|
||||
"TARGET_PACKAGE_HIT",
|
||||
"PLATFORM_BINDING_INPUT",
|
||||
"TARGET_PACKAGE_PLAN",
|
||||
"SKIPPED_ORDER",
|
||||
"ORDER_REQUEST",
|
||||
"ORDER_RETURN",
|
||||
"EOD_STATUS"
|
||||
],
|
||||
"fail_soft": true,
|
||||
"prefix": "QUANT60_JOINQUANT_EVIDENCE_V1 ",
|
||||
"schema_version": "1.0",
|
||||
"target_price_source": "CURRENT_DATA_DAY_OPEN"
|
||||
},
|
||||
"portable_core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||
"schema_version": 1,
|
||||
"target_package_count": 0,
|
||||
"target_package_ids": [],
|
||||
"target_package_sha256": [],
|
||||
"target_package_tape_sha256": null
|
||||
}
|
||||
|
||||
Vendored
+1225
-24
File diff suppressed because it is too large
Load Diff
Vendored
+851
-17
@@ -3,6 +3,8 @@
|
||||
|
||||
# __PORTABLE_CORE_BUNDLE_START__
|
||||
# Inlined by tools/bundle_platforms.py; do not edit this region.
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
|
||||
@@ -26,6 +28,45 @@ _SHORT_EXCHANGES = {
|
||||
"XBSE": "BJ",
|
||||
}
|
||||
|
||||
_TARGET_PACKAGE_FIELDS = {
|
||||
"schema_version",
|
||||
"package_type",
|
||||
"package_id",
|
||||
"decision_id",
|
||||
"release",
|
||||
"evidence_class",
|
||||
"model_id",
|
||||
"signal_as_of",
|
||||
"next_session",
|
||||
"model_sha256",
|
||||
"data_sha256",
|
||||
"feature_sha256",
|
||||
"risk_sha256",
|
||||
"cost_sha256",
|
||||
"optimizer_sha256",
|
||||
"config_sha256",
|
||||
"source_sha256",
|
||||
"target_weights",
|
||||
"universe",
|
||||
"constraints",
|
||||
"package_sha256",
|
||||
}
|
||||
|
||||
_TARGET_PACKAGE_HASH_FIELDS = (
|
||||
"model_sha256",
|
||||
"data_sha256",
|
||||
"feature_sha256",
|
||||
"risk_sha256",
|
||||
"cost_sha256",
|
||||
"optimizer_sha256",
|
||||
"config_sha256",
|
||||
"source_sha256",
|
||||
)
|
||||
|
||||
|
||||
class TargetPackageLookupMiss(ValueError, LookupError):
|
||||
"""An already verified tape has no exact requested decision."""
|
||||
|
||||
|
||||
def _sum_numeric(values):
|
||||
"""Sum numerics without trusting a hosted runtime's global ``sum`` name."""
|
||||
@@ -36,6 +77,15 @@ def _sum_numeric(values):
|
||||
return total
|
||||
|
||||
|
||||
def _any_true(values):
|
||||
"""Return built-in ``any`` semantics without trusting hosted globals."""
|
||||
|
||||
for value in values:
|
||||
if value:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _infer_exchange(code):
|
||||
if code.startswith(("4", "8")):
|
||||
return "XBSE"
|
||||
@@ -88,6 +138,396 @@ def convert_symbol(symbol, target="canonical"):
|
||||
return normalize_symbol(symbol, target)
|
||||
|
||||
|
||||
def _canonical_json(value):
|
||||
"""Match ``quant60.ledger.canonical_json`` for plain JSON values."""
|
||||
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
|
||||
|
||||
def _json_copy(value, name):
|
||||
try:
|
||||
return json.loads(_canonical_json(value))
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError("%s must contain finite JSON values only" % name) from error
|
||||
|
||||
|
||||
def _canonical_mapping(value, name, value_validator):
|
||||
if not hasattr(value, "items"):
|
||||
raise TypeError("%s must be a mapping" % name)
|
||||
output = {}
|
||||
for raw_symbol, raw_value in value.items():
|
||||
symbol = normalize_symbol(raw_symbol)
|
||||
if symbol in output:
|
||||
raise ValueError(
|
||||
"%s contains duplicate normalized symbol %s" % (name, symbol)
|
||||
)
|
||||
output[symbol] = value_validator(raw_value, "%s.%s" % (name, symbol))
|
||||
return dict((symbol, output[symbol]) for symbol in sorted(output))
|
||||
|
||||
|
||||
def _finite_non_negative(value, name):
|
||||
if isinstance(value, bool):
|
||||
raise ValueError("%s must be a finite non-negative number" % name)
|
||||
try:
|
||||
numeric = float(value)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("%s must be a finite non-negative number" % name)
|
||||
if not math.isfinite(numeric) or numeric < 0.0:
|
||||
raise ValueError("%s must be a finite non-negative number" % name)
|
||||
return numeric
|
||||
|
||||
|
||||
def _non_negative_quantity(value, name):
|
||||
numeric = _finite_non_negative(value, name)
|
||||
if numeric != math.floor(numeric):
|
||||
raise ValueError("%s must be a whole-share quantity" % name)
|
||||
return int(numeric)
|
||||
|
||||
|
||||
def _positive_price(value, name):
|
||||
numeric = _finite_non_negative(value, name)
|
||||
if numeric <= 0.0:
|
||||
raise ValueError("%s must be positive" % name)
|
||||
return numeric
|
||||
|
||||
|
||||
def _optional_non_empty_string(value, name):
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise ValueError("%s must be a non-empty string when provided" % name)
|
||||
return value
|
||||
|
||||
|
||||
def _sha256_hex(value, name, allow_none=False):
|
||||
if value is None and allow_none:
|
||||
return None
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
or len(value) != 64
|
||||
or _any_true(
|
||||
character not in "0123456789abcdef"
|
||||
for character in value
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
"%s must be a 64-character lowercase SHA-256 digest" % name
|
||||
)
|
||||
if value == "0" * 64:
|
||||
raise ValueError("%s cannot be an all-zero placeholder" % name)
|
||||
return value
|
||||
|
||||
|
||||
def _reject_quantity_fields(value, path="$"):
|
||||
if hasattr(value, "items"):
|
||||
for raw_key, child in value.items():
|
||||
key = str(raw_key)
|
||||
normalized = key.lower().replace("-", "_")
|
||||
if "quantity" in normalized or "quantities" in normalized:
|
||||
raise ValueError(
|
||||
"%s.%s: quantities do not belong in TargetPackageV1"
|
||||
% (path, key)
|
||||
)
|
||||
_reject_quantity_fields(child, "%s.%s" % (path, key))
|
||||
elif isinstance(value, list):
|
||||
for index, child in enumerate(value):
|
||||
_reject_quantity_fields(child, "%s[%d]" % (path, index))
|
||||
|
||||
|
||||
def _verify_identifier(value, name):
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
or re.match(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", value) is None
|
||||
):
|
||||
raise ValueError("%s is not a canonical identifier" % name)
|
||||
return value
|
||||
|
||||
|
||||
def _verify_target_package_clock(signal_as_of, next_session):
|
||||
if (
|
||||
not isinstance(signal_as_of, str)
|
||||
or re.match(
|
||||
r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T15:00:00\+08:00$",
|
||||
signal_as_of,
|
||||
)
|
||||
is None
|
||||
):
|
||||
raise ValueError("signal_as_of must be canonical China-market close")
|
||||
if (
|
||||
not isinstance(next_session, str)
|
||||
or re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", next_session) is None
|
||||
):
|
||||
raise ValueError("next_session must use canonical YYYY-MM-DD")
|
||||
if next_session <= signal_as_of[:10]:
|
||||
raise ValueError("next_session must follow signal_as_of")
|
||||
|
||||
|
||||
def _target_package_content_sha256(package):
|
||||
body = dict(package)
|
||||
body.pop("package_sha256", None)
|
||||
_reject_quantity_fields(body)
|
||||
try:
|
||||
payload = _canonical_json(body).encode("utf-8")
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError(
|
||||
"target package must contain finite JSON values only"
|
||||
) from error
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def verify_target_package(
|
||||
package,
|
||||
expected_signal_as_of=None,
|
||||
expected_next_session=None,
|
||||
expected_decision_id=None,
|
||||
expected_package_sha256=None,
|
||||
):
|
||||
"""Verify the platform-critical subset of a sealed ``TargetPackageV1``.
|
||||
|
||||
This intentionally duplicates a small, Python-3.6-compatible safety
|
||||
boundary from the richer local validator. Hosted runtimes must verify the
|
||||
embedded package again instead of trusting the bundling process.
|
||||
"""
|
||||
|
||||
if not hasattr(package, "items"):
|
||||
raise TypeError("target package must be a mapping")
|
||||
candidate = _json_copy(package, "target package")
|
||||
_reject_quantity_fields(candidate)
|
||||
fields = set(candidate)
|
||||
if fields != _TARGET_PACKAGE_FIELDS:
|
||||
missing = sorted(_TARGET_PACKAGE_FIELDS - fields)
|
||||
unknown = sorted(fields - _TARGET_PACKAGE_FIELDS)
|
||||
raise ValueError(
|
||||
"target package fields mismatch; missing=%s, unknown=%s"
|
||||
% (missing, unknown)
|
||||
)
|
||||
if candidate["schema_version"] != "1.0":
|
||||
raise ValueError("unsupported target package schema_version")
|
||||
if candidate["package_type"] != "TargetPackageV1":
|
||||
raise ValueError("unsupported target package type")
|
||||
for name in (
|
||||
"package_id",
|
||||
"decision_id",
|
||||
"release",
|
||||
"evidence_class",
|
||||
"model_id",
|
||||
):
|
||||
_verify_identifier(candidate[name], name)
|
||||
_verify_target_package_clock(
|
||||
candidate["signal_as_of"], candidate["next_session"]
|
||||
)
|
||||
for name in _TARGET_PACKAGE_HASH_FIELDS:
|
||||
_sha256_hex(candidate[name], name)
|
||||
_sha256_hex(candidate["package_sha256"], "package_sha256")
|
||||
|
||||
raw_weights = candidate["target_weights"]
|
||||
if not hasattr(raw_weights, "items") or not raw_weights:
|
||||
raise ValueError("target_weights must be a non-empty mapping")
|
||||
weights = {}
|
||||
for raw_symbol, raw_weight in raw_weights.items():
|
||||
symbol = normalize_symbol(raw_symbol)
|
||||
if symbol != raw_symbol:
|
||||
raise ValueError(
|
||||
"target_weights symbol must be canonical: %s" % raw_symbol
|
||||
)
|
||||
if symbol in weights:
|
||||
raise ValueError("duplicate target weight for %s" % symbol)
|
||||
weight = _finite_non_negative(
|
||||
raw_weight, "target_weights.%s" % symbol
|
||||
)
|
||||
if weight > 1.0:
|
||||
raise ValueError("target weight exceeds one for %s" % symbol)
|
||||
weights[symbol] = weight
|
||||
gross = math.fsum(weights.values())
|
||||
if gross > 1.0 + 1e-12:
|
||||
raise ValueError("target_weights gross exceeds one")
|
||||
|
||||
universe = candidate["universe"]
|
||||
if not hasattr(universe, "items") or not universe:
|
||||
raise ValueError("universe must be a non-empty mapping")
|
||||
canonical_universe = {}
|
||||
for raw_symbol, raw_state in universe.items():
|
||||
symbol = normalize_symbol(raw_symbol)
|
||||
if symbol != raw_symbol:
|
||||
raise ValueError("universe symbol must be canonical: %s" % raw_symbol)
|
||||
if symbol in canonical_universe:
|
||||
raise ValueError("duplicate universe symbol %s" % symbol)
|
||||
if not hasattr(raw_state, "items"):
|
||||
raise ValueError("universe state must be a mapping for %s" % symbol)
|
||||
state = raw_state.get("state")
|
||||
if state not in (
|
||||
"openable",
|
||||
"hold_only",
|
||||
"sell_only",
|
||||
"frozen",
|
||||
"excluded",
|
||||
):
|
||||
raise ValueError("unsupported universe state for %s" % symbol)
|
||||
canonical_universe[symbol] = state
|
||||
missing_universe = sorted(set(weights) - set(canonical_universe))
|
||||
if missing_universe:
|
||||
raise ValueError(
|
||||
"target symbols are absent from universe: %s" % missing_universe
|
||||
)
|
||||
for symbol, weight in weights.items():
|
||||
if canonical_universe[symbol] == "excluded" and weight > 1e-12:
|
||||
raise ValueError("excluded symbol has positive weight: %s" % symbol)
|
||||
|
||||
constraints = candidate["constraints"]
|
||||
if not hasattr(constraints, "items"):
|
||||
raise ValueError("constraints must be a mapping")
|
||||
if constraints.get("long_only") is not True:
|
||||
raise ValueError("TargetPackageV1 must be long-only")
|
||||
declared_gross = _finite_non_negative(
|
||||
constraints.get("actual_gross_weight"),
|
||||
"constraints.actual_gross_weight",
|
||||
)
|
||||
max_gross = _finite_non_negative(
|
||||
constraints.get("max_gross_weight"),
|
||||
"constraints.max_gross_weight",
|
||||
)
|
||||
max_single = _finite_non_negative(
|
||||
constraints.get("max_single_weight"),
|
||||
"constraints.max_single_weight",
|
||||
)
|
||||
if abs(declared_gross - gross) > 1e-12:
|
||||
raise ValueError("declared gross does not equal target_weights gross")
|
||||
if gross > max_gross + 1e-12 or max_gross > 1.0:
|
||||
raise ValueError("target_weights exceed max_gross_weight")
|
||||
if max_single <= 0.0 or max_single > 1.0:
|
||||
raise ValueError("invalid max_single_weight")
|
||||
if _any_true(
|
||||
weight > max_single + 1e-12 for weight in weights.values()
|
||||
):
|
||||
raise ValueError("target_weights exceed max_single_weight")
|
||||
model_claim = " ".join(
|
||||
(
|
||||
candidate["release"],
|
||||
candidate["evidence_class"],
|
||||
)
|
||||
).lower()
|
||||
if (
|
||||
"baseline" in model_claim
|
||||
and "portable-momentum" in candidate["model_id"].lower()
|
||||
):
|
||||
raise ValueError(
|
||||
"portable-momentum cannot claim a baseline target package"
|
||||
)
|
||||
|
||||
expected_hash = _target_package_content_sha256(candidate)
|
||||
if candidate["package_sha256"] != expected_hash:
|
||||
raise ValueError("target package content hash mismatch")
|
||||
if (
|
||||
expected_signal_as_of is not None
|
||||
and candidate["signal_as_of"] != expected_signal_as_of
|
||||
):
|
||||
raise ValueError("target package signal_as_of does not match request")
|
||||
if (
|
||||
expected_next_session is not None
|
||||
and candidate["next_session"] != expected_next_session
|
||||
):
|
||||
raise ValueError("target package next_session does not match request")
|
||||
if (
|
||||
expected_decision_id is not None
|
||||
and candidate["decision_id"] != expected_decision_id
|
||||
):
|
||||
raise ValueError("target package decision_id does not match request")
|
||||
if expected_package_sha256 is not None:
|
||||
_sha256_hex(
|
||||
expected_package_sha256,
|
||||
"expected_package_sha256",
|
||||
)
|
||||
if candidate["package_sha256"] != expected_package_sha256:
|
||||
raise ValueError("target package digest does not match request")
|
||||
return candidate
|
||||
|
||||
|
||||
def target_package_tape_sha256(packages):
|
||||
"""Hash the exact ordered canonical JSON package array."""
|
||||
|
||||
if not isinstance(packages, (list, tuple)):
|
||||
raise TypeError("target package tape must be a list or tuple")
|
||||
try:
|
||||
payload = _canonical_json(list(packages)).encode("utf-8")
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError(
|
||||
"target package tape must contain finite JSON values only"
|
||||
) from error
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def lookup_target_package_exact(
|
||||
packages,
|
||||
signal_as_of,
|
||||
next_session,
|
||||
decision_id=None,
|
||||
expected_tape_sha256=None,
|
||||
expected_count=None,
|
||||
):
|
||||
"""Verify a complete tape, then return one exact dual-clock decision."""
|
||||
|
||||
if not isinstance(packages, (list, tuple)) or not packages:
|
||||
raise ValueError("target package tape must be a non-empty list or tuple")
|
||||
_verify_target_package_clock(signal_as_of, next_session)
|
||||
decision_id = _optional_non_empty_string(decision_id, "decision_id")
|
||||
if expected_count is not None:
|
||||
if (
|
||||
isinstance(expected_count, bool)
|
||||
or not isinstance(expected_count, int)
|
||||
or expected_count < 0
|
||||
):
|
||||
raise ValueError("expected_count must be a non-negative integer")
|
||||
if len(packages) != expected_count:
|
||||
raise ValueError("target package tape count mismatch")
|
||||
if expected_tape_sha256 is not None:
|
||||
_sha256_hex(expected_tape_sha256, "expected_tape_sha256")
|
||||
if target_package_tape_sha256(packages) != expected_tape_sha256:
|
||||
raise ValueError("target package tape digest mismatch")
|
||||
|
||||
verified = []
|
||||
package_ids = set()
|
||||
decision_ids = set()
|
||||
clock_keys = set()
|
||||
for raw_package in packages:
|
||||
package = verify_target_package(raw_package)
|
||||
clock_key = (package["signal_as_of"], package["next_session"])
|
||||
if package["package_id"] in package_ids:
|
||||
raise ValueError("duplicate target package_id")
|
||||
if package["decision_id"] in decision_ids:
|
||||
raise ValueError("duplicate target decision_id")
|
||||
if clock_key in clock_keys:
|
||||
raise ValueError("duplicate target package clock")
|
||||
package_ids.add(package["package_id"])
|
||||
decision_ids.add(package["decision_id"])
|
||||
clock_keys.add(clock_key)
|
||||
verified.append(package)
|
||||
|
||||
matches = [
|
||||
package
|
||||
for package in verified
|
||||
if package["signal_as_of"] == signal_as_of
|
||||
and package["next_session"] == next_session
|
||||
]
|
||||
if not matches:
|
||||
raise TargetPackageLookupMiss(
|
||||
"no target package for exact signal_as_of/next_session clock"
|
||||
)
|
||||
if len(matches) != 1:
|
||||
raise ValueError("ambiguous exact target package decision")
|
||||
if (
|
||||
decision_id is not None
|
||||
and matches[0]["decision_id"] != decision_id
|
||||
):
|
||||
raise ValueError("exact target package decision_id mismatch")
|
||||
return _json_copy(matches[0], "target package")
|
||||
|
||||
|
||||
def momentum_score(prices, lookback=20, skip=0):
|
||||
"""Return trailing simple momentum, or ``None`` if history is insufficient.
|
||||
|
||||
@@ -314,6 +754,108 @@ def order_deltas(
|
||||
return result
|
||||
|
||||
|
||||
def build_target_weight_plan(
|
||||
target_weights,
|
||||
prices,
|
||||
current,
|
||||
sellable,
|
||||
equity,
|
||||
cash_buffer=0.02,
|
||||
lot_size=100,
|
||||
decision_id=None,
|
||||
package_sha256=None,
|
||||
):
|
||||
"""Bind canonical post-risk weights to one account without recomputing Alpha.
|
||||
|
||||
``prices`` must cover exactly every target or currently managed symbol.
|
||||
Missing prices fail closed even for a held symbol whose desired weight is
|
||||
zero. A missing ``sellable`` entry is safe and means zero sellable shares.
|
||||
"""
|
||||
|
||||
weights = _canonical_mapping(
|
||||
target_weights,
|
||||
"target_weights",
|
||||
_finite_non_negative,
|
||||
)
|
||||
gross = math.fsum(weights.values())
|
||||
if _any_true(weight > 1.0 for weight in weights.values()):
|
||||
raise ValueError("target weight must not exceed one")
|
||||
if gross > 1.0 + 1e-12:
|
||||
raise ValueError("target weight gross must not exceed one")
|
||||
|
||||
canonical_current = _canonical_mapping(
|
||||
current,
|
||||
"current",
|
||||
_non_negative_quantity,
|
||||
)
|
||||
canonical_sellable = _canonical_mapping(
|
||||
sellable,
|
||||
"sellable",
|
||||
_non_negative_quantity,
|
||||
)
|
||||
unknown_sellable = sorted(set(canonical_sellable) - set(canonical_current))
|
||||
if unknown_sellable:
|
||||
raise ValueError(
|
||||
"sellable contains symbols absent from current: %s"
|
||||
% unknown_sellable
|
||||
)
|
||||
for symbol, quantity in canonical_sellable.items():
|
||||
if quantity > canonical_current[symbol]:
|
||||
raise ValueError(
|
||||
"sellable exceeds current quantity for %s" % symbol
|
||||
)
|
||||
|
||||
canonical_prices = _canonical_mapping(
|
||||
prices,
|
||||
"prices",
|
||||
_positive_price,
|
||||
)
|
||||
managed_symbols = set(weights) | set(canonical_current)
|
||||
price_symbols = set(canonical_prices)
|
||||
missing_prices = sorted(managed_symbols - price_symbols)
|
||||
extra_prices = sorted(price_symbols - managed_symbols)
|
||||
if missing_prices or extra_prices:
|
||||
if missing_prices:
|
||||
raise KeyError(
|
||||
"prices do not cover managed symbols: missing=%s"
|
||||
% missing_prices
|
||||
)
|
||||
raise ValueError(
|
||||
"prices contain unmanaged symbols: extra=%s" % extra_prices
|
||||
)
|
||||
|
||||
complete_weights = dict(
|
||||
(symbol, weights.get(symbol, 0.0))
|
||||
for symbol in sorted(managed_symbols)
|
||||
)
|
||||
targets = target_quantities(
|
||||
complete_weights,
|
||||
canonical_prices,
|
||||
equity,
|
||||
lot_size,
|
||||
cash_buffer,
|
||||
)
|
||||
orders = order_deltas(
|
||||
targets,
|
||||
canonical_current,
|
||||
canonical_sellable,
|
||||
lot_size,
|
||||
)
|
||||
decision_id = _optional_non_empty_string(decision_id, "decision_id")
|
||||
package_sha256 = _sha256_hex(
|
||||
package_sha256,
|
||||
"package_sha256",
|
||||
allow_none=True,
|
||||
)
|
||||
return {
|
||||
"decision_id": decision_id,
|
||||
"package_sha256": package_sha256,
|
||||
"weights": complete_weights,
|
||||
"targets": targets,
|
||||
"orders": orders,
|
||||
}
|
||||
|
||||
|
||||
def build_rebalance_plan(
|
||||
price_history,
|
||||
current,
|
||||
@@ -359,14 +901,19 @@ def build_rebalance_plan(
|
||||
|
||||
__all__ = [
|
||||
"build_rebalance_plan",
|
||||
"build_target_weight_plan",
|
||||
"capped_target_weights",
|
||||
"convert_symbol",
|
||||
"lookup_target_package_exact",
|
||||
"momentum_score",
|
||||
"normalize_symbol",
|
||||
"order_deltas",
|
||||
"rank_momentum",
|
||||
"round_board_lot",
|
||||
"TargetPackageLookupMiss",
|
||||
"target_package_tape_sha256",
|
||||
"target_quantities",
|
||||
"verify_target_package",
|
||||
]
|
||||
# __PORTABLE_CORE_BUNDLE_END__
|
||||
|
||||
@@ -399,6 +946,9 @@ g = G()
|
||||
# __BASELINE_CONFIG_START__
|
||||
# Generated from configs/baseline.json; do not edit this region.
|
||||
CONFIG = {'account_id': 'test',
|
||||
'decision_mode': 'portable_momentum_smoke',
|
||||
'target_package_count': 0,
|
||||
'target_package_tape_sha256': None,
|
||||
'universe_mode': 'pit_index',
|
||||
'index_symbol': '000905.XSHG',
|
||||
'qmt_sector_name': '\u4e2d\u8bc1500',
|
||||
@@ -421,6 +971,77 @@ CONFIG = {'account_id': 'test',
|
||||
'rebalance_schedule': 'weekly_first_close'}
|
||||
# __BASELINE_CONFIG_END__
|
||||
|
||||
# __TARGET_PACKAGE_TAPE_START__
|
||||
# Generated by tools/bundle_platforms.py; do not edit this region.
|
||||
TARGET_PACKAGES = []
|
||||
# __TARGET_PACKAGE_TAPE_END__
|
||||
|
||||
|
||||
MOMENTUM_SMOKE_MODE = "portable_momentum_smoke"
|
||||
TARGET_PACKAGE_MODE = "target_package"
|
||||
|
||||
|
||||
def _validated_target_packages(config):
|
||||
mode = config.get("decision_mode")
|
||||
if mode not in (MOMENTUM_SMOKE_MODE, TARGET_PACKAGE_MODE):
|
||||
raise UnsupportedStrategyPeriod("unsupported decision_mode")
|
||||
if not isinstance(TARGET_PACKAGES, list):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"TARGET_PACKAGES must be a list"
|
||||
)
|
||||
expected_count = config.get("target_package_count")
|
||||
expected_sha256 = config.get("target_package_tape_sha256")
|
||||
if mode == MOMENTUM_SMOKE_MODE:
|
||||
if (
|
||||
type(expected_count) is not int
|
||||
or expected_count != 0
|
||||
or expected_sha256 is not None
|
||||
or TARGET_PACKAGES
|
||||
):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"portable_momentum_smoke cannot carry target packages"
|
||||
)
|
||||
return []
|
||||
if type(expected_count) is not int or expected_count <= 0:
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"target_package mode requires a positive target_package_count"
|
||||
)
|
||||
if expected_count != len(TARGET_PACKAGES):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"target package count mismatch"
|
||||
)
|
||||
actual_sha256 = target_package_tape_sha256(TARGET_PACKAGES)
|
||||
if expected_sha256 != actual_sha256:
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"target package tape sha256 mismatch"
|
||||
)
|
||||
verified = []
|
||||
package_ids = set()
|
||||
decision_ids = set()
|
||||
clocks = set()
|
||||
signal_clocks = set()
|
||||
for raw_package in TARGET_PACKAGES:
|
||||
package = verify_target_package(raw_package)
|
||||
package_id = package["package_id"]
|
||||
decision_id = package["decision_id"]
|
||||
clock = (package["signal_as_of"], package["next_session"])
|
||||
signal_as_of = package["signal_as_of"]
|
||||
if (
|
||||
package_id in package_ids
|
||||
or decision_id in decision_ids
|
||||
or clock in clocks
|
||||
or signal_as_of in signal_clocks
|
||||
):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"duplicate or ambiguous target package decision identity"
|
||||
)
|
||||
package_ids.add(package_id)
|
||||
decision_ids.add(decision_id)
|
||||
clocks.add(clock)
|
||||
signal_clocks.add(signal_as_of)
|
||||
verified.append(package)
|
||||
return verified
|
||||
|
||||
|
||||
def _param(ContextInfo, name, default=None):
|
||||
params = getattr(ContextInfo, "_param", {})
|
||||
@@ -442,22 +1063,25 @@ def init(ContextInfo):
|
||||
override = _param(ContextInfo, "q60_" + key, None)
|
||||
if override is not None:
|
||||
g.config[key] = override
|
||||
if (
|
||||
g.config.get("rebalance_schedule")
|
||||
!= "weekly_first_close"
|
||||
):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"unsupported rebalance_schedule"
|
||||
)
|
||||
if g.config.get("universe_mode") not in ("pit_index", "fixed"):
|
||||
raise UnsupportedStrategyPeriod("unsupported universe_mode")
|
||||
if (
|
||||
g.config.get("universe_mode") == "pit_index"
|
||||
and not g.config.get("dedicated_account_required")
|
||||
):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"pit_index mode requires a dedicated account"
|
||||
)
|
||||
g.target_packages = _validated_target_packages(g.config)
|
||||
g.decision_mode = g.config["decision_mode"]
|
||||
if g.config["decision_mode"] == MOMENTUM_SMOKE_MODE:
|
||||
if (
|
||||
g.config.get("rebalance_schedule")
|
||||
!= "weekly_first_close"
|
||||
):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"unsupported rebalance_schedule"
|
||||
)
|
||||
if g.config.get("universe_mode") not in ("pit_index", "fixed"):
|
||||
raise UnsupportedStrategyPeriod("unsupported universe_mode")
|
||||
if (
|
||||
g.config.get("universe_mode") == "pit_index"
|
||||
and not g.config.get("dedicated_account_required")
|
||||
):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"pit_index mode requires a dedicated account"
|
||||
)
|
||||
set_commission = getattr(ContextInfo, "set_commission", None)
|
||||
set_slippage = getattr(ContextInfo, "set_slippage", None)
|
||||
if not callable(set_commission) or not callable(set_slippage):
|
||||
@@ -487,6 +1111,7 @@ def init(ContextInfo):
|
||||
g.last_plan = None
|
||||
g.last_universe = None
|
||||
g.last_universe_as_of = None
|
||||
g.last_target_signal_as_of = None
|
||||
if (
|
||||
g.config.get("universe_mode") == "fixed"
|
||||
and hasattr(ContextInfo, "set_universe")
|
||||
@@ -494,6 +1119,16 @@ def init(ContextInfo):
|
||||
ContextInfo.set_universe(g.config["universe"])
|
||||
|
||||
|
||||
def _decision_mode():
|
||||
configured = g.config.get("decision_mode")
|
||||
frozen = getattr(g, "decision_mode", configured)
|
||||
if configured != frozen:
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"decision_mode cannot change after init"
|
||||
)
|
||||
return frozen
|
||||
|
||||
|
||||
def _bar_datetime(ContextInfo):
|
||||
timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
|
||||
return datetime.datetime.fromtimestamp(float(timetag) / 1000.0)
|
||||
@@ -670,7 +1305,10 @@ def _portfolio(ContextInfo, managed):
|
||||
convert_symbol(symbol, "canonical")
|
||||
for symbol in managed
|
||||
)
|
||||
dynamic = g.config.get("universe_mode") == "pit_index"
|
||||
dynamic = (
|
||||
_decision_mode() == MOMENTUM_SMOKE_MODE
|
||||
and g.config.get("universe_mode") == "pit_index"
|
||||
)
|
||||
for position in _trade_details(ContextInfo, "position") or []:
|
||||
code = str(
|
||||
_get_attr(position, ["stock_code", "m_strInstrumentID"], "")
|
||||
@@ -716,6 +1354,8 @@ def _portfolio(ContextInfo, managed):
|
||||
|
||||
|
||||
def compute_plan(ContextInfo, bar_time=None):
|
||||
if _decision_mode() == TARGET_PACKAGE_MODE:
|
||||
return _compute_target_package_plan(ContextInfo, bar_time)
|
||||
if bar_time is None:
|
||||
bar_time = _bar_datetime(ContextInfo)
|
||||
membership_timetag = int(
|
||||
@@ -748,6 +1388,136 @@ def compute_plan(ContextInfo, bar_time=None):
|
||||
)
|
||||
|
||||
|
||||
def _signal_as_of(bar_time):
|
||||
if (
|
||||
int(bar_time.hour) != 15
|
||||
or int(bar_time.minute) != 0
|
||||
or int(bar_time.second) != 0
|
||||
or int(bar_time.microsecond) != 0
|
||||
):
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"target_package mode requires the completed 15:00 daily close"
|
||||
)
|
||||
return bar_time.strftime("%Y-%m-%dT15:00:00+08:00")
|
||||
|
||||
|
||||
def _target_package_for_close(bar_time):
|
||||
signal_as_of = _signal_as_of(bar_time)
|
||||
config = g.config
|
||||
packages = _validated_target_packages(config)
|
||||
matches = [
|
||||
package
|
||||
for package in packages
|
||||
if package["signal_as_of"] == signal_as_of
|
||||
]
|
||||
if not matches:
|
||||
return None
|
||||
if len(matches) != 1:
|
||||
raise UnsupportedStrategyPeriod(
|
||||
"ambiguous target package signal clock"
|
||||
)
|
||||
package = matches[0]
|
||||
return lookup_target_package_exact(
|
||||
packages,
|
||||
signal_as_of,
|
||||
package["next_session"],
|
||||
decision_id=package["decision_id"],
|
||||
expected_tape_sha256=config["target_package_tape_sha256"],
|
||||
expected_count=config["target_package_count"],
|
||||
)
|
||||
|
||||
|
||||
def _target_close_prices(ContextInfo, bar_time, symbols):
|
||||
platform_symbols = [
|
||||
convert_symbol(symbol, "qmt")
|
||||
for symbol in sorted(symbols)
|
||||
]
|
||||
raw = ContextInfo.get_market_data_ex(
|
||||
fields=["close"],
|
||||
stock_code=platform_symbols,
|
||||
period="1d",
|
||||
start_time="",
|
||||
end_time=bar_time.strftime("%Y%m%d"),
|
||||
count=1,
|
||||
dividend_type="front_ratio",
|
||||
fill_data=False,
|
||||
subscribe=False,
|
||||
)
|
||||
if not hasattr(raw, "get"):
|
||||
raise PriceHistoryUnavailable(
|
||||
"target close query returned an invalid payload"
|
||||
)
|
||||
prices = {}
|
||||
for symbol in platform_symbols:
|
||||
values = _series_close(raw.get(symbol))
|
||||
if len(values) != 1:
|
||||
raise PriceHistoryUnavailable(
|
||||
"%s needs one completed daily close" % symbol
|
||||
)
|
||||
try:
|
||||
price = float(values[0])
|
||||
except (TypeError, ValueError):
|
||||
raise PriceHistoryUnavailable(
|
||||
"%s contains a missing/non-numeric close" % symbol
|
||||
)
|
||||
if not math.isfinite(price) or price <= 0.0:
|
||||
raise PriceHistoryUnavailable(
|
||||
"%s contains a missing/non-positive close" % symbol
|
||||
)
|
||||
prices[convert_symbol(symbol, "canonical")] = price
|
||||
return prices
|
||||
|
||||
|
||||
def _target_lineage(package):
|
||||
return {
|
||||
"package_id": package["package_id"],
|
||||
"package_sha256": package["package_sha256"],
|
||||
"decision_id": package["decision_id"],
|
||||
"signal_as_of": package["signal_as_of"],
|
||||
"next_session": package["next_session"],
|
||||
"release": package["release"],
|
||||
"model_id": package["model_id"],
|
||||
}
|
||||
|
||||
|
||||
def _compute_target_package_plan(ContextInfo, bar_time=None):
|
||||
if bar_time is None:
|
||||
bar_time = _bar_datetime(ContextInfo)
|
||||
package = _target_package_for_close(bar_time)
|
||||
if package is None:
|
||||
return None
|
||||
managed = sorted(package["universe"])
|
||||
current, sellable, equity = _portfolio(ContextInfo, managed)
|
||||
if equity <= 0:
|
||||
raise ValueError("QMT account equity must be positive")
|
||||
plan = build_target_weight_plan(
|
||||
target_weights=package["target_weights"],
|
||||
prices=_target_close_prices(
|
||||
ContextInfo,
|
||||
bar_time,
|
||||
set(package["target_weights"]) | set(current),
|
||||
),
|
||||
current=current,
|
||||
sellable=sellable,
|
||||
equity=equity,
|
||||
cash_buffer=g.config["cash_buffer"],
|
||||
lot_size=g.config["lot_size"],
|
||||
decision_id=package["decision_id"],
|
||||
package_sha256=package["package_sha256"],
|
||||
)
|
||||
plan["decision_mode"] = TARGET_PACKAGE_MODE
|
||||
plan["lineage"] = _target_lineage(package)
|
||||
plan["universe"] = package["universe"]
|
||||
plan["execution_contract"] = {
|
||||
"signal_price_source": "COMPLETED_DAILY_CLOSE",
|
||||
"submit_trigger": "NEXT_BAR_FIRST_TICK",
|
||||
"quick_trade": 0,
|
||||
"declared_next_session": package["next_session"],
|
||||
"real_platform_observed": False,
|
||||
}
|
||||
return plan
|
||||
|
||||
|
||||
def _orders_allowed(ContextInfo):
|
||||
# This hosted wrapper is deliberately backtest-only. Its cached account
|
||||
# query and in-memory weekly marker cannot provide crash-safe live
|
||||
@@ -810,12 +1580,76 @@ def _submit_delta(ContextInfo, canonical, delta, week_key):
|
||||
)
|
||||
|
||||
|
||||
def _submit_target_delta(ContextInfo, canonical, delta, package_sha256):
|
||||
config = g.config
|
||||
order_code = convert_symbol(canonical, "qmt")
|
||||
operation = 23 if delta > 0 else 24
|
||||
volume = abs(int(delta))
|
||||
user_order_id = "q60t-%s-%s" % (
|
||||
str(package_sha256)[:8],
|
||||
order_code.split(".")[0],
|
||||
)
|
||||
function = globals()["passorder"]
|
||||
arg_count = getattr(getattr(function, "__code__", None), "co_argcount", 11)
|
||||
if arg_count <= 8:
|
||||
function(
|
||||
operation,
|
||||
1101,
|
||||
config["account_id"],
|
||||
order_code,
|
||||
5,
|
||||
-1,
|
||||
volume,
|
||||
ContextInfo,
|
||||
)
|
||||
else:
|
||||
function(
|
||||
operation,
|
||||
1101,
|
||||
config["account_id"],
|
||||
order_code,
|
||||
5,
|
||||
-1,
|
||||
volume,
|
||||
"quant60",
|
||||
0,
|
||||
user_order_id,
|
||||
ContextInfo,
|
||||
)
|
||||
|
||||
|
||||
def handlebar(ContextInfo):
|
||||
if not _is_backtest(ContextInfo) and hasattr(ContextInfo, "is_last_bar"):
|
||||
if not ContextInfo.is_last_bar():
|
||||
return None
|
||||
|
||||
bar_time = _bar_datetime(ContextInfo)
|
||||
if _decision_mode() == TARGET_PACKAGE_MODE:
|
||||
signal_as_of = _signal_as_of(bar_time)
|
||||
if g.last_target_signal_as_of == signal_as_of:
|
||||
return None
|
||||
plan = compute_plan(ContextInfo, bar_time)
|
||||
# Consume the clock before crossing passorder so a callback retry
|
||||
# cannot duplicate an already accepted order prefix.
|
||||
g.last_target_signal_as_of = signal_as_of
|
||||
if plan is None:
|
||||
return None
|
||||
g.last_plan = plan
|
||||
if not _orders_allowed(ContextInfo):
|
||||
return plan
|
||||
ordered = sorted(
|
||||
plan["orders"].items(),
|
||||
key=lambda item: item[1],
|
||||
)
|
||||
for canonical, delta in ordered:
|
||||
if int(delta) != 0:
|
||||
_submit_target_delta(
|
||||
ContextInfo,
|
||||
canonical,
|
||||
delta,
|
||||
plan["lineage"]["package_sha256"],
|
||||
)
|
||||
return plan
|
||||
year, week, unused = bar_time.isocalendar()
|
||||
del unused
|
||||
week_key = (year, week)
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"artifacts": {
|
||||
"joinquant": {
|
||||
"core": "src/quant60/portable_core.py",
|
||||
"core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||
"effective_config_sha256": "640980026f397df2e6d0f9e3884b58c7e2343e1be70ca1b629f6111b889ec7cb",
|
||||
"output": "dist/target-package/joinquant_strategy.py",
|
||||
"output_sha256": "0586930ddc483f4489f09b562f4d4232b458b703e6a1194af809b707211be8c4",
|
||||
"wrapper": "platforms/joinquant_strategy.py",
|
||||
"wrapper_sha256": "0c672b64665b7c1d4409ba288b00d1608e976800af3a4f9a6467767ce4def592"
|
||||
},
|
||||
"qmt_builtin": {
|
||||
"core": "src/quant60/portable_core.py",
|
||||
"core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||
"effective_config_sha256": "ace13da661a2921e6bf75e48a574fd605fb6eef87c0025b89460082326a39d36",
|
||||
"output": "dist/target-package/qmt_builtin_strategy.py",
|
||||
"output_sha256": "21f634a9831341fef2d86a6df612d01c79121794a48e525da76df226dc0729ed",
|
||||
"wrapper": "platforms/qmt_builtin_strategy.py",
|
||||
"wrapper_sha256": "a1e53dc22ebd9bb01be43675786151606bbed273d8ab883fb933461e379d2727"
|
||||
}
|
||||
},
|
||||
"baseline_config": "configs/baseline.json",
|
||||
"baseline_config_sha256": "939a7df386b06bc82869940572c35544e9361913b94d9e03027411e51b1006e1",
|
||||
"decision_mode": "target_package",
|
||||
"joinquant_observation": {
|
||||
"eod_api_policy": "best_effort_get_orders_get_trades",
|
||||
"eod_chunk_characters": 1800,
|
||||
"eod_chunk_encoding": "base64-canonical-json",
|
||||
"events": [
|
||||
"INIT",
|
||||
"TARGET_PACKAGE_NOOP",
|
||||
"TARGET_PACKAGE_HIT",
|
||||
"PLATFORM_BINDING_INPUT",
|
||||
"TARGET_PACKAGE_PLAN",
|
||||
"SKIPPED_ORDER",
|
||||
"ORDER_REQUEST",
|
||||
"ORDER_RETURN",
|
||||
"EOD_STATUS"
|
||||
],
|
||||
"fail_soft": true,
|
||||
"prefix": "QUANT60_JOINQUANT_EVIDENCE_V1 ",
|
||||
"schema_version": "1.0",
|
||||
"target_price_source": "CURRENT_DATA_DAY_OPEN"
|
||||
},
|
||||
"portable_core_sha256": "f9de9f18e4008bb0fc80edb0b21cae6ea06a61828d89e9b7c11057f98225fc19",
|
||||
"schema_version": 1,
|
||||
"target_package_count": 1,
|
||||
"target_package_ids": [
|
||||
"TP-20240311-795dcfba"
|
||||
],
|
||||
"target_package_sha256": [
|
||||
"baa15c8156aa6cb54d3775d0cd562613a9d0a4f15b0c39f2a60e0f8b8c6f1f3a"
|
||||
],
|
||||
"target_package_tape_sha256": "5383c376951637b8d4b1ca42fc6db80347abda74c0181c0f70d6c411fb97ea64"
|
||||
}
|
||||
+2081
File diff suppressed because it is too large
Load Diff
+1735
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user