@zigrivers/scaffold 3.13.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +32 -10
  2. package/content/knowledge/research/research-architecture.md +385 -0
  3. package/content/knowledge/research/research-conventions.md +248 -0
  4. package/content/knowledge/research/research-dev-environment.md +303 -0
  5. package/content/knowledge/research/research-experiment-loop.md +429 -0
  6. package/content/knowledge/research/research-experiment-tracking.md +336 -0
  7. package/content/knowledge/research/research-ml-architecture-search.md +383 -0
  8. package/content/knowledge/research/research-ml-evaluation.md +407 -0
  9. package/content/knowledge/research/research-ml-experiment-tracking.md +466 -0
  10. package/content/knowledge/research/research-ml-training-patterns.md +413 -0
  11. package/content/knowledge/research/research-observability.md +395 -0
  12. package/content/knowledge/research/research-overfitting-prevention.md +306 -0
  13. package/content/knowledge/research/research-project-structure.md +264 -0
  14. package/content/knowledge/research/research-quant-backtesting.md +326 -0
  15. package/content/knowledge/research/research-quant-market-data.md +366 -0
  16. package/content/knowledge/research/research-quant-metrics.md +335 -0
  17. package/content/knowledge/research/research-quant-requirements.md +223 -0
  18. package/content/knowledge/research/research-quant-risk.md +469 -0
  19. package/content/knowledge/research/research-quant-strategy-patterns.md +412 -0
  20. package/content/knowledge/research/research-requirements.md +201 -0
  21. package/content/knowledge/research/research-security.md +374 -0
  22. package/content/knowledge/research/research-sim-compute-management.md +538 -0
  23. package/content/knowledge/research/research-sim-engine-patterns.md +448 -0
  24. package/content/knowledge/research/research-sim-parameter-spaces.md +425 -0
  25. package/content/knowledge/research/research-sim-validation.md +456 -0
  26. package/content/knowledge/research/research-testing.md +334 -0
  27. package/content/methodology/research-ml-research.yml +23 -0
  28. package/content/methodology/research-overlay.yml +65 -0
  29. package/content/methodology/research-quant-finance.yml +29 -0
  30. package/content/methodology/research-simulation.yml +23 -0
  31. package/dist/cli/commands/adopt.d.ts.map +1 -1
  32. package/dist/cli/commands/adopt.js +30 -8
  33. package/dist/cli/commands/adopt.js.map +1 -1
  34. package/dist/cli/commands/adopt.serialization.test.js +49 -0
  35. package/dist/cli/commands/adopt.serialization.test.js.map +1 -1
  36. package/dist/cli/commands/adopt.test.js +8 -0
  37. package/dist/cli/commands/adopt.test.js.map +1 -1
  38. package/dist/cli/commands/build.d.ts.map +1 -1
  39. package/dist/cli/commands/build.js +191 -180
  40. package/dist/cli/commands/build.js.map +1 -1
  41. package/dist/cli/commands/complete.d.ts.map +1 -1
  42. package/dist/cli/commands/complete.js +16 -12
  43. package/dist/cli/commands/complete.js.map +1 -1
  44. package/dist/cli/commands/complete.test.js +14 -5
  45. package/dist/cli/commands/complete.test.js.map +1 -1
  46. package/dist/cli/commands/init.d.ts +4 -0
  47. package/dist/cli/commands/init.d.ts.map +1 -1
  48. package/dist/cli/commands/init.js +75 -51
  49. package/dist/cli/commands/init.js.map +1 -1
  50. package/dist/cli/commands/init.test.js +33 -27
  51. package/dist/cli/commands/init.test.js.map +1 -1
  52. package/dist/cli/commands/reset.d.ts.map +1 -1
  53. package/dist/cli/commands/reset.js +44 -40
  54. package/dist/cli/commands/reset.js.map +1 -1
  55. package/dist/cli/commands/reset.test.js +42 -20
  56. package/dist/cli/commands/reset.test.js.map +1 -1
  57. package/dist/cli/commands/rework.d.ts.map +1 -1
  58. package/dist/cli/commands/rework.js +16 -12
  59. package/dist/cli/commands/rework.js.map +1 -1
  60. package/dist/cli/commands/rework.test.js +12 -3
  61. package/dist/cli/commands/rework.test.js.map +1 -1
  62. package/dist/cli/commands/run.d.ts.map +1 -1
  63. package/dist/cli/commands/run.js +318 -298
  64. package/dist/cli/commands/run.js.map +1 -1
  65. package/dist/cli/commands/run.test.js +92 -120
  66. package/dist/cli/commands/run.test.js.map +1 -1
  67. package/dist/cli/commands/skip.d.ts.map +1 -1
  68. package/dist/cli/commands/skip.js +19 -15
  69. package/dist/cli/commands/skip.js.map +1 -1
  70. package/dist/cli/commands/skip.test.js +22 -11
  71. package/dist/cli/commands/skip.test.js.map +1 -1
  72. package/dist/cli/commands/update.d.ts.map +1 -1
  73. package/dist/cli/commands/update.js +3 -1
  74. package/dist/cli/commands/update.js.map +1 -1
  75. package/dist/cli/commands/update.test.js +8 -4
  76. package/dist/cli/commands/update.test.js.map +1 -1
  77. package/dist/cli/commands/version.d.ts.map +1 -1
  78. package/dist/cli/commands/version.js +3 -1
  79. package/dist/cli/commands/version.js.map +1 -1
  80. package/dist/cli/commands/version.test.js +9 -5
  81. package/dist/cli/commands/version.test.js.map +1 -1
  82. package/dist/cli/index.d.ts.map +1 -1
  83. package/dist/cli/index.js +2 -0
  84. package/dist/cli/index.js.map +1 -1
  85. package/dist/cli/init-flag-families.d.ts +6 -1
  86. package/dist/cli/init-flag-families.d.ts.map +1 -1
  87. package/dist/cli/init-flag-families.js +32 -1
  88. package/dist/cli/init-flag-families.js.map +1 -1
  89. package/dist/cli/init-flag-families.test.js +47 -0
  90. package/dist/cli/init-flag-families.test.js.map +1 -1
  91. package/dist/cli/output/interactive.d.ts +1 -0
  92. package/dist/cli/output/interactive.d.ts.map +1 -1
  93. package/dist/cli/output/interactive.js +5 -0
  94. package/dist/cli/output/interactive.js.map +1 -1
  95. package/dist/cli/shutdown.d.ts +51 -0
  96. package/dist/cli/shutdown.d.ts.map +1 -0
  97. package/dist/cli/shutdown.js +199 -0
  98. package/dist/cli/shutdown.js.map +1 -0
  99. package/dist/cli/shutdown.test.d.ts +2 -0
  100. package/dist/cli/shutdown.test.d.ts.map +1 -0
  101. package/dist/cli/shutdown.test.js +316 -0
  102. package/dist/cli/shutdown.test.js.map +1 -0
  103. package/dist/config/schema.d.ts +272 -16
  104. package/dist/config/schema.d.ts.map +1 -1
  105. package/dist/config/schema.js +25 -1
  106. package/dist/config/schema.js.map +1 -1
  107. package/dist/config/schema.test.js +103 -3
  108. package/dist/config/schema.test.js.map +1 -1
  109. package/dist/core/assembly/overlay-loader.d.ts +12 -0
  110. package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
  111. package/dist/core/assembly/overlay-loader.js +30 -0
  112. package/dist/core/assembly/overlay-loader.js.map +1 -1
  113. package/dist/core/assembly/overlay-loader.test.js +66 -1
  114. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  115. package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
  116. package/dist/core/assembly/overlay-state-resolver.js +48 -19
  117. package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
  118. package/dist/core/assembly/overlay-state-resolver.test.js +80 -0
  119. package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
  120. package/dist/e2e/init.test.js +5 -4
  121. package/dist/e2e/init.test.js.map +1 -1
  122. package/dist/e2e/project-type-overlays.test.js +119 -0
  123. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  124. package/dist/project/adopt.d.ts.map +1 -1
  125. package/dist/project/adopt.js +3 -1
  126. package/dist/project/adopt.js.map +1 -1
  127. package/dist/project/detectors/disambiguate.js +1 -1
  128. package/dist/project/detectors/disambiguate.js.map +1 -1
  129. package/dist/project/detectors/index.d.ts.map +1 -1
  130. package/dist/project/detectors/index.js +2 -1
  131. package/dist/project/detectors/index.js.map +1 -1
  132. package/dist/project/detectors/ml.d.ts.map +1 -1
  133. package/dist/project/detectors/ml.js +2 -6
  134. package/dist/project/detectors/ml.js.map +1 -1
  135. package/dist/project/detectors/research.d.ts +4 -0
  136. package/dist/project/detectors/research.d.ts.map +1 -0
  137. package/dist/project/detectors/research.js +141 -0
  138. package/dist/project/detectors/research.js.map +1 -0
  139. package/dist/project/detectors/research.test.d.ts +2 -0
  140. package/dist/project/detectors/research.test.d.ts.map +1 -0
  141. package/dist/project/detectors/research.test.js +235 -0
  142. package/dist/project/detectors/research.test.js.map +1 -0
  143. package/dist/project/detectors/shared-signals.d.ts +3 -0
  144. package/dist/project/detectors/shared-signals.d.ts.map +1 -0
  145. package/dist/project/detectors/shared-signals.js +9 -0
  146. package/dist/project/detectors/shared-signals.js.map +1 -0
  147. package/dist/project/detectors/types.d.ts +6 -2
  148. package/dist/project/detectors/types.d.ts.map +1 -1
  149. package/dist/project/detectors/types.js.map +1 -1
  150. package/dist/state/lock-manager.d.ts +1 -0
  151. package/dist/state/lock-manager.d.ts.map +1 -1
  152. package/dist/state/lock-manager.js +1 -1
  153. package/dist/state/lock-manager.js.map +1 -1
  154. package/dist/types/config.d.ts +7 -1
  155. package/dist/types/config.d.ts.map +1 -1
  156. package/dist/wizard/copy/core.d.ts.map +1 -1
  157. package/dist/wizard/copy/core.js +4 -0
  158. package/dist/wizard/copy/core.js.map +1 -1
  159. package/dist/wizard/copy/index.d.ts.map +1 -1
  160. package/dist/wizard/copy/index.js +2 -0
  161. package/dist/wizard/copy/index.js.map +1 -1
  162. package/dist/wizard/copy/research.d.ts +3 -0
  163. package/dist/wizard/copy/research.d.ts.map +1 -0
  164. package/dist/wizard/copy/research.js +27 -0
  165. package/dist/wizard/copy/research.js.map +1 -0
  166. package/dist/wizard/copy/types.d.ts +5 -1
  167. package/dist/wizard/copy/types.d.ts.map +1 -1
  168. package/dist/wizard/flags.d.ts +7 -1
  169. package/dist/wizard/flags.d.ts.map +1 -1
  170. package/dist/wizard/questions.d.ts +4 -2
  171. package/dist/wizard/questions.d.ts.map +1 -1
  172. package/dist/wizard/questions.js +27 -1
  173. package/dist/wizard/questions.js.map +1 -1
  174. package/dist/wizard/questions.test.js +51 -0
  175. package/dist/wizard/questions.test.js.map +1 -1
  176. package/dist/wizard/wizard.d.ts +3 -2
  177. package/dist/wizard/wizard.d.ts.map +1 -1
  178. package/dist/wizard/wizard.js +3 -1
  179. package/dist/wizard/wizard.js.map +1 -1
  180. package/package.json +1 -1
