programgarden-core 2.1.0__tar.gz → 2.2.0__tar.gz

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 (86) hide show
  1. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/PKG-INFO +1 -1
  2. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/account_futures.py +6 -0
  3. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/account_korea_stock.py +6 -0
  4. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/account_stock.py +6 -0
  5. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest_futures.py +7 -0
  6. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest_korea_stock.py +7 -0
  7. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest_stock.py +7 -0
  8. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/broker.py +10 -0
  9. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/code.py +3 -0
  10. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/data.py +4 -0
  11. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/data_futures.py +5 -0
  12. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/data_korea_stock.py +5 -0
  13. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/data_stock.py +5 -0
  14. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/infra.py +9 -2
  15. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/open_orders_futures.py +7 -0
  16. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/open_orders_korea_stock.py +7 -0
  17. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/open_orders_stock.py +7 -0
  18. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/order.py +61 -0
  19. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/realtime_futures.py +5 -0
  20. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/realtime_korea_stock.py +5 -0
  21. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/realtime_stock.py +5 -0
  22. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/session_gate.py +1 -1
  23. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/trigger.py +7 -0
  24. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/registry/node_registry.py +14 -0
  25. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/pyproject.toml +1 -1
  26. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/README.md +0 -0
  27. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/__init__.py +0 -0
  28. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/bases/__init__.py +0 -0
  29. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/bases/client.py +0 -0
  30. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/bases/components.py +0 -0
  31. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/bases/listener.py +0 -0
  32. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/bases/mixins.py +0 -0
  33. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/bases/products.py +0 -0
  34. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/bases/sql.py +0 -0
  35. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/bases/storage.py +0 -0
  36. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/code_node.py +0 -0
  37. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/exceptions.py +0 -0
  38. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/expression/__init__.py +0 -0
  39. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/expression/evaluator.py +0 -0
  40. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/i18n/__init__.py +0 -0
  41. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/i18n/locales/en.json +0 -0
  42. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/i18n/locales/ko.json +0 -0
  43. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/i18n/translator.py +0 -0
  44. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/korea_alias.py +0 -0
  45. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/__init__.py +0 -0
  46. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/connection_rule.py +0 -0
  47. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/credential.py +0 -0
  48. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/edge.py +0 -0
  49. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/event.py +0 -0
  50. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/exchange.py +0 -0
  51. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/field_binding.py +0 -0
  52. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/job.py +0 -0
  53. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/order_diagnostics.py +0 -0
  54. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/plugin_resource.py +0 -0
  55. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/resilience.py +0 -0
  56. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/resource.py +0 -0
  57. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/validation.py +0 -0
  58. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/models/workflow.py +0 -0
  59. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/__init__.py +0 -0
  60. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/ai.py +0 -0
  61. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/backtest.py +0 -0
  62. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/base.py +0 -0
  63. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/calculation.py +0 -0
  64. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/condition.py +0 -0
  65. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/display.py +0 -0
  66. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/event.py +0 -0
  67. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
  68. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
  69. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/market_external.py +0 -0
  70. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/market_status.py +0 -0
  71. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/orderable_quantity_futures.py +0 -0
  72. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/portfolio.py +0 -0
  73. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/risk.py +0 -0
  74. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/symbol.py +0 -0
  75. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/symbol_futures.py +0 -0
  76. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
  77. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/nodes/symbol_stock.py +0 -0
  78. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/presets/__init__.py +0 -0
  79. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/presets/news_analyst.json +0 -0
  80. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/presets/risk_manager.json +0 -0
  81. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/presets/strategist.json +0 -0
  82. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/presets/technical_analyst.json +0 -0
  83. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/registry/__init__.py +0 -0
  84. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/registry/credential_registry.py +0 -0
  85. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/registry/plugin_registry.py +0 -0
  86. {programgarden_core-2.1.0 → programgarden_core-2.2.0}/programgarden_core/retry_executor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden-core
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -65,6 +65,12 @@ class OverseasFuturesAccountNode(BaseNode):
65
65
  "Returns three ports: held_symbols (symbol list), balance (margin/equity summary), positions (per-contract P&L)",
66
66
  "is_tool_enabled=True — AI Agent can call this node as a tool to inspect futures portfolio state",
67
67
  "One-shot REST call; supports paper_trading mode via OverseasFuturesBrokerNode",
68
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
69
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
70
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
71
+ "Its recording (held_symbols, balance, positions) is what the workflow's guard reads; the replay's simulated ledger is seeded only from fixture.broker.account, and a holding used by a scenario must appear in both with the same integer quantity",
72
+ "A frame that submits no order must expect exactly the seeded cash and positions; an insufficient_cash case needs both the seeded cash and the recorded orderable_amount below quantity times the quoted price",
73
+ "Retained across realtime events (queried once at startup); re-runs on every schedule tick",
68
74
  ]
