programgarden-core 1.30.0__tar.gz → 1.31.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-1.30.0 → programgarden_core-1.31.0}/PKG-INFO +21 -9
  2. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/README.md +19 -8
  3. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/__init__.py +1 -0
  4. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/i18n/locales/en.json +15 -3
  5. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/i18n/locales/ko.json +15 -3
  6. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/__init__.py +2 -0
  7. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/account_futures.py +2 -2
  8. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/account_korea_stock.py +2 -2
  9. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/backtest_futures.py +6 -6
  10. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/backtest_korea_stock.py +6 -6
  11. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/backtest_stock.py +6 -6
  12. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/broker.py +2 -2
  13. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/data.py +1 -1
  14. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/display.py +1 -1
  15. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/fundamental_korea_stock.py +6 -6
  16. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/fundamental_stock.py +6 -6
  17. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/infra.py +9 -9
  18. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/open_orders_futures.py +2 -2
  19. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/open_orders_korea_stock.py +2 -2
  20. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/order.py +144 -156
  21. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/orderable_quantity_futures.py +34 -6
  22. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/realtime_futures.py +8 -8
  23. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/realtime_korea_stock.py +8 -8
  24. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/realtime_stock.py +8 -8
  25. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/risk.py +2 -10
  26. programgarden_core-1.31.0/programgarden_core/nodes/session_gate.py +141 -0
  27. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/symbol.py +13 -13
  28. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/symbol_futures.py +11 -7
  29. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/symbol_korea_stock.py +3 -3
  30. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/symbol_stock.py +3 -3
  31. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/registry/node_registry.py +7 -3
  32. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/pyproject.toml +2 -1
  33. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/bases/__init__.py +0 -0
  34. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/bases/client.py +0 -0
  35. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/bases/components.py +0 -0
  36. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/bases/listener.py +0 -0
  37. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/bases/mixins.py +0 -0
  38. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/bases/products.py +0 -0
  39. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/bases/sql.py +0 -0
  40. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/bases/storage.py +0 -0
  41. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/code_node.py +0 -0
  42. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/exceptions.py +0 -0
  43. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/expression/__init__.py +0 -0
  44. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/expression/evaluator.py +0 -0
  45. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/i18n/__init__.py +0 -0
  46. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/i18n/translator.py +0 -0
  47. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/korea_alias.py +0 -0
  48. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/__init__.py +0 -0
  49. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/connection_rule.py +0 -0
  50. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/credential.py +0 -0
  51. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/edge.py +0 -0
  52. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/event.py +0 -0
  53. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/exchange.py +0 -0
  54. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/field_binding.py +0 -0
  55. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/job.py +0 -0
  56. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/order_diagnostics.py +0 -0
  57. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/plugin_resource.py +0 -0
  58. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/resilience.py +0 -0
  59. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/resource.py +0 -0
  60. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/validation.py +0 -0
  61. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/models/workflow.py +0 -0
  62. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/account_stock.py +0 -0
  63. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/ai.py +0 -0
  64. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/backtest.py +0 -0
  65. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/base.py +0 -0
  66. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/calculation.py +0 -0
  67. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/code.py +0 -0
  68. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/condition.py +0 -0
  69. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/data_futures.py +0 -0
  70. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/data_korea_stock.py +0 -0
  71. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/data_stock.py +0 -0
  72. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/event.py +0 -0
  73. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/market_external.py +0 -0
  74. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/market_status.py +0 -0
  75. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/open_orders_stock.py +0 -0
  76. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/portfolio.py +0 -0
  77. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/nodes/trigger.py +0 -0
  78. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/presets/__init__.py +0 -0
  79. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/presets/news_analyst.json +0 -0
  80. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/presets/risk_manager.json +0 -0
  81. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/presets/strategist.json +0 -0
  82. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/presets/technical_analyst.json +0 -0
  83. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/registry/__init__.py +0 -0
  84. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/registry/credential_registry.py +0 -0
  85. {programgarden_core-1.30.0 → programgarden_core-1.31.0}/programgarden_core/registry/plugin_registry.py +0 -0
  86. {programgarden_core-1.30.0 → programgarden_core-1.31.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: 1.30.0
3
+ Version: 1.31.0
4
4
  Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Classifier: Programming Language :: Python :: 3.14
13
13
  Requires-Dist: pydantic (>=2.0.0,<3.0.0)
14
+ Requires-Dist: tzdata (>=2025.2,<2026.0)
14
15
  Description-Content-Type: text/markdown
15
16
 
16
17
  # ProgramGarden Core
@@ -24,7 +25,7 @@ ProgramGarden은 AI 시대에 맞춰 파이썬을 모르는 투자자도 개인
24
25
 
25
26
  ## 주요 특징
26
27
 
27
- - **노드 기반 DSL**: 68개 내장 노드를 조합하여 워크플로우를 정의하는 도메인 특화 언어
28
+ - **노드 기반 DSL**: 73 core nodes를 조합하여 워크플로우를 정의하는 도메인 특화 언어
28
29
  - **타입 안전한 스키마**: Pydantic 모델 기반의 FieldSchema, 입출력 포트 정의로 IDE 친화적이고 안전한 개발
29
30
  - **레지스트리 시스템**: NodeTypeRegistry와 PluginRegistry로 노드/플러그인 메타데이터 관리
30
31
  - **다국어 지원(i18n)**: 한국어/영어 번역 파일을 통한 노드 설명, 필드명 자동 번역
@@ -42,23 +43,23 @@ poetry add programgarden-core
42
43
 
43
44
  요구 사항: Python 3.12+
44
45
 
45
- ## 노드 카테고리 (11개, 68개 코어 노드)
46
+ ## Node categories (11 categories, 73 core nodes)
46
47
 
47
48
  | 카테고리 | 노드 수 | 대표 노드 |
48
49
  |----------|---------|----------|
49
50
  | infra | 8 | StartNode, BrokerNode, ThrottleNode, SplitNode, AggregateNode, IfNode, KoreaStockBrokerNode |
50
- | account | 12 | AccountNode, OpenOrdersNode, RealAccountNode, RealOrderEventNode (해외주식/선물 + 국내주식) |
51
- | market | 20 | MarketDataNode, HistoricalDataNode, RealMarketDataNode, WatchlistNode, ExclusionListNode, CurrencyRateNode, KoreaStock* |
51
+ | account | 13 | AccountNode, OpenOrdersNode, RealAccountNode, RealOrderEventNode (해외주식/선물 + 국내주식) |
52
+ | market | 22 | MarketDataNode, HistoricalDataNode, RealMarketDataNode, WatchlistNode, ExclusionListNode, CurrencyRateNode, KoreaStock* |
52
53
  | condition | 2 | ConditionNode, LogicNode |
53
54
  | order | 10 | NewOrderNode, ModifyOrderNode, CancelOrderNode, PositionSizingNode (해외주식/선물 + 국내주식) |
54
55
  | risk | 1 | PortfolioNode |
55
- | schedule | 2 | ScheduleNode, TradingHoursFilterNode |
56
- | data | 3 | SQLiteNode, HTTPRequestNode, FieldMappingNode |
56
+ | schedule | 3 | ScheduleNode, TradingHoursFilterNode, SessionGateNode |
57
+ | data | 4 | SQLiteNode, HTTPRequestNode, FieldMappingNode, CodeNode |
57
58
  | display | 6 | TableDisplayNode, LineChartNode, CandlestickChartNode, SummaryDisplayNode |
58
59
  | analysis | 2 | BacktestEngineNode, BenchmarkCompareNode |
59
60
  | ai | 2 | LLMModelNode, AIAgentNode |
60
61
 
61
- > community 패키지에서 4개 노드 추가: TelegramNode(messaging), FearGreedIndexNode(market), FundamentalDataNode(market), FileReaderNode(data) → 전체 73개
62
+ > Community adds five nodes: TelegramNode, FearGreedIndexNode, FundamentalDataNode, FileReaderNode and PerformanceReportNode, for 78 total.
62
63
 
63
64
  ## 사용 예시
64
65
 
@@ -85,7 +86,7 @@ print(schema.config_schema)
85
86
 
86
87
  ```
87
88
  programgarden_core/
88
- ├── nodes/ # 69개 노드 정의 (base.py, infra.py, account.py, ...)
89
+ ├── nodes/ # 73 core node definitions
89
90
  ├── bases/ # Finance 베이스 클래스
90
91
  ├── models/ # Pydantic 모델 (FieldSchema, Edge, WorkflowDefinition 등)
91
92
  ├── registry/ # NodeTypeRegistry, PluginRegistry
@@ -131,3 +132,14 @@ unbound or cross-product nodes. Reuse one broker for all consumers. Account-free
131
132
  workflows and unrelated credentials remain supported. See
132
133
  [the shared contract](../../docs/workflow_account_policy.md).
133
134
 
135
+
136
+ ## Cancellation result contract (unreleased P4)
137
+
138
+ CancelOrder metadata describes request acknowledgement (`accepted`,
139
+ `confirmation_pending=true`), not final cancellation. Replacement orders require
140
+ separate matching completion evidence. See
141
+ [the cancellation contract](../../docs/cancellation-acknowledgements.md).
142
+
143
+ Session windows and IfNode routing inside Split are described in
144
+ [Session gates and guarded Split](../../docs/session-gates-and-guarded-split.md).
145
+
@@ -9,7 +9,7 @@ ProgramGarden은 AI 시대에 맞춰 파이썬을 모르는 투자자도 개인
9
9
 
10
10
  ## 주요 특징
11
11
 
12
- - **노드 기반 DSL**: 68개 내장 노드를 조합하여 워크플로우를 정의하는 도메인 특화 언어
12
+ - **노드 기반 DSL**: 73 core nodes를 조합하여 워크플로우를 정의하는 도메인 특화 언어
13
13
  - **타입 안전한 스키마**: Pydantic 모델 기반의 FieldSchema, 입출력 포트 정의로 IDE 친화적이고 안전한 개발
14
14
  - **레지스트리 시스템**: NodeTypeRegistry와 PluginRegistry로 노드/플러그인 메타데이터 관리
15
15
  - **다국어 지원(i18n)**: 한국어/영어 번역 파일을 통한 노드 설명, 필드명 자동 번역
@@ -27,23 +27,23 @@ poetry add programgarden-core
27
27
 
28
28
  요구 사항: Python 3.12+
29
29
 
30
- ## 노드 카테고리 (11개, 68개 코어 노드)
30
+ ## Node categories (11 categories, 73 core nodes)
31
31
 
32
32
  | 카테고리 | 노드 수 | 대표 노드 |
33
33
  |----------|---------|----------|
34
34
  | infra | 8 | StartNode, BrokerNode, ThrottleNode, SplitNode, AggregateNode, IfNode, KoreaStockBrokerNode |
35
- | account | 12 | AccountNode, OpenOrdersNode, RealAccountNode, RealOrderEventNode (해외주식/선물 + 국내주식) |
36
- | market | 20 | MarketDataNode, HistoricalDataNode, RealMarketDataNode, WatchlistNode, ExclusionListNode, CurrencyRateNode, KoreaStock* |
35
+ | account | 13 | AccountNode, OpenOrdersNode, RealAccountNode, RealOrderEventNode (해외주식/선물 + 국내주식) |
36
+ | market | 22 | MarketDataNode, HistoricalDataNode, RealMarketDataNode, WatchlistNode, ExclusionListNode, CurrencyRateNode, KoreaStock* |
37
37
  | condition | 2 | ConditionNode, LogicNode |
38
38
  | order | 10 | NewOrderNode, ModifyOrderNode, CancelOrderNode, PositionSizingNode (해외주식/선물 + 국내주식) |
39
39
  | risk | 1 | PortfolioNode |
40
- | schedule | 2 | ScheduleNode, TradingHoursFilterNode |
41
- | data | 3 | SQLiteNode, HTTPRequestNode, FieldMappingNode |
40
+ | schedule | 3 | ScheduleNode, TradingHoursFilterNode, SessionGateNode |
41
+ | data | 4 | SQLiteNode, HTTPRequestNode, FieldMappingNode, CodeNode |
42
42
  | display | 6 | TableDisplayNode, LineChartNode, CandlestickChartNode, SummaryDisplayNode |
43
43
  | analysis | 2 | BacktestEngineNode, BenchmarkCompareNode |
44
44
  | ai | 2 | LLMModelNode, AIAgentNode |
45
45
 
46
- > community 패키지에서 4개 노드 추가: TelegramNode(messaging), FearGreedIndexNode(market), FundamentalDataNode(market), FileReaderNode(data) → 전체 73개
46
+ > Community adds five nodes: TelegramNode, FearGreedIndexNode, FundamentalDataNode, FileReaderNode and PerformanceReportNode, for 78 total.
47
47
 
48
48
  ## 사용 예시
49
49
 
@@ -70,7 +70,7 @@ print(schema.config_schema)
70
70
 
71
71
  ```
72
72
  programgarden_core/
73
- ├── nodes/ # 69개 노드 정의 (base.py, infra.py, account.py, ...)
73
+ ├── nodes/ # 73 core node definitions
74
74
  ├── bases/ # Finance 베이스 클래스
75
75
  ├── models/ # Pydantic 모델 (FieldSchema, Edge, WorkflowDefinition 등)
76
76
  ├── registry/ # NodeTypeRegistry, PluginRegistry
@@ -115,3 +115,14 @@ connection. Additional connections produce `DUPLICATE_BROKER_NODE`, including
115
115
  unbound or cross-product nodes. Reuse one broker for all consumers. Account-free
116
116
  workflows and unrelated credentials remain supported. See
117
117
  [the shared contract](../../docs/workflow_account_policy.md).
118
+
119
+
120
+ ## Cancellation result contract (unreleased P4)
121
+
122
+ CancelOrder metadata describes request acknowledgement (`accepted`,
123
+ `confirmation_pending=true`), not final cancellation. Replacement orders require
124
+ separate matching completion evidence. See
125
+ [the cancellation contract](../../docs/cancellation-acknowledgements.md).
126
+
127
+ Session windows and IfNode routing inside Split are described in
128
+ [Session gates and guarded Split](../../docs/session-gates-and-guarded-split.md).
@@ -88,6 +88,7 @@ __all__ = [
88
88
  # Nodes - Trigger
89
89
  "ScheduleNode",
90
90
  "TradingHoursFilterNode",
91
+ "SessionGateNode",
91
92
  # Nodes - Condition
92
93
  "ConditionNode",
93
94
  "LogicNode",
@@ -931,10 +931,10 @@
931
931
  "ports.blocked_reason": "Block reason",
932
932
  "ports.blocked_symbols": "Blocked symbols",
933
933
  "ports.buy_signal": "Buy signal",
934
- "ports.cancel_result": "Cancel result",
934
+ "ports.cancel_result": "Cancel request result",
935
935
  "ports.cancel_trigger": "Cancel execution trigger",
936
936
  "ports.cancelled": "Order cancelled",
937
- "ports.cancelled_order_id": "Cancelled order ID",
937
+ "ports.cancelled_order_id": "Cancellation target order ID",
938
938
  "ports.cancelled_orders": "Cancelled orders",
939
939
  "ports.closed": "Exchange closed",
940
940
  "ports.combined_curve": "Combined equity curve",
@@ -1344,5 +1344,17 @@
1344
1344
  "fields.OverseasFuturesOrderableQuantityNode.query_type": "New positions, liquidation or total capacity.",
1345
1345
  "enums.query_type.new": "New",
1346
1346
  "enums.query_type.close": "Liquidation",
1347
- "enums.query_type.total": "Total"
1347
+ "enums.query_type.total": "Total",
1348
+ "nodes.SessionGateNode.allowed": "True only inside a configured window; bind to IfNode.",
1349
+ "nodes.SessionGateNode.closed_dates": "Explicit YYYY-MM-DD exclusions. Either local calendar date or opening date blocks the window.",
1350
+ "nodes.SessionGateNode.days": "Allowed opening weekdays. The post-midnight portion belongs to the previous opening day.",
1351
+ "nodes.SessionGateNode.description": "Check configured local windows immediately; enforce allowed using IfNode.",
1352
+ "nodes.SessionGateNode.local_date": "Current calendar date in the configured timezone.",
1353
+ "nodes.SessionGateNode.local_time": "Current local ISO timestamp including UTC offset.",
1354
+ "nodes.SessionGateNode.name": "Session gate",
1355
+ "nodes.SessionGateNode.reason": "inside_configured_window or outside_configured_window.",
1356
+ "nodes.SessionGateNode.session_date": "Opening date of the matched window, or an empty string when blocked.",
1357
+ "nodes.SessionGateNode.timezone": "IANA timezone; daylight-saving transitions use the installed timezone database.",
1358
+ "nodes.SessionGateNode.trigger": "Evaluate the current local time when triggered.",
1359
+ "nodes.SessionGateNode.windows": "Local HH:MM start/end windows; start inclusive, end exclusive. Overnight windows are supported."
1348
1360
  }
@@ -931,10 +931,10 @@
931
931
  "ports.blocked_reason": "차단 사유",
932
932
  "ports.blocked_symbols": "차단된 종목",
933
933
  "ports.buy_signal": "매수 시그널",
934
- "ports.cancel_result": "취소 결과",
934
+ "ports.cancel_result": "취소 요청 결과",
935
935
  "ports.cancel_trigger": "취소 실행 트리거",
936
936
  "ports.cancelled": "취소됨",
937
- "ports.cancelled_order_id": "취소된 주문번호",
937
+ "ports.cancelled_order_id": "취소 대상 주문번호",
938
938
  "ports.cancelled_orders": "취소된 주문",
939
939
  "ports.closed": "거래소 폐장",
940
940
  "ports.combined_curve": "통합 자산 곡선",
@@ -1344,5 +1344,17 @@
1344
1344
  "fields.OverseasFuturesOrderableQuantityNode.query_type": "신규 진입·청산·총 주문가능수량 중 조회할 항목입니다.",
1345
1345
  "enums.query_type.new": "신규",
1346
1346
  "enums.query_type.close": "청산",
1347
- "enums.query_type.total": "총가능"
1347
+ "enums.query_type.total": "총가능",
1348
+ "nodes.SessionGateNode.allowed": "허용 구간 안에서만 true입니다. IfNode에 연결하세요.",
1349
+ "nodes.SessionGateNode.closed_dates": "YYYY-MM-DD 제외일 목록입니다. 현지 날짜 또는 구간 시작 날짜가 해당하면 차단합니다.",
1350
+ "nodes.SessionGateNode.days": "시작일 기준 허용 요일입니다. 자정 이후 구간은 전날의 시작 요일에 속합니다.",
1351
+ "nodes.SessionGateNode.description": "설정한 현지 시간대를 즉시 확인합니다. IfNode로 허용 여부를 분기하세요.",
1352
+ "nodes.SessionGateNode.local_date": "설정한 시간대의 현재 날짜입니다.",
1353
+ "nodes.SessionGateNode.local_time": "UTC 오프셋을 포함한 현재 현지 ISO 시각입니다.",
1354
+ "nodes.SessionGateNode.name": "거래 시간대 확인",
1355
+ "nodes.SessionGateNode.reason": "inside_configured_window 또는 outside_configured_window입니다.",
1356
+ "nodes.SessionGateNode.session_date": "허용 구간의 시작일입니다. 차단되면 빈 문자열입니다.",
1357
+ "nodes.SessionGateNode.timezone": "IANA 시간대입니다. 설치된 시간대 데이터로 서머타임을 반영합니다.",
1358
+ "nodes.SessionGateNode.trigger": "실행 시점의 현지 시간을 확인합니다.",
1359
+ "nodes.SessionGateNode.windows": "현지 HH:MM 구간입니다. 시작은 포함하고 종료는 제외하며 자정을 넘는 구간도 지원합니다."
1348
1360
  }
