# 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, convert_symbol # __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", "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__ 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 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 if ( g.config.get("universe_mode") == "fixed" and hasattr(ContextInfo, "set_universe") ): ContextInfo.set_universe(g.config["universe"]) 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 = 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 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 _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 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) 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