Applies to: the current executable QuantDinger strategy contract Audience: first-time strategy authors, indicator-conversion users, and developers targeting both backtest and live execution
QuantDinger has one current executable Python strategy contract: Strategy API V2. The same source compiles into a strategy manifest used by backtest and live runtimes for instruments, subscriptions, events, order intents, portfolio accounting, and protection rules.
The source owns its market, instruments, timeframe, schedules, and trading logic. Single-timeframe is the default; a source declares several timeframes only when its logic explicitly requires cross-timeframe confirmation. Run forms provide dates, initial capital, costs, source-permitted leverage, and user parameters; they do not override source-controlled markets, symbols, or timeframes.
Chart indicators are separate artifacts. Their plots, signals, and layers cannot place orders. Convert an indicator into Strategy API V2 before backtesting or deploying it.
"""SPY 20-Day Moving Average
Trades a long-only SPY regime from completed daily bars.
"""
# @param period int 20 Moving-average period range=5:100:5
# @param target_pct float 0.95 Target portfolio weight range=0.1:1.0:0.05
def initialize(context):
g.symbol = "USStock:SPY"
context.set_universe([g.symbol])
context.subscribe(
frequency="1d",
fields=["open", "high", "low", "close", "volume"],
)
context.set_warmup(120)
context.set_benchmark("USStock:SPY")
def handle_data(context, data):
period = int(context.params.get("period", 20))
target_pct = float(context.params.get("target_pct", 0.95))
bars = get_history(
period + 1,
"1d",
"close",
g.symbol,
)
if len(bars) < period:
return
price = float(bars["close"].iloc[-1])
average = float(bars["close"].tail(period).mean())
position = get_position(g.symbol)
desired = target_pct if price > average else 0.0
if desired > 0 and position.amount <= 0:
order_target_percent(
g.symbol,
desired,
reason="ma_long_entry",
stop_loss_pct=0.05,
)
elif desired == 0 and position.amount > 0:
order_target_percent(
g.symbol,
0.0,
reason="ma_long_exit",
)Workflow:
- Create a script in the Strategy IDE and paste the source.
- Save the source.
- Verify it and inspect the compiled manifest.
- Choose dates, capital, commission, slippage, and parameters.
- Inspect executions, closed trades, the order ledger, equity, and holdings.
- Create a deployment only after the backtest behaves as intended. New deployments start stopped.
Hard compiler requirements:
- Source is non-empty and executes in the safe sandbox.
initialize(context)exists.initializedeclares a static universe, index, or named pool throughcontext.set_universe(...).- If no subscription is declared, the compiler adds a default daily subscription; this guide still recommends an explicit
context.subscribe. - The source exposes
handle_data,on_rebalance, or at least one registered schedule callback. - Leveraged strategies satisfy the Crypto-swap-only policy.
The project authoring standard additionally requires:
- Start with a triple-quoted docstring. Its first line is the strategy name; following lines describe universe, signals, schedule, and risk.
- Use English identifiers and source comments.
- Use stable, auditable parameter and reason names.
- Avoid look-ahead, implicit reversals, unbounded scaling, and uncapped exposure.
initialize runs during compilation/manifest discovery. Use it for declarations and initial g state. Do not request market data, inspect real positions, or place orders there.
Important compiler-facing API rules:
context.paramsis not available insideinitialize; read it from handlers or scheduled callbacks.get_historyis count-first and usesfield:get_history(count, frequency, field, symbol). Do not pass afields=keyword to it.data.historyis a separate API:data.history(symbols, count, fields).- Single-instrument history returns a DataFrame; multi-instrument history returns a dictionary of DataFrames.
get_positionreturns a Position object, not a dictionary.- A pandas DataFrame or Series cannot be used directly as a Boolean condition. Use
len(...),.empty,.any(), or.all(). - Undefined platform APIs and unsupported arguments are rejected during verification instead of failing later in a live session.
Compilation discovers:
- API version and source hash;
- CTA or portfolio classification;
- static or dynamic universe;
- subscribed instruments, all timeframes, the driving timeframe, and fields;
- schedules;
- benchmark;
- lifecycle handlers;
- factor and fundamental dependencies;
- warm-up bars;
- leverage permission and maximum;
- custom metadata.
Verification endpoint:
POST /api/strategies/verify
Content-Type: application/json
{"code": "...complete Strategy API V2 source..."}A valid response contains valid: true and the manifest. Verify the final saved source before deployment, not only an earlier draft.
| Market | Example |
|---|---|
| China A-share | CNStock:600519.SH |
| US equity | USStock:MSFT |
| Hong Kong equity | HKStock:00700.HK |
| Crypto spot | Crypto:BTC/USDT@spot |
| Venue-specific Crypto spot | Crypto:BTC/USDT@okx:spot |
| Crypto perpetual | Crypto:BTC/USDT@swap |
| Venue-specific perpetual | Crypto:BTC/USDT@okx:swap |
| Forex | Forex:EUR/USD |
| Futures | Futures:ES |
| MOEX | MOEX:SBER |
The parser also normalizes selected aliases, such as 600519.XSHG to CNStock:600519.SH and BTCUSDT to BTC/USDT.
Production strategies should use the full market prefix. Crypto defaults to spot when no market type is present. Only swap instruments can permit contract leverage. Parsing a market name does not by itself guarantee data coverage or live-trading support; see the live venue matrix in Section 18.
Static single instrument:
context.set_universe(["USStock:SPY"])Static basket:
context.set_universe([
"USStock:AAPL",
"USStock:MSFT",
"USStock:NVDA",
])Index universe:
context.set_universe(index="INDEX:SP500")
members = get_index_stocks("INDEX:SP500")Named platform pool:
context.set_universe(pool="sp500")
members = get_universe_stocks()Dynamic universes resolve point-in-time constituents. Do not copy today's pool members into source and then use them for a historical backtest.
A dynamic universe, more than one static instrument, or on_rebalance normally classifies the manifest as portfolio. One static instrument normally classifies it as CTA.
context.subscribe(
frequency="1d",
fields=["open", "high", "low", "close", "volume"],
)
context.set_warmup(260)
context.set_benchmark("USStock:SPY")Rules:
- Frequency belongs in source, for example
1m,5m,1h,4h,1d, or1w. - Aliases such as
daily,day, anddnormalize to1d. - Omitting symbols subscribes the current universe.
set_warmupasks the data service for history before the requested backtest start. It does not remove the need forlen(bars)guards.- A benchmark is for comparison; it is not traded automatically.
- A source may declare at most eight distinct timeframes. Native timeframes are
1m,3m,5m,15m,30m,1h,4h,1d, and1w. - Five-, fifteen-, and thirty-minute bars can be combined with hourly, daily, and weekly bars in one source. Use lowercase
1wfor weekly data. Monthly bars are not currently a native Strategy API V2 timeframe.
Multi-timeframe data is an optional capability, not a mode that every strategy or robot must use. A normal strategy declares and reads one timeframe by default; subscriptions are added only when the user or existing source explicitly requires cross-timeframe validation. Grid, DCA, and other robots that do not depend on cross-timeframe signals do not gain extra periods merely because the capability exists.
A strategy may subscribe to and read several independently loaded timeframes. The runtime does not build higher bars ad hoc from the driving frame. This example treats a completed one-minute golden cross as the entry event and confirms it against the bullish alignment of completed hourly bars:
def initialize(context):
g.symbol = "Crypto:BTC/USDT@swap"
context.set_universe([g.symbol])
context.subscribe(frequency="1m")
context.subscribe(frequency="1h")
context.set_warmup(62)
def handle_data(context, data):
bars_1m = get_history(32, "1m", "close", g.symbol)
bars_1h = get_history(52, "1h", "close", g.symbol)
if len(bars_1m) < 31 or len(bars_1h) < 50:
return
close_1m = bars_1m["close"]
fast_now = float(close_1m.tail(10).mean())
fast_prev = float(close_1m.iloc[:-1].tail(10).mean())
slow_now = float(close_1m.tail(30).mean())
slow_prev = float(close_1m.iloc[:-1].tail(30).mean())
golden_cross = fast_prev <= slow_prev and fast_now > slow_now
death_cross = fast_prev >= slow_prev and fast_now < slow_now
hourly_bullish = float(bars_1h["close"].tail(20).mean()) > float(
bars_1h["close"].tail(50).mean()
)
amount = float(get_position(g.symbol).amount or 0.0)
if amount <= 0 and golden_cross and hourly_bullish:
order_target_percent(g.symbol, 0.95, reason="one_minute_cross_hourly_confirmed")
elif amount > 0 and (death_cross or not hourly_bullish):
order_target_percent(g.symbol, 0.0, reason="cross_or_hourly_filter_exit")Multi-timeframe runtime rules:
- The fastest subscribed timeframe becomes
drivingFrequency. The example runshandle_dataonce per completed one-minute bar; declaration order does not change the driving timeframe. get_history(..., frequency, ...)routes to that timeframe's independent frame. Requesting an unsubscribed timeframe raisesstrategyV2.frequencyNotSubscribedinstead of silently falling back.- A higher-timeframe bar becomes visible only when its close is no later than the driving bar's close. For example, a four-hour bar opened at 08:00 cannot affect a signal until 12:00.
set_warmup(n)requests an appropriate lookback for every subscribed timeframe. The source must still guard the length of every returned DataFrame.- The requested backtest window must satisfy every timeframe's provider limit. An instrument missing any required timeframe is not run with a partial bundle.
- Provider history limits still apply independently. Intraday feeds often retain less history than daily or weekly feeds, so a
5m + 1wdesign may need a shorter backtest range even though both subscriptions are valid. Crypto venues, stock feeds, and forex feeds may expose different historical depths. - Manifest
primaryFrequencyremains the first declaration for compatibility. Scheduling, execution, and performance annualization usedrivingFrequency. data.history(..., frequency="4h"),data.current(..., frequency="4h"),indicator(..., frequency="4h"), andfactor(..., frequency="4h")also accept an explicit timeframe.data[symbol]uses the driving timeframe.
When cross-timeframe confirmation is explicitly requested, a design can filter trend on the higher timeframe, confirm entries on the driving timeframe, and keep orders idempotent. Because handle_data runs on the fastest timeframe, a persistent higher-timeframe bullish state must not become an unconditional scale-in on every lower-timeframe bar. “Higher timeframe is bullish” and “higher timeframe has just crossed bullish” are different conditions; strategy code and AI generation must preserve the user's intended meaning.
AI-generated strategy source follows the same contract: when a request names one timeframe, it must remain single-timeframe and the AI must not invent confirmation periods. When a request names several timeframes, generation and repair must preserve every requested subscription rather than selecting one, resampling the lowest frame, or rewriting all history reads to the driver.
Supported handlers:
def initialize(context):
pass
def before_trading_start(context, data):
pass
def handle_data(context, data):
pass
def on_rebalance(context, panel):
pass
def after_trading_end(context, data):
passSchedule registration:
def initialize(context):
context.set_universe(["USStock:SPY"])
context.subscribe(frequency="5m")
run_daily(rebalance, time="09:35")
run_weekly(weekly_review, weekday=1, time="09:40")
run_monthly(monthly_rebalance, monthday=1, time="09:45")Rules:
weekdayis 1–7, with Monday as 1.- A monthday past the end of a month resolves to that month's last day.
- On daily or lower-frequency bars, a specific intraday time does not create a nonexistent bar.
- Prefer
callback(context, data); the runtime also adapts callbacks that accept only context. - A portfolio strategy with no registered schedules invokes
on_rebalance. - Backtests invoke
before_trading_startandafter_trading_endon every event timestamp. Live sessions invokebefore_trading_startonly when a newly processed bar enters a new calendar date; the current live runtime does not invokeafter_trading_end. Put live-critical close logic inhandle_dataor a schedule.
Schedule time is interpreted in the live user's configured timezone. If no user timezone is available, the server TZ setting is used, then UTC. Set the user's timezone explicitly and test schedules against exchange sessions. Backtest timestamps follow the supplied market-data clock, so verify timezone alignment before relying on an exact intraday time.
Backtests expose only point-in-time-visible data:
- At a new bar, orders queued after the previous close execute first, using the current open.
before_trading_startand due schedule callbacks see data only through the previous bar; their orders can be processed at the current open.- The current completed bar becomes visible and
handle_dataruns. - Orders emitted by
handle_datawait for the next bar open. after_trading_endalso sees the current bar; its new orders wait for the next bar.
This implements “confirm on close, fill at next open” without future leakage. Never use negative shifts or future rows to move execution earlier.
Live sessions process each closed bar once and preserve g state while the session remains alive. Receiving the same bar twice should not duplicate strategy work. Cross-restart state is opt-in as described in Section 9.
- The final row exposed by
get_history,data.current,data.history, and indicator functions must be a completed bar. A real-time trade or mark price must not rewrite, overwrite, or extend that bar's OHLC. - Moving averages, breakouts, patterns, factors, and all other entry or scale-in signals must use completed bars so backtests, live runs, and restart replay share the same semantics.
- Real-time prices are reserved for stop loss, take profit, trailing protection, and equity risk. They must not make an unfinished candle look complete or revise a confirmed strategy signal.
- Multi-timeframe strategies apply the completed-bar rule separately to every timeframe. A lower-timeframe advance never exposes a still-forming four-hour or daily bar.
- A future tick-driven strategy product needs a separate API, replay model, and contract. Do not simulate it inside ordinary Strategy API V2 source.
Common context fields:
| Field | Meaning |
|---|---|
context.params |
run parameters |
context.current_dt |
current event timestamp |
context.previous_trading_date |
previous event timestamp |
context.portfolio.starting_cash |
initial capital |
context.portfolio.available_cash |
available cash |
context.portfolio.total_value |
current equity |
context.portfolio.positions |
current position map |
context.data |
data view |
Use data.current(symbol, field, frequency="1h") for a current visible value, data.history(symbols, count, fields, frequency="4h") for a selected timeframe, and data[symbol] for the driving timeframe's current visible DataFrame.
Persist state across callbacks on g:
def initialize(context):
g.last_signal = ""
g.rebalance_count = 0Do not store strategy state in files, databases, or external module services. g is the per-run user state namespace.
By default, g survives callbacks in the current process but is rebuilt by initialize after a session restart. Opt into runtime-state snapshots when the strategy cannot reconstruct its cycle from positions and order status:
PERSIST_RUNTIME_STATE = TrueThe equivalent deployment parameter is persist_runtime_state=true. When enabled, the runtime snapshots supported g values, the last processed bar, schedule clock, client-order statuses, and last exit reasons. Protection-engine state is restored independently. Keep persisted values JSON-like and still reconcile them with real account positions after restart; a snapshot is not an exchange ledger.
# @param fast_period int 20 Fast moving-average period range=2:100:1
# @param slow_period int 50 Slow moving-average period range=3:250:1
# @param target_pct float 0.95 Target weight values=0.5,0.75,0.95
# @param enabled bool true Enable entriesRead values through context:
fast_period = int(context.params.get("fast_period", 20))
slow_period = int(context.params.get("slow_period", 50))
target_pct = float(context.params.get("target_pct", 0.95))
enabled = bool(context.params.get("enabled", True))Declared defaults and code fallbacks must agree. The parameter panel supplies context.params; the fallback remains the final default when a value is absent.
Symbols, market, timeframe, and leverage permission are source contract fields. Do not disguise them as ordinary run-form overrides.
Single-instrument history:
bars = get_history(
60,
"1d",
["open", "high", "low", "close", "volume"],
"USStock:SPY",
)One instrument returns a DataFrame. Multiple instruments return a dict of canonical instrument keys to DataFrames:
frames = data.history(
["USStock:AAPL", "USStock:MSFT"],
count=30,
fields=["close", "volume"],
)Technical indicators and factors:
rsi_value = factor("rsi", g.symbol, period=14)
macd = indicator("MACD", g.symbol, fastperiod=12, slowperiod=26, signalperiod=9)
scores = get_factors(symbols, ["momentum_20", "volatility_20"])Fundamentals:
fundamentals = get_fundamentals(
["PE", "PB", "ROE", "MARKET_CAP"],
symbols,
)Other public aliases include REVENUE_GROWTH, DEBT_TO_EQUITY, and FREE_CASH_FLOW. Use only real point-in-time fields supported by the platform; do not invent fields or read future reports.
Pass a symbol to factor/indicator in a multi-asset strategy. The symbol may be omitted only when the data portal has exactly one instrument.
Read positions:
position = get_position(g.symbol)
all_positions = get_positions()Common Position fields:
symbolamountavg_costlast_pricemarket_valueposition_side
In a hedge-mode swap strategy, read each leg explicitly:
long_position = get_position(g.symbol, position_side="long")
short_position = get_position(g.symbol, position_side="short")Do not treat get_position(symbol) as a synthetic net position in hedge mode. get_positions() may contain leg-aware keys such as symbol::long and symbol::short. Use abs(position.amount) when checking whether a leg is open.
Do not confuse these definitions from different layers:
| Name | Layer | Meaning |
|---|---|---|
direction_mode |
strategy manifest | allowed capability: long_only, short_only, both, or neutral |
position_side |
position/order | long or short leg in swap hedge mode; spot has long inventory only |
| order value/target | strategy source | requested quantity, value, or weight change/target; short targets are negative in source |
open/add/reduce/close |
runtime order intent | canonical action derived from synchronized position and target delta; submitted quantity is absolute |
execution_mode |
deployment | signal emits signals, while live submits real orders |
coexistence_mode |
account ownership | strict or advanced manual/strategy inventory policy; it is not trade direction |
Order functions:
| Function | Meaning |
|---|---|
order(symbol, amount) |
add/subtract a quantity |
order_value(symbol, value) |
add/subtract quote-currency value |
order_target(symbol, amount) |
set a target quantity |
order_target_value(symbol, value) |
set a target quote value |
order_target_percent(symbol, percent) |
set a target share of portfolio equity |
Target APIs are usually best for repeatable rebalancing. Give every order a stable reason:
order_target_percent(
g.symbol,
0.5,
reason="breakout_long_entry",
)Common order options:
| Option | Meaning |
|---|---|
reason |
stable audit reason |
position_side |
long or short leg for swap hedge mode |
client_order_id |
stable idempotency and status reference, at most 100 characters |
order_type |
market or limit |
limit_price |
required positive price for a limit order |
execution_algo |
market, limit, or maker_then_market |
maker_wait_sec |
maker wait before market fallback |
maker_offset_bps |
maker-price offset in basis points |
Example with a stable client reference:
def submit_entry():
g.entry_ref = order(
g.symbol,
1,
position_side="long",
order_type="limit",
limit_price=100.0,
client_order_id="breakout-long-20250102",
reason="breakout_long_entry",
)
def monitor_entry(cancel_requested):
status = get_order_status(g.entry_ref)
working = ("queued", "deferred", "submitted", "open", "partial")
if cancel_requested and status["status"] in working:
cancel_order(g.entry_ref)In new strategies, every order that may be retried, cancelled, reconciled, or used to advance a cycle must supply a stable client_order_id. The order helper returns that ID for get_order_status. Legacy source without an explicit ID may still receive None, but that is migration behavior rather than the new-strategy contract.
Typical working states are unknown, queued, deferred, submitted, open, and partial. Terminal states include filled, rejected, failed, cancelled/canceled, and expired. partial is not a full fill and must not advance state by the planned quantity. A terminal order status and synchronized exchange position can become visible at slightly different times; confirm both before reusing capital, beginning a new cycle, or opening the opposite leg. Live cancellation is also asynchronous. consume_last_exit_reason(symbol) returns and clears the most recently recorded protection exit reason.
Write spot and all non-Crypto markets as long-only under the current product policy. A long exit and a short entry are independent; do not turn a zero target into a negative position automatically.
The engine accounts for commission, slippage, lot size, liquidity caps, price limits, and suspensions. Deferred and rejected requests appear in the order audit ledger. “No fill” does not necessarily mean “no signal.”
In live execution, an active same-leg request suppresses duplicate requests until reconciliation resolves it. A target that crosses through zero is executed close-first: the runtime closes the current leg, waits for confirmed reconciliation, then opens the opposite leg. Strategy state must advance from confirmed order status or synchronized positions, not merely because an order function was called.
Attach protection to an entry:
order_target_percent(
g.symbol,
0.8,
reason="breakout_long_entry",
stop_loss_pct=0.03,
take_profit_pct=0.08,
trailing_stop_pct=0.025,
trailing_activation_pct=0.02,
time_limit_seconds=86400 * 10,
)Or set defaults for later entries:
set_default_protection(
stop_loss_pct=0.03,
take_profit_pct=0.08,
)Percentage fields are ratios: 0.03 means 3%. Values are clamped to safe ranges, and negatives become zero.
Backtest behavior:
- A gap through a protection threshold fills at the available bar open.
- An intrabar touch fills at the trigger price.
- If several protections trigger in one bar, conservative mode prioritizes stop-loss, trailing stop, time limit, then take-profit.
Live execution checks the same protection semantics on an independent price clock instead of waiting for the next strategy bar. Protection state can be persisted and restored after a session restart.
Only a static universe consisting entirely of Crypto swap instruments may declare:
def initialize(context):
g.symbol = "Crypto:BTC/USDT@okx:swap"
context.set_universe([g.symbol])
context.subscribe(frequency="1h")
context.allow_leverage(max_leverage=5)Rules:
- Do not call
allow_leveragefor Crypto spot, equities, index/pool universes, or non-Crypto markets. - Dynamic universes cannot enable contract leverage.
- Backtest/deployment leverage cannot exceed the source maximum.
- A run form cannot force leverage on when the source has not permitted it.
- The runtime applies the selected leverage; do not multiply order sizing by leverage again.
- Shorting belongs only in swap strategies and requires independent short-entry, short-exit, and risk rules.
New Crypto swap strategies should declare their capability in initialize:
context.set_metadata(direction_mode="both")Supported values are long_only, short_only, both, and neutral. This declaration does not place orders or override strategy signals. It lets deployment validation reserve the correct hedge-mode leg or legs and reject new entry signals that exceed the declared capability. both and neutral require hedge mode for live execution.
Every new Crypto swap strategy must declare direction_mode and pass an explicit position_side on each contract-position read and order call. Compiler inference from legacy DIRECTION = 1/-1 constants or literal legs exists only for migration; it is not the recommended contract and must not be used by new templates. Write spot strategies as long_only.
The following example keeps a one-contract long core and independently enables a one-contract short hedge below the moving average:
"""BTC Long Core With Short Hedge
Maintains independent long and short swap legs in exchange hedge mode.
"""
def initialize(context):
g.symbol = "Crypto:BTC/USDT@okx:swap"
context.set_universe([g.symbol])
context.subscribe(frequency="1h")
context.set_warmup(60)
context.allow_leverage(max_leverage=5)
context.set_metadata(direction_mode="both")
def handle_data(context, data):
bars = get_history(51, "1h", "close", g.symbol)
if len(bars) < 51:
return
price = float(bars["close"].iloc[-1])
average = float(bars["close"].tail(50).mean())
long_position = get_position(g.symbol, position_side="long")
short_position = get_position(g.symbol, position_side="short")
if abs(float(long_position.amount or 0.0)) < 0.5:
order_target(
g.symbol,
1,
position_side="long",
reason="core_long",
)
hedge_required = price < average
if hedge_required and abs(float(short_position.amount or 0.0)) < 0.5:
order_target(
g.symbol,
-1,
position_side="short",
reason="open_short_hedge",
)
elif not hedge_required and abs(float(short_position.amount or 0.0)) >= 0.5:
order_target(
g.symbol,
0,
position_side="short",
reason="close_short_hedge",
)The quantity unit follows the venue instrument specification; do not assume one contract always equals one base coin. Before live start, the platform confirms the account position mode. both and neutral fail closed when hedge mode cannot be confirmed. A running strategy reserves its account/exchange/market/symbol leg; overlapping ownership raises strategyV2.liveLegConflict. In confirmed hedge mode, separate long-only and short-only strategies may own opposite legs, but a strategy declaring both or neutral owns both legs.
Never maintain authoritative quantities only in g.long_qty/g.short_qty. An order can be rejected, deferred, partially filled, or rounded by venue rules. Read synchronized leg positions and order status before updating cycle state.
"""Dual EMA Long Trend
Trades a long-only daily SPY trend with a protected entry and next-open fills.
"""
# @param fast_period int 20 Fast EMA period range=5:80:5
# @param slow_period int 50 Slow EMA period range=20:250:10
# @param target_pct float 0.95 Target portfolio weight range=0.1:1.0:0.05
# @param stop_loss_pct float 0.05 Entry stop-loss ratio range=0.01:0.15:0.01
def initialize(context):
g.symbol = "USStock:SPY"
context.set_universe([g.symbol])
context.subscribe(frequency="1d")
context.set_warmup(300)
context.set_benchmark("USStock:SPY")
def handle_data(context, data):
fast_period = int(context.params.get("fast_period", 20))
slow_period = int(context.params.get("slow_period", 50))
target_pct = float(context.params.get("target_pct", 0.95))
stop_loss_pct = float(context.params.get("stop_loss_pct", 0.05))
if fast_period >= slow_period:
log.warning("fast_period must be smaller than slow_period")
return
bars = get_history(
slow_period + 2,
"1d",
"close",
g.symbol,
)
if len(bars) < slow_period + 1:
return
close = bars["close"]
fast_now = float(close.ewm(span=fast_period, adjust=False).mean().iloc[-1])
slow_now = float(close.ewm(span=slow_period, adjust=False).mean().iloc[-1])
position = get_position(g.symbol)
if fast_now > slow_now and position.amount <= 0:
order_target_percent(
g.symbol,
target_pct,
reason="dual_ema_long_entry",
stop_loss_pct=stop_loss_pct,
)
elif fast_now < slow_now and position.amount > 0:
order_target_percent(
g.symbol,
0.0,
reason="dual_ema_long_exit",
)Why it is structured this way:
- Universe, frequency, and benchmark live in source.
- Warm-up covers the slow EMA, while runtime length is still checked.
- Invalid fast/slow combinations stop the current event.
- Entry and exit are exclusive; the bearish condition exits a long but does not short.
- A completed daily bar emits an order for the next open.
- Protection is attached to the entry; the exit targets zero.
"""S&P 500 Momentum Basket
Selects the strongest five point-in-time pool members and rebalances weekly.
"""
# @param holdings int 5 Number of holdings range=3:20:1
# @param max_weight float 0.18 Maximum weight per holding range=0.05:0.3:0.01
def initialize(context):
context.set_universe(pool="sp500")
context.subscribe(frequency="1d")
context.set_warmup(80)
context.set_benchmark("USStock:SPY")
run_weekly(rebalance, weekday=1, time="09:35")
def rebalance(context, data):
holdings = int(context.params.get("holdings", 5))
max_weight = float(context.params.get("max_weight", 0.18))
symbols = get_universe_stocks()
if len(symbols) < holdings:
return
scores = get_factors(symbols, "momentum_20")
if scores.empty or "momentum_20" not in scores.columns:
return
ranked = scores["momentum_20"].dropna().sort_values(ascending=False)
selected = list(ranked.head(holdings).index)
if not selected:
return
target_weight = min(max_weight, 0.95 / len(selected))
current = get_positions()
for symbol in current:
if symbol not in selected:
order_target_percent(symbol, 0.0, reason="weekly_remove")
for symbol in selected:
order_target_percent(symbol, target_weight, reason="weekly_select")This strategy class must use point-in-time universe and factor data. Evaluate coverage, survivorship bias, turnover, trading costs, lot sizes, and unfilled orders in addition to headline return.
Core backtest request:
{
"code": "...",
"startDate": "2024-01-01",
"endDate": "2025-12-31",
"initialCapital": 100000,
"commission": 0.0005,
"slippage": 0.0005,
"leverageEnabled": false,
"leverage": 1,
"params": {},
"persist": true
}You may supply sourceId or strategyId to load saved source. The request cannot override source markets, instruments, or frequency.
Inspect:
resultStatus:no_signals,open_position_only, orcompleted_trades.totalExecutions: fill count.totalTrades: closed round-trip count, not fill count.rawTrades/executions: opens, adds, reductions, and closes.closedTrades: completed round trips.orderLedger: fills, deferrals, rejections, and reasons.holdingSnapshots/rebalanceRecords: portfolio evolution.equityCurve, drawdown, win rate, Profit Factor, benchmark, and excess return.dataProvenance/executionAssumptions: data origin and fill model.
- Commission is charged on every fill. A completed round trip deducts both allocated entry commission and exit commission from realized profit.
- Slippage is applied according to the reported execution assumptions.
- Crypto funding payments are currently not modeled in Strategy API V2 backtests. Confirm
executionAssumptions.fundingMode == "not_modeled"; do not compare a leveraged swap backtest directly with live net profit without estimating funding separately. - Live trading uses venue-reported fill fees and, where available, funding/account-ledger records. A fee may be charged in quote, base, or a discount token, so conversion and reconciliation can lag the fill.
- Test a range of commission and slippage assumptions. A strategy whose edge disappears under a small cost increase is not robust.
The backtest center also supports factor research and parameter tuning. Tuning accepts grid or random parameter spaces, caps a request at 500 variants, and reports out-of-sample validation for the selected result. Backtests may consume a system-configured credit amount; a failed execution is refunded automatically. UI request timeouts do not prove the server job failed—check backtest history before submitting a duplicate run.
Zero executions can be valid: insufficient history, conditions never met, poor parameters, missing data, or rejected orders. Read logs and the order ledger before treating it as an engine failure.
Core deployment fields include:
sourceIdnameinitialCapitalexecutionMode:signalorlive- optional
credentialId,params, leverage, position side, and notifications
A new deployment is stopped and must be started explicitly. Stop it before deletion.
Current live-account boundaries:
| Market | Supported live venues | Product boundary |
|---|---|---|
| Crypto | Binance, Bitget, Bybit, OKX, Gate, HTX | spot and swap according to venue/account capability |
| USStock | Alpaca, IBKR | current broker policy is long-only |
| Other parsed markets | none | backtest/data availability does not imply live support |
Mixed-market live deployment is unsupported. Other markets cannot be forced through a mismatched credential.
- A live strategy owns only its allocated strategy position. Manual holdings and positions owned by another strategy are not available for it to close.
- Advanced coexistence supports both Crypto spot and derivatives. Baselines are keyed by credential, market type, canonical symbol, and position leg; spot has long inventory only, while derivatives use long/short legs.
- The reconciliation identity is
account position = strategy allocation + protected manual position + unknown delta. New entries require the unknown delta to remain within tolerance.
| Ownership mode | Protected manual position | Behavior |
|---|---|---|
strict (default) |
always 0 | unallocated account inventory pauses same-side entries/adds; it never auto-closes a position |
advanced |
records account position - strategy position after explicit confirmation |
strategy inventory may coexist with that floor; any later unknown delta pauses entries/adds again |
- Drift pauses same-side opens/adds only. The first state transition logs account, strategy, protected, and unknown quantities; an unchanged blocked state does not spam duplicate logs.
- Grid strategies run the same ownership check on every resting-order sync. Drift cancels unfilled entry orders on that leg. If the account is below its protected allocation or the protection ledger cannot be verified, potentially oversized exits are also cancelled and rebuilt from strategy inventory, existing exits, and the protected floor.
- Closes/reductions remain available but are capped by the strategy ledger, actual exchange inventory, and protected baseline. They can never cross protected manual inventory, and are not a tool for absorbing an unknown delta.
- The Ownership & Repair page exposes
protect_manual(record the current delta and enable advanced mode),strict_mode(clear the floor and return to strict mode), andrecheck(refresh and reconcile). These actions update ownership records only and never trade automatically. - Advanced coexistence is QuantDinger ledger isolation, not physical venue isolation. Spot inventory still shares the account balance; same-side derivatives still share venue entry price, margin, and liquidation risk.
- Same account/exchange/market/symbol/leg ownership is exclusive. Confirmed hedge mode can allow separate long-only and short-only strategies on opposite legs;
both/neutralreserves both. - Minimum quantity, quantity step, minimum notional, available margin, leverage, and venue caps are applied after strategy sizing. The final submitted quantity can differ from the raw request.
- Only derivative opens/adds configure margin mode and leverage. Closes/reductions skip account configuration so a configuration endpoint failure cannot block an exit. After Binance HTTP 408,
-1007, or “execution status unknown,” the runtime reads configuration back and proceeds only when observed margin mode/leverage matches the target. - Optional account-risk limits can reject orders for gross notional, estimated margin, gross leverage, or per-symbol notional. Treat those as risk warnings that require configuration or sizing changes, not as reasons to bypass the guard.
- Market data, private WebSocket events, and periodic REST reconciliation work together. WebSocket improves latency; REST remains the recovery source after disconnects or missed events.
Use signal mode first to validate notifications, signal frequency, and state restoration. A successful backtest does not prove that credentials, balances, venue rules, minimum order sizes, and network health are ready for live trading.
Strategy source runs in a safe execution environment. File, network, database, process, dynamic execution, reflection, and unsafe imports are prohibited. Do not use eval, exec, compile, open, dunder bypasses, or external state.
Allowed import roots are numpy, pandas, math, json, datetime, time, collections, functools, itertools, statistics, decimal, fractions, and copy. File/URL/database methods such as pandas read_/to_, NumPy load/save, pickle-like deserialization, and string-expression evaluators remain blocked even through an allowed module.
| Error | Meaning | Fix |
|---|---|---|
strategyV2.codeRequired |
empty source | submit complete source |
strategyV2.initializeRequired |
initialize missing | add it |
strategyV2.initializeFailed:... |
initialization failed | keep initialize declarative |
strategyV2.universeRequired |
universe missing | call set_universe |
strategyV2.handlerRequired |
no handler/schedule | add a handler or schedule |
strategyV2.leverageCryptoSwapOnly |
invalid leverage market | use static Crypto swaps only |
strategyV2.leverageNotAllowed |
run requests unpermitted leverage | permit it legally or disable it |
strategyV2.leverageExceedsStrategyLimit |
requested leverage too high | lower the request |
strategyV2.dataUnavailable:... |
instrument data unavailable | check canonical symbol and range |
strategyV2.frequencyUnsupported:... |
source declares an unsupported timeframe | use a native timeframe listed in Section 6 |
strategyV2.tooManyFrequencies:8 |
source declares more than eight timeframes | remove unnecessary subscriptions |
strategyV2.frequencyNotSubscribed:... |
source reads an undeclared timeframe | subscribe it in initialize or correct the call |
strategyV2.noMarketData |
live cycle has no usable frame | verify symbol, source, connection, and subscribed timeframe |
strategyV2.initializeParamsUnavailable |
params read during discovery | move the read to a handler |
strategyV2.directionModeViolation:... |
entry exceeds declared direction | fix metadata or signal direction; exits remain allowed |
strategyV2.dualDirectionHedgeModeRequired:... |
account is not in hedge mode | enable venue hedge/dual-side mode |
strategyV2.hedgeModeUnknown:... |
account mode could not be confirmed | repair credential/API access and retry |
strategyV2.liveLegConflict:... |
another live strategy owns the leg | stop/reconfigure the conflicting strategy |
position_drift_detected:... |
account, strategy, and protected baseline contain an unknown delta | recheck, protect manual inventory, or restore strict mode in Ownership & Repair; do not bypass |
unallocated_account_position |
account position exceeds strategy plus protected inventory | verify and protect the delta as manual inventory, or restore equality manually |
account_below_protected_allocation |
account position is below strategy plus protected inventory | stop new entries and reconcile venue, strategy ledger, and baseline |
| invalid amount/minimum notional | rounded quantity cannot be submitted | increase capital/weight or choose a suitable instrument |
| account-risk rejection | configured account exposure limit exceeded | reduce size/leverage or deliberately revise the limit |
strategyV2.runtimeFailed:... |
handler raised | inspect the named handler and cause |
The system preset catalog currently uses system_seed version=11. It contains eight CTA templates (single MA, dual MA, bullish candle through three averages, trend-filtered bullish candle, Turtle, indicator resonance, MACD/KDJ, and SuperTrend) and four portfolio templates (market-cap barbell, momentum Top N, low volatility, and quality growth). Presets are examples and the executable baseline for the current recommended Strategy API V2 contract.
Every system preset must satisfy these rules:
- Declare
direction_modeexplicitly; Crypto swap templates read and write explicitposition_sidelegs. - A bidirectional trend template closes the opposite leg, waits for fill and position synchronization, and only then opens the target leg. It must not replace two hedge legs with one net-position variable.
- Reconstructible state comes from synchronized
amount,avg_cost, and order status. State that cannot be rebuilt reliably must enablePERSIST_RUNTIME_STATE. - Every catalog revision must pass parameter-contract, compilation, direction-capability, and synthetic-backtest tests. After copying a preset, revalidate the manifest whenever market, direction, or frequency changes.
Robot templates generate editable Strategy API V2 source. The generated source—not the preview alone—is the deployable contract. Verify it after every manual edit.
Current generated-source contract revisions are GRID_TEMPLATE_VERSION = 6 for grid, DCA_TEMPLATE_VERSION = 6 for DCA, and ROBOT_TEMPLATE_VERSION = 6 for martingale and layered martingale. These constants help diagnose generated source; they do not replace manifest or preflight validation.
| Template | Trigger and sizing | Current boundary |
|---|---|---|
| Grid | range split into arithmetic/geometric cells; each filled entry arms its paired cell exit | live uses resting limit orders; backtest replays OHLC touches |
| DCA | fixed elapsed-minute interval and fixed capital fraction per purchase | Crypto spot, long-only |
| Martingale | adverse-price levels with increasing allocation | capped levels, total budget, and cycle risk required |
| Layered martingale | martingale levels organized into multiple allocation groups | same hard caps plus per-group limits |
Grid rules:
- A grid cell is a lifecycle: entry ready → entry working/filled → paired exit working/filled → next cycle. It is not “buy every lower level and liquidate the whole position at one price.”
max_open_orderslimits simultaneously armed entries. Stableclient_order_idvalues prevent duplicate cell orders.- Live grid execution uses exchange limit orders and fill reconciliation. Backtests use bar high/low touch replay and cannot know the exact intrabar path when several levels are crossed in one bar; use a sufficiently fine timeframe.
- Neutral grids require swap hedge mode and own both legs. Spot grids are long-only.
DCA rules:
- The interval is measured in elapsed minutes, not “number of K-lines.” The handler can only act when a subscribed bar is processed, so an interval shorter than the source timeframe becomes effective on the next available bar.
- Each purchase is capped by both per-order percentage and total cycle budget. Optional price filters, take-profit, hard stop, and trailing protection do not remove the need for a maximum order count.
- A submitted purchase first becomes pending. Only a
filledresult fromget_order_statusincrements the order count and charges actual filled notional plus fees to cycle spend; callingorder_valuedoes not imply a fill. partialremains pending and must not be treated as fully filled.rejected,failed,cancelled/canceled, orexpiredreleases pending state without consuming count or filled budget, and a retry uses a new stable client reference.- Generated DCA source enables
PERSIST_RUNTIME_STATE. After an exit, reset the cycle only when the account position is confirmed flat and an explicit exit reason exists; a short position-sync delay must not start a duplicate cycle.
Martingale rules:
- Each level needs a price trigger, planned allocation, maximum attempts, stable client reference, and confirmed-fill transition.
- Generated martingale and layered-martingale sources enable
PERSIST_RUNTIME_STATE. Preserve the recovery and final-sweep logic when editing them. - A rejected or partial order must not advance a level as if fully filled. After restart, reconcile state with the strategy-owned position before issuing another level or close.
- Martingale is a high-tail-risk sizing method. Always cap total deployed capital, levels, leverage, stop loss, and restart-after-stop behavior.
- The file has an English docstring covering name, universe, signals, schedule, and risk.
-
initializeonly declares universe, subscription, warm-up, benchmark, schedules, leverage permission, and initialg. - Instruments are canonical and Crypto explicitly distinguishes spot/swap.
- The source owns instruments and frequency; no run-form override is assumed.
- Parameter defaults and code fallbacks agree.
- Every history window checks actual length.
- No future rows, negative shifts, or centered rolling.
- Indicators and entry/scale-in signals use completed bars only; real-time prices do not overwrite the last OHLC and are limited to protection and equity risk.
- Long exits and short entries are independent.
- Hedge-mode code reads and writes explicit
position_sidelegs. -
direction_mode,position_side,execution_mode, and accountcoexistence_modeare not conflated. - Exposure is capped; grid, DCA, martingale, and scaling layers have hard limits.
- Every order has an auditable reason.
- Retryable/working orders use stable client IDs and do not advance state before confirmation.
- Risk percentages use decimal ratios.
- Leverage is declared only for Crypto swaps and is not multiplied twice.
- Schedule timezone and cross-restart state requirements are explicit.
- The manifest verifies successfully.
- The order ledger is reviewed, not only the equity curve.
- Both entry and exit fees are included; swap funding is evaluated separately from the current backtest.
- Robustness is tested across periods and cost assumptions.
- At least one successful backtest exists before publication.
- Credentials, market, balance, lot size, and notifications are checked before live use.
- Reusing existing spot or derivative inventory includes an explicit strict/advanced ownership choice and verified protected baseline.