feat: preserve Quant OS target-package vertical slice
This commit is contained in:
@@ -2,7 +2,14 @@
|
||||
"""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
|
||||
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
|
||||
@@ -34,6 +41,9 @@ 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",
|
||||
@@ -63,6 +73,76 @@ CONFIG = {
|
||||
}
|
||||
# __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", {})
|
||||
@@ -84,22 +164,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):
|
||||
@@ -129,6 +212,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")
|
||||
@@ -136,6 +220,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)
|
||||
@@ -312,7 +406,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"], "")
|
||||
@@ -358,6 +455,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(
|
||||
@@ -390,6 +489,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
|
||||
@@ -452,12 +681,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)
|
||||
|
||||
Reference in New Issue
Block a user