@@ -62,6 +62,7 @@ from programgarden_core.nodes.symbol import (
62
62
  SymbolFilterNode,
63
63
  ExclusionListNode,
64
64
  )
65
+ from programgarden_core.nodes.session_gate import SessionGateNode
65
66
  from programgarden_core.nodes.trigger import (
66
67
  ScheduleNode,
67
68
  TradingHoursFilterNode,
@@ -164,6 +165,7 @@ __all__ = [
164
165
  # Trigger
165
166
  "ScheduleNode",
166
167
  "TradingHoursFilterNode",
168
+ "SessionGateNode",
167
169
  # Condition
168
170
  "ConditionNode",
169
171
  "LogicNode",
@@ -116,7 +116,7 @@ class OverseasFuturesAccountNode(BaseNode):
116
116
  },
117
117
  {
118
118
  "title": "Margin check before futures order",
119
- "description": "Read account margin and place a new order only when available margin exceeds the threshold.",
119
+ "description": 'Read account margin and place a new order only when available margin exceeds the threshold. Component demonstration only: add validated signal, account/pending, sizing, session and persistent duplicate-submission guards before live trading.',
120
120
  "workflow_snippet": {
121
121
  "id": "futures-account-margin-check",
122
122
  "name": "Futures Margin Check",
@@ -125,7 +125,7 @@ class OverseasFuturesAccountNode(BaseNode):
125
125
  {"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "futures_cred", "paper_trading": True},
126
126
  {"id": "account", "type": "OverseasFuturesAccountNode"},
127
127
  {"id": "if_margin", "type": "IfNode", "left": "{{ nodes.account.balance.orderable_amount }}", "operator": ">=", "right": 10000},
128
- {"id": "order", "type": "OverseasFuturesNewOrderNode", "symbol": "ESH26", "exchange": "CME", "side": "buy", "order_type": "limit", "quantity": 1, "price": 5200.0},
128
+ {'id': 'order', 'type': 'OverseasFuturesNewOrderNode', 'side': 'buy', 'order_type': 'limit', 'order': {'symbol': 'ESH26', 'exchange': 'CME', 'quantity': 1, 'price': 5200.0}},
129
129
  ],
130
130
  "edges": [
131
131
  {"from": "start", "to": "broker"},
@@ -115,7 +115,7 @@ class KoreaStockAccountNode(BaseNode):
115
115
  },
116
116
  {
117
117
  "title": "Cash check before Korea stock order",
118
- "description": "Read account balance and only place an order when orderable cash exceeds 1,000,000 KRW.",
118
+ "description": 'Read account balance and only place an order when orderable cash exceeds 1,000,000 KRW. Component demonstration only: add validated signal, account/pending, sizing, session and persistent duplicate-submission guards before live trading.',
119
119
  "workflow_snippet": {
120
120
  "id": "korea-stock-cash-check",
121
121
  "name": "Korea Stock Cash Check",
@@ -124,7 +124,7 @@ class KoreaStockAccountNode(BaseNode):
124
124
  {"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
125
125
  {"id": "account", "type": "KoreaStockAccountNode"},
126
126
  {"id": "if_cash", "type": "IfNode", "left": "{{ nodes.account.balance.orderable_amount }}", "operator": ">=", "right": 1000000},
127
- {"id": "order", "type": "KoreaStockNewOrderNode", "symbol": "005930", "side": "buy", "order_type": "limit", "quantity": 1, "price": 75000},
127
+ {'id': 'order', 'type': 'KoreaStockNewOrderNode', 'side': 'buy', 'order_type': 'limit', 'order': {'symbol': '005930', 'quantity': 1, 'price': 75000}},
128
128
  ],
129
129
  "edges": [
130
130
  {"from": "start", "to": "broker"},
@@ -125,17 +125,17 @@ class OverseasFuturesHistoricalDataNode(BaseNode):
125
125
  "nodes": [
126
126
  {"id": "start", "type": "StartNode"},
127
127
  {"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
128
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "ESH26", "exchange": "CME"}]},
128
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': 'ESH26', 'exchange': 'CME'}]},
129
129
  {"id": "historical", "type": "OverseasFuturesHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(60, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1d", "adjust": False},
130
130
  {"id": "condition", "type": "ConditionNode", "plugin": "BollingerBands", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 20, "std": 2.0, "direction": "below_lower"}},
131
- ],
131
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
132
132
  "edges": [
133
133
  {"from": "start", "to": "broker"},
134
134
  {"from": "broker", "to": "split"},
135
135
  {"from": "split", "to": "historical"},
136
136
  {"from": "broker", "to": "historical"},
137
137
  {"from": "historical", "to": "condition"},
138
- ],
138
+ {'from': 'condition', 'to': 'split_results'}],
139
139
  "credentials": [
140
140
  {
141
141
  "credential_id": "broker_cred",
@@ -158,17 +158,17 @@ class OverseasFuturesHistoricalDataNode(BaseNode):
158
158
  "nodes": [
159
159
  {"id": "start", "type": "StartNode"},
160
160
  {"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
161
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "MHIH26", "exchange": "HKEX"}]},
161
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': 'MHIH26', 'exchange': 'HKEX'}]},
162
162
  {"id": "historical", "type": "OverseasFuturesHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(10, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1h", "adjust": False},
163
163
  {"id": "condition", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
164
- ],
164
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
165
165
  "edges": [
166
166
  {"from": "start", "to": "broker"},
167
167
  {"from": "broker", "to": "split"},
168
168
  {"from": "split", "to": "historical"},
169
169
  {"from": "broker", "to": "historical"},
170
170
  {"from": "historical", "to": "condition"},
171
- ],
171
+ {'from': 'condition', 'to': 'split_results'}],
172
172
  "credentials": [
173
173
  {
174
174
  "credential_id": "broker_cred",
@@ -125,17 +125,17 @@ class KoreaStockHistoricalDataNode(BaseNode):
125
125
  "nodes": [
126
126
  {"id": "start", "type": "StartNode"},
127
127
  {"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
128
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "005930"}]},
128
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': '005930'}]},
129
129
  {"id": "historical", "type": "KoreaStockHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(60, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1d", "adjust": True},
130
130
  {"id": "condition", "type": "ConditionNode", "plugin": "RSI", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 14, "threshold": 30, "direction": "below"}},
131
- ],
131
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
132
132
  "edges": [
133
133
  {"from": "start", "to": "broker"},
134
134
  {"from": "broker", "to": "split"},
135
135
  {"from": "split", "to": "historical"},
136
136
  {"from": "broker", "to": "historical"},
137
137
  {"from": "historical", "to": "condition"},
138
- ],
138
+ {'from': 'condition', 'to': 'split_results'}],
139
139
  "credentials": [
140
140
  {
141
141
  "credential_id": "broker_cred",
@@ -158,17 +158,17 @@ class KoreaStockHistoricalDataNode(BaseNode):
158
158
  "nodes": [
159
159
  {"id": "start", "type": "StartNode"},
160
160
  {"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
161
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "247540"}]},
161
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': '247540'}]},
162
162
  {"id": "historical", "type": "KoreaStockHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(365, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1w", "adjust": True},
163
163
  {"id": "condition", "type": "ConditionNode", "plugin": "BollingerBands", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 20, "std": 2.0, "direction": "below_lower"}},