69
75
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
70
76
  {
@@ -64,6 +64,12 @@ class KoreaStockAccountNode(BaseNode):
64
64
  "Returns three ports: held_symbols (symbol list with KRX codes), balance (KRW cash/equity summary), positions (per-symbol P&L in KRW)",
65
65
  "is_tool_enabled=True — AI Agent can call this node as a tool to inspect Korea stock portfolio state",
66
66
  "Real trading only (KoreaStockBrokerNode does not support paper_trading mode); no mock data",
67
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
68
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
69
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
70
+ "Its recording (held_symbols, balance, positions) is what the workflow's guard reads; the replay's simulated ledger is seeded only from fixture.broker.account, and a holding used by a scenario must appear in both with the same integer quantity",
71
+ "A frame that submits no order must expect exactly the seeded cash and positions; an insufficient_cash case needs both the seeded cash and the recorded orderable_amount below quantity times the quoted price",
72
+ "Retained across realtime events (queried once at startup); re-runs on every schedule tick",
67
73
  ]
68
74
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
69
75
  {
@@ -66,6 +66,12 @@ class OverseasStockAccountNode(BaseNode):
66
66
  "Returns three ports: held_symbols (symbol list), balance (cash/equity summary), positions (per-symbol P&L)",
67
67
  "is_tool_enabled=True — AI Agent can call this node as a tool to inspect portfolio state",
68
68
  "One-shot REST call: safe to use in scheduled or on-demand workflows without WebSocket overhead",
69
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
70
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
71
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
72
+ "Its recording (held_symbols, balance, positions) is what the workflow's guard reads; the replay's simulated ledger is seeded only from fixture.broker.account, and a holding used by a scenario must appear in both with the same integer quantity",
73
+ "A frame that submits no order must expect exactly the seeded cash and positions; an insufficient_cash case needs both the seeded cash and the recorded orderable_amount below quantity times the quoted price",
74
+ "Retained across realtime events (queried once at startup); re-runs on every schedule tick",
69
75
  ]
70
76
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
71
77
  {
@@ -102,6 +102,13 @@ class OverseasFuturesHistoricalDataNode(BaseNode):
102
102
  "adjust=True applies roll-adjusted prices for continuous contract backtesting",
103
103
  "is_tool_enabled=True — AI Agent can fetch futures history for technical analysis autonomously",
104
104
  "Item-based execution: pair with SplitNode to fetch OHLCV for multiple contracts",
105
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
106
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
107
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
108
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
109
+ "The value port is one row object whose time_series lists bars of exactly {date, open, high, low, close, volume}; the values port is the array of such rows",
110
+ "This ohlcv_data is not the realtime shape: a *RealMarketDataNode records ohlcv_data as a symbol-keyed object of bar lists, although both are typed ohlcv_data",
111
+ "A previous close is the close of the corresponding bar in time_series; a change cannot be computed from a single bar",
105
112
  ]
106
113
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
107
114
  {
@@ -102,6 +102,13 @@ class KoreaStockHistoricalDataNode(BaseNode):
102
102
  "start_date / end_date accept YYYYMMDD strings or `{{ date.ago(N, format='yyyymmdd') }}` expressions",
103
103
  "is_tool_enabled=True — AI Agent can fetch domestic historical data for fundamental/technical analysis",
104
104
  "Symbol format: 6-digit KRX code (e.g., '005930') without exchange field",
105
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
106
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
107
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
108
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
109
+ "The value port is one row object whose time_series lists bars of exactly {date, open, high, low, close, volume}; the values port is the array of such rows",
110
+ "This ohlcv_data is not the realtime shape: a *RealMarketDataNode records ohlcv_data as a symbol-keyed object of bar lists, although both are typed ohlcv_data",
111
+ "A previous close is the close of the corresponding bar in time_series; a change cannot be computed from a single bar",
105
112
  ]
106
113
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
107
114
  {
@@ -102,6 +102,13 @@ class OverseasStockHistoricalDataNode(BaseNode):
102
102
  "adjust=True applies split/dividend-adjusted prices for accurate long-term backtesting",
103
103
  "is_tool_enabled=True — AI Agent can fetch historical data autonomously for technical analysis",
104
104
  "Item-based execution: pair with SplitNode to fetch history for each symbol in a watchlist",
105
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
106
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
107
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
108
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
109
+ "The value port is one row object whose time_series lists bars of exactly {date, open, high, low, close, volume}; the values port is the array of such rows",
110
+ "This ohlcv_data is not the realtime shape: a *RealMarketDataNode records ohlcv_data as a symbol-keyed object of bar lists, although both are typed ohlcv_data",
111
+ "A previous close is the close of the corresponding bar in time_series; a change cannot be computed from a single bar",
105
112
  ]
