# coding: gbk """QMT built-in Python 3.6 strategy wrapper. Keep this source ASCII-only.""" # __PORTABLE_CORE_BUNDLE_START__ # Inlined by tools/bundle_platforms.py; do not edit this region. import hashlib import json import math import re _CANONICAL_EXCHANGES = { "SH": "XSHG", "SSE": "XSHG", "XSHG": "XSHG", "SZ": "XSHE", "SZE": "XSHE", "SZSE": "XSHE", "XSHE": "XSHE", "BJ": "XBSE", "BSE": "XBSE", "XBSE": "XBSE", } _SHORT_EXCHANGES = { "XSHG": "SH", "XSHE": "SZ", "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.""" total = 0.0 for value in values: total += value 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" if code.startswith(("5", "6", "9")): return "XSHG" return "XSHE" def _split_symbol(symbol): if not isinstance(symbol, str): raise TypeError("symbol must be a string") value = symbol.strip().upper().replace(" ", "") if not value: raise ValueError("symbol must not be empty") match = re.match(r"^(\d{6})\.(XSHG|XSHE|XBSE|SH|SZ|BJ)$", value) if match: return match.group(1), _CANONICAL_EXCHANGES[match.group(2)] match = re.match(r"^(XSHG|XSHE|XBSE|SH|SZ|BJ)[\.:]?(\d{6})$", value) if match: return match.group(2), _CANONICAL_EXCHANGES[match.group(1)] match = re.match(r"^(\d{6})[\.:](SSE|SZE|SZSE|BSE)$", value) if match: return match.group(1), _CANONICAL_EXCHANGES[match.group(2)] if re.match(r"^\d{6}$", value): return value, _infer_exchange(value) raise ValueError("unsupported A-share symbol: %s" % symbol) def normalize_symbol(symbol, target="canonical"): """Convert common A-share codes to canonical/JQ, QMT, or Qlib format.""" code, exchange = _split_symbol(symbol) target_name = str(target).strip().lower() if target_name in ("canonical", "joinquant", "jq"): return "%s.%s" % (code, exchange) if target_name in ("qmt", "xtquant"): return "%s.%s" % (code, _SHORT_EXCHANGES[exchange]) if target_name == "qlib": return "%s%s" % (_SHORT_EXCHANGES[exchange], code) raise ValueError("unsupported target platform: %s" % target) def convert_symbol(symbol, target="canonical"): """Alias kept as the stable adapter-facing name.""" 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. ``skip=0`` uses the latest completed decision bar. Set ``skip=1`` only when intentionally implementing a 20-1 convention. A lookback of N needs N+skip+1 observations. """ lookback = int(lookback) skip = int(skip) if lookback <= 0 or skip < 0: raise ValueError("lookback must be positive and skip non-negative") values = list(prices) if len(values) < lookback + skip + 1: return None end_index = len(values) - 1 - skip start_index = end_index - lookback start = float(values[start_index]) end = float(values[end_index]) if ( start <= 0.0 or end <= 0.0 or not math.isfinite(start) or not math.isfinite(end) ): return None return end / start - 1.0 def rank_momentum(price_history, lookback=20, skip=0, top_n=None): """Rank ``{symbol: closes}`` by momentum with deterministic tie-breaking.""" ranked = [] for symbol in sorted(price_history): score = momentum_score(price_history[symbol], lookback, skip) if score is not None and math.isfinite(score): ranked.append((normalize_symbol(symbol), score)) ranked.sort(key=lambda item: (-item[1], item[0])) if top_n is not None: ranked = ranked[: max(0, int(top_n))] return ranked def capped_target_weights( scores, max_weight=0.20, gross_target=1.0, min_score=0.0 ): """Long-only score-proportional weights with iterative cap redistribution.""" max_weight = float(max_weight) gross_target = float(gross_target) min_score = float(min_score) if not math.isfinite(max_weight) or not 0.0 < max_weight <= 1.0: raise ValueError("max_weight must be finite and lie in (0, 1]") if not math.isfinite(gross_target) or not 0.0 <= gross_target <= 1.0: raise ValueError("gross_target must be finite and lie in [0, 1]") if not math.isfinite(min_score): raise ValueError("min_score must be finite") if hasattr(scores, "items"): items = list(scores.items()) else: items = list(scores) normalized = {} for symbol, raw_score in items: name = normalize_symbol(symbol) score = float(raw_score) if not math.isfinite(score): raise ValueError("score must be finite for %s" % name) normalized[name] = score weights = dict((symbol, 0.0) for symbol in normalized) active = dict( (symbol, score) for symbol, score in normalized.items() if score > min_score and score > 0.0 ) remaining = min(gross_target, len(active) * max_weight) while active and remaining > 1e-15: score_sum = _sum_numeric(active.values()) if score_sum <= 0.0: break capped = [] for symbol in sorted(active): proposed = remaining * active[symbol] / score_sum if proposed >= max_weight - 1e-15: weights[symbol] = max_weight capped.append(symbol) if not capped: for symbol in sorted(active): weights[symbol] = remaining * active[symbol] / score_sum remaining = 0.0 break for symbol in capped: active.pop(symbol) remaining -= max_weight remaining = max(0.0, remaining) return weights def round_board_lot(quantity, lot_size=100): """Round an absolute long position down to a board-lot quantity.""" lot_size = int(lot_size) if lot_size <= 0: raise ValueError("lot_size must be positive") if not math.isfinite(float(quantity)): raise ValueError("quantity must be finite") quantity = max(0, int(quantity)) return quantity // lot_size * lot_size def _is_star_market(symbol): code, exchange = _split_symbol(symbol) return exchange == "XSHG" and code.startswith(("688", "689")) def target_quantities( weights, prices, equity, lot_size=100, cash_buffer=0.02 ): """Convert target weights to affordable board-lot long quantities.""" equity = float(equity) cash_buffer = float(cash_buffer) if ( not math.isfinite(equity) or equity < 0.0 or not math.isfinite(cash_buffer) or not 0.0 <= cash_buffer < 1.0 ): raise ValueError("invalid equity or cash_buffer") positive_weights = [] for raw_symbol, raw_weight in weights.items(): weight = float(raw_weight) if not math.isfinite(weight): raise ValueError( "weight must be finite for %s" % normalize_symbol(raw_symbol) ) positive_weights.append(max(0.0, weight)) requested_gross = _sum_numeric(positive_weights) gross_ceiling = 1.0 - cash_buffer scale = ( min(1.0, gross_ceiling / requested_gross) if requested_gross > 0.0 else 1.0 ) result = {} for raw_symbol in sorted(weights): symbol = normalize_symbol(raw_symbol) weight = float(weights[raw_symbol]) if not math.isfinite(weight): raise ValueError("weight must be finite for %s" % symbol) weight = max(0.0, weight) price = prices.get(raw_symbol) if price is None: price = prices.get(symbol) if price is None: raise KeyError("missing price for %s" % symbol) price = float(price) if price <= 0.0 or not math.isfinite(price): raise ValueError("invalid price for %s" % symbol) # Weights are absolute equity weights. cash_buffer is a ceiling, not # a second multiplicative haircut: gross=.95 and buffer=.02 stays .95. budget = equity * weight * scale quantity = round_board_lot(budget / price, lot_size) # STAR Market auction orders must contain at least 200 shares. Keep # the portable planner conservative and on the configured 100-share # grid even though quantities above 200 may increment by one share. if _is_star_market(symbol) and 0 < quantity < 200: quantity = 0 result[symbol] = quantity return result def order_deltas( targets, current, sellable=None, lot_size=100 ): """Return signed board-lot orders; negative sells respect sellable quantity.""" canonical_targets = {} canonical_current = {} canonical_sellable = {} for symbol, quantity in targets.items(): canonical_targets[normalize_symbol(symbol)] = max(0, int(quantity)) for symbol, quantity in current.items(): canonical_current[normalize_symbol(symbol)] = max(0, int(quantity)) if sellable is not None: for symbol, quantity in sellable.items(): canonical_sellable[normalize_symbol(symbol)] = max(0, int(quantity)) result = {} symbols = sorted(set(canonical_targets) | set(canonical_current)) for symbol in symbols: target = round_board_lot(canonical_targets.get(symbol, 0), lot_size) if _is_star_market(symbol) and 0 < target < 200: target = 0 held = canonical_current.get(symbol, 0) delta = target - held if delta > 0: delta = round_board_lot(delta, lot_size) if _is_star_market(symbol) and 0 < delta < 200: delta = 0 elif delta < 0: available = held if sellable is not None: available = min(held, canonical_sellable.get(symbol, 0)) requested = min(-delta, available) # A complete liquidation may submit the entire odd-lot balance. # This also covers the STAR exception for a remaining balance # below 200 shares. Partial orders stay on the configured grid. full_liquidation = ( target == 0 and requested == held and available >= held ) if full_liquidation: delta = -held else: delta = -round_board_lot(requested, lot_size) if _is_star_market(symbol) and 0 < -delta < 200: delta = 0 if delta: result[symbol] = delta 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, sellable, equity, lookback=20, skip=0, top_n=10, max_weight=0.20, gross_target=1.0, cash_buffer=0.02, lot_size=100, ): """Build the complete portable signal -> weight -> quantity -> order plan.""" ranked = rank_momentum(price_history, lookback, skip, top_n) scores = dict(ranked) weights = capped_target_weights(scores, max_weight, gross_target, 0.0) for raw_symbol in price_history: symbol = normalize_symbol(raw_symbol) if symbol not in scores: scores[symbol] = momentum_score( price_history[raw_symbol], lookback, skip ) if symbol not in weights: weights[symbol] = 0.0 prices = {} for raw_symbol, history in price_history.items(): if not history: continue prices[normalize_symbol(raw_symbol)] = history[-1] targets = target_quantities( weights, prices, equity, lot_size, cash_buffer ) orders = order_deltas(targets, current, sellable, lot_size) return { "scores": scores, "weights": weights, "targets": targets, "orders": orders, } __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__ import datetime import math class PriceHistoryUnavailable(RuntimeError): pass class UnmanagedPositionError(RuntimeError): pass class UnsupportedStrategyPeriod(RuntimeError): pass class G(object): pass # QMT explicitly documents that custom attributes written to ContextInfo can # roll back before the next handlebar call. Keep all user-owned mutable state # in a module global and use ContextInfo only for platform-owned fields/APIs. g = G() # __BASELINE_CONFIG_START__ # Generated from configs/baseline.json; do not edit this region. CONFIG = {'account_id': 'test', 'decision_mode': 'target_package', 'target_package_count': 1, 'target_package_tape_sha256': '5383c376951637b8d4b1ca42fc6db80347abda74c0181c0f70d6c411fb97ea64', 'universe_mode': 'fixed', 'index_symbol': '000905.XSHG', 'qmt_sector_name': '\u4e2d\u8bc1500', 'dedicated_account_required': True, 'exclude_st': True, 'universe': ['000001.SZ', '000333.SZ', '000651.SZ', '002415.SZ', '002594.SZ', '300059.SZ', '300750.SZ', '600000.SH', '600519.SH', '601012.SH', '601318.SH', '688981.SH'], 'lookback': 20, 'skip': 0, 'top_n': 20, 'max_weight': 0.05, 'gross_target': 0.95, 'cash_buffer': 0.02, 'lot_size': 100, 'participation_rate': 0.1, 'slippage_bps': 2.0, 'commission_rate': 0.0002, 'minimum_commission': 5.0, 'stamp_duty_rate': 0.0005, 'transfer_fee_rate': 1e-05, '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 = [{'config_sha256': 'e4edf1eb730597e220266c1f8007ddb5e1ab4d26a490cd10738823527d030470', 'constraints': {'actual_gross_weight': 0.4, 'constraint_state': 'FEASIBLE', 'long_only': True, 'max_gross_weight': 0.9, 'max_one_way_turnover': 0.45, 'max_single_weight': 0.1}, 'cost_sha256': 'e2a2d8e6d3829c105a9f6c9ec80d9df18c2dee0553aafeb761abb79d378769a7', 'data_sha256': 'f5b3e66b0a94eeccb44a114bf3a0bbfad44b5d5da9d5b698c81e83a3e0007d03', 'decision_id': 'RD-20240311', 'evidence_class': 'synthetic-research', 'feature_sha256': '24e671cebc35afe7497b69c59d48bb5bad9401e30c1607f0fc41a69f59082aa1', 'model_id': 'deterministic-ridge-alpha-5', 'model_sha256': 'e14e8df7e2f86e6b3178a6e500d1d2c0427bb02d187b13cb4ec672bbf5f9f334', 'next_session': '2024-03-12', 'optimizer_sha256': 'b6aba6ab22fc375e89e4bfbdffb10c7261a2fae01b0183265caacb589643c31e', 'package_id': 'TP-20240311-795dcfba', 'package_sha256': 'baa15c8156aa6cb54d3775d0cd562613a9d0a4f15b0c39f2a60e0f8b8c6f1f3a', 'package_type': 'TargetPackageV1', 'release': 'quant60-research-candidate-v1', 'risk_sha256': 'a3d95aa6cd569d2452856b148df053b040b2acc2e6b993bbd1072b48b46cde5a', 'schema_version': '1.0', 'signal_as_of': '2024-03-11T15:00:00+08:00', 'source_sha256': '86c1e0514f608b2012b4084dbc947eada90e8015fccbdf8ae13ae99340ede178', 'target_weights': {'000001.XSHE': 0.0, '000333.XSHE': 0.0, '000651.XSHE': 0.0, '002415.XSHE': 0.0, '002594.XSHE': 0.0, '300059.XSHE': 0.0, '300750.XSHE': 0.0, '600000.XSHG': 0.0, '600519.XSHG': 0.1, '601012.XSHG': 0.1, '601318.XSHG': 0.1, '688981.XSHG': 0.1}, 'universe': {'000001.XSHE': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '000333.XSHE': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '000651.XSHE': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '002415.XSHE': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '002594.XSHE': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '300059.XSHE': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '300750.XSHE': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '600000.XSHG': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '600519.XSHG': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '601012.XSHG': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '601318.XSHG': {'reasons': ['ELIGIBLE'], 'state': 'openable'}, '688981.XSHG': {'reasons': ['ELIGIBLE'], 'state': 'openable'}}}] # __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", {}) if isinstance(params, dict) and name in params: return params[name] return default def init(ContextInfo): period = str( getattr(ContextInfo, "period", _param(ContextInfo, "period", "1d")) ).strip().lower() if period not in ("1d", "day"): raise UnsupportedStrategyPeriod( "quant60 QMT wrapper requires a 1d driving period" ) g.config = dict(CONFIG) for key in CONFIG: override = _param(ContextInfo, "q60_" + key, None) if override is not None: g.config[key] = override 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): raise UnsupportedStrategyPeriod( "QMT backtest commission/slippage APIs are unavailable" ) commission = ( float(g.config["commission_rate"]) + float(g.config["transfer_fee_rate"]) ) set_commission( 0, [ 0.0, float(g.config["stamp_duty_rate"]), commission, commission, 0.0, float(g.config["minimum_commission"]), ], ) set_slippage( 2, float(g.config["slippage_bps"]) / 10000.0, ) g.last_week = None 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") ): 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) def _series_close(frame): if frame is None: return [] if hasattr(frame, "__getitem__"): try: values = frame["close"] if hasattr(values, "tolist"): values = values.tolist() return list(values) except (KeyError, TypeError): pass if isinstance(frame, dict): values = frame.get("close", []) return list(values) return [] def _decision_universe(ContextInfo, membership_timetag): config = g.config if config.get("universe_mode") == "fixed": values = list(config["universe"]) else: function = getattr( ContextInfo, "get_stock_list_in_sector", None, ) if not callable(function): raise PriceHistoryUnavailable( "historical sector membership API is unavailable" ) values = function( config["qmt_sector_name"], int(membership_timetag), ) if not values: raise PriceHistoryUnavailable( "no PIT index members for %s on %s" % (config["index_symbol"], membership_timetag) ) normalized = sorted( set(convert_symbol(symbol, "qmt") for symbol in values) ) if not normalized: raise PriceHistoryUnavailable("decision universe is empty") g.last_universe = list(normalized) g.last_universe_as_of = int(membership_timetag) return normalized def _history(ContextInfo, end_time, universe): config = g.config count = int(config["lookback"]) + int(config["skip"]) + 1 raw = ContextInfo.get_market_data_ex( fields=["close"], stock_code=universe, period="1d", start_time="", end_time=end_time, count=count, dividend_type="front_ratio", fill_data=False, subscribe=False, ) output = {} for symbol in universe: values = _series_close(raw.get(symbol)) if len(values) != count: raise PriceHistoryUnavailable( "%s needs %d completed daily closes, got %d" % (symbol, count, len(values)) ) clean = [] for value in values: try: number = float(value) except (TypeError, ValueError): raise PriceHistoryUnavailable( "%s contains a missing/non-numeric close" % symbol ) if not math.isfinite(number) or number <= 0.0: raise PriceHistoryUnavailable( "%s contains a missing/non-positive close" % symbol ) clean.append(number) output[convert_symbol(symbol, "canonical")] = clean return output def _filter_st_universe(ContextInfo, universe, decision_date): if not g.config.get("exclude_st", True): return list(universe) function = getattr(ContextInfo, "get_his_st_data", None) if not callable(function): raise PriceHistoryUnavailable( "historical ST API is unavailable; QMT VIP ST data is required" ) output = [] for symbol in universe: raw = function(symbol) if not isinstance(raw, dict): raise PriceHistoryUnavailable( "%s historical ST query returned an invalid payload" % symbol ) excluded = False for status in ("ST", "*ST", "PT"): periods = raw.get(status, []) if not isinstance(periods, (list, tuple)): raise PriceHistoryUnavailable( "%s historical %s periods are invalid" % (symbol, status) ) for period in periods: if ( not isinstance(period, (list, tuple)) or len(period) != 2 ): raise PriceHistoryUnavailable( "%s historical %s period is invalid" % (symbol, status) ) start = str(period[0]) end = str(period[1]) if ( len(start) != 8 or len(end) != 8 or not start.isdigit() or not end.isdigit() or start > end ): raise PriceHistoryUnavailable( "%s historical %s date range is invalid" % (symbol, status) ) if start <= decision_date <= end: excluded = True if not excluded: output.append(symbol) if not output: raise PriceHistoryUnavailable( "PIT ST exclusion removed the entire decision universe" ) g.last_universe = list(output) return output def _get_attr(obj, names, default=0): for name in names: if hasattr(obj, name): value = getattr(obj, name) if value is not None: return value return default def _trade_details(ContextInfo, kind): account_id = g.config["account_id"] if hasattr(ContextInfo, "get_trade_detail_data"): return ContextInfo.get_trade_detail_data(account_id, "STOCK", kind) fn = globals().get("get_trade_detail_data") if fn is None: return [] return fn(account_id, "STOCK", kind) def _portfolio(ContextInfo, managed): current = {} sellable = {} managed = set( convert_symbol(symbol, "canonical") for symbol in managed ) 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"], "") ) market = str(_get_attr(position, ["market", "m_strExchangeID"], "")) if "." not in code and market: code = code + "." + market if not code: continue canonical = convert_symbol(code, "canonical") quantity = int( _get_attr(position, ["volume", "m_nVolume", "total_amount"], 0) ) if quantity and canonical not in managed and not dynamic: raise UnmanagedPositionError( "account contains unmanaged position %s; use a dedicated " "strategy account or reconcile ownership before trading" % canonical ) if canonical not in managed and not dynamic: continue current[canonical] = quantity sellable[canonical] = int( _get_attr( position, ["can_use_volume", "m_nCanUseVolume", "closeable_amount"], current[canonical], ) ) equity = 0.0 assets = _trade_details(ContextInfo, "account") or [] if assets: equity = float( _get_attr( assets[0], ["total_asset", "m_dBalance", "m_dTotalAsset", "asset"], 0.0, ) ) if equity <= 0: equity = float(_param(ContextInfo, "asset", 0.0) or 0.0) return current, sellable, equity 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( ContextInfo.get_bar_timetag(ContextInfo.barpos) ) end_time = bar_time.strftime("%Y%m%d") universe = _decision_universe(ContextInfo, membership_timetag) universe = _filter_st_universe( ContextInfo, universe, end_time, ) history = _history(ContextInfo, end_time, universe) current, sellable, equity = _portfolio(ContextInfo, universe) if equity <= 0: raise ValueError("QMT account equity must be positive") config = g.config return build_rebalance_plan( price_history=history, current=current, sellable=sellable, equity=equity, lookback=config["lookback"], skip=config["skip"], top_n=config["top_n"], max_weight=config["max_weight"], gross_target=config["gross_target"], cash_buffer=config["cash_buffer"], lot_size=config["lot_size"], ) 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 # idempotency or a complete fresh reconciliation barrier. Real/shadow # broker integration belongs behind the external XtTrader adapter. return _is_backtest(ContextInfo) def _is_backtest(ContextInfo): if hasattr(ContextInfo, "do_back_test"): value = getattr(ContextInfo, "do_back_test") if callable(value): value = value() if value is True: return True mode = str( getattr(ContextInfo, "trade_mode", _param(ContextInfo, "trade_mode", "")) ).strip().lower() return mode == "backtest" def _submit_delta(ContextInfo, canonical, delta, week_key): config = g.config order_code = convert_symbol(canonical, "qmt") operation = 23 if delta > 0 else 24 volume = abs(int(delta)) user_order_id = "q60-%04d%02d-%s" % ( int(week_key[0]), int(week_key[1]), order_code.split(".")[0], ) function = globals()["passorder"] arg_count = getattr(getattr(function, "__code__", None), "co_argcount", 11) if arg_count <= 8: # qmttools native-Python compatibility signature. function( operation, 1101, config["account_id"], order_code, 5, -1, volume, ContextInfo, ) else: # QMT built-in hosted signature. function( operation, 1101, config["account_id"], order_code, 5, -1, volume, "quant60", 0, user_order_id, ContextInfo, ) 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) if g.last_week is None: # The strategy may be attached to a run in the middle of a week. # Treat the first observed week as an initialization window; only a # later ISO-week transition proves that this is the first observed # trading session of a complete strategy week. g.last_week = week_key return None if g.last_week == week_key: return None plan = compute_plan(ContextInfo, bar_time) g.last_week = week_key g.last_plan = plan if not _orders_allowed(ContextInfo): return plan # Sells before buys. passorder is the QMT-hosted order boundary. ordered = sorted(plan["orders"].items(), key=lambda item: item[1]) for canonical, delta in ordered: if int(delta) != 0: _submit_delta(ContextInfo, canonical, delta, week_key) return plan