164
- ],
164
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
165
165
  "edges": [
166
166
  {"from": "start", "to": "broker"},
167
167
  {"from": "broker", "to": "split"},
168
168
  {"from": "split", "to": "historical"},
169
169
  {"from": "broker", "to": "historical"},
170
170
  {"from": "historical", "to": "condition"},
171
- ],
171
+ {'from': 'condition', 'to': 'split_results'}],
172
172
  "credentials": [
173
173
  {
174
174
  "credential_id": "broker_cred",
@@ -125,17 +125,17 @@ class OverseasStockHistoricalDataNode(BaseNode):
125
125
  "nodes": [
126
126
  {"id": "start", "type": "StartNode"},
127
127
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
128
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "AAPL", "exchange": "NASDAQ"}]},
128
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': 'AAPL', 'exchange': 'NASDAQ'}]},
129
129
  {"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(30, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "1d", "adjust": False},
130
130
  {"id": "condition", "type": "ConditionNode", "plugin": "RSI", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"period": 14, "threshold": 30, "direction": "below"}},
131
- ],
131
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
132
132
  "edges": [
133
133
  {"from": "start", "to": "broker"},
134
134
  {"from": "broker", "to": "split"},
135
135
  {"from": "split", "to": "historical"},
136
136
  {"from": "broker", "to": "historical"},
137
137
  {"from": "historical", "to": "condition"},
138
- ],
138
+ {'from': 'condition', 'to': 'split_results'}],
139
139
  "credentials": [
140
140
  {
141
141
  "credential_id": "broker_cred",
@@ -158,17 +158,17 @@ class OverseasStockHistoricalDataNode(BaseNode):
158
158
  "nodes": [
159
159
  {"id": "start", "type": "StartNode"},
160
160
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
161
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "MSFT", "exchange": "NASDAQ"}]},
161
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': 'MSFT', 'exchange': 'NASDAQ'}]},
162
162
  {"id": "historical", "type": "OverseasStockHistoricalDataNode", "symbol": "{{ nodes.split.item }}", "start_date": "{{ date.ago(5, format='yyyymmdd') }}", "end_date": "{{ date.today(format='yyyymmdd') }}", "interval": "5m", "adjust": False},