@@ -0,0 +1,412 @@
1
+ ---
2
+ name: research-quant-strategy-patterns
3
+ description: Trading strategy patterns including signal generation, entry/exit rules, position sizing methods, stop-loss patterns, and multi-asset allocation
4
+ topics: [research, quant-finance, strategy, signals, position-sizing, kelly-criterion, stop-loss, allocation, volatility-targeting]
5
+ ---
6
+
7
+ Trading strategies are composed of discrete building blocks that can be mixed, matched, and tested independently: signal generation (what to trade and when), entry/exit rules (how to initiate and close positions), position sizing (how much capital to allocate), and stop-loss management (how to limit downside). Treating each block as a separate, testable component enables systematic exploration of the strategy design space. Resist the temptation to build monolithic strategies -- instead, compose simple, well-tested components.
8
+
9
+ ## Summary
10
+
11
+ Build strategies from composable components: signal generators (trend indicators, mean-reversion z-scores, momentum factors), entry/exit rules (confirmation logic, filter conditions, time-based exits), position sizing methods (fixed fraction, Kelly criterion, volatility targeting), and stop-loss patterns (ATR-based, trailing, time-based). Test each component in isolation before combining. Use a signal-to-position pipeline that separates alpha generation from risk management.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Signal Generation Patterns
16
+
17
+ Signals are the core alpha source. They convert market data into directional predictions:
18
+
19
+ ```python
20
+ # strategies/signals/base.py
21
+ from abc import ABC, abstractmethod
22
+ import pandas as pd
23
+ import numpy as np
24
+
25
+ class SignalGenerator(ABC):
26
+ """Base class for signal generators."""
27
+
28
+ @abstractmethod
29
+ def generate(self, data: pd.DataFrame) -> pd.Series:
30
+ """
31
+ Generate trading signals from market data.
32
+
33
+ Returns:
34
+ Series of signal values. Convention:
35
+ - Positive values = bullish signal (strength)
36
+ - Negative values = bearish signal (strength)
37
+ - Zero = no signal
38
+ """
39
+ ...
40
+
41
+
42
+ class MovingAverageCrossover(SignalGenerator):
43
+ """Dual moving average crossover signal."""
44
+
45
+ def __init__(self, fast_period: int = 10, slow_period: int = 50):
46
+ self.fast_period = fast_period
47
+ self.slow_period = slow_period
48
+
49
+ def generate(self, data: pd.DataFrame) -> pd.Series:
50
+ fast_ma = data["close"].rolling(self.fast_period).mean()
51
+ slow_ma = data["close"].rolling(self.slow_period).mean()
52
+ # Signal strength = normalized distance between MAs
53
+ signal = (fast_ma - slow_ma) / slow_ma
54
+ return signal
55
+
56
+
57
+ class MeanReversionZScore(SignalGenerator):
58
+ """Z-score based mean reversion signal."""
59
+
60
+ def __init__(self, lookback: int = 20, entry_z: float = 2.0):
61
+ self.lookback = lookback
62
+ self.entry_z = entry_z
63
+
64
+ def generate(self, data: pd.DataFrame) -> pd.Series:
65
+ rolling_mean = data["close"].rolling(self.lookback).mean()
66
+ rolling_std = data["close"].rolling(self.lookback).std()
67
+ z_score = (data["close"] - rolling_mean) / rolling_std
68
+ # Negative z-score = price below mean = buy signal for mean reversion
69
+ signal = -z_score
70
+ return signal
71
+
72
+
73
+ class MomentumFactor(SignalGenerator):
74
+ """Cross-sectional momentum signal for multi-asset allocation."""
75
+
76
+ def __init__(self, lookback: int = 252, skip_recent: int = 21):
77
+ self.lookback = lookback
78
+ self.skip_recent = skip_recent # Skip most recent month (reversal)
79
+
80
+ def generate(self, data: pd.DataFrame) -> pd.Series:
81
+ # Total return over lookback, excluding recent reversal period
82
+ total_return = (
83
+ data["close"].shift(self.skip_recent)
84
+ / data["close"].shift(self.lookback)
85
+ - 1
86
+ )
87
+ return total_return
88
+ ```
89
+
90
+ ### Entry/Exit Rule Patterns
91
+
92
+ Entry and exit rules add confirmation, filtering, and timing logic on top of raw signals:
93
+
94
+ ```python
95
+ # strategies/rules/entry_exit.py
96
+ import pandas as pd
97
+ import numpy as np
98
+ from dataclasses import dataclass
99
+
100
+ @dataclass
101
+ class TradeSignal:
102
+ """A confirmed trade signal with entry parameters."""
103
+ direction: int # +1 = long, -1 = short, 0 = no trade
104
+ strength: float # Signal strength for position sizing
105
+ entry_type: str # "market", "limit", "stop"
106
+ limit_price: float | None = None
107
+ stop_price: float | None = None
108
+
109
+ class ConfirmationFilter:
110
+ """Require multiple signals to agree before entering a trade."""
111
+
112
+ def __init__(self, signals: list[pd.Series], min_agreement: int = 2):
113
+ self.signals = signals
114
+ self.min_agreement = min_agreement
115
+
116
+ def filter(self) -> pd.Series:
117
+ """Return confirmed signals where enough generators agree."""
118
+ # Count how many signals are positive (bullish) at each point
119
+ bullish = sum((s > 0).astype(int) for s in self.signals)
120
+ bearish = sum((s < 0).astype(int) for s in self.signals)
121
+
122
+ confirmed = pd.Series(0.0, index=self.signals[0].index)
123
+ confirmed[bullish >= self.min_agreement] = 1.0
124
+ confirmed[bearish >= self.min_agreement] = -1.0
125
+ return confirmed
126
+
127
+
128
+ class VolumeFilter:
129
+ """Require minimum volume before entering a trade."""
130
+
131
+ def __init__(self, min_volume_ratio: float = 1.5):
132
+ self.min_volume_ratio = min_volume_ratio
133
+
134
+ def apply(self, signal: pd.Series, volume: pd.Series,
135
+ lookback: int = 20) -> pd.Series:
136
+ """Zero out signals on low-volume bars."""
137
+ avg_volume = volume.rolling(lookback).mean()
138
+ volume_ratio = volume / avg_volume
139
+ filtered = signal.copy()
140
+ filtered[volume_ratio < self.min_volume_ratio] = 0.0
141
+ return filtered
142
+
143
+
144
+ class TimeBasedExit:
145
+ """Force exit after a maximum holding period."""
146
+
147
+ def __init__(self, max_bars: int = 20):
148
+ self.max_bars = max_bars
149
+
150
+ def apply(self, positions: pd.Series) -> pd.Series:
151
+ """Apply time-based exit to existing positions."""
152
+ result = positions.copy()
153
+ bars_in_position = 0
154
+
155
+ for i in range(1, len(result)):
156
+ if result.iloc[i] != 0:
157
+ bars_in_position += 1
158
+ if bars_in_position >= self.max_bars:
159
+ result.iloc[i] = 0
160
+ bars_in_position = 0
161
+ else:
162
+ bars_in_position = 0
163
+
164
+ return result
165
+ ```
166
+
167
+ ### Position Sizing Methods
168
+
169
+ Position sizing is arguably more important than signal generation. It determines how much capital to risk on each trade:
170
+
171
+ ```python
172
+ # strategies/sizing/position_sizing.py
173
+ import numpy as np
174
+ import pandas as pd
175
+ from abc import ABC, abstractmethod
176
+
177
+ class PositionSizer(ABC):
178
+ """Base class for position sizing methods."""
179
+
180
+ @abstractmethod
181
+ def size(self, signal_strength: float, price: float,
182
+ portfolio_value: float, **kwargs) -> int:
183
+ """Return number of shares to trade."""
184
+ ...
185
+
186
+
187
+ class FixedFraction(PositionSizer):
188
+ """Risk a fixed fraction of portfolio per trade."""
189
+
190
+ def __init__(self, risk_pct: float = 0.02, atr_multiplier: float = 2.0):
191
+ self.risk_pct = risk_pct
192
+ self.atr_multiplier = atr_multiplier
193
+
194
+ def size(self, signal_strength: float, price: float,
195
+ portfolio_value: float, atr: float = 0.0, **kwargs) -> int:
196
+ """
197
+ Size based on fixed-fraction risk.
198
+
199
+ risk_amount = portfolio * risk_pct
200
+ stop_distance = atr * atr_multiplier
201
+ shares = risk_amount / stop_distance
202
+ """
203
+ risk_amount = portfolio_value * self.risk_pct
204
+ stop_distance = atr * self.atr_multiplier if atr > 0 else price * 0.02
205
+ if stop_distance == 0:
206
+ return 0
207
+ shares = int(risk_amount / stop_distance)
208
+ return shares
209
+
210
+
211
+ class KellyCriterion(PositionSizer):
212
+ """Kelly criterion position sizing with fractional Kelly for safety."""
213
+
214
+ def __init__(self, kelly_fraction: float = 0.25, max_position_pct: float = 0.10):
215
+ self.kelly_fraction = kelly_fraction # Use quarter-Kelly for safety
216
+ self.max_position_pct = max_position_pct
217
+
218
+ def size(self, signal_strength: float, price: float,
219
+ portfolio_value: float, win_rate: float = 0.5,
220
+ avg_win_loss_ratio: float = 1.5, **kwargs) -> int:
221
+ """
222
+ Kelly criterion: f* = (bp - q) / b
223
+ where b = avg_win/avg_loss, p = win_rate, q = 1 - p
224
+ """
225
+ b = avg_win_loss_ratio
226
+ p = win_rate
227
+ q = 1 - p
228
+ kelly_pct = (b * p - q) / b if b > 0 else 0.0
229
+
230
+ # Apply fractional Kelly and cap
231
+ position_pct = min(
232
+ kelly_pct * self.kelly_fraction,
233
+ self.max_position_pct,
234
+ )
235
+ position_pct = max(position_pct, 0.0) # Never negative
236
+
237
+ notional = portfolio_value * position_pct
238
+ shares = int(notional / price) if price > 0 else 0
239
+ return shares
240
+
241
+
242
+ class VolatilityTargeting(PositionSizer):
243
+ """Size positions to target a specific portfolio volatility."""
244
+
245
+ def __init__(self, target_vol: float = 0.15, lookback: int = 20):
246
+ self.target_vol = target_vol # 15% annualized target
247
+ self.lookback = lookback
248
+
249
+ def size(self, signal_strength: float, price: float,
250
+ portfolio_value: float, returns: pd.Series | None = None,
251
+ **kwargs) -> int:
252
+ """
253
+ Scale position to achieve target volatility contribution.
254
+
255
+ position_weight = target_vol / (asset_vol * sqrt(252))
256
+ """
257
+ if returns is None or len(returns) < self.lookback:
258
+ return 0
259
+
260
+ asset_vol = returns.tail(self.lookback).std() * np.sqrt(252)
261
+ if asset_vol == 0:
262
+ return 0
263
+
264
+ weight = self.target_vol / asset_vol
265
+ weight = min(weight, 2.0) # Cap at 2x leverage per asset
266
+ notional = portfolio_value * weight * abs(signal_strength)
267
+ shares = int(notional / price) if price > 0 else 0
268
+ return shares
269
+ ```
270
+
271
+ ### Stop-Loss Patterns
272
+
273
+ Stop-losses protect against large losses. The stop type should match the strategy timeframe and volatility:
274
+
275
+ ```python
276
+ # strategies/risk/stop_loss.py
277
+ import pandas as pd
278
+ import numpy as np
279
+
280
+ class ATRStop:
281
+ """ATR-based stop-loss that adapts to volatility."""
282
+
283
+ def __init__(self, atr_period: int = 14, atr_multiplier: float = 2.0):
284
+ self.atr_period = atr_period
285
+ self.atr_multiplier = atr_multiplier
286
+
287
+ def compute_stops(self, data: pd.DataFrame, direction: int) -> pd.Series:
288
+ """Compute stop-loss levels based on ATR."""
289
+ atr = self._compute_atr(data)
290
+ if direction == 1: # Long: stop below entry
291
+ stops = data["close"] - atr * self.atr_multiplier
292
+ else: # Short: stop above entry
293
+ stops = data["close"] + atr * self.atr_multiplier
294
+ return stops
295
+
296
+ def _compute_atr(self, data: pd.DataFrame) -> pd.Series:
297
+ high_low = data["high"] - data["low"]
298
+ high_close = (data["high"] - data["close"].shift(1)).abs()
299
+ low_close = (data["low"] - data["close"].shift(1)).abs()
300
+ true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
301
+ return true_range.rolling(self.atr_period).mean()
302
+
303
+
304
+ class TrailingStop:
305
+ """Trailing stop that locks in profits as price moves favorably."""
306
+
307
+ def __init__(self, trail_pct: float = 0.05):
308
+ self.trail_pct = trail_pct
309
+
310
+ def track(self, prices: pd.Series, direction: int) -> pd.Series:
311
+ """Track trailing stop level through a position."""
312
+ stops = pd.Series(index=prices.index, dtype=float)
313
+
314
+ if direction == 1: # Long
315
+ peak = prices.iloc[0]
316
+ for i, price in enumerate(prices):
317
+ peak = max(peak, price)
318
+ stops.iloc[i] = peak * (1 - self.trail_pct)
319
+ else: # Short
320
+ trough = prices.iloc[0]
321
+ for i, price in enumerate(prices):
322
+ trough = min(trough, price)
323
+ stops.iloc[i] = trough * (1 + self.trail_pct)
324
+
325
+ return stops
326
+
327
+
328
+ class TimeStop:
329
+ """Exit after a maximum holding period regardless of P&L."""
330
+
331
+ def __init__(self, max_bars: int = 20):
332
+ self.max_bars = max_bars
333
+
334
+ def should_exit(self, bars_held: int) -> bool:
335
+ return bars_held >= self.max_bars
336
+ ```
337
+
338
+ ### Multi-Asset Allocation
339
+
340
+ When trading multiple instruments, allocation determines the portfolio weights across assets:
341
+
342
+ ```python
343
+ # strategies/allocation/multi_asset.py
344
+ import numpy as np
345
+ import pandas as pd
346
+
347
+ def equal_weight(signals: dict[str, float]) -> dict[str, float]:
348
+ """Equal weight across all assets with active signals."""
349
+ active = {k: v for k, v in signals.items() if v != 0}
350
+ if not active:
351
+ return {k: 0.0 for k in signals}
352
+ weight = 1.0 / len(active)
353
+ return {k: weight * np.sign(v) if v != 0 else 0.0 for k, v in signals.items()}
354
+
355
+
356
+ def signal_weighted(signals: dict[str, float]) -> dict[str, float]:
357
+ """Weight proportional to signal strength."""
358
+ total = sum(abs(v) for v in signals.values())
359
+ if total == 0:
360
+ return {k: 0.0 for k in signals}
361
+ return {k: v / total for k, v in signals.items()}
362
+
363
+
364
+ def inverse_volatility(
365
+ returns: pd.DataFrame,
366
+ lookback: int = 60,
367
+ ) -> dict[str, float]:
368
+ """Allocate inversely proportional to recent volatility."""
369
+ recent = returns.tail(lookback)
370
+ vols = recent.std() * np.sqrt(252)
371
+ inv_vol = 1.0 / vols.replace(0, np.inf)
372
+ weights = inv_vol / inv_vol.sum()
373
+ return weights.to_dict()
374
+
375
+
376
+ def risk_parity(
377
+ returns: pd.DataFrame,
378
+ lookback: int = 60,
379
+ target_vol: float = 0.10,
380
+ ) -> dict[str, float]:
381
+ """
382
+ Risk parity — equal risk contribution from each asset.
383
+
384
+ Each asset contributes equally to portfolio volatility.
385
+ """
386
+ recent = returns.tail(lookback)
387
+ cov = recent.cov() * 252
388
+ n = len(cov)
389
+
390
+ # Simple approximation: inverse vol with correlation adjustment
391
+ vols = np.sqrt(np.diag(cov))
392
+ inv_vol = 1.0 / vols
393
+ weights = inv_vol / inv_vol.sum()
394
+
395
+ # Scale to target volatility
396
+ port_vol = np.sqrt(weights @ cov.values @ weights)
397
+ if port_vol > 0:
398
+ weights *= target_vol / port_vol
399
+
400
+ return dict(zip(returns.columns, weights))
401
+ ```
402
+
403
+ ### Strategy Composition Pattern
404
+
405
+ Compose strategies from independent, tested components using a pipeline:
406
+
407
+ ```
408
+ Signal Generators → Confirmation Filter → Position Sizer → Stop Manager → Allocation
409
+ (alpha) (noise reduction) (risk mgmt) (protection) (portfolio)
410
+ ```
411
+
412
+ Each component is tested in isolation before the pipeline is assembled. This enables systematic A/B testing of individual components (e.g., "does adding a volume filter improve the momentum signal?") without rebuilding the entire strategy.
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: research-requirements
3
+ description: Research project requirements including experiment goals, success metrics, iteration budgets, stopping criteria, and hypothesis documentation
4
+ topics: [research, requirements, experiment-goals, metrics, stopping-criteria, hypothesis]
5
+ ---
6
+
7
+ Research projects differ from product engineering in a fundamental way: the requirements are not a list of features to build but a set of questions to answer. A research PRD defines the hypothesis space, the metrics that will determine success or failure, the computational and time budgets for exploration, and the stopping criteria that prevent infinite iteration. Without these constraints, autonomous experiment loops run forever and produce nothing actionable.
8
+
9
+ ## Summary
10
+
11
+ Define research requirements as structured hypotheses with measurable success criteria. Establish iteration budgets (wall-clock time, compute cost, number of runs) and stopping criteria (convergence thresholds, diminishing returns, budget exhaustion) before the first experiment. Document every hypothesis with its rationale, expected outcome, and evaluation method. Use a decision log to record keep/discard decisions with justifications.
12
+
13
+ ## Deep Guidance
14
+
15
+ ### Hypothesis Documentation
16
+
17
+ Every research project starts with one or more hypotheses. Each hypothesis must be specific enough to be falsifiable and must specify how it will be evaluated:
18
+
19
+ ```markdown
20
+ # Hypothesis Registry
21
+
22
+ ## H-001: Momentum crossover with adaptive lookback
23
+ - **Statement**: A momentum crossover strategy using adaptive lookback periods
24
+ (10-50 day range, optimised per asset) will achieve a Sharpe ratio > 1.5
25
+ on out-of-sample data (2020-2023).
26
+ - **Rationale**: Fixed lookback periods fail to adapt to regime changes.
27
+ Adaptive periods should capture both trending and mean-reverting regimes.
28
+ - **Success criteria**: Sharpe ratio > 1.5, max drawdown < 15%, positive
29
+ returns in at least 3 of 4 years.
30
+ - **Evaluation method**: Walk-forward analysis with 252-day training window,
31
+ 63-day test window, no look-ahead bias.
32
+ - **Budget**: 500 experiment runs, 48 hours wall-clock time.
33
+ - **Status**: In progress
34
+ - **Decision**: [pending]
35
+ ```
36
+
37
+ ### Success Metrics Framework
38
+
39
+ Research metrics must distinguish between primary objectives and guardrails:
40
+
41
+ ```python
42
+ # configs/metrics.py
43
+ from dataclasses import dataclass, field
44
+ from typing import Optional
45
+
46
+ @dataclass
47
+ class MetricDefinition:
48
+ """A single metric with its target and guardrail thresholds."""
49
+ name: str
50
+ direction: str # "maximize" or "minimize"
51
+ target: float # Primary success threshold
52
+ guardrail: Optional[float] = None # Hard constraint (never violated)
53
+ description: str = ""
54
+
55
+ @dataclass
56
+ class SuccessCriteria:
57
+ """Complete success criteria for a research project."""
58
+ primary: MetricDefinition # The one metric to optimize
59
+ secondary: list[MetricDefinition] = field(default_factory=list)
60
+ guardrails: list[MetricDefinition] = field(default_factory=list)
61
+
62
+ def is_success(self, results: dict[str, float]) -> bool:
63
+ """Check if results meet all success criteria."""
64
+ # Primary metric must meet target
65
+ primary_val = results[self.primary.name]
66
+ if self.primary.direction == "maximize" and primary_val < self.primary.target:
67
+ return False
68
+ if self.primary.direction == "minimize" and primary_val > self.primary.target:
69
+ return False
70
+
71
+ # All guardrails must be satisfied
72
+ for g in self.guardrails:
73
+ val = results[g.name]
74
+ if g.direction == "maximize" and val < g.guardrail:
75
+ return False
76
+ if g.direction == "minimize" and val > g.guardrail:
77
+ return False
78
+
79
+ return True
80
+
81
+ # Example: trading strategy research
82
+ criteria = SuccessCriteria(
83
+ primary=MetricDefinition(
84
+ name="sharpe_ratio",
85
+ direction="maximize",
86
+ target=1.5,
87
+ description="Risk-adjusted return on out-of-sample data",
88
+ ),
89
+ guardrails=[
90
+ MetricDefinition(
91
+ name="max_drawdown",
92
+ direction="minimize",
93
+ target=0.15,
94
+ guardrail=0.25,
95
+ description="Maximum peak-to-trough decline",
96
+ ),
97
+ MetricDefinition(
98
+ name="num_trades",
99
+ direction="maximize",
100
+ target=100,
101
+ guardrail=30,
102
+ description="Minimum trades for statistical significance",
103
+ ),
104
+ ],
105
+ )
106
+ ```
107
+
108
+ ### Iteration Budgets
109
+
110
+ Unbounded iteration is the primary failure mode of research projects. Define hard limits before starting:
111
+
112
+ | Budget Type | Example Limit | Enforcement |
113
+ |-------------|--------------|-------------|
114
+ | Run count | 500 experiments | Counter in experiment runner |
115
+ | Wall-clock time | 48 hours | Watchdog timer / cron kill |
116
+ | Compute cost | $200 | Cloud billing alerts |
117
+ | Convergence patience | 50 runs without improvement | Early stopping callback |
118
+ | Human review interval | Every 100 runs | Checkpoint gate |
119
+
120
+ ```python
121
+ # configs/budget.py
122
+ from dataclasses import dataclass
123
+ from datetime import timedelta
124
+
125
+ @dataclass
126
+ class IterationBudget:
127
+ """Hard limits on experiment iteration."""
128
+ max_runs: int = 500
129
+ max_wall_time: timedelta = timedelta(hours=48)
130
+ max_cost_usd: float = 200.0
131
+ patience: int = 50 # Runs without improvement before early stop
132
+ checkpoint_interval: int = 100 # Runs between human review gates
133
+
134
+ def is_exhausted(self, runs: int, elapsed: timedelta, cost: float,
135
+ runs_since_improvement: int) -> tuple[bool, str]:
136
+ """Check if any budget limit has been reached."""
137
+ if runs >= self.max_runs:
138
+ return True, f"Run limit reached ({runs}/{self.max_runs})"
139
+ if elapsed >= self.max_wall_time:
140
+ return True, f"Time limit reached ({elapsed})"
141
+ if cost >= self.max_cost_usd:
142
+ return True, f"Cost limit reached (${cost:.2f}/${self.max_cost_usd})"
143
+ if runs_since_improvement >= self.patience:
144
+ return True, f"Patience exhausted ({runs_since_improvement} runs without improvement)"
145
+ return False, ""
146
+ ```
147
+
148
+ ### Stopping Criteria
149
+
150
+ Stopping criteria are decision functions that determine when to halt iteration. They are distinct from budget exhaustion (which is a hard stop) -- stopping criteria detect when further iteration is unlikely to help:
151
+
152
+ 1. **Convergence**: The objective metric has plateaued (moving average change < epsilon for N runs).
153
+ 2. **Diminishing returns**: Each successive improvement is smaller than the previous. Extrapolate the improvement curve to estimate remaining gain vs. cost.
154
+ 3. **Statistical saturation**: Additional runs are not changing the confidence interval on the primary metric.
155
+ 4. **Guardrail violation**: A constraint metric has entered an unacceptable range and parameter adjustments are not recovering it.
156
+
157
+ ```python
158
+ import numpy as np
159
+
160
+ def detect_convergence(metric_history: list[float], window: int = 20,
161
+ epsilon: float = 1e-4) -> bool:
162
+ """Detect if a metric has converged (plateau detection)."""
163
+ if len(metric_history) < window * 2:
164
+ return False
165
+ recent = np.array(metric_history[-window:])
166
+ prior = np.array(metric_history[-2 * window:-window])
167
+ return abs(recent.mean() - prior.mean()) < epsilon
168
+
169
+ def detect_diminishing_returns(improvements: list[float],
170
+ min_improvement: float = 0.001) -> bool:
171
+ """Detect if improvements are shrinking below a useful threshold."""
172
+ if len(improvements) < 5:
173
+ return False
174
+ recent = improvements[-5:]
175
+ return all(abs(imp) < min_improvement for imp in recent)
176
+ ```
177
+
178
+ ### Decision Log
179
+
180
+ Every keep/discard decision in the experiment loop must be logged with its rationale. This is the audit trail that makes research reproducible and reviewable:
181
+
182
+ ```markdown
183
+ # Decision Log
184
+
185
+ | Run | Hypothesis | Result | Decision | Rationale |
186
+ |-----|-----------|--------|----------|-----------|
187
+ | 042 | H-001 (lookback=20) | Sharpe=1.2, DD=18% | Keep | Best Sharpe so far, DD within guardrail |
188
+ | 043 | H-001 (lookback=10) | Sharpe=0.8, DD=22% | Discard | Sharpe below baseline, high drawdown |
189
+ | 044 | H-001 (lookback=30) | Sharpe=1.3, DD=12% | Keep | New best, low drawdown |
190
+ | 045 | H-002 (mean-revert) | Sharpe=0.4, DD=8% | Discard | Sharpe far below target despite low DD |
191
+ ```
192
+
193
+ ### Requirements Anti-Patterns
194
+
195
+ Avoid these common failures in research project requirements:
196
+
197
+ - **Vague success criteria**: "Find a good strategy" is not testable. Specify a number.
198
+ - **No stopping criteria**: "Keep trying until it works" leads to infinite iteration.
199
+ - **Optimising too many metrics**: Pick one primary metric. Everything else is a guardrail.
200
+ - **No out-of-sample plan**: If success criteria are evaluated on training data, the project will "succeed" on noise.
201
+ - **Scope creep during iteration**: The hypothesis should be fixed before iteration starts. If you want to explore a different hypothesis, start a new experiment, don't modify the current one mid-run.