106
113
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
107
114
  {
@@ -101,6 +101,10 @@ class OverseasStockBrokerNode(BaseBrokerNode):
101
101
  "Creates and caches the LS-Sec session so every downstream overseas_stock node shares the same login",
102
102
  "`connection` output is auto-injected into every overseas_stock node by the executor — no manual binding required",
103
103
  "credential_types=['broker_ls_overseas_stock'] ensures only the correct credential is selected",
104
+ "On the first run its recording's request carries no connection; its output is {\"connection\": {provider, product, paper_trading, broker_node_id, and credential_id only when the account is linked}}, and an unlinked account has no credential_id key (never null)",
105
+ "On a schedule tick the executor injects this broker's own retained output.connection into its request, so a broker tick recording must repeat that connection or the replay refuses it",
106
+ "Downstream product-scoped nodes reference it by request.connection.broker_node_id set to this node's id; the compiler completes provider/product/paper_trading (and credential_id) from this broker's recorded output.connection",
107
+ "paper_trading must be false; the executor raises if it is true",
104
108
  ]
105
109
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
106
110
  {
@@ -295,6 +299,9 @@ class OverseasFuturesBrokerNode(BaseBrokerNode):
295
299
  "Only broker that natively supports LS-Sec paper trading (set paper_trading=True)",
296
300
  "Auto-injects the connection into every overseas_futures node in the same workflow",
297
301
  "credential_types=['broker_ls_overseas_futures'] keeps the selector scoped",
302
+ "On the first run its recording's request carries no connection; its output is {\"connection\": {provider, product, paper_trading, broker_node_id, and credential_id only when the account is linked}}, and an unlinked account has no credential_id key (never null)",
303
+ "On a schedule tick the executor injects this broker's own retained output.connection into its request, so a broker tick recording must repeat that connection or the replay refuses it",
304
+ "Downstream product-scoped nodes reference it by request.connection.broker_node_id set to this node's id; the compiler completes provider/product/paper_trading (and credential_id) from this broker's recorded output.connection",
298
305
  ]
299
306
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
300
307
  {
@@ -455,6 +462,9 @@ class KoreaStockBrokerNode(BaseBrokerNode):
455
462
  "Dedicated credential type `broker_ls_korea_stock` keeps KRX credentials isolated from overseas ones",
456
463
  "Auto-injects connection into every korea_stock scoped node",
457
464
  "Executes the Korean-specific workflow P&L tracker (KRW-denominated) when account nodes are wired",
465
+ "On the first run its recording's request carries no connection; its output is {\"connection\": {provider, product, paper_trading, broker_node_id, and credential_id only when the account is linked}}, and an unlinked account has no credential_id key (never null)",
466
+ "On a schedule tick the executor injects this broker's own retained output.connection into its request, so a broker tick recording must repeat that connection or the replay refuses it",
467
+ "Downstream product-scoped nodes reference it by request.connection.broker_node_id set to this node's id; the compiler completes provider/product/paper_trading (and credential_id) from this broker's recorded output.connection",
458
468
  ]
459
469
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
460
470
  {
@@ -168,6 +168,9 @@ class CodeNode(BaseNode):
168
168
  "Safe helper namespaces available on context (date/finance/stats/format/lst) mirror expression bindings",
169
169
  "Batch-friendly: receives the whole upstream array in `data` (one subprocess call, loop in-code)",
170
170
  "Multi-upstream: accepts any number of upstream producers — bind each one to its own expression-bound `params` key ({{ nodes.<id>.<port> }}) and read it with params.get(...)",
171
+ "The top-level output keys error (truthy) and reason in {no_symbol, no_price, invalid_input} are engine failure markers; never declare or expect them as ports — put a diagnostic inside a nested object port (e.g. result.error) or a differently named port",
172
+ "A computation node: it receives no external recording in a suite (its code runs) and keeps no state across ticks; a durable one-shot guard needs ThrottleNode or SQLiteNode",
173
+ "Completing with a boolean false does not gate a downstream order node; route the boolean through an IfNode true edge",
171
174
  ]
172
175
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
173
176
  {
@@ -176,6 +176,10 @@ class SQLiteNode(BaseNode):
176
176
  "Outputs rows (list), affected_count (int), and last_insert_id (int) to cover all read/write patterns",
177
177
  "is_tool_enabled=True — AI Agent can use SQLiteNode as a tool to read or write local state",
178
178
  "Databases are scoped to /app/data/ directory; db_name selects the file",
179
+ "Has a dedicated replay adapter (execute_sqlite) and takes no external recording; it runs against a file-backed database rooted at the run's storage directory",
180
+ "State written on one tick or event frame persists to that database and stays visible on later frames within the run, so a durable duplicate guard can be built on it",
181
+ "Each execution runs exactly ONE SQL statement (execute_query and every simple-mode action call db.execute once); 'CREATE TABLE …; INSERT …' in one query fails with 'You can only execute one statement at a time'",
182
+ "Neither mode creates a table: an insert/select into a missing table fails with 'no such table'; create it first with a separate execute_query SQLiteNode (CREATE TABLE IF NOT EXISTS …) placed upstream, then read/write it here",
179
183
  ]