163
163
  {"id": "condition", "type": "ConditionNode", "plugin": "MACD", "items": {"from": "{{ item.time_series }}", "extract": {"symbol": "{{ item.symbol }}", "exchange": "{{ item.exchange }}", "date": "{{ row.date }}", "close": "{{ row.close }}"}}, "fields": {"fast": 12, "slow": 26, "signal": 9, "direction": "bullish_cross"}},
164
- ],
164
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
165
165
  "edges": [
166
166
  {"from": "start", "to": "broker"},
167
167
  {"from": "broker", "to": "split"},
168
168
  {"from": "split", "to": "historical"},
169
169
  {"from": "broker", "to": "historical"},
170
170
  {"from": "historical", "to": "condition"},
171
- ],
171
+ {'from': 'condition', 'to': 'split_results'}],
172
172
  "credentials": [
173
173
  {
174
174
  "credential_id": "broker_cred",
@@ -150,7 +150,7 @@ class OverseasStockBrokerNode(BaseBrokerNode):
150
150
  },
151
151
  {
152
152
  "title": "Broker → Historical → Condition → Order",
153
- "description": "Full trading path: broker session feeds historical data, a condition plugin decides, then the order node fires when the condition passes.",
153
+ "description": 'Full trading path: broker session feeds historical data, a condition plugin decides, then the order node fires when the condition passes. Component demonstration only: add validated signal, account/pending, sizing, session and persistent duplicate-submission guards before live trading.',
154
154
  "workflow_snippet": {
155
155
  "id": "broker-stock-trade",
156
156
  "name": "Broker + RSI + order",
@@ -181,7 +181,7 @@ class OverseasStockBrokerNode(BaseBrokerNode):
181
181
  },
182
182
  "fields": {"period": 14, "threshold": 30, "direction": "below"},
183
183
  },
184
- {"id": "order", "type": "OverseasStockNewOrderNode", "symbol": "AAPL", "exchange": "NASDAQ", "side": "buy", "quantity": 1, "price": 150.0},
184
+ {'id': 'order', 'type': 'OverseasStockNewOrderNode', 'side': 'buy', 'order': {'symbol': 'AAPL', 'exchange': 'NASDAQ', 'quantity': 1, 'price': 150.0}},
185
185
  ],
