# coding: gbk """QMT built-in Python 3.6 strategy wrapper. Keep this source ASCII-only.""" # __PORTABLE_CORE_BUNDLE_START__ from quant60.portable_core import ( build_rebalance_plan, build_target_weight_plan, convert_symbol, lookup_target_package_exact, target_package_tape_sha256, 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__ 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", "dedicated_account_required": True, "exclude_st": True, "universe": [ "600000.SH", "000001.SZ", "300750.SZ", "000333.SZ", ], "lookback": 20, # T close is the signal clock; quickTrade=0 executes on the next step. "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": 0.00001, "rebalance_schedule": "weekly_first_close", } # __BASELINE_CONFIG_END__ # __TARGET_PACKAGE_TAPE_START__ 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", {}) 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