180
184
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
181
185
  {
@@ -90,6 +90,11 @@ class OverseasFuturesMarketDataNode(BaseNode):
90
90
  "is_tool_enabled=True — AI Agent can call this node to look up live futures prices autonomously",
91
91
  "Supported exchanges: CME, EUREX, SGX, HKEX — symbol format includes contract month code (e.g., ESH26)",
92
92
  "Broker connection is auto-injected via DAG traversal from OverseasFuturesBrokerNode",
93
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
94
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
95
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
96
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
97
+ "In replay the one-shot quote records its `values` port as a flat row list at nodes.<id>.output.values; this is not the realtime symbol-keyed ohlcv_data shape",
93
98
  ]
94
99
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
95
100
  {
@@ -85,6 +85,11 @@ class KoreaStockMarketDataNode(BaseNode):
85
85
  "Item-based execution: pair with SplitNode to query multiple domestic stocks in sequence",
86
86
  "is_tool_enabled=True — AI Agent can call this node to look up KRX stock prices autonomously",
87
87
  "Real-trading only — KoreaStock product does not support paper trading (paper_trading=False enforced)",
88
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
89
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
90
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
91
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
92
+ "In replay the one-shot quote records its `values` port as a flat row list at nodes.<id>.output.values; this is not the realtime symbol-keyed ohlcv_data shape",
88
93
  ]
89
94
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
90
95
  {
@@ -87,6 +87,11 @@ class OverseasStockMarketDataNode(BaseNode):
87
87
  "is_tool_enabled=True — AI Agent can call this node to look up live prices autonomously",
88
88
  "Broker connection is auto-injected via DAG traversal — no explicit binding needed",
89
89
  "REST-based polling; for continuous streaming use OverseasStockRealMarketDataNode",
90
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
91
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
92
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
93
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
94
+ "In replay the one-shot quote records its `values` port as a flat row list at nodes.<id>.output.values; this is not the realtime symbol-keyed ohlcv_data shape",
90
95
  ]
91
96
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
92
97
  {
@@ -66,6 +66,9 @@ class StartNode(BaseNode):
66
66
  "Zero configuration — no fields, no credentials",
67
67
  "Produces a simple trigger signal that flows through main edges",
68
68
  "Always completes instantly and never fails",
69
+ "Exactly one StartNode per workflow; a graph rooted at a ScheduleNode is rejected (MISSING_START_NODE)",
70
+ "Emits no subsequent event: a StartNode-only workflow runs once and cannot carry a duplicate or events scenario",
71
+ "The start port is a bare trigger signal; an output contract cannot assert a data type on it",
69
72
  ]
70
73
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
71
74
  {
@@ -75,7 +78,7 @@ class StartNode(BaseNode):
75
78
  },
76
79
  {
77
80
  "pattern": "Omitting StartNode and wiring ScheduleNode as the root",
78
- "reason": "Works for scheduled workflows but some example templates and validators assume a StartNode anchor.",
81
+ "reason": "The engine rejects a graph rooted at a ScheduleNode (MISSING_START_NODE); a ScheduleNode is never the root.",
79
82
  "alternative": "Always include a StartNode and connect it to the ScheduleNode as the first main edge.",
80
83
  },
81
84
  ]
@@ -233,6 +236,9 @@ class ThrottleNode(BaseNode):
233
236
  "Two modes: 'skip' (drop during cooldown) and 'latest' (buffer newest, emit at window close)",
234
237
  "pass_first=True emits the first event immediately, useful for warm-start flows",
235
238
  "Emits _throttle_stats output for observability (received / passed / skipped counts)",
239
+ "Cooldown state persists in context.node_state, so it survives re-triggers within one execution: with pass_first=true the first event passes and a further event within interval_sec is throttled",
240
+ "While throttled it emits only its `_`-prefixed ports (_throttled/_throttle_stats), which never leak downstream, so a node placed after it does not re-execute",
241
+ "interval_sec is bounded between 0.1 and 300 seconds",
236
242
  ]
237
243
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
238
244
  {
@@ -974,7 +980,8 @@ class IfNode(BaseNode):
974
980
  "12 comparison operators including ==, !=, >, >=, <, <=, in, not_in, contains, not_contains, is_empty, is_not_empty",
975
981
  "Three outputs: `true` / `false` payloads + `result` boolean — edges use from_port to route",
976
982
  "Expression binding on both `left` and `right` operands — supports full `{{ nodes.X.Y }}` syntax",
977
- "Cascading skip — downstream of the inactive branch is auto-skipped by the executor",
983
+ "Skips only descendants of the untaken port that have no other active path; a node also fed by a non-If upstream still runs when that upstream completes",
984
+ "Evaluates exactly one left/operator/right comparison per node; the untaken branch is the false port, and a CodeNode returning false skips nothing",
978
985
  ]