186
186
  "edges": [
187
187
  {"from": "start", "to": "broker"},
@@ -212,7 +212,7 @@ class SQLiteNode(BaseNode):
212
212
  "operation": "simple",
213
213
  "table": "hwm",
214
214
  "action": "upsert",
215
- "values": {"symbol": "{{ nodes.market.value.symbol }}", "peak": "{{ nodes.market.value.current_price }}"},
215
+ "values": {"symbol": "{{ nodes.market.value.symbol }}", "peak": '{{ nodes.market.value.price }}'},
216
216
  "on_conflict": "symbol",
217
217
  },
218
218
  {
@@ -209,7 +209,7 @@ class TableDisplayNode(BaseDisplayNode):
209
209
  {"id": "start", "type": "StartNode"},
210
210
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
211
211
  {"id": "account", "type": "OverseasStockAccountNode"},
212
- {"id": "table", "type": "TableDisplayNode", "title": "Current Positions", "data": "{{ nodes.account.positions }}", "columns": ["symbol", "exchange", "quantity", "avg_price", "pnl"], "sort_by": "pnl", "sort_order": "desc", "limit": 20},
212
+ {"id": "table", "type": "TableDisplayNode", "title": "Current Positions", "data": "{{ nodes.account.positions }}", "columns": ["symbol", "exchange", "quantity", "avg_price", 'pnl_amount'], "sort_by": "pnl", "sort_order": "desc", "limit": 20},
213
213
  ],
