1425 lines
49 KiB
Markdown
1425 lines
49 KiB
Markdown
# QuantDinger v3 Python Strategy Development Guide
|
||
|
||
> **Platform contract (required reading)**: [Signal & Execution Standard v1](./SIGNAL_EXECUTION_STANDARD.md) — backtest/live alignment, two-way vs four-way signals, exit ownership, and release checklist. This guide focuses on tutorials and examples.
|
||
|
||
This guide is written from a **developer** point of view. Its goal is not only to list the current contracts, but to answer the practical question:
|
||
|
||
**How do I build an indicator strategy that is clear, backtestable, and ready to become a saved trading strategy?**
|
||
|
||
QuantDinger currently supports two Python authoring models:
|
||
|
||
- **IndicatorStrategy**: dataframe-based code for indicator research, chart rendering, and signal-style backtests.
|
||
- **ScriptStrategy**: event-driven code for runtime execution, strategy backtests, and live trading.
|
||
|
||
If you are starting a new strategy, the default recommendation is:
|
||
|
||
1. Start with `IndicatorStrategy`.
|
||
2. Prove the signal logic visually and in backtests.
|
||
3. Move to `ScriptStrategy` only if you need bar-by-bar state, dynamic position management, or execution control.
|
||
|
||
---
|
||
|
||
## 1. Start With the Right Mental Model
|
||
|
||
The most common source of confusion is mixing up **signal logic**, **risk defaults**, and **runtime execution**.
|
||
|
||
### 1.1 IndicatorStrategy
|
||
|
||
Think of `IndicatorStrategy` as:
|
||
|
||
- compute indicator series from `df`
|
||
- generate boolean `buy` / `sell` signals
|
||
- declare default strategy settings through metadata comments
|
||
- return chart-friendly `output`
|
||
|
||
This is the best fit for:
|
||
|
||
- indicator research
|
||
- strategy prototyping
|
||
- parameter tuning
|
||
- signal-based backtests
|
||
- saved strategies that still follow a signal-first workflow
|
||
|
||
### 1.2 ScriptStrategy
|
||
|
||
Think of `ScriptStrategy` as:
|
||
|
||
- maintain runtime logic bar by bar
|
||
- inspect current position state through `ctx.position`
|
||
- place explicit actions with `ctx.buy()`, `ctx.sell()`, and `ctx.close_position()`
|
||
- manage exits and sizing in code when needed
|
||
|
||
This is the best fit for:
|
||
|
||
- stateful execution logic
|
||
- dynamic stop-loss or take-profit handling
|
||
- partial exits, scale-ins, cooldowns, or other runtime rules
|
||
- strategies that behave more like trading bots than pure indicators
|
||
|
||
### 1.3 The Most Important Separation
|
||
|
||
For `IndicatorStrategy`, you usually have **three layers**:
|
||
|
||
1. **Indicator layer**: moving averages, RSI, ATR, bands, filters.
|
||
2. **Signal layer**: `df['buy']` and `df['sell']`.
|
||
3. **Risk defaults layer**: `# @strategy stopLossPct ...`, `takeProfitPct`, `entryPct`, and related defaults.
|
||
|
||
Do not mix these into one thing.
|
||
|
||
In particular:
|
||
|
||
- `buy` / `sell` decide **when the strategy wants to enter or exit**
|
||
- `# @strategy` decides **how the engine should size and protect positions by default**
|
||
- leverage belongs in product configuration, not in indicator code
|
||
|
||
---
|
||
|
||
## 2. Which Mode Should You Use?
|
||
|
||
| Use Case | Recommended Mode |
|
||
|----------|------------------|
|
||
| Build indicators, overlays, and signal markers | `IndicatorStrategy` |
|
||
| Research entry and exit rules on a dataframe | `IndicatorStrategy` |
|
||
| Add fixed stop-loss, take-profit, or entry sizing defaults | `IndicatorStrategy` |
|
||
| Need runtime position state and bar-by-bar control | `ScriptStrategy` |
|
||
| Need dynamic exits based on current open position | `ScriptStrategy` |
|
||
| Need partial close, scale-in/out, or bot-like logic | `ScriptStrategy` |
|
||
|
||
Rule of thumb:
|
||
|
||
- If your logic can be described as "when condition A happens, buy; when condition B happens, sell", start with `IndicatorStrategy`.
|
||
- If your logic sounds like "after entry, keep watching the open position and react differently depending on state", you probably need `ScriptStrategy`.
|
||
|
||
---
|
||
|
||
## 3. How To Develop an IndicatorStrategy
|
||
|
||
This is the recommended workflow for most new strategy development.
|
||
|
||
### 3.1 Step 1: Declare metadata and defaults first
|
||
|
||
At the top of the script, define name, description, tunable params, and strategy defaults.
|
||
|
||
```python
|
||
my_indicator_name = "Trend Pullback Strategy"
|
||
my_indicator_description = "Buy pullbacks in an uptrend and exit on weakness."
|
||
|
||
# signal_form: two_way
|
||
# exit_owner: engine
|
||
# flip_mode: R2
|
||
|
||
# @param fast_len int 20 Fast EMA length
|
||
# @param slow_len int 50 Slow EMA length
|
||
# @param rsi_len int 14 RSI length
|
||
# @param rsi_floor float 45 Minimum RSI for long entries
|
||
|
||
# @strategy stopLossPct 0.03
|
||
# @strategy takeProfitPct 0.06
|
||
# @strategy entryPct 0.25
|
||
# @strategy trailingEnabled true
|
||
# @strategy trailingStopPct 0.02
|
||
# @strategy trailingActivationPct 0.04
|
||
# @strategy tradeDirection long
|
||
```
|
||
|
||
Use `# @param` for values the user may tune often.
|
||
|
||
Format:
|
||
|
||
```python
|
||
# @param <name> <int|float|bool|str|string> <default> <description>
|
||
```
|
||
|
||
Best practice:
|
||
|
||
- read declared params through `params.get(...)`
|
||
- `string` is accepted as the same type family as `str`
|
||
- if you declare params but hardcode the values instead, the built-in code quality checker will flag it
|
||
|
||
Use `# @strategy` for strategy defaults such as:
|
||
|
||
- `stopLossPct`: stop-loss ratio, for example `0.03` = **3% adverse underlying price move** (`0.001` = 0.1%)
|
||
- `takeProfitPct`: take-profit ratio, for example `0.06` = **6% favorable price move**
|
||
- `entryPct`: fraction of capital on entry (`1` = **100%**, `0.25` = 25%)
|
||
- `trailingEnabled`
|
||
- `trailingStopPct`
|
||
- `trailingActivationPct`
|
||
- `tradeDirection`: `long`, `short`, or `both`
|
||
|
||
Important:
|
||
|
||
- These are **defaults consumed by the engine**.
|
||
- They are not extra dataframe columns.
|
||
- Do not put `leverage` here.
|
||
- Keep the values realistic and confirm them in backtests; the parser is intentionally more permissive than the toy examples shown here.
|
||
|
||
### 3.2 Step 2: Copy the dataframe and compute indicators
|
||
|
||
Indicator code runs in a sandbox. `pd`, `np`, and a `params` dictionary are already available.
|
||
|
||
Recommended baseline:
|
||
|
||
```python
|
||
df = df.copy()
|
||
```
|
||
|
||
Expected columns usually include:
|
||
|
||
- `open`
|
||
- `high`
|
||
- `low`
|
||
- `close`
|
||
- `volume`
|
||
|
||
A `time` column may exist, but do not rely on a fixed type.
|
||
|
||
Avoid:
|
||
|
||
- network access
|
||
- file I/O
|
||
- subprocesses
|
||
- unsafe metaprogramming such as `eval`, `exec`, `open`, `__import__`, `getattr`, or `setattr`
|
||
- `import operator` (and dunder-bypass patterns such as string-built `__class__` / `__globals__`)
|
||
|
||
Allowed `import` roots (anything else is rejected by validation):
|
||
|
||
`numpy`, `pandas`, `math`, `json`, `datetime`, `time`, `collections`, `functools`, `itertools`, `statistics`, `decimal`, `fractions`, `copy`
|
||
|
||
`pd`, `np`, and `params` are already injected — you usually do not need `import pandas` / `import numpy`.
|
||
|
||
### 3.3 Step 3: Turn raw conditions into clean `buy` / `sell` signals
|
||
|
||
The backtest engine reads **boolean** columns:
|
||
|
||
- `df['buy']`
|
||
- `df['sell']`
|
||
|
||
They should:
|
||
|
||
- match the dataframe length exactly
|
||
- be boolean after `fillna(False)`
|
||
- usually be edge-triggered, unless repeated signals are intentional
|
||
|
||
Recommended pattern:
|
||
|
||
```python
|
||
raw_buy = (ema_fast > ema_slow) & (ema_fast.shift(1) <= ema_slow.shift(1))
|
||
raw_sell = (ema_fast < ema_slow) & (ema_fast.shift(1) >= ema_slow.shift(1))
|
||
|
||
df['buy'] = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
|
||
df['sell'] = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)
|
||
```
|
||
|
||
This keeps your signals from firing on every bar of the same regime.
|
||
|
||
#### 3.3.1 How `tradeDirection` maps `buy` / `sell` at execution time
|
||
|
||
After an indicator is saved as a strategy, the backend normalizes `df['buy']` / `df['sell']` into execution signals. In `both` mode, **do not** treat `buy` as a standalone `close_short` column:
|
||
|
||
| `tradeDirection` | `buy=True` | `sell=True` |
|
||
|------------------|------------|-------------|
|
||
| `long` | `open_long` | `close_long` |
|
||
| `short` | `close_short` | `open_short` |
|
||
| `both` | `open_long`; if currently short, **close short then open long** | `open_short`; if currently long, **close long then open short** |
|
||
|
||
Notes:
|
||
|
||
- `both` matches `BacktestService` `_both_mode`; live execution is aligned with that semantics.
|
||
- Putting short take-profit / stop-loss into `df['buy']` means **exit short and possibly flip long**, not "flat only".
|
||
- For flat-only exits, use `long`/`short`, explicit four-way columns, or `ScriptStrategy` with `ctx.close_position()`.
|
||
|
||
Typical dual-Keltner style wiring:
|
||
|
||
```python
|
||
df['buy'] = sig_buy_entry | sig_short_tp | sig_short_sl
|
||
df['sell'] = sig_sell_entry | sig_long_tp | sig_long_sl
|
||
```
|
||
|
||
If backtests show "close short then open long" on the same bar, that is usually expected under `both`, not a engine bug.
|
||
|
||
### 3.4 Step 4: Decide who owns the exit logic
|
||
|
||
This is where stop-loss, take-profit, and position management usually become confusing.
|
||
|
||
There are **two valid exit styles** in `IndicatorStrategy`, and the header contract must make the choice explicit.
|
||
|
||
#### Style A: Signal-managed exits
|
||
|
||
Your indicator logic itself decides when to exit by setting `df['sell']`.
|
||
|
||
Examples:
|
||
|
||
- moving-average bearish crossover
|
||
- RSI falling below a threshold
|
||
- close dropping below an ATR-based stop line
|
||
- mean reversion target hit
|
||
|
||
Use this style when the exit is part of the strategy idea itself.
|
||
|
||
Declare it like this:
|
||
|
||
```python
|
||
# exit_owner: indicator
|
||
# @strategy trailingEnabled false
|
||
```
|
||
|
||
The current backend interprets `exit_owner: indicator` as: server-side fixed stop-loss, fixed take-profit, and trailing stop do not close positions. Exits come from indicator signals. `entryPct` / `tradeDirection` can still be used as defaults.
|
||
|
||
#### Style B: Engine-managed exits
|
||
|
||
You let the strategy engine apply fixed defaults declared with `# @strategy`, such as:
|
||
|
||
- `stopLossPct`
|
||
- `takeProfitPct`
|
||
- `entryPct`
|
||
- trailing settings
|
||
|
||
Use this style when the signal logic should stay simple, and you want the engine to handle fixed protective rules.
|
||
|
||
Declare it like this:
|
||
|
||
```python
|
||
# exit_owner: engine
|
||
```
|
||
|
||
`exit_owner: engine` keeps server-side price risk active. You may still keep structural reverse `close_*` signals, but do not also encode tight in-indicator TP/SL touch exits.
|
||
|
||
#### Best practice
|
||
|
||
Pick one primary owner for exits whenever possible.
|
||
|
||
For example:
|
||
|
||
- if your edge is "enter on crossover, exit on reverse crossover" with no extra fixed price-risk exits, make indicator signals the owner and write `# exit_owner: indicator`
|
||
- if your edge is "enter on signal and let a fixed 3% stop + 6% target manage the trade", or if `close_*` only represents structural trend reversal, make the engine the price-exit owner and write `# exit_owner: engine`
|
||
|
||
Do not write `exit_owner: layered`. The current platform does not implement a third owner. If you truly need a mixed design, write it as `engine` and document which `close_*` signals are structural reversals rather than tight TP/SL.
|
||
|
||
### 3.5 Step 5: Build the `output` object
|
||
|
||
Your script must assign a final `output` dictionary:
|
||
|
||
```python
|
||
output = {
|
||
"name": "My Strategy",
|
||
"plots": [],
|
||
"signals": []
|
||
}
|
||
```
|
||
|
||
Supported keys:
|
||
|
||
- `name`
|
||
- `plots`
|
||
- `signals`
|
||
- `calculatedVars` as optional metadata
|
||
|
||
Each plot item should contain:
|
||
|
||
- `name`
|
||
- `data` with length exactly `len(df)`
|
||
- `color`
|
||
- `overlay`
|
||
- optional `type`
|
||
|
||
Each signal item should contain:
|
||
|
||
- `type`: `buy` or `sell`
|
||
- `text`
|
||
- `color`
|
||
- `data`: list with `None` on bars without a marker
|
||
|
||
### 3.6 Step 6: Validate backtest semantics
|
||
|
||
Indicator backtests are signal-driven:
|
||
|
||
- the engine reads `df['buy']` and `df['sell']`
|
||
- signals are treated as bar-close confirmation
|
||
- fills are typically on the **next bar open**
|
||
|
||
This matters because:
|
||
|
||
- an intrabar-looking stop drawn on the current candle is not the same as a next-bar-open fill
|
||
- using `shift(-1)` in signal logic introduces look-ahead bias
|
||
|
||
Practical nuance:
|
||
|
||
- the normalized strategy snapshot can execute with either `next_bar_open` or `same_bar_close`
|
||
- most indicator workflows should still be designed with a "confirm on close, usually fill on next open" mental model
|
||
- if product settings change the execution timing, rerun the backtest and review fills instead of assuming the semantics stayed the same
|
||
|
||
#### 3.6.1 Why live entry/exit times can diverge from backtests
|
||
|
||
| Topic | Indicator backtest | Indicator live (default) |
|
||
|-------|-------------------|-------------------------|
|
||
| Bars | Historical **closed** OHLC | Last bar may be updated with the **latest tick** (`high` / `low` / `close`) |
|
||
| Touch conditions | Known only after the bar closes | May fire earlier on the **forming** bar |
|
||
| Signal evaluation | Bar-by-bar on the backtest timeline | Indicator may be recomputed every tick; `exit_signal_mode=immediate` fires exits right away |
|
||
| Orders per tick | Queued in backtest order | Indicator mode usually **one signal per tick** (closes first) |
|
||
|
||
If your logic uses `high >= line` / `low <= line`, backtests are often **later** than live; realtime bar updates can trigger **earlier** exits.
|
||
|
||
Alignment tips:
|
||
|
||
- Set `signal_mode` and `exit_signal_mode` to **`confirmed`** when you want live to track closed-bar backtests.
|
||
- If in-indicator `sig_*_tp` / `sig_*_sl` exits exist, declare `# exit_owner: indicator` and keep `# @strategy trailingEnabled false`; otherwise indicator exits plus server trailing can duplicate closes and amplify timing drift.
|
||
- Before going live, compare backtest fills with logs for `Signal submitted` and `server_trailing_stop`.
|
||
|
||
If a live close briefly shows amount `0`, the worker **re-syncs exchange positions and resolves size again**; rejection happens only when the exchange is already flat.
|
||
|
||
---
|
||
|
||
## 4. How To Write Stop-Loss, Take-Profit, and Position Sizing
|
||
|
||
This section is the practical answer to the most common implementation question.
|
||
|
||
### 4.1 Fixed stop-loss, take-profit, and entry sizing in IndicatorStrategy
|
||
|
||
If you want fixed risk defaults, write them as `# @strategy` lines:
|
||
|
||
```python
|
||
# @strategy stopLossPct 0.03
|
||
# @strategy takeProfitPct 0.06
|
||
# @strategy entryPct 0.25
|
||
# @strategy tradeDirection long
|
||
```
|
||
|
||
Meaning:
|
||
|
||
- `stopLossPct 0.03`: use a 3% stop-loss default
|
||
- `takeProfitPct 0.06`: use a 6% take-profit default
|
||
- `entryPct 0.25`: allocate 25% of capital on entry
|
||
- `tradeDirection long`: long-only by default
|
||
|
||
This is the correct choice when you want:
|
||
|
||
- simple signal code
|
||
- consistent defaults in backtests
|
||
- strategy settings that the UI and engine can understand directly
|
||
|
||
### 4.2 Indicator-driven exits in IndicatorStrategy
|
||
|
||
If your "stop-loss" is actually part of the indicator model, write it as a `sell` signal.
|
||
|
||
Example: exit a long when close falls below an ATR-style stop line.
|
||
|
||
```python
|
||
atr = (df['high'] - df['low']).rolling(14).mean()
|
||
stop_line = df['close'].rolling(20).max() - atr * 2.0
|
||
|
||
raw_sell = df['close'] < stop_line.shift(1)
|
||
df['sell'] = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)
|
||
```
|
||
|
||
In this style:
|
||
|
||
- the exit belongs to your indicator logic
|
||
- the engine is not inventing the stop for you
|
||
- you should explain this in the strategy description or comments
|
||
|
||
### 4.3 How position management works in IndicatorStrategy
|
||
|
||
For indicator strategies, position management is intentionally simple:
|
||
|
||
- use `entryPct` for default entry sizing
|
||
- use `tradeDirection` to limit long, short, or both
|
||
- use engine-managed stop, take-profit, or trailing defaults if they are fixed
|
||
|
||
If you need:
|
||
|
||
- scale-in / scale-out
|
||
- partial exits
|
||
- different logic before and after entry
|
||
- stop movement that depends on the current live position state
|
||
- cooldowns after a stop
|
||
|
||
then the strategy has outgrown `IndicatorStrategy` and should move to `ScriptStrategy`.
|
||
|
||
---
|
||
|
||
## 5. Full IndicatorStrategy Example
|
||
|
||
This example shows a complete developer-oriented pattern: metadata, defaults, indicator calculation, signal generation, and chart output.
|
||
|
||
```python
|
||
my_indicator_name = "EMA Pullback Strategy"
|
||
my_indicator_description = "Buy pullbacks above the slow EMA and exit on trend failure."
|
||
|
||
# @param fast_len int 20 Fast EMA length
|
||
# @param slow_len int 50 Slow EMA length
|
||
# @param rsi_len int 14 RSI length
|
||
# @param rsi_floor float 50 Minimum RSI for entry
|
||
|
||
# @strategy stopLossPct 0.03
|
||
# @strategy takeProfitPct 0.06
|
||
# @strategy entryPct 0.25
|
||
# @strategy tradeDirection long
|
||
|
||
df = df.copy()
|
||
|
||
fast_len = int(params.get('fast_len', 20))
|
||
slow_len = int(params.get('slow_len', 50))
|
||
rsi_len = int(params.get('rsi_len', 14))
|
||
rsi_floor = float(params.get('rsi_floor', 50.0))
|
||
|
||
ema_fast = df['close'].ewm(span=fast_len, adjust=False).mean()
|
||
ema_slow = df['close'].ewm(span=slow_len, adjust=False).mean()
|
||
|
||
delta = df['close'].diff()
|
||
gain = delta.clip(lower=0).ewm(alpha=1 / rsi_len, adjust=False).mean()
|
||
loss = (-delta.clip(upper=0)).ewm(alpha=1 / rsi_len, adjust=False).mean()
|
||
rs = gain / loss.replace(0, np.nan)
|
||
rsi = 100 - (100 / (1 + rs))
|
||
|
||
trend_up = ema_fast > ema_slow
|
||
pullback_done = df['close'] > ema_fast
|
||
rsi_ok = rsi > rsi_floor
|
||
|
||
raw_buy = trend_up & pullback_done & rsi_ok & (~trend_up.shift(1).fillna(False))
|
||
raw_sell = (ema_fast < ema_slow) | (rsi < 45)
|
||
|
||
buy = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
|
||
sell = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)
|
||
|
||
df['buy'] = buy
|
||
df['sell'] = sell
|
||
|
||
buy_marks = [df['low'].iloc[i] * 0.995 if buy.iloc[i] else None for i in range(len(df))]
|
||
sell_marks = [df['high'].iloc[i] * 1.005 if sell.iloc[i] else None for i in range(len(df))]
|
||
|
||
output = {
|
||
"name": my_indicator_name,
|
||
"plots": [
|
||
{
|
||
"name": "EMA Fast",
|
||
"data": ema_fast.fillna(0).tolist(),
|
||
"color": "#1890ff",
|
||
"overlay": True
|
||
},
|
||
{
|
||
"name": "EMA Slow",
|
||
"data": ema_slow.fillna(0).tolist(),
|
||
"color": "#faad14",
|
||
"overlay": True
|
||
},
|
||
{
|
||
"name": "RSI",
|
||
"data": rsi.fillna(0).tolist(),
|
||
"color": "#722ed1",
|
||
"overlay": False
|
||
}
|
||
],
|
||
"signals": [
|
||
{
|
||
"type": "buy",
|
||
"text": "B",
|
||
"data": buy_marks,
|
||
"color": "#00E676"
|
||
},
|
||
{
|
||
"type": "sell",
|
||
"text": "S",
|
||
"data": sell_marks,
|
||
"color": "#FF5252"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
What this example teaches:
|
||
|
||
- indicators are computed first
|
||
- entries and exits are expressed as boolean signals
|
||
- fixed risk defaults are declared separately through `# @strategy`
|
||
- chart output is treated as a final rendering step, not mixed into signal logic
|
||
|
||
### 5.1 A platform-UI-aligned example
|
||
|
||
This version is closer to how developers actually use QuantDinger today:
|
||
|
||
- tune common values through `# @param`
|
||
- expose default stop / take-profit / entry sizing through `# @strategy`
|
||
- set `tradeDirection` explicitly so the saved strategy and backtest panel stay aligned
|
||
- keep leverage outside the code and let the product UI own it
|
||
|
||
```python
|
||
my_indicator_name = "Breakout Retest With Direction Control"
|
||
my_indicator_description = "Breakout-and-retest logic with platform-friendly params and default risk settings."
|
||
|
||
# signal_form: two_way
|
||
# exit_owner: engine
|
||
# flip_mode: R2
|
||
|
||
# @param breakout_len int 20 Breakout lookback bars
|
||
# @param retest_buffer float 0.002 Retest tolerance ratio
|
||
# @param volume_mult float 1.2 Minimum volume filter
|
||
# @param ema_filter_len int 50 Trend filter EMA length
|
||
|
||
# @strategy stopLossPct 0.02
|
||
# @strategy takeProfitPct 0.05
|
||
# @strategy entryPct 0.2
|
||
# @strategy trailingEnabled true
|
||
# @strategy trailingStopPct 0.015
|
||
# @strategy trailingActivationPct 0.03
|
||
# @strategy tradeDirection both
|
||
|
||
df = df.copy()
|
||
|
||
breakout_len = int(params.get('breakout_len', 20))
|
||
retest_buffer = float(params.get('retest_buffer', 0.002))
|
||
volume_mult = float(params.get('volume_mult', 1.2))
|
||
ema_filter_len = int(params.get('ema_filter_len', 50))
|
||
|
||
ema_filter = df['close'].ewm(span=ema_filter_len, adjust=False).mean()
|
||
range_high = df['high'].rolling(breakout_len).max().shift(1)
|
||
range_low = df['low'].rolling(breakout_len).min().shift(1)
|
||
volume_avg = df['volume'].rolling(breakout_len).mean()
|
||
|
||
long_breakout = df['close'] > range_high
|
||
long_retest_ok = df['low'] <= range_high * (1 + retest_buffer)
|
||
long_volume_ok = df['volume'] >= volume_avg * volume_mult
|
||
long_trend_ok = df['close'] > ema_filter
|
||
|
||
short_breakout = df['close'] < range_low
|
||
short_retest_ok = df['high'] >= range_low * (1 - retest_buffer)
|
||
short_volume_ok = df['volume'] >= volume_avg * volume_mult
|
||
short_trend_ok = df['close'] < ema_filter
|
||
|
||
raw_buy = long_breakout & long_retest_ok & long_volume_ok & long_trend_ok
|
||
raw_sell = short_breakout & short_retest_ok & short_volume_ok & short_trend_ok
|
||
|
||
buy = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
|
||
sell = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)
|
||
|
||
df['buy'] = buy
|
||
df['sell'] = sell
|
||
|
||
buy_marks = [df['low'].iloc[i] * 0.995 if buy.iloc[i] else None for i in range(len(df))]
|
||
sell_marks = [df['high'].iloc[i] * 1.005 if sell.iloc[i] else None for i in range(len(df))]
|
||
|
||
output = {
|
||
"name": my_indicator_name,
|
||
"plots": [
|
||
{
|
||
"name": "EMA Filter",
|
||
"data": ema_filter.fillna(0).tolist(),
|
||
"color": "#1890ff",
|
||
"overlay": True
|
||
},
|
||
{
|
||
"name": "Range High",
|
||
"data": range_high.fillna(0).tolist(),
|
||
"color": "#52c41a",
|
||
"overlay": True
|
||
},
|
||
{
|
||
"name": "Range Low",
|
||
"data": range_low.fillna(0).tolist(),
|
||
"color": "#f5222d",
|
||
"overlay": True
|
||
}
|
||
],
|
||
"signals": [
|
||
{
|
||
"type": "buy",
|
||
"text": "L",
|
||
"data": buy_marks,
|
||
"color": "#00E676"
|
||
},
|
||
{
|
||
"type": "sell",
|
||
"text": "S",
|
||
"data": sell_marks,
|
||
"color": "#FF5252"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Why this example maps cleanly to the UI:
|
||
|
||
- `# @param` values can be tuned by AI or by manual parameter editing workflows
|
||
- `# @strategy` defaults line up with saved strategy defaults and backtest-side risk settings
|
||
- `tradeDirection both` makes it obvious that the code is designed for long and short signals
|
||
- leverage is still controlled in the product panel instead of being hidden inside source code
|
||
|
||
---
|
||
|
||
## 6. When You Should Switch to ScriptStrategy
|
||
|
||
Move to `ScriptStrategy` when the strategy needs runtime state rather than pure dataframe signals.
|
||
|
||
Typical triggers:
|
||
|
||
- the stop-loss depends on the current open position rather than only on historical series
|
||
- you want to adjust stops after entry
|
||
- you need partial close or pyramiding
|
||
- you want different logic for first entry versus re-entry
|
||
- you need cooldown logic, execution throttling, or bot-style workflows
|
||
|
||
### 6.1 Required functions
|
||
|
||
The safest product-facing contract is:
|
||
|
||
- `def on_init(ctx): ...`
|
||
- `def on_bar(ctx, bar): ...`
|
||
|
||
Why this matters:
|
||
|
||
- the runtime compiler strictly requires `on_bar`
|
||
- some product-side validation paths still expect both `on_init` and `on_bar`
|
||
- to avoid validator/runtime mismatches, define both functions even if `on_init` only initializes state or writes a log line
|
||
|
||
### 6.2 Available objects
|
||
|
||
`bar` typically exposes:
|
||
|
||
- `bar.open`
|
||
- `bar.high`
|
||
- `bar.low`
|
||
- `bar.close`
|
||
- `bar.volume`
|
||
- `bar.timestamp`
|
||
|
||
`ctx` currently exposes:
|
||
|
||
- `ctx.param(name, default=None)`
|
||
- `ctx.bars(n=1)`
|
||
- `ctx.position`
|
||
- `ctx.balance`
|
||
- `ctx.equity`
|
||
- `ctx.log(message)`
|
||
- `ctx.buy(price=None, amount=None)`
|
||
- `ctx.sell(price=None, amount=None)`
|
||
- `ctx.close_position()`
|
||
|
||
Notes:
|
||
|
||
- `ctx` does not expose the full trading config object directly
|
||
- keep leverage, symbol, venue, and credentials in product configuration rather than hardcoding them into the script
|
||
- use `ctx.param(...)` for script-level defaults that belong in source code
|
||
|
||
`ctx.position` supports both numeric checks and field access patterns such as:
|
||
|
||
```python
|
||
if not ctx.position:
|
||
...
|
||
|
||
if ctx.position > 0:
|
||
...
|
||
|
||
if ctx.position["side"] == "long":
|
||
...
|
||
```
|
||
|
||
### 6.3 Script example with runtime exits
|
||
|
||
```python
|
||
def on_init(ctx):
|
||
ctx.log("strategy initialized")
|
||
|
||
|
||
def on_bar(ctx, bar):
|
||
stop_loss_pct = ctx.param("stop_loss_pct", 0.03)
|
||
take_profit_pct = ctx.param("take_profit_pct", 0.06)
|
||
order_amount = ctx.param("order_amount", 1)
|
||
|
||
bars = ctx.bars(30)
|
||
if len(bars) < 20:
|
||
return
|
||
|
||
closes = [b.close for b in bars]
|
||
ma_fast = sum(closes[-10:]) / 10
|
||
ma_slow = sum(closes[-20:]) / 20
|
||
|
||
if not ctx.position and ma_fast > ma_slow:
|
||
ctx.buy(price=bar.close, amount=order_amount)
|
||
return
|
||
|
||
if not ctx.position:
|
||
return
|
||
|
||
if ctx.position["side"] != "long":
|
||
return
|
||
|
||
entry_price = ctx.position["entry_price"]
|
||
|
||
if bar.close <= entry_price * (1 - stop_loss_pct):
|
||
ctx.close_position()
|
||
return
|
||
|
||
if bar.close >= entry_price * (1 + take_profit_pct):
|
||
ctx.close_position()
|
||
return
|
||
|
||
if ma_fast < ma_slow:
|
||
ctx.close_position()
|
||
```
|
||
|
||
Use this style when stop-loss and take-profit truly belong to runtime position management instead of pure indicator output.
|
||
|
||
Important sizing note:
|
||
|
||
- in the current system, saved-strategy backtests still derive position sizing primarily from normalized trading config such as `entryPct`
|
||
- treat `amount` in `ctx.buy()` / `ctx.sell()` as runtime order intent, not as the only source of truth for backtest sizing
|
||
- always verify actual exposure with a saved-strategy backtest before promoting the script to paper or live trading
|
||
|
||
### 6.4 Normal script mode vs bot mode
|
||
|
||
Most `ScriptStrategy` workflows run on **closed bars**:
|
||
|
||
- the engine evaluates `on_bar(ctx, bar)` after a bar is confirmed
|
||
- this is the best mental model for standard strategy backtests and bar-by-bar live execution
|
||
|
||
There is also a bot-style runtime mode in the current system:
|
||
|
||
- bot mode may feed `on_bar` with synthetic tick-like bars built from the latest price
|
||
- this is more suitable for grid, DCA, or other bot-style execution patterns
|
||
- if you write code intended for bot mode, test it separately from standard bar-close strategy behavior
|
||
|
||
### 6.5 A platform-live-oriented ScriptStrategy example
|
||
|
||
This example is closer to how a platform-facing live strategy is usually written:
|
||
|
||
- use `ctx.param(...)` for script defaults
|
||
- inspect `ctx.position` before deciding whether to open, reverse, reduce, or fully close
|
||
- use `ctx.buy()` / `ctx.sell()` for directional intent
|
||
- use `ctx.close_position()` when you want an explicit full exit
|
||
|
||
```python
|
||
def on_init(ctx):
|
||
ctx.log("live strategy initialized")
|
||
|
||
|
||
def on_bar(ctx, bar):
|
||
fast_len = int(ctx.param("fast_len", 10))
|
||
slow_len = int(ctx.param("slow_len", 30))
|
||
risk_pct = float(ctx.param("risk_pct", 0.25))
|
||
stop_loss_pct = float(ctx.param("stop_loss_pct", 0.02))
|
||
take_profit_pct = float(ctx.param("take_profit_pct", 0.05))
|
||
allow_short = bool(ctx.param("allow_short", True))
|
||
|
||
bars = ctx.bars(slow_len + 5)
|
||
if len(bars) < slow_len:
|
||
return
|
||
|
||
closes = [b.close for b in bars]
|
||
fast_ma = sum(closes[-fast_len:]) / fast_len
|
||
slow_ma = sum(closes[-slow_len:]) / slow_len
|
||
price = bar.close
|
||
|
||
if not ctx.position:
|
||
if fast_ma > slow_ma:
|
||
ctx.buy(price=price, amount=risk_pct)
|
||
return
|
||
if allow_short and fast_ma < slow_ma:
|
||
ctx.sell(price=price, amount=risk_pct)
|
||
return
|
||
return
|
||
|
||
if ctx.position["side"] == "long":
|
||
entry_price = float(ctx.position["entry_price"])
|
||
if price <= entry_price * (1 - stop_loss_pct):
|
||
ctx.close_position()
|
||
return
|
||
if price >= entry_price * (1 + take_profit_pct):
|
||
ctx.close_position()
|
||
return
|
||
if allow_short and fast_ma < slow_ma:
|
||
ctx.sell(price=price, amount=risk_pct)
|
||
return
|
||
|
||
if ctx.position["side"] == "short":
|
||
entry_price = float(ctx.position["entry_price"])
|
||
if price >= entry_price * (1 + stop_loss_pct):
|
||
ctx.close_position()
|
||
return
|
||
if price <= entry_price * (1 - take_profit_pct):
|
||
ctx.close_position()
|
||
return
|
||
if fast_ma > slow_ma:
|
||
ctx.buy(price=price, amount=risk_pct)
|
||
return
|
||
```
|
||
|
||
What this example demonstrates:
|
||
|
||
- `ctx.param(...)` keeps script defaults visible and editable in source
|
||
- `ctx.position` is the switch that separates flat / long / short behavior
|
||
- `ctx.buy()` and `ctx.sell()` express directional intent, not just "open long" or "open short" in isolation
|
||
- `ctx.close_position()` is the clearest choice when your rule means "exit everything now"
|
||
|
||
Backtest vs live differences you should remember:
|
||
|
||
- standard script backtests and normal live mode both think in confirmed-bar logic, but bot mode may call the script with synthetic tick-like bars
|
||
- `amount` is best treated as runtime order intent; saved-strategy backtests still size mainly from normalized trading config such as `entryPct`
|
||
- `ctx.sell()` while long, or `ctx.buy()` while short, may behave like a close-plus-reverse style intent depending on runtime state and product configuration
|
||
- if you want a guaranteed full flatten action, prefer `ctx.close_position()` over relying on implicit interpretation
|
||
|
||
---
|
||
|
||
## 7. Backtesting, Persistence, and Current Limits
|
||
|
||
Saved strategies are resolved by the backend into a normalized snapshot for backtesting and execution. Common fields include:
|
||
|
||
- `strategy_type`
|
||
- `strategy_mode`
|
||
- `strategy_code`
|
||
- `indicator_config`
|
||
- `trading_config`
|
||
|
||
Current run types include:
|
||
|
||
- `indicator`
|
||
- `strategy_indicator`
|
||
- `strategy_script`
|
||
|
||
Current limitations:
|
||
|
||
- cross-sectional strategies are not supported in the current strategy snapshot flow
|
||
- `ScriptStrategy` does not support `cross_sectional` live execution mode
|
||
- script-strategy backtests do not use the indicator MTF execution path
|
||
- strategy backtests expect a valid symbol and non-empty code
|
||
|
||
### 7.1 Script backtest fill assumptions (not indicator strict mode)
|
||
|
||
Script backtests have **no** indicator IDE strict/non-strict toggle. The UI label should read:
|
||
|
||
**Script standard · bar-by-bar · next-bar open fill**
|
||
|
||
1. Candles are replayed at the strategy timeframe; each closed bar calls `on_bar(ctx, bar)`.
|
||
2. Orders come from `ctx.buy()` / `ctx.sell()` / `ctx.close_position()` inside the script.
|
||
3. The simulator fills at the **next bar open** by default (plus slippage/fees), aligned with `execution.signalTiming = next_bar_open`.
|
||
4. Position size still mainly follows `entryPct` and related trading config, not `amount` alone.
|
||
|
||
### 7.2 Trading Bot vs ScriptStrategy vs IndicatorStrategy
|
||
|
||
| Type | Code storage | Entry | Notes |
|
||
|------|--------------|-------|-------|
|
||
| IndicatorStrategy | `qd_indicator_codes.code` | Indicator IDE | `df` + boolean signals |
|
||
| ScriptStrategy | `qd_strategies_trading.strategy_code` | Strategy Studio | `on_init` + `on_bar` |
|
||
| Trading Bot | same `strategy_code`, `strategy_mode=bot` | Bot wizard | Grid live logic is engine-side |
|
||
|
||
**Clone as Script** (bot detail) copies `strategy_code` into a new ScriptStrategy on **Strategy Studio**, not into Indicator IDE.
|
||
|
||
- **Grid bots** ship a placeholder script (`on_bar: pass`); the editor may look nearly empty by design.
|
||
- **Martingale / trend / DCA** bots generate full Python templates.
|
||
- If code is missing after clone, refresh and edit again — the app fetches `/api/strategies/detail` for the full `strategy_code`.
|
||
|
||
---
|
||
|
||
## 8. Best Practices
|
||
|
||
### 8.1 Avoid look-ahead bias
|
||
|
||
- use completed-bar information only
|
||
- prefer `shift(1)` for confirmation
|
||
- do not use `shift(-1)` in signal logic
|
||
|
||
### 8.2 Handle NaNs explicitly
|
||
|
||
Rolling and EWM calculations create leading NaNs. Clean them before signal generation.
|
||
|
||
### 8.3 Keep all series aligned
|
||
|
||
Every `plot['data']` and `signal['data']` list must match `len(df)` exactly.
|
||
|
||
### 8.4 Prefer vectorized indicator logic
|
||
|
||
For `IndicatorStrategy`, core calculations should be pandas-native whenever possible.
|
||
|
||
### 8.5 Keep runtime scripts deterministic
|
||
|
||
For `ScriptStrategy`, avoid hidden state outside `ctx`, avoid randomness, and make order intent explicit.
|
||
|
||
### 8.6 Put configuration in the right layer
|
||
|
||
- use `# @param` and `# @strategy` for indicator defaults
|
||
- use `ctx.param()` for script defaults
|
||
- keep leverage, signal timing, venue configuration, and credentials outside the strategy code
|
||
|
||
---
|
||
|
||
## 9. Troubleshooting
|
||
|
||
### 9.1 `column "strategy_mode" does not exist`
|
||
|
||
Your database schema is older than the running code. Apply the required migration on `qd_strategies_trading`.
|
||
|
||
### 9.2 `Strategy script must define on_bar(ctx, bar)`
|
||
|
||
Your `ScriptStrategy` code is missing the required handler.
|
||
|
||
### 9.3 `Missing required functions: on_init, on_bar`
|
||
|
||
The current UI verifier expects both functions to exist in the source text.
|
||
|
||
### 9.4 `Strategy code is empty and cannot be backtested`
|
||
|
||
The saved strategy does not contain valid code for the selected mode.
|
||
|
||
### 9.5 Marker or plot length mismatch
|
||
|
||
All chart output arrays must align exactly with the dataframe length.
|
||
|
||
### 9.6 Strategy behaves strangely in backtest
|
||
|
||
Check these first:
|
||
|
||
- did you accidentally use future data?
|
||
- are your `buy` / `sell` signals edge-triggered?
|
||
- are you mixing signal-driven exits with engine-driven exits without documenting it?
|
||
- are your `# @strategy` defaults aligned with the strategy idea?
|
||
|
||
### 9.7 Backend logs
|
||
|
||
If strategy creation, verification, backtest, or execution fails, check backend logs first. Common issue classes:
|
||
|
||
- schema mismatch
|
||
- invalid JSON or config payloads
|
||
- code verification failure
|
||
- market or symbol mismatch
|
||
- credential or exchange configuration issues
|
||
|
||
---
|
||
|
||
## 10. Full Workflow: Indicator IDE -> Saved Strategy -> Live Trading
|
||
|
||
This is the most practical product workflow for most teams.
|
||
|
||
### 10.1 Prototype in Indicator IDE
|
||
|
||
Start in the Indicator IDE when you are still shaping the idea:
|
||
|
||
1. Write indicator logic on `df`.
|
||
2. Declare tunable inputs with `# @param`.
|
||
3. Declare default risk settings with `# @strategy`.
|
||
4. Add chart-friendly `plots` and `signals`.
|
||
5. Run indicator-side backtests until the signal density and fills make sense.
|
||
|
||
At this stage, the goal is not to perfect live execution. The goal is to make the logic visible, testable, and easy to iterate.
|
||
|
||
### 10.2 Tune and validate in the product
|
||
|
||
Once the indicator behaves correctly:
|
||
|
||
1. Use the code quality checker to catch missing metadata or suspicious patterns.
|
||
2. Run backtests with realistic symbol, timeframe, commission, slippage, and leverage settings.
|
||
3. If needed, use AI tuning or structured tuning to compare parameter combinations.
|
||
4. Apply tuned values back into the code so the source remains the single visible truth.
|
||
|
||
Recommended mindset:
|
||
|
||
- let the code describe the signal logic
|
||
- let `# @param` and `# @strategy` describe tunable defaults
|
||
- let the panel own market, leverage, date range, and execution environment
|
||
|
||
### 10.3 Save the indicator as a strategy
|
||
|
||
Once the signal model is stable:
|
||
|
||
1. Save the current indicator code.
|
||
2. Create or save a strategy record from the product flow.
|
||
3. Confirm that the saved strategy snapshot has the expected mode, defaults, and trading config.
|
||
4. Run strategy backtests from the persisted record, not only from the raw editor state.
|
||
|
||
Why this matters:
|
||
|
||
- saved-strategy backtests are closer to the real execution path
|
||
- normalized snapshots may apply execution timing and trading-config defaults differently from the raw editor view
|
||
- this is the right place to catch symbol, mode, config, and persistence mismatches
|
||
|
||
### 10.4 Decide whether IndicatorStrategy is enough
|
||
|
||
Stay with `IndicatorStrategy` if:
|
||
|
||
- entries and exits are mainly signal-based
|
||
- fixed stop-loss / take-profit / trailing defaults are enough
|
||
- you do not need position-state-dependent runtime logic
|
||
|
||
Promote to `ScriptStrategy` if:
|
||
|
||
- the open position must be monitored bar by bar
|
||
- exits depend on current position state rather than only on historical series
|
||
- you need cooldowns, partial exits, scale-ins, or bot-like runtime behavior
|
||
|
||
### 10.5 Move to paper or live trading carefully
|
||
|
||
Before enabling live trading:
|
||
|
||
1. Verify exchange, broker, symbol, and credential configuration.
|
||
2. Recheck execution timing assumptions (`signal_mode` / `exit_signal_mode` vs backtest — §3.6.1).
|
||
3. Under `tradeDirection both`, confirm `buy` / `sell` flip semantics (§3.3.1) and avoid duplicate indicator + `trailingEnabled` exits (§11.7).
|
||
4. Confirm that leverage, direction, and sizing live in the right product configuration layer.
|
||
5. Start with conservative sizing and narrow symbol scope.
|
||
6. Review runtime logs and actual order behavior (including `server_trailing_stop` and close rejections) before scaling up.
|
||
|
||
Live trading should be treated as a separate validation stage, not as a continuation of editor-only experimentation.
|
||
|
||
---
|
||
|
||
## 11. Common Mistakes vs Correct Patterns
|
||
|
||
This section highlights the mistakes that most often cause misleading backtests, confusing strategy behavior, or product/runtime mismatches.
|
||
|
||
### 11.1 Declaring `# @param` but never reading it
|
||
|
||
Wrong:
|
||
|
||
```python
|
||
# @param fast_len int 20 Fast EMA length
|
||
|
||
df = df.copy()
|
||
fast_len = 20
|
||
ema_fast = df['close'].ewm(span=fast_len, adjust=False).mean()
|
||
```
|
||
|
||
Correct:
|
||
|
||
```python
|
||
# @param fast_len int 20 Fast EMA length
|
||
|
||
df = df.copy()
|
||
fast_len = int(params.get('fast_len', 20))
|
||
ema_fast = df['close'].ewm(span=fast_len, adjust=False).mean()
|
||
```
|
||
|
||
Why:
|
||
|
||
- declaring a param tells the product and AI tuning flow that the value is intended to be adjustable
|
||
- if the code never reads `params.get(...)`, the declaration becomes cosmetic and the quality checker may warn
|
||
|
||
### 11.2 Making `buy` / `sell` fire on every bar
|
||
|
||
Wrong:
|
||
|
||
```python
|
||
df['buy'] = df['close'] > ema_fast
|
||
df['sell'] = df['close'] < ema_fast
|
||
```
|
||
|
||
Correct:
|
||
|
||
```python
|
||
raw_buy = df['close'] > ema_fast
|
||
raw_sell = df['close'] < ema_fast
|
||
|
||
df['buy'] = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
|
||
df['sell'] = (raw_sell.fillna(False) & (~raw_sell.shift(1).fillna(False))).astype(bool)
|
||
```
|
||
|
||
Why:
|
||
|
||
- repeated signals on every bar can distort entries, exits, and chart markers
|
||
- edge-triggered signals are usually closer to the intended strategy semantics
|
||
|
||
### 11.3 Writing leverage into the strategy source
|
||
|
||
Wrong:
|
||
|
||
```python
|
||
# @strategy leverage 10
|
||
```
|
||
|
||
Correct:
|
||
|
||
```python
|
||
# @strategy entryPct 0.2
|
||
# @strategy stopLossPct 0.02
|
||
# @strategy takeProfitPct 0.05
|
||
```
|
||
|
||
Then set leverage in the product panel or saved-strategy trading configuration.
|
||
|
||
Why:
|
||
|
||
- leverage belongs to execution configuration, not indicator metadata
|
||
- hiding leverage in code makes backtests harder to read and easier to misconfigure
|
||
|
||
### 11.4 Using `shift(-1)` and accidentally introducing look-ahead bias
|
||
|
||
Wrong:
|
||
|
||
```python
|
||
df['buy'] = (df['close'].shift(-1) > ema_fast).fillna(False)
|
||
```
|
||
|
||
Correct:
|
||
|
||
```python
|
||
raw_buy = df['close'] > ema_fast
|
||
df['buy'] = (raw_buy.fillna(False) & (~raw_buy.shift(1).fillna(False))).astype(bool)
|
||
```
|
||
|
||
Why:
|
||
|
||
- `shift(-1)` reaches into future data
|
||
- strategies that look amazing with future leakage usually collapse in real execution
|
||
|
||
### 11.5 Treating `ctx.buy(..., amount=...)` as absolute backtest size
|
||
|
||
Wrong mental model:
|
||
|
||
```python
|
||
ctx.buy(price=bar.close, amount=1.0)
|
||
```
|
||
|
||
"This guarantees the backtest always uses exactly 100% of capital."
|
||
|
||
Correct mental model:
|
||
|
||
```python
|
||
position_pct = float(ctx.param("risk_pct", 0.25))
|
||
ctx.buy(price=bar.close, amount=position_pct)
|
||
```
|
||
|
||
And then verify the saved-strategy backtest using the normalized trading config.
|
||
|
||
Why:
|
||
|
||
- in the current system, saved-strategy backtests still derive sizing mainly from normalized config such as `entryPct`
|
||
- `amount` is best treated as runtime order intent, not as the only source of truth for historical sizing
|
||
|
||
### 11.6 Using `ctx.sell()` or `ctx.buy()` when you really mean "fully flatten now"
|
||
|
||
Risky:
|
||
|
||
```python
|
||
if stop_hit:
|
||
ctx.sell(price=bar.close, amount=0.25)
|
||
```
|
||
|
||
Clearer:
|
||
|
||
```python
|
||
if stop_hit:
|
||
ctx.close_position()
|
||
```
|
||
|
||
Why:
|
||
|
||
- `ctx.buy()` / `ctx.sell()` express directional intent and may be interpreted through current position state
|
||
- if your rule means "exit everything now", `ctx.close_position()` is the least ambiguous choice
|
||
|
||
### 11.7 Mixing signal exits and engine exits without documenting it
|
||
|
||
Risky:
|
||
|
||
```python
|
||
# @strategy stopLossPct 0.02
|
||
# @strategy takeProfitPct 0.05
|
||
# @strategy trailingEnabled true
|
||
|
||
df['sell'] = some_other_exit_condition
|
||
# plus tp/sl flags merged into df['buy'] / df['sell']
|
||
```
|
||
|
||
Better (pick one primary exit path and comment it):
|
||
|
||
```python
|
||
# Option A: exits only from indicator signals (touch-based strategies)
|
||
# exit_owner: indicator
|
||
# @strategy trailingEnabled false
|
||
# Primary exit: close_* or tp/sl conditions inside df['buy'] / df['sell']
|
||
# stopLossPct / takeProfitPct / trailing* are not server-side exits
|
||
|
||
# Option B: exits from engine risk (fixed SL/TP/trailing)
|
||
# exit_owner: engine
|
||
# @strategy trailingEnabled true
|
||
# @strategy trailingStopPct 0.0025
|
||
# @strategy trailingActivationPct 0.0037
|
||
# df['buy'] / df['sell'] or open_* should focus on entries;
|
||
# structural reverse close_* is OK, tight in-indicator tp/sl is not
|
||
```
|
||
|
||
Why:
|
||
|
||
- duplicate exits produce `server_trailing_stop` next to indicator closes, timing skew, and sometimes `invalid amount (0.0)` when the position is already flat.
|
||
- `exit_owner: indicator` disables server-side fixed stop-loss, fixed take-profit, and trailing stop in backtest/live; those `# @strategy` exit fields no longer close the position.
|
||
- `exit_owner: engine` keeps server-side price risk active; indicator `close_*` signals should express structural reversals, not a second tight TP/SL system.
|
||
- the real problem is when nobody knows which exit path is supposed to dominate; `exit_owner` is the boundary.
|
||
|
||
### 11.8 Putting all tp/sl flags into `buy` / `sell` under `both`
|
||
|
||
```python
|
||
df['buy'] = entry_long | short_tp | short_sl
|
||
df['sell'] = entry_short | long_tp | long_sl
|
||
# @strategy tradeDirection both
|
||
```
|
||
|
||
Under `both`, `short_tp` inside `buy` is a **flip-long** intent, not flat-only. For flat-only exits, change `tradeDirection`, use four-way columns, or move to `ScriptStrategy`. Use `confirmed` signal modes if live fires earlier than backtests (§3.6.1).
|
||
|
||
### 11.9 Live log: `invalid amount (0.0) for close_*`
|
||
|
||
Common causes:
|
||
|
||
1. Server stop-loss / take-profit / trailing already closed the leg; an indicator `close_*` was still submitted.
|
||
2. Local DB lag vs the exchange — the worker retries **sync + exchange size** before rejecting.
|
||
|
||
Mitigation: §11.7 (one exit owner), §3.3.1 (`both` semantics). If the indicator owns TP/SL, use `# exit_owner: indicator`; if the engine owns price exits, do not also write tight in-indicator TP/SL booleans.
|
||
|
||
---
|
||
|
||
## 12. Platform Reference Sheet
|
||
|
||
Use this section as a fast "what is supported right now?" reference when writing strategy code.
|
||
|
||
### 12.1 `# @strategy` supported keys
|
||
|
||
| Key | Meaning | Typical Example | Notes |
|
||
|-----|---------|-----------------|-------|
|
||
| `stopLossPct` | Default stop-loss ratio | `# @strategy stopLossPct 0.02` | Engine-managed default risk setting |
|
||
| `takeProfitPct` | Default take-profit ratio | `# @strategy takeProfitPct 0.05` | Engine parser is more permissive than the toy examples |
|
||
| `entryPct` | Default capital allocation ratio | `# @strategy entryPct 0.25` | Common source of backtest sizing |
|
||
| `trailingEnabled` | Enable trailing stop logic | `# @strategy trailingEnabled true` | Boolean |
|
||
| `trailingStopPct` | Trailing stop ratio | `# @strategy trailingStopPct 0.015` | Used with trailing enabled |
|
||
| `trailingActivationPct` | Profit threshold before trailing activates | `# @strategy trailingActivationPct 0.03` | Used with trailing enabled |
|
||
| `tradeDirection` | Direction filter | `# @strategy tradeDirection both` | `long`, `short`, or `both` |
|
||
|
||
Important:
|
||
|
||
- these keys are for indicator-side strategy defaults
|
||
- **all values use 0–1 decimal ratios** (same as `StrategyConfigParser`, backtest, and live)
|
||
- **stop/take-profit/trailing thresholds are underlying price moves, not divided by leverage**
|
||
- **`entryPct 1` means 100% of available capital**, not 1%
|
||
- with `# exit_owner: indicator`, `stopLossPct` / `takeProfitPct` / `trailing*` do not trigger server-side closes in backtest or live; exits come from indicator signals
|
||
- with `# exit_owner: engine`, those server-side price-risk fields are active
|
||
- do not put `leverage` in `# @strategy`
|
||
- keep exchange, symbol, credentials, and leverage in product configuration
|
||
|
||
### 12.2 Contract header comments
|
||
|
||
| Key | Values | Meaning |
|
||
|-----|--------|---------|
|
||
| `signal_form` | `two_way` / `four_way` | Execution signal form |
|
||
| `exit_owner` | `indicator` / `engine` | Price-exit owner; `layered` is not supported |
|
||
| `flip_mode` | `R1` / `R2` | Flip timing: R1 opens on the next bar; R2 closes then opens on the same bar |
|
||
|
||
Recommended top-of-file block:
|
||
|
||
```python
|
||
# signal_form: four_way
|
||
# exit_owner: indicator
|
||
# flip_mode: R1
|
||
```
|
||
|
||
### 12.3 `# @param` quick format
|
||
|
||
| Part | Example | Meaning |
|
||
|------|---------|---------|
|
||
| Name | `fast_len` | Parameter key |
|
||
| Type | `int` / `float` / `bool` / `str` / `string` | Supported types |
|
||
| Default | `20` | Default value shown to the system |
|
||
| Description | `Fast EMA length` | Human-readable hint |
|
||
|
||
Example:
|
||
|
||
```python
|
||
# @param fast_len int 20 Fast EMA length
|
||
# @param allow_short bool true Allow short entries
|
||
```
|
||
|
||
And then read them with:
|
||
|
||
```python
|
||
fast_len = int(params.get('fast_len', 20))
|
||
allow_short = bool(params.get('allow_short', True))
|
||
```
|
||
|
||
### 12.3 `ctx` methods and fields for `ScriptStrategy`
|
||
|
||
| Item | Type | Meaning |
|
||
|------|------|---------|
|
||
| `ctx.param(name, default)` | method | Read or initialize script-level defaults |
|
||
| `ctx.bars(n=1)` | method | Get recent bars up to the current runtime index |
|
||
| `ctx.log(message)` | method | Write strategy log messages |
|
||
| `ctx.buy(price=None, amount=None)` | method | Express buy / long-side intent |
|
||
| `ctx.sell(price=None, amount=None)` | method | Express sell / short-side intent |
|
||
| `ctx.close_position()` | method | Explicitly flatten current position |
|
||
| `ctx.position` | field | Current position object |
|
||
| `ctx.balance` | field | Runtime balance snapshot |
|
||
| `ctx.equity` | field | Runtime equity snapshot |
|
||
|
||
`ctx.position` common fields:
|
||
|
||
| Field | Meaning |
|
||
|-------|---------|
|
||
| `side` | `long`, `short`, or empty when flat |
|
||
| `size` | Current position size |
|
||
| `entry_price` | Average entry price |
|
||
| `direction` | `1`, `-1`, or `0` |
|
||
| `amount` | Runtime amount mirror |
|
||
|
||
### 12.4 `bar` fields for `ScriptStrategy`
|
||
|
||
| Field | Meaning |
|
||
|-------|---------|
|
||
| `bar.open` | Open price |
|
||
| `bar.high` | High price |
|
||
| `bar.low` | Low price |
|
||
| `bar.close` | Close price |
|
||
| `bar.volume` | Volume |
|
||
| `bar.timestamp` | Time value from runtime feed |
|
||
|
||
### 12.5 `output` structure for `IndicatorStrategy`
|
||
|
||
Top-level structure:
|
||
|
||
```python
|
||
output = {
|
||
"name": my_indicator_name,
|
||
"plots": [],
|
||
"signals": [],
|
||
"calculatedVars": {}
|
||
}
|
||
```
|
||
|
||
Supported top-level keys:
|
||
|
||
| Key | Required | Meaning |
|
||
|-----|----------|---------|
|
||
| `name` | recommended | Display name |
|
||
| `plots` | recommended | Chart series output |
|
||
| `signals` | recommended | Buy/sell marker output |
|
||
| `calculatedVars` | optional | Extra metadata or computed values |
|
||
|
||
Each `plot` item commonly contains:
|
||
|
||
| Key | Meaning |
|
||
|-----|---------|
|
||
| `name` | Plot label |
|
||
| `data` | List aligned to `len(df)` |
|
||
| `color` | Display color |
|
||
| `overlay` | Whether to draw on price chart |
|
||
| `type` | Optional rendering hint |
|
||
|
||
Each `signal` item commonly contains:
|
||
|
||
| Key | Meaning |
|
||
|-----|---------|
|
||
| `type` | `buy` or `sell` |
|
||
| `text` | Marker label |
|
||
| `color` | Marker color |
|
||
| `data` | List aligned to `len(df)`, using `None` where no marker exists |
|
||
|
||
### 12.6 Fast reminders
|
||
|
||
- `df['buy']` and `df['sell']` should be boolean and length-aligned
|
||
- prefer edge-triggered signals
|
||
- avoid `shift(-1)` in signal logic
|
||
- prefer `ctx.close_position()` when the rule clearly means "exit everything now"
|
||
- treat `amount` as runtime order intent, then verify sizing with saved-strategy backtests
|
||
|
||
---
|
||
|
||
## 13. Recommended Development Workflow
|
||
|
||
1. Prototype the idea as an `IndicatorStrategy`.
|
||
2. Validate plots, signal density, and next-bar-open backtest behavior.
|
||
3. Add clear `# @param` and `# @strategy` metadata.
|
||
4. Decide explicitly whether exits are signal-managed or engine-managed.
|
||
5. Save the strategy and run strategy backtests from the persisted record.
|
||
6. Promote to `ScriptStrategy` only when you truly need runtime position logic.
|
||
7. Move to paper or live trading only after configuration, credentials, and market semantics are verified.
|
||
|