979
986
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
980
987
  {
@@ -62,6 +62,13 @@ class OverseasFuturesOpenOrdersNode(BaseNode):
62
62
  "Returns `open_orders` (list of unfilled futures order dicts) and `count` (integer) ports",
63
63
  "is_tool_enabled=True — AI Agent can query pending futures orders as a tool call",
64
64
  "Works in both paper_trading and real trading modes via the upstream OverseasFuturesBrokerNode",
65
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
66
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
67
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
68
+ "Retained across realtime events (queried once at startup); re-runs on every schedule tick",
69
+ "Each recorded row needs a non-empty unique order_id, and count must equal the number of rows; a snapshot lists only still-open orders, so a guard blocks on presence (count > 0), not on a derived remaining_quantity",
70
+ "order_events (modify/cancel completions: [{request_node, symbol_key, event_id, applied}]) is a top-level field of the recording beside request/as_of/output, never inside output",
71
+ "An order this workflow did not place (any non-SIM- id) is taken exactly as recorded: never retired by order_events, never reserving simulated cash, and listed in every OpenOrders recording of the same tick or in none",
65
72
  ]
66
73
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
67
74
  {
@@ -61,6 +61,13 @@ class KoreaStockOpenOrdersNode(BaseNode):
61
61
  "Returns `open_orders` (list of unfilled domestic order dicts) and `count` (integer) ports",
62
62
  "is_tool_enabled=True — AI Agent can query pending Korea stock orders as a tool call",
63
63
  "Real trading only — KoreaStockBrokerNode does not support paper_trading mode",
64
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
65
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
66
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
67
+ "Retained across realtime events (queried once at startup); re-runs on every schedule tick",
68
+ "Each recorded row needs a non-empty unique order_id, and count must equal the number of rows; a snapshot lists only still-open orders, so a guard blocks on presence (count > 0), not on a derived remaining_quantity",
69
+ "order_events (modify/cancel completions: [{request_node, symbol_key, event_id, applied}]) is a top-level field of the recording beside request/as_of/output, never inside output",
70
+ "An order this workflow did not place (any non-SIM- id) is taken exactly as recorded: never retired by order_events, never reserving simulated cash, and listed in every OpenOrders recording of the same tick or in none",
64
71
  ]
65
72
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
66
73
  {
@@ -62,6 +62,13 @@ class OverseasStockOpenOrdersNode(BaseNode):
62
62
  "Returns `open_orders` (list of unfilled order dicts) and `count` (integer) ports",
63
63
  "is_tool_enabled=True — AI Agent can query pending orders as a tool call",
64
64
  "One-shot REST call: suitable for scheduled checks or pre-trade validation steps",
65
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
66
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
67
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
68
+ "Retained across realtime events (queried once at startup); re-runs on every schedule tick",
69
+ "Each recorded row needs a non-empty unique order_id, and count must equal the number of rows; a snapshot lists only still-open orders, so a guard blocks on presence (count > 0), not on a derived remaining_quantity",
70
+ "order_events (modify/cancel completions: [{request_node, symbol_key, event_id, applied}]) is a top-level field of the recording beside request/as_of/output, never inside output",
71
+ "An order this workflow did not place (any non-SIM- id) is taken exactly as recorded: never retired by order_events, never reserving simulated cash, and listed in every OpenOrders recording of the same tick or in none",
65
72
  ]
66
73
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
67
74
  {
@@ -391,6 +391,16 @@ class OverseasStockNewOrderNode(BaseOrderNode):
391
391
  "retry is disabled by default (resilience.retry.enabled=False); only pure network-connection failures may be retried, never a submitted order",
392
392
  "US daytime (Blue Ocean) session accepts LIMIT orders only — a market order is refused at intake with rsp_cd=00891 and no order number (00891 is a generic order-price rejection: it was also observed on 2026-09-24 for a price of 1 USD or more sent with more than two decimals — '주문단가 오류. $1이상은 소수점 2째 자리까지 입력가능합니다'). A workflow scheduled across both the daytime and the regular overnight session must therefore use order_type='limit' for it to work in both.",
393
393
  "A limit order needs a price. Buy orders fall back to a current-price lookup, and so do sell orders since engine 1.37.3, but that lookup (g3101) only accepts exchange codes 81/82 — it fails for a holding listed on any other market. Prefer feeding the price the account balance already carries: price='{{ item.current_price }}'.",
394
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
395
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
396
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
397
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
398
+ "Every executed new-order node places an order, booked in replay as SIM-<node_id> (SIM-<node_id>#2 on a repeat, SIM-REPLACE-<modify_node> for a replacement); a node behind a false IfNode books nothing, and one intended order maps to one new-order node",
399
+ "The result port is an array of one order object {order_id, exchange, symbol, side, quantity, price, status}",
400
+ "A rejected order is still booked with status rejected; a timeout books unknown and reserves the cost; cash is debited on fills only; live_order_count is always 0 in replay",
401
+ "It may depend only on its gating IfNode; any other upstream completion triggers it even when the gate is false",
402
+ "The engine does not refuse a buy for a held symbol; only a workflow guard can block buying while holding",
403
+ "An overseas-stock market buy is submitted live as a limit order at the current price (LS accepts limit buys only; sells may be market or limit); replay mirrors it, so the booked intent is order_type limit at the quote",
394
404
  ]
