feat(trading-strategies): add declarative strategy graphs (GraphStrategy)#1190
Open
bennycode wants to merge 2 commits into
Open
feat(trading-strategies): add declarative strategy graphs (GraphStrategy)#1190bennycode wants to merge 2 commits into
bennycode wants to merge 2 commits into
Conversation
…egy) Strategies can now be expressed as JSON graphs of typed building blocks (candle source, batcher, field, indicator, if, order advice) and executed by GraphStrategy, an interpreter implementing TradingSessionStrategy — so graphs backtest and trade through the same executors as hand-written strategies. - Zod-validated graph schema (versioned) with node-addressable errors, cycle detection, port type checking, and single-writer inputs - Node registry with registerNodeType() as a public extension point; evaluators support async evaluate() and an async init() hook for loading external datasets deterministically - Warm-up safety: nodes emit only when stable; held values between ticks make mixed timeframes structurally lookahead-free - Equivalence proven by tests: the SMA crossover template graph produces advice-for-advice and balance-identical backtests vs SmaCrossoverStrategy
This was referenced Jul 12, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new “strategy graph” subsystem to packages/trading-strategies, allowing strategies to be represented as versioned, Zod-validated JSON graphs executed by a GraphStrategy interpreter, with built-in node types and a registry for extensions.
Changes:
- Introduces
GraphStrategyto compile/validate and execute declarative strategy graphs via node evaluators. - Adds graph schema + built-in node definitions/registry (batching, fields, indicators, conditionals, advice sinks) and a SMA crossover template graph.
- Exposes the new graph API from the package entrypoint and adds a comprehensive Vitest suite (validation, equivalence, triggers, custom nodes).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/trading-strategies/src/strategy-graph/templates.ts | Adds a reusable SMA-crossover starter/fixture graph definition. |
| packages/trading-strategies/src/strategy-graph/NodeRegistry.ts | Defines built-in node types, Zod config schemas, and the extensible node registry. |
| packages/trading-strategies/src/strategy-graph/index.ts | Public exports for the strategy-graph module (strategy, schema, registry, templates). |
| packages/trading-strategies/src/strategy-graph/GraphStrategy.ts | Implements graph compilation, validation, topological ordering, and per-candle interpretation. |
| packages/trading-strategies/src/strategy-graph/GraphStrategy.test.ts | Adds validation, equivalence, trigger semantics, advice payload, and custom node tests. |
| packages/trading-strategies/src/strategy-graph/GraphSchema.ts | Introduces the versioned Zod schema for graph JSON inputs. |
| packages/trading-strategies/src/index.ts | Re-exports the strategy-graph module from the package root API. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+157
to
+167
| const nodeIds = Object.keys(graph.nodes); | ||
| const dependents = new Map<string, string[]>(nodeIds.map(id => [id, []])); | ||
| const pendingInputs = new Map<string, number>(nodeIds.map(id => [id, 0])); | ||
|
|
||
| for (const {from, to} of graph.connections) { | ||
| dependents.get(from.node)!.push(to.node); | ||
| pendingInputs.set(to.node, pendingInputs.get(to.node)! + 1); | ||
| } | ||
|
|
||
| const queue = nodeIds.filter(id => pendingInputs.get(id) === 0); | ||
| const order: string[] = []; |
Comment on lines
+186
to
+189
| // Guarantee at least one path can produce advice; a graph without a sink can never trade. | ||
| if (![...definitions.values()].some(definition => definition.category === 'sink')) { | ||
| throw new Error('Graph has no advice node — it can never produce a trade'); | ||
| } |
Comment on lines
+99
to
+103
| const result = definition.configSchema.safeParse(node.config ?? {}); | ||
| if (!result.success) { | ||
| const issues = result.error.issues.map(issue => `${issue.path.join('.')}: ${issue.message}`).join('; '); | ||
| throw new Error(`Node "${id}": invalid config — ${issues}`); | ||
| } |
- Sort node ids and dependents in the topological sort so evaluation order (and the first-advice-wins rule) depends only on graph structure, never on JSON key or connection ordering — with a regression test racing two advice nodes under reversed serialization - Drop the leading ': ' from config errors produced by refinement issues with an empty path - Reword the missing-sink error to match the actual check (any sink node), since registerNodeType() allows custom sinks
Owner
Author
|
Addressed all three review findings in dbdec89:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Strategies expressed as data instead of code: a versioned, Zod-validated JSON graph of typed building blocks, executed by
GraphStrategy— an interpreter implementingTradingSessionStrategy. Graphs backtest and trade through the exact same executors as hand-written strategies; there is no separate toy runtime whose results could diverge.Building blocks (v1)
source:candlebatcherfieldindicatorifonChangetrigger = crossover edge detectionadviceOrderAdvicewhen its trigger firesDesign highlights
getResultOrThrow()strictly behind theisStablegate); held values between ticks make mixed timeframes structurally lookahead-free — a 5m indicator holds its last closed bar between batch closes.registerNodeType()lets third parties add nodes (news signals, on-chain data). Evaluators support asyncevaluate()and an asyncinit()hook (wired to the session/backtest init from fix(trading-strategies): call strategy init in BacktestExecutor to mirror TradingSession #1189) for loading external datasets deterministically.Proof
The SMA crossover template graph produces advice-for-advice identical output to the hand-written
SmaCrossoverStrategyacross shared- and mixed-timeframe configs, and a balance-identical backtest to 8 decimal places (same trades, fees, P&L) viaBacktestExecutor.Test plan
21 new tests: equivalence (3 configs + full backtest), validation errors, trigger semantics (
onChangevsalways), advice payloads, custom node registration incl. async evaluate/init. Full suite: 283 green, lint clean, build clean.