214
214
  "edges": [
215
215
  {"from": "start", "to": "broker"},
@@ -109,17 +109,17 @@ class KoreaStockFundamentalNode(BaseNode):
109
109
  "nodes": [
110
110
  {"id": "start", "type": "StartNode"},
111
111
  {"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
112
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "005930"}, {"symbol": "000660"}]},
112
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': '005930'}, {'symbol': '000660'}]},
113
113
  {"id": "fundamental", "type": "KoreaStockFundamentalNode", "symbol": "{{ nodes.split.item }}"},
114
114
  {"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.fundamental.value }}"},
115
- ],
115
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
116
116
  "edges": [
117
117
  {"from": "start", "to": "broker"},
118
118
  {"from": "broker", "to": "split"},
119
119
  {"from": "split", "to": "fundamental"},
120
120
  {"from": "broker", "to": "fundamental"},
121
121
  {"from": "fundamental", "to": "display"},
122
- ],
122
+ {'from': 'display', 'to': 'split_results'}],
123
123
  "credentials": [
124
124
  {
125
125
  "credential_id": "broker_cred",
@@ -142,17 +142,17 @@ class KoreaStockFundamentalNode(BaseNode):
142
142
  "nodes": [
143
143
  {"id": "start", "type": "StartNode"},
144
144
  {"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
145
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "005930"}, {"symbol": "005380"}, {"symbol": "051910"}]},
145
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': '005930'}, {'symbol': '005380'}, {'symbol': '051910'}]},
146
146
  {"id": "fundamental", "type": "KoreaStockFundamentalNode", "symbol": "{{ nodes.split.item }}"},
147
147
  {"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.fundamental.value }}"},
148
- ],
148
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
149
149
  "edges": [
150
150
  {"from": "start", "to": "broker"},
151
151
  {"from": "broker", "to": "split"},
152
152
  {"from": "split", "to": "fundamental"},
153
153
  {"from": "broker", "to": "fundamental"},
154
154
  {"from": "fundamental", "to": "display"},
155
- ],
155
+ {'from': 'display', 'to': 'split_results'}],
156
156
  "credentials": [
157
157
  {
158
158
  "credential_id": "broker_cred",
@@ -108,17 +108,17 @@ class OverseasStockFundamentalNode(BaseNode):
108
108
  "nodes": [
109
109
  {"id": "start", "type": "StartNode"},
110
110
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
111
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "AAPL", "exchange": "NASDAQ"}]},
111
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': 'AAPL', 'exchange': 'NASDAQ'}]},
112
112
  {"id": "fundamental", "type": "OverseasStockFundamentalNode", "symbol": "{{ nodes.split.item }}"},