395
405
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
396
406
  {
@@ -659,6 +669,12 @@ class OverseasStockModifyOrderNode(BaseModifyOrderNode):
659
669
  "Modifies price and/or quantity of a single open order identified by original_order_id + symbol + exchange",
660
670
  "Retry disabled by default (same safety rationale as new-order nodes — a duplicate modify can cause unintended fills)",
661
671
  "Rate-limited to 1 concurrent execution with 5-second minimum interval to prevent rapid repeated modifications",
672
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
673
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
674
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
675
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
676
+ "A modify is only acknowledged in the tick; the original order is replaced only when a matching completion is observed via a downstream *OpenOrdersNode recording's top-level order_events array",
677
+ "Replay supports limit-to-limit modification only; the replacement order is booked as SIM-REPLACE-<node_id>",
662
678
  ]
663
679
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
664
680
  {
@@ -946,6 +962,11 @@ class OverseasStockCancelOrderNode(BaseModifyOrderNode):
946
962
  "Cancels a single open order identified by original_order_id + symbol + exchange; auto-iterate to cancel multiple",
947
963
  "Retry disabled by default — a duplicate cancel on an already-cancelled order returns an error from the broker",
948
964
  "Rate-limited to 1 concurrent execution with 5-second minimum interval",
965
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
966
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
967
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
968
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
969
+ "A cancel is only acknowledged in the tick; the order is cancelled only when a matching completion is observed via a downstream *OpenOrdersNode recording's top-level order_events array",
949
970
  ]
950
971
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
951
972
  {
@@ -1184,6 +1205,15 @@ class OverseasFuturesNewOrderNode(BaseOrderNode):
1184
1205
  "is_tool_enabled=True — AI Agent can call this node as a tool to place futures orders",
1185
1206
  "Retry disabled by default to prevent duplicate futures contract positions",
1186
1207
  "Rate-limited to 1 concurrent execution with 5-second minimum interval",
1208
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
1209
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
1210
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
1211
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
1212
+ "Every executed new-order node places an order, booked in replay as SIM-<node_id> (SIM-<node_id>#2 on a repeat, SIM-REPLACE-<modify_node> for a replacement); a node behind a false IfNode books nothing, and one intended order maps to one new-order node",
1213
+ "The result port is an array of one order object {order_id, exchange, symbol, side, quantity, price, status}",
1214
+ "A rejected order is still booked with status rejected; a timeout books unknown and reserves the cost; cash is debited on fills only; live_order_count is always 0 in replay",
1215
+ "It may depend only on its gating IfNode; any other upstream completion triggers it even when the gate is false",
1216
+ "The simulator refuses a futures order that adds to a same-direction position; position reversal is not supported",
1187
1217
  ]
1188
1218
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
1189
1219
  {
@@ -1433,6 +1463,12 @@ class OverseasFuturesModifyOrderNode(BaseModifyOrderNode):
1433
1463
  "Modifies price and/or quantity of a single open futures order by original_order_id + symbol + exchange",
1434
1464
  "Supports the same exchanges as OverseasFuturesNewOrderNode (CME, EUREX, SGX, HKEX)",
1435
1465
  "Retry disabled by default — duplicate modify on a futures order can create unexpected position delta",
1466
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
1467
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
1468
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
1469
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
1470
+ "A modify is only acknowledged in the tick; the original order is replaced only when a matching completion is observed via a downstream *OpenOrdersNode recording's top-level order_events array",
1471
+ "Replay supports limit-to-limit modification only; the replacement order is booked as SIM-REPLACE-<node_id>",
1436
1472
  ]
1437
1473
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
1438
1474
  {
@@ -1697,6 +1733,11 @@ class OverseasFuturesCancelOrderNode(BaseModifyOrderNode):
1697
1733
  "Cancels a single open futures order by original_order_id + symbol + exchange; auto-iterate for batch cancellation",
1698
1734
  "Retry disabled by default — a duplicate cancel returns a broker error if the order was already cancelled",
1699
1735
  "Rate-limited to 1 concurrent execution with 5-second minimum interval",
1736
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
1737
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
1738
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
1739
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
1740
+ "A cancel is only acknowledged in the tick; the order is cancelled only when a matching completion is observed via a downstream *OpenOrdersNode recording's top-level order_events array",
1700
1741
  ]
1701
1742
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
1702
1743
  {
@@ -1935,6 +1976,15 @@ class KoreaStockNewOrderNode(BaseOrderNode):
1935
1976
  "is_tool_enabled=True — AI Agent can call this node as a tool to place Korean stock orders",
1936
1977
  "Real-market only — paper_trading mode is not supported for Korean domestic stocks; KoreaStockBrokerNode enforces this",
1937
1978
  "Retry disabled by default to prevent duplicate KRW-denominated orders",
1979
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
1980
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
1981
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
1982
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
1983
+ "Every executed new-order node places an order, booked in replay as SIM-<node_id> (SIM-<node_id>#2 on a repeat, SIM-REPLACE-<modify_node> for a replacement); a node behind a false IfNode books nothing, and one intended order maps to one new-order node",
1984
+ "The result port is an array of one order object {order_id, exchange, symbol, side, quantity, price, status}",
1985
+ "A rejected order is still booked with status rejected; a timeout books unknown and reserves the cost; cash is debited on fills only; live_order_count is always 0 in replay",
1986
+ "It may depend only on its gating IfNode; any other upstream completion triggers it even when the gate is false",
1987
+ "The engine does not refuse a buy for a held symbol; only a workflow guard can block buying while holding",
1938
1988
  ]
