779 lines
24 KiB
Python
779 lines
24 KiB
Python
# 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
|