feat: add Quant OS A-share baseline

This commit is contained in:
2026-07-26 12:54:04 +08:00
commit 48c5f64bbd
98 changed files with 31874 additions and 0 deletions
+485
View File
@@ -0,0 +1,485 @@
# 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