1939
1989
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
1940
1990
  {
@@ -2194,6 +2244,12 @@ class KoreaStockModifyOrderNode(BaseModifyOrderNode):
2194
2244
  "Modifies price and/or quantity of a single open Korean stock order by original_order_id + symbol",
2195
2245
  "No exchange field required — Korean stock symbol (6-digit code) uniquely identifies the instrument",
2196
2246
  "Retry disabled by default to prevent double-modification risk in KRW-denominated orders",
2247
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
2248
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
2249
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
2250
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
2251
+ "A modify is only acknowledged in the tick; the original order is replaced only when a matching completion is observed via a downstream *OpenOrdersNode recording's top-level order_events array",
2252
+ "Replay supports limit-to-limit modification only; the replacement order is booked as SIM-REPLACE-<node_id>",
2197
2253
  ]
2198
2254
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
2199
2255
  {
@@ -2447,6 +2503,11 @@ class KoreaStockCancelOrderNode(BaseModifyOrderNode):
2447
2503
  "Cancels a single open Korean stock order by original_order_id + symbol; auto-iterate for batch cancellation",
2448
2504
  "No exchange field required — Korean stock symbols uniquely identify KOSPI/KOSDAQ instruments",
2449
2505
  "Retry disabled by default — a duplicate cancel returns an error if the order was already cancelled",
2506
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
2507
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
2508
+ "A recording is {request, as_of, item, output}: a single call sets item to null and a per-symbol-iterated call records {items: {\"EXCHANGE:SYMBOL\": record}}; output keys must be declared ports and row fields must be among the port's documented fields",
2509
+ "On a schedule tick this node re-runs and needs a fresh recording in that tick frame; on a realtime event only nodes downstream of the streaming source re-run",
2510
+ "A cancel is only acknowledged in the tick; the order is cancelled only when a matching completion is observed via a downstream *OpenOrdersNode recording's top-level order_events array",
2450
2511
  ]
2451
2512
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
2452
2513
  {
@@ -69,6 +69,11 @@ class OverseasFuturesRealMarketDataNode(BaseNode):
69
69
  "Supports CME, EUREX, SGX, HKEX contracts — symbol must include contract month code (e.g., ESH26)",
70
70
  "Item-based execution: one subscription per node; use multiple nodes to watch multiple contracts",
71
71
  "Automatically re-subscribes after WebSocket reconnection events",
72
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
73
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
74
+ "In replay it records ohlcv_data (and its data alias) as a symbol-keyed object of bar lists keyed by the bare symbol, each bar carrying only date, open, high, low, close, volume",
75
+ "It does not record a top-level symbol port; the live node emits symbol only inside its event payload, so a recording that includes it is rejected",
76
+ "As a streaming source it emits market_data and realtime_update events; on each event only nodes downstream of it re-run, while upstream startup snapshots stay retained",
72
77
  ]
73
78
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
74
79
  {
@@ -68,6 +68,11 @@ class KoreaStockRealMarketDataNode(BaseNode):
68
68
  "Symbol format: 6-digit KRX code (e.g., '005930') without exchange field — domestic market implied",
69
69
  "Item-based execution: one subscription per node; use multiple nodes to watch multiple domestic stocks",
70
70
  "Real-trading only — KoreaStock product does not support paper trading",
71
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
72
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
73
+ "In replay it records ohlcv_data (and its data alias) as a symbol-keyed object of bar lists keyed by the bare symbol, each bar carrying only date, open, high, low, close, volume",
74
+ "It does not record a top-level symbol port; the live node emits symbol only inside its event payload, so a recording that includes it is rejected",
75
+ "As a streaming source it emits market_data and realtime_update events; on each event only nodes downstream of it re-run, while upstream startup snapshots stay retained",
71
76
  ]
72
77
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
73
78
  {
@@ -71,6 +71,11 @@ class OverseasStockRealMarketDataNode(BaseNode):
71
71
  "Item-based execution: one subscription per node; use multiple nodes or SplitNode to watch several symbols",
72
72
  "Automatically re-subscribes after WebSocket reconnection events",
73
73
  "Does NOT include order book (bid/ask levels) — only trade (GSC) events; ask/bid from GSH are in the data port",
74
+ "Replay resolves this node's request with the upstream broker's connection identity (provider, product, paper_trading, broker_node_id, and credential_id when the account is linked); a recording whose request omits that connection does not match",
75
+ "Only those five connection keys are allowed; each present key is a nonempty string except paper_trading, which is a boolean",
76
+ "In replay it records ohlcv_data (and its data alias) as a symbol-keyed object of bar lists keyed by the bare symbol, each bar carrying only date, open, high, low, close, volume",
77
+ "It does not record a top-level symbol port; the live node emits symbol only inside its event payload, so a recording that includes it is rejected",
78
+ "As a streaming source it emits market_data and realtime_update events; on each event only nodes downstream of it re-run, while upstream startup snapshots stay retained",
74
79
  ]
