|
| 1 | +--- |
| 2 | +name: custom-indicator |
| 3 | +description: > |
| 4 | + Creates a custom indicator class and integrates it into a QuantConnect algorithm via MCP. |
| 5 | + Invoke when the user wants an indicator LEAN does not provide, needs to combine built-in |
| 6 | + indicators beyond extension logic, or describes a formula to compute on price/volume data. |
| 7 | + Trigger phrases: "create a custom indicator", "implement a custom [name] indicator", |
| 8 | + "write an indicator for [formula]", "I need an indicator that LEAN doesn't have", |
| 9 | + "add a custom indicator to my algorithm", "combine built-in indicators with custom logic", |
| 10 | + "PythonIndicator", "custom indicator class", "write a [name] indicator". |
| 11 | +--- |
| 12 | + |
| 13 | +# /custom-indicator -- QuantConnect Custom Indicator |
| 14 | + |
| 15 | +Creates a custom indicator class and wires it into an algorithm. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Step 1 -- Gather Indicator Information |
| 20 | + |
| 21 | +Ask the user (sequentially if needed): |
| 22 | + |
| 23 | +1. **Indicator name**: What should the class be called? (e.g., `CustomVolatility`, `LogMomentum`) |
| 24 | + |
| 25 | +2. **Formula / logic**: What does this indicator compute? Describe the math or logic step by step. |
| 26 | + |
| 27 | +3. **Input type**: |
| 28 | + - Single price value -> py`IndicatorDataPoint` (use py`input_.value`cs`input.Value`) |
| 29 | + - Full OHLCV bar -> py`TradeBar` (use py`input_.close`cs`input.Close`, py`input_.volume`cs`input.Volume`, etc.) |
| 30 | + - Quote bar (bid/ask) -> `QuoteBar` |
| 31 | + - Default to `TradeBar` when the formula needs more than just a single price. |
| 32 | + |
| 33 | +4. **Period**: Does the indicator need a rolling lookback window? If yes, how many bars? |
| 34 | + Use py`RollingWindow[float](period)`cs`RollingWindow<decimal>(period)`. |
| 35 | + |
| 36 | +5. **Internal built-in indicators**: Does the computation internally use any LEAN built-in |
| 37 | + indicators (e.g., SMA, EMA, RSI)? If yes, list them. They must be constructed and updated |
| 38 | + inside py`update`cs`ComputeNextValue`. |
| 39 | + |
| 40 | +6. **Algorithm scope**: |
| 41 | + - Single fixed symbol -> register in py`initialize`cs`Initialize` |
| 42 | + - Universe of symbols -> register in py`on_securities_changed`cs`OnSecuritiesChanged`, deregister on removal |
| 43 | + |
| 44 | +7. **Warm-up strategy** (default to manual): |
| 45 | + - **Manual** (recommended): loop over py`self.history[TradeBar](symbol, period + 1)`cs`History<TradeBar>(symbol, period + 1, Resolution.Daily)` and call py`indicator.update(bar)`cs`indicator.Update(bar)` before py`self.register_indicator`cs`RegisterIndicator`. |
| 46 | + - **Automatic**: set py`self.settings.automatic_indicator_warm_up = True`cs`Settings.AutomaticIndicatorWarmUp = true` in py`initialize`cs`Initialize`. |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## Step 2 -- Generate the Code |
| 51 | + |
| 52 | +### Style rules |
| 53 | + |
| 54 | +Apply these rules to all generated code: |
| 55 | + |
| 56 | +- **Imports (Python)**: `from AlgorithmImports import *` only. `AlgorithmImports` already re-exports `math`, `numpy`, and all common libraries -- no additional imports needed. |
| 57 | +- **Imports (C#)**: Leave all project `using` statements as-is. |
| 58 | +- **Comments**: Capital first letter, space after `#` / `//`, ends with a period. |
| 59 | +- **Blank lines (Python)**: 2 blank lines before each class, 1 before each method, none inside method bodies. |
| 60 | +- **Blank lines (C#)**: 1 blank line between methods, none inside method bodies. |
| 61 | + |
| 62 | +### File naming |
| 63 | + |
| 64 | +| Language | Class name | File name | |
| 65 | +|---|---|---| |
| 66 | +| Python | `CustomVolatility` | `custom_volatility.py` | |
| 67 | +| C# | `CustomVolatility` | `CustomVolatility.cs` | |
| 68 | + |
| 69 | +The indicator class always goes in its own file. Never inline it in `main.py` / `Main.cs`. |
| 70 | + |
| 71 | +### Indicator class template |
| 72 | + |
| 73 | +```python |
| 74 | +# region imports |
| 75 | +from AlgorithmImports import * |
| 76 | +# endregion |
| 77 | + |
| 78 | + |
| 79 | +class CustomVolatility(PythonIndicator): |
| 80 | + |
| 81 | + def __init__(self, period): |
| 82 | + super().__init__() |
| 83 | + self.value = 0 |
| 84 | + self._window = RollingWindow[float](period) |
| 85 | + |
| 86 | + def update(self, input_: BaseData): |
| 87 | + price = input_.value |
| 88 | + if price <= 0: |
| 89 | + return |
| 90 | + self._window.add(price) |
| 91 | + if self._window.is_ready: |
| 92 | + prices = np.array(list(self._window)[::-1]) |
| 93 | + log_diffs = np.diff(np.log(prices)) |
| 94 | + self.value = np.std(log_diffs) * math.sqrt(252) * 100.0 |
| 95 | + return self.is_ready |
| 96 | + |
| 97 | + @property |
| 98 | + def is_ready(self) -> bool: |
| 99 | + return self._window.is_ready |
| 100 | +``` |
| 101 | + |
| 102 | +```csharp |
| 103 | +public class CustomVolatility : Indicator |
| 104 | +{ |
| 105 | + private readonly RollingWindow<decimal> _window; |
| 106 | + |
| 107 | + public CustomVolatility(int period) : base("CustomVolatility") |
| 108 | + { |
| 109 | + _window = new RollingWindow<decimal>(period); |
| 110 | + WarmUpPeriod = period; |
| 111 | + } |
| 112 | + |
| 113 | + public override bool IsReady => _window.IsReady; |
| 114 | + |
| 115 | + protected override decimal ComputeNextValue(IndicatorDataPoint input) |
| 116 | + { |
| 117 | + _window.Add(input.Value); |
| 118 | + if (!IsReady) return 0m; |
| 119 | + // Compute from _window here. |
| 120 | + return 0m; |
| 121 | + } |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +For OHLCV input in C#, inherit from `BarIndicator` instead of `Indicator`: |
| 126 | + |
| 127 | +```csharp |
| 128 | +public class CustomVolatility : BarIndicator |
| 129 | +{ |
| 130 | + public CustomVolatility(int period) : base("CustomVolatility") |
| 131 | + { |
| 132 | + WarmUpPeriod = period; |
| 133 | + } |
| 134 | + |
| 135 | + public override bool IsReady => true; |
| 136 | + |
| 137 | + protected override decimal ComputeNextValue(IBaseDataBar input) |
| 138 | + { |
| 139 | + // input.Close, input.Open, input.High, input.Low, input.Volume |
| 140 | + return 0m; |
| 141 | + } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +Key rules: |
| 146 | + |
| 147 | +- Python: inherit from `PythonIndicator`, not `Indicator`. |
| 148 | +- py`self.value`cs`Value` holds the current computed value. |
| 149 | +- py`update` must return py`self.is_ready`cs`IsReady` (bool). Omitting the return silently breaks warm-up. |
| 150 | +- Guard invalid input (e.g., `price <= 0`) before touching the window. |
| 151 | +- `RollingWindow` iterates newest-first in Python; use `[::-1]` to get chronological order for numpy. |
| 152 | +- C#: set `WarmUpPeriod` in the constructor so LEAN knows how many bars are needed. |
| 153 | + |
| 154 | +### Algorithm integration -- single symbol |
| 155 | + |
| 156 | +```python |
| 157 | +def initialize(self): |
| 158 | + symbol = self.add_equity("SPY", Resolution.DAILY).symbol |
| 159 | + self._indicator = CustomVolatility(period) |
| 160 | + for bar in self.history[TradeBar](symbol, period + 1): |
| 161 | + self._indicator.update(bar) |
| 162 | + self.register_indicator(symbol, self._indicator) |
| 163 | +``` |
| 164 | + |
| 165 | +```csharp |
| 166 | +private CustomVolatility _indicator; |
| 167 | + |
| 168 | +public override void Initialize() |
| 169 | +{ |
| 170 | + var symbol = AddEquity("SPY", Resolution.Daily).Symbol; |
| 171 | + _indicator = new CustomVolatility(period); |
| 172 | + foreach (var bar in History<TradeBar>(symbol, period + 1, Resolution.Daily)) |
| 173 | + _indicator.Update(bar); |
| 174 | + RegisterIndicator(symbol, _indicator); |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +### Algorithm integration -- universe |
| 179 | + |
| 180 | +```python |
| 181 | +def on_securities_changed(self, changes): |
| 182 | + for security in changes.added_securities: |
| 183 | + security.indicator = CustomVolatility(period) |
| 184 | + for bar in self.history[TradeBar](security, period + 1): |
| 185 | + security.indicator.update(bar) |
| 186 | + self.register_indicator(security, security.indicator) |
| 187 | + for security in changes.removed_securities: |
| 188 | + self.deregister_indicator(security.indicator) |
| 189 | + self.liquidate(security) |
| 190 | +``` |
| 191 | + |
| 192 | +```csharp |
| 193 | +private readonly Dictionary<Symbol, CustomVolatility> _indicators = new(); |
| 194 | + |
| 195 | +public override void OnSecuritiesChanged(SecurityChanges changes) |
| 196 | +{ |
| 197 | + foreach (var security in changes.AddedSecurities) |
| 198 | + { |
| 199 | + var ind = new CustomVolatility(period); |
| 200 | + foreach (var bar in History<TradeBar>(security.Symbol, period + 1, Resolution.Daily)) |
| 201 | + ind.Update(bar); |
| 202 | + RegisterIndicator(security.Symbol, ind); |
| 203 | + _indicators[security.Symbol] = ind; |
| 204 | + } |
| 205 | + foreach (var security in changes.RemovedSecurities) |
| 206 | + { |
| 207 | + if (_indicators.Remove(security.Symbol, out var ind)) |
| 208 | + DeregisterIndicator(ind); |
| 209 | + Liquidate(security.Symbol); |
| 210 | + } |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +Gate all reads on py`security.indicator.is_ready`cs`_indicators[symbol].IsReady` before accessing the value. |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +## Step 3 -- Write Files via MCP |
| 219 | + |
| 220 | +1. **Indicator file**: `quantconnect:create_file` with the indicator class. |
| 221 | +2. **Algorithm file**: `quantconnect:update_file_contents` for `main.py` / `Main.cs`. |
| 222 | +3. Python only: add import after `from AlgorithmImports import *`: |
| 223 | + ```python |
| 224 | + from custom_volatility import CustomVolatility |
| 225 | + ``` |
| 226 | + C# does not need this -- all project files share the same namespace. |
| 227 | + |
| 228 | +--- |
| 229 | + |
| 230 | +## Step 4 -- Compile |
| 231 | + |
| 232 | +1. `quantconnect:create_compile`. |
| 233 | +2. Poll `quantconnect:read_compile` until `BuildSuccess` or `BuildError`. |
| 234 | +3. On `BuildError`: parse error messages, fix code via MCP, loop back to Step 4. |
| 235 | + |
| 236 | +--- |
| 237 | + |
| 238 | +## Step 5 -- Backtest and Verify |
| 239 | + |
| 240 | +1. `quantconnect:create_backtest`. |
| 241 | +2. Poll `quantconnect:read_backtest` until complete. |
| 242 | +3. Check both conditions: |
| 243 | + |
| 244 | + **Condition A: Indicator produces values** |
| 245 | + - Look for runtime errors about indicator not being ready. |
| 246 | + - If py`is_ready`cs`IsReady` never becomes true: verify py`history`cs`History` returned at least `period` bars and that py`update`cs`Update` is called on each. Fix the warm-up loop and re-run from Step 4. |
| 247 | + |
| 248 | + **Condition B: Algorithm logic fires** |
| 249 | + - If algorithm has entry/exit conditions: confirm at least one order was placed. |
| 250 | + - If no orders but no errors: add a log line as the first line of py`on_data`cs`OnData` and re-backtest. Relax the entry condition if needed. |
| 251 | + |
| 252 | +4. Report: compile clean, backtest complete, indicator py`is_ready`cs`IsReady` confirmed, trades placed. |
0 commit comments