113
113
  {"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.fundamental.value }}"},
114
- ],
114
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
115
115
  "edges": [
116
116
  {"from": "start", "to": "broker"},
117
117
  {"from": "broker", "to": "split"},
118
118
  {"from": "split", "to": "fundamental"},
119
119
  {"from": "broker", "to": "fundamental"},
120
120
  {"from": "fundamental", "to": "display"},
121
- ],
121
+ {'from': 'display', 'to': 'split_results'}],
122
122
  "credentials": [
123
123
  {
124
124
  "credential_id": "broker_cred",
@@ -141,17 +141,17 @@ class OverseasStockFundamentalNode(BaseNode):
141
141
  "nodes": [
142
142
  {"id": "start", "type": "StartNode"},
143
143
  {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
144
- {"id": "split", "type": "SplitNode", "items": [{"symbol": "AAPL", "exchange": "NASDAQ"}, {"symbol": "MSFT", "exchange": "NASDAQ"}]},
144
+ {'id': 'split', 'type': 'SplitNode', 'array': [{'symbol': 'AAPL', 'exchange': 'NASDAQ'}, {'symbol': 'MSFT', 'exchange': 'NASDAQ'}]},
145
145
  {"id": "fundamental", "type": "OverseasStockFundamentalNode", "symbol": "{{ nodes.split.item }}"},
146
146
  {"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.fundamental.value }}"},
147
- ],
147
+ {'id': 'split_results', 'type': 'AggregateNode', 'mode': 'collect'}],
148
148
  "edges": [
149
149
  {"from": "start", "to": "broker"},
150
150
  {"from": "broker", "to": "split"},
151
151
  {"from": "split", "to": "fundamental"},
152
152
  {"from": "broker", "to": "fundamental"},
153
153
  {"from": "fundamental", "to": "display"},
154
- ],
154
+ {'from': 'display', 'to': 'split_results'}],
155
155
  "credentials": [
156
156
  {
157
157
  "credential_id": "broker_cred",