75
80
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
76
81
  {
@@ -59,7 +59,7 @@ class SessionGateNode(BaseNode):
59
59
  'when_not_to_use':['An authoritative exchange-open or holiday feed is required.','Wait until opening: use TradingHoursFilterNode.'],
60
60
  'typical_scenarios':['ScheduleNode -> SessionGateNode -> IfNode(allowed) -> strategy','Reservation -> SessionGateNode -> IfNode(allowed) -> OrderNode'],
61
61
  }
62
- _features: ClassVar[list[str]] = ['No credentials or network.','No sleeping and no dry-run bypass.','IANA timezone/DST and opening-weekday semantics.','Explicit IfNode controls downstream work.']
62
+ _features: ClassVar[list[str]] = ['No credentials or network.','No sleeping and no dry-run bypass.','IANA timezone/DST and opening-weekday semantics.','Explicit IfNode controls downstream work.','Output is a pure function of the evaluation instant and the configuration (windows are start-inclusive and end-exclusive, plus days, closed_dates and the IANA timezone); replay evaluates it at the scenario as_of.','Two frames at the same instant produce the same allowed value; an outside-window scenario must sit at an instant outside the window.','allowed skips nothing by itself; bind it to an IfNode and hang the order chain on the IfNode true edge.']
63
63
  _anti_patterns: ClassVar[list[dict[str,str]]] = [{'pattern':'Connect directly to an order and assume an ordinary edge checks allowed.','reason':'Edges sequence execution; false output does not automatically skip successors.','alternative':'Bind allowed to IfNode and use its true edge.'}]
64
64
  _node_guide: ClassVar[dict[str,Any]] = {
65
65
  'input_handling':'Configure timezone, windows, days and optional closed_dates. No current-time override is accepted from workflow data.',
@@ -95,6 +95,12 @@ class ScheduleNode(BaseNode):
95
95
  "Timezone-aware (IANA names) — 'America/New_York', 'Asia/Seoul', 'UTC'",
96
96
  "max_duration_hours caps total runtime; the scheduler exits cleanly at the limit",
97
97
  "enabled=False freezes the trigger without removing the node from the DAG",
98
+ "A tick re-executes the whole main flow: the ScheduleNode returns {trigger: true} without re-registering, so every node downstream of it runs again on each tick",
99
+ "Startup account and open-order snapshots are not retained across schedule ticks; they are retained only across realtime events",
100
+ "In replay a schedule_tick must fall on the cron's next firing instant after the previous frame, evaluated in this node's timezone (default America/New_York)",
101
+ "enabled=false emits no tick; ticks past count or max_duration_hours are refused; an invalid timezone is rejected at startup",
102
+ "Emits exactly one subsequent event type: schedule_tick",
103
+ "Requires exactly one StartNode upstream; the ScheduleNode is never the workflow root",
98
104
  ]
99
105
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
100
106
  {
@@ -319,6 +325,7 @@ class TradingHoursFilterNode(BaseNode):
319
325
  "`days` whitelist supports weekend-only or weekday-only flows",
320
326
  "Outside the window this node waits; `passed` activates on entry and `blocked` activates only on timeout/shutdown",
321
327
  "max_wait_hours safeguards long waits — the node timeouts instead of stalling forever",
328
+ "In replay an instant outside the window is refused (REPLAY_TIME_WAIT_BLOCKED); only in-window instants are replayable and the node then emits passed=true, so it cannot express an after-hours branch in a suite",
322
329
  ]
323
330
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
324
331
  {
@@ -98,6 +98,20 @@ class NodeTypeSchema(BaseModel):
98
98
  "- pitfalls: List[str] — connection caveats and shape requirements"
99
99
  ),
100
100
  )
101
+ execution: Optional[Dict[str, Any]] = Field(
102
+ default=None,
103
+ description=(
104
+ "Machine-readable execution/replay semantics block (closed value "
105
+ "sets), derived from the engine's own replay contract code — never "
106
+ "authored per class. Populated by "
107
+ "`programgarden.replay_semantics.attach_execution` when the schema is "
108
+ "served through the tool registry; the core registry itself leaves it "
109
+ "None (core must not import programgarden). Keys: role, replay, "
110
+ "request, iteration, reruns_on, emits_events, on_event, output_ports, "
111
+ "dead_ports, internal_ports, reserved_output_ports, gating, order, "
112
+ "time_rules, venue."
113
+ ),
114
+ )
101
115
 
102
116
  # === Version metadata (UI change detection) ===
103
117
  version: str = Field(
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden-core"
8
- version = "2.1.0"
8
+ version = "2.2.0"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의"
11
11
  readme = "README.md"