programgarden-core 1.19.0__tar.gz → 1.20.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 (84) hide show
  1. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/PKG-INFO +1 -1
  2. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/account_futures.py +2 -2
  3. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/account_korea_stock.py +2 -2
  4. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/account_stock.py +2 -2
  5. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/base.py +224 -27
  6. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/data_futures.py +9 -7
  7. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/data_korea_stock.py +11 -6
  8. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/data_stock.py +15 -10
  9. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/realtime_futures.py +10 -7
  10. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/realtime_korea_stock.py +10 -7
  11. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/realtime_stock.py +10 -7
  12. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/symbol.py +4 -2
  13. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/symbol_futures.py +2 -1
  14. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/symbol_korea_stock.py +2 -2
  15. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/symbol_stock.py +2 -2
  16. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/pyproject.toml +1 -1
  17. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/README.md +0 -0
  18. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/__init__.py +0 -0
  19. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/bases/__init__.py +0 -0
  20. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/bases/client.py +0 -0
  21. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/bases/components.py +0 -0
  22. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/bases/listener.py +0 -0
  23. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/bases/mixins.py +0 -0
  24. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/bases/products.py +0 -0
  25. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/bases/sql.py +0 -0
  26. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/bases/storage.py +0 -0
  27. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/code_node.py +0 -0
  28. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/exceptions.py +0 -0
  29. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/expression/__init__.py +0 -0
  30. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/expression/evaluator.py +0 -0
  31. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/i18n/__init__.py +0 -0
  32. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/i18n/locales/en.json +0 -0
  33. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/i18n/locales/ko.json +0 -0
  34. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/i18n/translator.py +0 -0
  35. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/korea_alias.py +0 -0
  36. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/__init__.py +0 -0
  37. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/connection_rule.py +0 -0
  38. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/credential.py +0 -0
  39. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/edge.py +0 -0
  40. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/event.py +0 -0
  41. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/exchange.py +0 -0
  42. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/field_binding.py +0 -0
  43. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/job.py +0 -0
  44. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/order_diagnostics.py +0 -0
  45. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/plugin_resource.py +0 -0
  46. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/resilience.py +0 -0
  47. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/resource.py +0 -0
  48. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/validation.py +0 -0
  49. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/models/workflow.py +0 -0
  50. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/__init__.py +0 -0
  51. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/ai.py +0 -0
  52. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/backtest.py +0 -0
  53. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/backtest_futures.py +0 -0
  54. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/backtest_korea_stock.py +0 -0
  55. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/backtest_stock.py +0 -0
  56. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/broker.py +0 -0
  57. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/calculation.py +0 -0
  58. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/code.py +0 -0
  59. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/condition.py +0 -0
  60. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/data.py +0 -0
  61. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/display.py +0 -0
  62. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/event.py +0 -0
  63. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
  64. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
  65. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/infra.py +0 -0
  66. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/market_external.py +0 -0
  67. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/market_status.py +0 -0
  68. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/open_orders_futures.py +0 -0
  69. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
  70. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/open_orders_stock.py +0 -0
  71. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/order.py +0 -0
  72. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/portfolio.py +0 -0
  73. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/risk.py +0 -0
  74. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/nodes/trigger.py +0 -0
  75. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/presets/__init__.py +0 -0
  76. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/presets/news_analyst.json +0 -0
  77. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/presets/risk_manager.json +0 -0
  78. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/presets/strategist.json +0 -0
  79. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/presets/technical_analyst.json +0 -0
  80. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/registry/__init__.py +0 -0
  81. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/registry/credential_registry.py +0 -0
  82. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/registry/node_registry.py +0 -0
  83. {programgarden_core-1.19.0 → programgarden_core-1.20.0}/programgarden_core/registry/plugin_registry.py +0 -0
  84. {programgarden_core-1.19.0 → programgarden_core-1.20.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.19.0
3
+ Version: 1.20.0
4
4
  Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -19,7 +19,7 @@ from programgarden_core.nodes.base import (
19
19
  ProductScope,
20
20
  BrokerProvider,
21
21
  OVERSEAS_FUTURES_BALANCE_FIELDS,
22
- POSITION_FIELDS,
22
+ OVERSEAS_FUTURES_POSITION_FIELDS,
23
23
  SYMBOL_LIST_FIELDS,
24
24
  )
25
25
 
@@ -171,7 +171,7 @@ class OverseasFuturesAccountNode(BaseNode):
171
171
  _outputs: List[OutputPort] = [
172
172
  OutputPort(name="held_symbols", type="symbol_list", description="i18n:ports.held_symbols", fields=SYMBOL_LIST_FIELDS),
173
173
  OutputPort(name="balance", type="balance_data", description="i18n:ports.balance", fields=OVERSEAS_FUTURES_BALANCE_FIELDS),
174
- OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=POSITION_FIELDS),
174
+ OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=OVERSEAS_FUTURES_POSITION_FIELDS),
175
175
  ]
176
176
 
177
177
  _version: ClassVar[str] = "1.0.0"
@@ -18,7 +18,7 @@ from programgarden_core.nodes.base import (
18
18
  ProductScope,
19
19
  BrokerProvider,
20
20
  KOREA_STOCK_BALANCE_FIELDS,
21
- POSITION_FIELDS,
21
+ KOREA_STOCK_POSITION_FIELDS,
22
22
  SYMBOL_LIST_FIELDS,
23
23
  )
24
24
 
@@ -170,7 +170,7 @@ class KoreaStockAccountNode(BaseNode):
170
170
  _outputs: List[OutputPort] = [
171
171
  OutputPort(name="held_symbols", type="symbol_list", description="i18n:ports.held_symbols", fields=SYMBOL_LIST_FIELDS),
172
172
  OutputPort(name="balance", type="balance_data", description="i18n:ports.balance", fields=KOREA_STOCK_BALANCE_FIELDS),
173
- OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=POSITION_FIELDS),
173
+ OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=KOREA_STOCK_POSITION_FIELDS),
174
174
  ]
175
175
 
176
176
  _version: ClassVar[str] = "1.0.0"
@@ -19,7 +19,7 @@ from programgarden_core.nodes.base import (
19
19
  ProductScope,
20
20
  BrokerProvider,
21
21
  OVERSEAS_STOCK_BALANCE_FIELDS,
22
- POSITION_FIELDS,
22
+ OVERSEAS_STOCK_POSITION_FIELDS,
23
23
  SYMBOL_LIST_FIELDS,
24
24
  )
25
25
 
@@ -200,7 +200,7 @@ class OverseasStockAccountNode(BaseNode):
200
200
  name="positions",
201
201
  type="position_data",
202
202
  description="i18n:ports.positions",
203
- fields=POSITION_FIELDS,
203
+ fields=OVERSEAS_STOCK_POSITION_FIELDS,
204
204
  example=[
205
205
  {"symbol": "AAPL", "exchange": "NASDAQ", "quantity": 10, "avg_price": 175.20, "pnl_rate": 6.99},
206
206
  {"symbol": "TSLA", "exchange": "NASDAQ", "quantity": 5, "avg_price": 240.00, "pnl_rate": -2.50},
@@ -112,11 +112,81 @@ class OutputPort(BaseModel):
112
112
 
113
113
 
114
114
  # === OutputPort.fields 공통 상수 ===
115
+ #
116
+ # 🔴 **상수 공유는 노드가 같은 키를 내보낼 때만 한다.**
117
+ # resolver 는 이 선언으로 `nodes.<id>.<port>.<field>` 의 필드 존재를 **정적 검증**한다.
118
+ # 그래서 선언이 런타임과 어긋나면 검증이 양방향으로 다 틀린다:
119
+ # 실제 있는 필드를 바인딩 → INVALID_EXPRESSION_REF 로 **부당 거부** (동작하는 워크플로우를 막음)
120
+ # 실제 없는 필드를 바인딩 → **통과** 시킨 뒤 런타임에 조용히 None (표에 '-' 만 찍힘)
121
+ # 노드 하나를 맞추려고 공유 상수에 필드를 더하면 **다른 노드가 반대 방향으로 거짓말**한다.
122
+ # → 런타임 키가 다르면 **노드별 상수로 쪼갠다.** 증명은 `tests/test_output_schema_contract.py`
123
+ # (executor 의 런타임 dict 리터럴을 AST 로 뽑아 선언과 대조).
124
+
125
+ # WatchlistNode / SymbolFilterNode / ExclusionListNode / 계좌 노드의 held_symbols —
126
+ # 정말로 exchange+symbol 둘만 내보내는 포트 전용.
115
127
  SYMBOL_LIST_FIELDS: List[Dict[str, str]] = [
116
128
  {"name": "exchange", "type": "string", "description": "거래소 코드 (NASDAQ, NYSE, CME 등)"},
117
129
  {"name": "symbol", "type": "string", "description": "종목코드"},
118
130
  ]
119
131
 
132
+ # ── ScreenerNode.symbols (조건 검색 결과) ──
133
+ # LS(g3101 enrich) 분기와 yfinance 분기가 **같은 키 집합**을 내보낸다.
134
+ SCREENER_SYMBOL_FIELDS: List[Dict[str, str]] = [
135
+ {"name": "exchange", "type": "string", "description": "거래소 코드"},
136
+ {"name": "symbol", "type": "string", "description": "종목코드"},
137
+ {"name": "name", "type": "string", "description": "종목명 (상류 종목 리스트가 줬거나 yfinance 조회로 채움)"},
138
+ {"name": "market", "type": "string", "description": "시장 구분 (상류가 준 값 통과 — 예: KOSPI/KOSDAQ)"},
139
+ {"name": "price", "type": "number", "description": "현재가 (필터 시점)"},
140
+ {"name": "market_cap", "type": "number", "description": "시가총액"},
141
+ {"name": "volume", "type": "number", "description": "거래량"},
142
+ {"name": "sector", "type": "string", "description": "섹터 (yfinance 분기에서만 채워짐 — LS 분기는 빈 문자열)"},
143
+ ]
144
+
145
+ # ── MarketUniverseNode.symbols (지수 구성종목) ──
146
+ MARKET_UNIVERSE_SYMBOL_FIELDS: List[Dict[str, str]] = [
147
+ {"name": "exchange", "type": "string", "description": "거래소 코드"},
148
+ {"name": "symbol", "type": "string", "description": "종목코드"},
149
+ {"name": "name", "type": "string", "description": "종목명"},
150
+ ]
151
+
152
+ # ── OverseasStockSymbolQueryNode.symbols (g3190 마스터) ──
153
+ OVERSEAS_STOCK_SYMBOL_QUERY_FIELDS: List[Dict[str, str]] = [
154
+ {"name": "exchange", "type": "string", "description": "거래소 이름 (NASDAQ, NYSE 등)"},
155
+ {"name": "exchange_code", "type": "string", "description": "LS 거래소 코드"},
156
+ {"name": "symbol", "type": "string", "description": "종목코드"},
157
+ {"name": "name", "type": "string", "description": "종목명 (한글 우선, 없으면 영문)"},
158
+ {"name": "isin", "type": "string", "description": "ISIN 코드"},
159
+ {"name": "price", "type": "number", "description": "전일 종가"},
160
+ {"name": "market_cap", "type": "number", "description": "시가총액"},
161
+ {"name": "shares_outstanding", "type": "number", "description": "상장 주식 수"},
162
+ {"name": "currency", "type": "string", "description": "통화 코드"},
163
+ {"name": "suspend", "type": "string", "description": "거래정지 여부 (Y/N)"},
164
+ {"name": "sellonly", "type": "string", "description": "매도전용 여부 (Y/N)"},
165
+ ]
166
+
167
+ # ── KoreaStockSymbolQueryNode.symbols (t9945 마스터) ──
168
+ KOREA_STOCK_SYMBOL_QUERY_FIELDS: List[Dict[str, str]] = [
169
+ {"name": "exchange", "type": "string", "description": "거래소 코드 (항상 KRX)"},
170
+ {"name": "market", "type": "string", "description": "시장 구분 (KOSPI / KOSDAQ)"},
171
+ {"name": "symbol", "type": "string", "description": "종목코드 (6자리)"},
172
+ {"name": "name", "type": "string", "description": "종목명"},
173
+ {"name": "is_etf", "type": "boolean", "description": "ETF 여부"},
174
+ {"name": "product", "type": "string", "description": "상품 구분 (항상 korea_stock)"},
175
+ ]
176
+
177
+ # ── OverseasFuturesSymbolQueryNode.symbols (o3101 마스터) ──
178
+ OVERSEAS_FUTURES_SYMBOL_QUERY_FIELDS: List[Dict[str, str]] = [
179
+ {"name": "exchange", "type": "string", "description": "거래소 코드 (CME, HKEX 등 — 주문에 쓰는 값)"},
180
+ {"name": "exchange_code", "type": "string", "description": "LS 거래소 코드 (exchange 와 동일)"},
181
+ {"name": "exchange_name", "type": "string", "description": "거래소 한글명 (표시용 — 주문에 쓰면 안 된다)"},
182
+ {"name": "symbol", "type": "string", "description": "종목코드 (월물 포함)"},
183
+ {"name": "name", "type": "string", "description": "종목명"},
184
+ {"name": "base_product", "type": "string", "description": "기초상품 코드 (예: ES)"},
185
+ {"name": "base_product_name", "type": "string", "description": "기초상품명"},
186
+ {"name": "currency", "type": "string", "description": "통화 코드"},
187
+ {"name": "contract_month", "type": "string", "description": "월물 (YYYYMM)"},
188
+ ]
189
+
120
190
  # ── Partial-failure metadata (internal) ──
121
191
  # Balance dicts may include these underscore-prefixed keys when the
122
192
  # upstream API call partially fails. Consumers (PositionSizingNode,
@@ -187,16 +257,22 @@ KOREA_STOCK_REAL_BALANCE_FIELDS: List[Dict[str, str]] = [
187
257
  ]
188
258
 
189
259
  # ── 국내주식 시세 전용 (t1102 기반) ──
260
+ # 런타임 정본: executor.py `MarketDataNodeExecutor._fetch_korea_stock`
261
+ # (해외와 같은 이름 규칙을 쓴다 — 예전 선언의 current_price/open_price/market_cap 은 런타임에 없었다.)
190
262
  KOREA_STOCK_PRICE_DATA_FIELDS: List[Dict[str, str]] = [
191
263
  {"name": "symbol", "type": "string", "description": "종목코드 (6자리)"},
264
+ {"name": "exchange", "type": "string", "description": "거래소 코드 (KRX)"},
192
265
  {"name": "name", "type": "string", "description": "종목명"},
193
- {"name": "current_price", "type": "number", "description": "현재가"},
266
+ {"name": "price", "type": "number", "description": "현재가 (원)"},
267
+ {"name": "change", "type": "number", "description": "전일대비 (하락이면 음수)"},
268
+ {"name": "change_pct", "type": "number", "description": "등락률 (%)"},
194
269
  {"name": "volume", "type": "number", "description": "거래량"},
195
- {"name": "change_percent", "type": "number", "description": "등락률 (%)"},
196
- {"name": "open_price", "type": "number", "description": "시가"},
197
- {"name": "high_price", "type": "number", "description": "고가"},
198
- {"name": "low_price", "type": "number", "description": "저가"},
199
- {"name": "market_cap", "type": "number", "description": "시가총액 (억원)"},
270
+ {"name": "open", "type": "number", "description": "시가"},
271
+ {"name": "high", "type": "number", "description": "고가"},
272
+ {"name": "low", "type": "number", "description": "저가"},
273
+ {"name": "close", "type": "number", "description": "종가 (현재가와 동일)"},
274
+ {"name": "per", "type": "number", "description": "PER"},
275
+ {"name": "pbr", "type": "number", "description": "PBR"},
200
276
  ]
201
277
 
202
278
  # ── 국내주식 펀더멘털 전용 ──
@@ -216,14 +292,114 @@ KOREA_STOCK_FUNDAMENTAL_FIELDS: List[Dict[str, str]] = [
216
292
  {"name": "industry", "type": "string", "description": "업종명"},
217
293
  ]
218
294
 
219
- POSITION_FIELDS: List[Dict[str, str]] = [
295
+ # ── 보유 포지션 (positions 포트) ──
296
+ # 🔴 옛 공유 상수 `POSITION_FIELDS` 는 `pnl` / `pnl_percent` 를 선언했으나 **런타임에 그런 키는 없다**
297
+ # (실제는 `pnl_amount` / `pnl_rate`). 챗봇이 카탈로그를 보고 `pnl` 을 바인딩하면 정적 검증은
298
+ # 통과하고 런타임엔 조용히 None 이 됐다. 상품별로 키가 다르므로 노드별로 쪼갠다.
299
+
300
+ # 해외주식 REST 계좌 (COSOQ00201 block4)
301
+ OVERSEAS_STOCK_POSITION_FIELDS: List[Dict[str, str]] = [
220
302
  {"name": "exchange", "type": "string", "description": "거래소 코드"},
221
303
  {"name": "symbol", "type": "string", "description": "종목코드"},
304
+ {"name": "name", "type": "string", "description": "종목명"},
305
+ {"name": "qty", "type": "number", "description": "보유 수량"},
306
+ {"name": "quantity", "type": "number", "description": "보유 수량 (주문 노드 호환 별칭)"},
307
+ {"name": "direction", "type": "string", "description": "포지션 방향 (주식은 항상 long)"},
308
+ {"name": "close_side", "type": "string", "description": "청산 주문 방향 (주식은 항상 sell)"},
309
+ {"name": "avg_price", "type": "number", "description": "평균 매입가"},
310
+ {"name": "current_price", "type": "number", "description": "현재가"},
311
+ {"name": "pnl_amount", "type": "number", "description": "평가 손익 (금액)"},
312
+ {"name": "pnl_rate", "type": "number", "description": "수익률 (%)"},
313
+ {"name": "eval_amount", "type": "number", "description": "평가 금액"},
314
+ {"name": "purchase_amount", "type": "number", "description": "매입 금액"},
315
+ {"name": "currency", "type": "string", "description": "통화 코드"},
316
+ {"name": "market", "type": "string", "description": "시장 구분명"},
317
+ ]
318
+
319
+ # 국내주식 REST 계좌 (t0424)
320
+ KOREA_STOCK_POSITION_FIELDS: List[Dict[str, str]] = [
321
+ {"name": "exchange", "type": "string", "description": "거래소 코드 (항상 KRX)"},
322
+ {"name": "symbol", "type": "string", "description": "종목코드"},
323
+ {"name": "name", "type": "string", "description": "종목명"},
222
324
  {"name": "quantity", "type": "number", "description": "보유 수량"},
325
+ {"name": "price", "type": "number", "description": "현재가 (주문 노드 호환 별칭)"},
223
326
  {"name": "avg_price", "type": "number", "description": "평균 매입가"},
224
327
  {"name": "current_price", "type": "number", "description": "현재가"},
225
- {"name": "pnl", "type": "number", "description": "평가 손익"},
226
- {"name": "pnl_percent", "type": "number", "description": "수익률 (%)"},
328
+ {"name": "pnl_amount", "type": "number", "description": "평가 손익 (금액)"},
329
+ {"name": "pnl_rate", "type": "number", "description": "수익률 (%)"},
330
+ {"name": "sellable_qty", "type": "number", "description": "매도 가능 수량"},
331
+ {"name": "eval_amount", "type": "number", "description": "평가 금액"},
332
+ {"name": "product", "type": "string", "description": "상품 구분 (항상 korea_stock)"},
333
+ ]
334
+
335
+ # 해외선물 REST 계좌 (CIDBQ01500 block2)
336
+ # 주의: 이 TR 응답에는 수익률(pnl_rate)이 없다 — 손익 금액(pnl_amount)만 온다.
337
+ OVERSEAS_FUTURES_POSITION_FIELDS: List[Dict[str, str]] = [
338
+ {"name": "exchange", "type": "string", "description": "거래소 코드"},
339
+ {"name": "symbol", "type": "string", "description": "종목코드 (월물 포함)"},
340
+ {"name": "name", "type": "string", "description": "종목명"},
341
+ {"name": "direction", "type": "string", "description": "포지션 방향 (long / short)"},
342
+ {"name": "close_side", "type": "string", "description": "청산 주문 방향 (sell / buy)"},
343
+ {"name": "quantity", "type": "number", "description": "보유 계약 수"},
344
+ {"name": "price", "type": "number", "description": "현재가 (주문 노드 호환 별칭)"},
345
+ {"name": "entry_price", "type": "number", "description": "진입 가격"},
346
+ {"name": "current_price", "type": "number", "description": "현재가"},
347
+ {"name": "pnl_amount", "type": "number", "description": "평가 손익 (금액)"},
348
+ {"name": "currency", "type": "string", "description": "통화 코드"},
349
+ ]
350
+
351
+ # ── 실시간 계좌 포지션 (RealAccountNode.positions) ──
352
+ # 실시간 노드는 두 갈래로 positions 를 내보낸다: ① REST 스냅샷 직렬화 ② WebSocket tracker.
353
+ # 둘의 키가 다르면 **같은 포트인데 순간마다 다른 필드**가 나온다 → 두 갈래를 같은 키로 통일했다.
354
+ OVERSEAS_STOCK_REAL_POSITION_FIELDS: List[Dict[str, str]] = [
355
+ {"name": "exchange", "type": "string", "description": "거래소 코드"},
356
+ {"name": "market_code", "type": "string", "description": "LS 시장 코드"},
357
+ {"name": "symbol", "type": "string", "description": "종목코드"},
358
+ {"name": "name", "type": "string", "description": "종목명"},
359
+ {"name": "qty", "type": "number", "description": "보유 수량"},
360
+ {"name": "quantity", "type": "number", "description": "보유 수량 (주문 노드 호환 별칭)"},
361
+ {"name": "price", "type": "number", "description": "현재가 (주문 노드 호환 별칭)"},
362
+ {"name": "avg_price", "type": "number", "description": "평균 매입가"},
363
+ {"name": "current_price", "type": "number", "description": "현재가"},
364
+ {"name": "pnl_amount", "type": "number", "description": "평가 손익 (금액)"},
365
+ {"name": "pnl_rate", "type": "number", "description": "수익률 (%)"},
366
+ {"name": "eval_amount", "type": "number", "description": "평가 금액"},
367
+ {"name": "currency", "type": "string", "description": "통화 코드"},
368
+ {"name": "product", "type": "string", "description": "상품 구분 (항상 overseas_stock)"},
369
+ ]
370
+
371
+ KOREA_STOCK_REAL_POSITION_FIELDS: List[Dict[str, str]] = [
372
+ {"name": "exchange", "type": "string", "description": "거래소 코드 (항상 KRX)"},
373
+ {"name": "market_code", "type": "string", "description": "시장 구분 코드"},
374
+ {"name": "symbol", "type": "string", "description": "종목코드"},
375
+ {"name": "name", "type": "string", "description": "종목명"},
376
+ {"name": "qty", "type": "number", "description": "보유 수량"},
377
+ {"name": "quantity", "type": "number", "description": "보유 수량 (주문 노드 호환 별칭)"},
378
+ {"name": "price", "type": "number", "description": "현재가 (주문 노드 호환 별칭)"},
379
+ {"name": "avg_price", "type": "number", "description": "평균 매입가"},
380
+ {"name": "current_price", "type": "number", "description": "현재가"},
381
+ {"name": "pnl_amount", "type": "number", "description": "평가 손익 (금액)"},
382
+ {"name": "pnl_rate", "type": "number", "description": "수익률 (%)"},
383
+ {"name": "eval_amount", "type": "number", "description": "평가 금액"},
384
+ {"name": "currency", "type": "string", "description": "통화 코드 (항상 KRW)"},
385
+ {"name": "product", "type": "string", "description": "상품 구분 (항상 korea_stock)"},
386
+ ]
387
+
388
+ OVERSEAS_FUTURES_REAL_POSITION_FIELDS: List[Dict[str, str]] = [
389
+ {"name": "exchange", "type": "string", "description": "거래소 코드"},
390
+ {"name": "symbol", "type": "string", "description": "종목코드 (월물 포함)"},
391
+ {"name": "name", "type": "string", "description": "종목명"},
392
+ {"name": "direction", "type": "string", "description": "포지션 방향 (long / short)"},
393
+ {"name": "close_side", "type": "string", "description": "청산 주문 방향 (sell / buy)"},
394
+ {"name": "qty", "type": "number", "description": "보유 계약 수"},
395
+ {"name": "quantity", "type": "number", "description": "보유 계약 수 (주문 노드 호환 별칭)"},
396
+ {"name": "price", "type": "number", "description": "현재가 (주문 노드 호환 별칭)"},
397
+ {"name": "entry_price", "type": "number", "description": "진입 가격"},
398
+ {"name": "current_price", "type": "number", "description": "현재가"},
399
+ {"name": "pnl_amount", "type": "number", "description": "평가 손익 (금액)"},
400
+ {"name": "pnl_rate", "type": "number", "description": "수익률 (%)"},
401
+ {"name": "currency", "type": "string", "description": "통화 코드"},
402
+ {"name": "product", "type": "string", "description": "상품 구분 (항상 overseas_futures)"},
227
403
  ]
228
404
 
229
405
  ORDER_RESULT_FIELDS: List[Dict[str, str]] = [
@@ -236,16 +412,44 @@ ORDER_RESULT_FIELDS: List[Dict[str, str]] = [
236
412
  {"name": "status", "type": "string", "description": "주문 상태"},
237
413
  ]
238
414
 
415
+ # ⚠️ 이 선언은 **런타임이 실제로 내보내는 키와 정확히 같아야 한다.**
416
+ # resolver 가 이 이름들로 필드 존재를 정적 검증하므로, 어긋나면 두 방향으로 다 틀린다:
417
+ # 실제 있는 필드를 바인딩 → INVALID_EXPRESSION_REF 로 **거부**
418
+ # 실제 없는 필드를 바인딩 → **통과**시킨 뒤 런타임에 조용히 None
419
+ # (2026-07-13 실측: 선언이 current_price/change_percent 였으나 런타임은 price/change_pct 였다.)
420
+ # 런타임 정본: executor.py `MarketDataNodeExecutor._fetch_overseas_stock`
239
421
  PRICE_DATA_FIELDS: List[Dict[str, str]] = [
240
- {"name": "exchange", "type": "string", "description": "거래소 코드"},
241
422
  {"name": "symbol", "type": "string", "description": "종목코드"},
242
- {"name": "current_price", "type": "number", "description": "현재가"},
423
+ {"name": "exchange", "type": "string", "description": "거래소 코드"},
424
+ {"name": "price", "type": "number", "description": "현재가"},
425
+ {"name": "change", "type": "number", "description": "전일대비 (하락이면 음수)"},
426
+ {"name": "change_pct", "type": "number", "description": "등락률 (%)"},
243
427
  {"name": "volume", "type": "number", "description": "거래량"},
244
- {"name": "change_percent", "type": "number", "description": "등락률 (%)"},
428
+ {"name": "open", "type": "number", "description": "시가"},
429
+ {"name": "high", "type": "number", "description": "고가"},
430
+ {"name": "low", "type": "number", "description": "저가"},
431
+ {"name": "close", "type": "number", "description": "종가 (현재가와 동일)"},
245
432
  {"name": "per", "type": "number", "description": "PER (주가수익비율)"},
246
433
  {"name": "eps", "type": "number", "description": "EPS (주당순이익)"},
247
434
  ]
248
435
 
436
+ # ── 해외선물 시세 전용 (o3105 기반) ──
437
+ # 해외주식과 모양이 다르다(symbol_name 있음 / per·eps 없음). 상수를 공유하면 드리프트가 난다.
438
+ # 런타임 정본: executor.py `MarketDataNodeExecutor._fetch_overseas_futures`
439
+ OVERSEAS_FUTURES_PRICE_DATA_FIELDS: List[Dict[str, str]] = [
440
+ {"name": "symbol", "type": "string", "description": "종목코드 (월물 심볼)"},
441
+ {"name": "exchange", "type": "string", "description": "거래소 코드"},
442
+ {"name": "symbol_name", "type": "string", "description": "종목명"},
443
+ {"name": "price", "type": "number", "description": "현재가"},
444
+ {"name": "change", "type": "number", "description": "전일대비 (하락이면 음수)"},
445
+ {"name": "change_pct", "type": "number", "description": "등락률 (%)"},
446
+ {"name": "volume", "type": "number", "description": "거래량"},
447
+ {"name": "open", "type": "number", "description": "시가"},
448
+ {"name": "high", "type": "number", "description": "고가"},
449
+ {"name": "low", "type": "number", "description": "저가"},
450
+ {"name": "close", "type": "number", "description": "종가 (현재가와 동일)"},
451
+ ]
452
+
249
453
  FUNDAMENTAL_DATA_FIELDS: List[Dict[str, str]] = [
250
454
  {"name": "exchange", "type": "string", "description": "거래소 코드"},
251
455
  {"name": "symbol", "type": "string", "description": "종목코드"},
@@ -324,26 +528,19 @@ ORDER_EVENT_FIELDS: List[Dict[str, str]] = [
324
528
  {"name": "timestamp", "type": "string", "description": "이벤트 시각"},
325
529
  ]
326
530
 
531
+ # ── 실시간 시세 노드의 봉(bar) ──
532
+ # 🔴 실시간 노드의 `ohlcv_data` / `data` 는 **같은 객체**다 (`data` 는 별칭).
533
+ # 값의 모양은 `{"AAPL": [bar, ...]}` — 종목코드로 키를 잡은 dict 이고, bar 가 아래 필드다.
534
+ # 옛 선언은 `exchange` / `timestamp` 를 약속했지만 런타임 bar 에 그런 키는 없고(실제는 `date`),
535
+ # `data` 포트는 아예 `current_price` / `bid_price` / `ask_price` / `change_percent` 를 약속했는데
536
+ # **그 값을 만드는 코드(`_build_full_data`)는 아무도 호출하지 않는 죽은 코드였다.**
327
537
  OHLCV_DATA_FIELDS: List[Dict[str, str]] = [
328
- {"name": "exchange", "type": "string", "description": "거래소 코드"},
329
- {"name": "symbol", "type": "string", "description": "종목코드"},
538
+ {"name": "date", "type": "string", "description": " 일자 (YYYYMMDD)"},
330
539
  {"name": "open", "type": "number", "description": "시가"},
331
540
  {"name": "high", "type": "number", "description": "고가"},
332
541
  {"name": "low", "type": "number", "description": "저가"},
333
- {"name": "close", "type": "number", "description": "종가"},
334
- {"name": "volume", "type": "number", "description": "거래량"},
335
- {"name": "timestamp", "type": "string", "description": "캔들 시각"},
336
- ]
337
-
338
- MARKET_DATA_FULL_FIELDS: List[Dict[str, str]] = [
339
- {"name": "exchange", "type": "string", "description": "거래소 코드"},
340
- {"name": "symbol", "type": "string", "description": "종목코드"},
341
- {"name": "current_price", "type": "number", "description": "현재가"},
342
- {"name": "bid_price", "type": "number", "description": "매수호가 (해외주식 실시간에서는 미제공)"},
343
- {"name": "ask_price", "type": "number", "description": "매도호가 (해외주식 실시간에서는 미제공)"},
542
+ {"name": "close", "type": "number", "description": "종가 (= 최근 체결가)"},
344
543
  {"name": "volume", "type": "number", "description": "거래량"},
345
- {"name": "change_percent", "type": "number", "description": "등락률 (%)"},
346
- {"name": "timestamp", "type": "string", "description": "시세 시각"},
347
544
  ]
348
545
 
349
546
  CONDITION_RESULT_FIELDS: List[Dict[str, str]] = [
@@ -22,7 +22,7 @@ from programgarden_core.nodes.base import (
22
22
  OutputPort,
23
23
  ProductScope,
24
24
  BrokerProvider,
25
- PRICE_DATA_FIELDS,
25
+ OVERSEAS_FUTURES_PRICE_DATA_FIELDS,
26
26
  )
27
27
 
28
28
 
@@ -74,7 +74,7 @@ class OverseasFuturesMarketDataNode(BaseNode):
74
74
  ],
75
75
  }
76
76
  _features: ClassVar[List[str]] = [
77
- "Returns a single futures contract's snapshot: current_price, volume, open_interest, change_percent, bid, ask",
77
+ "Returns a single futures contract's snapshot: symbol, exchange, symbol_name, price, change, change_pct, volume, open, high, low, close",
78
78
  "Item-based execution: pair with SplitNode to query multiple contracts in sequence",
79
79
  "is_tool_enabled=True — AI Agent can call this node to look up live futures prices autonomously",
80
80
  "Supported exchanges: CME, EUREX, SGX, HKEX — symbol format includes contract month code (e.g., ESH26)",
@@ -121,7 +121,7 @@ class OverseasFuturesMarketDataNode(BaseNode):
121
121
  }
122
122
  ],
123
123
  },
124
- "expected_output": "values port: array of {symbol, exchange, current_price, volume, open_interest, change_percent, bid, ask}.",
124
+ "expected_output": "values port: array of {symbol, exchange, symbol_name, price, change, change_pct, volume, open, high, low, close}.",
125
125
  },
126
126
  {
127
127
  "title": "Multi-contract scan — compare HKEX futures prices",
@@ -161,9 +161,9 @@ class OverseasFuturesMarketDataNode(BaseNode):
161
161
  "Broker connection auto-injected from OverseasFuturesBrokerNode."
162
162
  ),
163
163
  "output_consumption": (
164
- "Consume the `values` port ONLY — an array of {symbol, exchange, current_price, volume, open_interest, change_percent, bid, ask}. "
164
+ "Consume the `values` port ONLY — an array of {symbol, exchange, symbol_name, price, change, change_pct, volume, open, high, low, close}. "
165
165
  "⚠️ The `value` (singular) port is NOT populated at runtime — the executor emits `values` only; "
166
- "binding `{{ nodes.market.value }}` (or `.value.current_price`) silently resolves to None. "
166
+ "binding `{{ nodes.market.value }}` (or `.value.price`) silently resolves to None. "
167
167
  "TableDisplayNode.data ← `{{ nodes.market.values }}`; PositionSizingNode.market_data ← `{{ nodes.market.values }}`."
168
168
  ),
169
169
  "common_combinations": [
@@ -183,11 +183,13 @@ class OverseasFuturesMarketDataNode(BaseNode):
183
183
  InputPort(name="trigger", type="signal", description="i18n:ports.trigger", required=False),
184
184
  ]
185
185
  _outputs: List[OutputPort] = [
186
- OutputPort(name="value", type="market_data", description="i18n:ports.market_data_value", fields=PRICE_DATA_FIELDS),
186
+ OutputPort(name="value", type="market_data", description="i18n:ports.market_data_value",
187
+ fields=OVERSEAS_FUTURES_PRICE_DATA_FIELDS),
187
188
  OutputPort(
188
189
  name="values",
189
190
  type="array",
190
- description="Array of per-contract market quotes — [{symbol, exchange, current_price, ...}, ...]",
191
+ description="Array of per-contract market quotes — [{symbol, exchange, price, change, change_pct, ...}, ...]",
192
+ fields=OVERSEAS_FUTURES_PRICE_DATA_FIELDS,
191
193
  ),
192
194
  ]
193
195
 
@@ -74,7 +74,7 @@ class KoreaStockMarketDataNode(BaseNode):
74
74
  ],
75
75
  }
76
76
  _features: ClassVar[List[str]] = [
77
- "Returns a single domestic stock's snapshot: current_price, volume, change_percent, bid, ask, per, eps, 52w_high, 52w_low",
77
+ "Returns a single domestic stock's snapshot: symbol, exchange, name, price, change, change_pct, volume, open, high, low, close, per, pbr",
78
78
  "Symbol format is a 6-digit KRX code (e.g., '005930') — no exchange field required (domestic market only)",
79
79
  "Item-based execution: pair with SplitNode to query multiple domestic stocks in sequence",
80
80
  "is_tool_enabled=True — AI Agent can call this node to look up KRX stock prices autonomously",
@@ -121,7 +121,7 @@ class KoreaStockMarketDataNode(BaseNode):
121
121
  }
122
122
  ],
123
123
  },
124
- "expected_output": "values port: array of {symbol, current_price, volume, change_percent, bid, ask, per, eps} per domestic stock.",
124
+ "expected_output": "values port: array of {symbol, exchange, name, price, change, change_pct, volume, open, high, low, close, per, pbr} per domestic stock.",
125
125
  },
126
126
  {
127
127
  "title": "Price lookup for domestic PositionSizingNode",
@@ -165,11 +165,11 @@ class KoreaStockMarketDataNode(BaseNode):
165
165
  ),
166
166
  "output_consumption": (
167
167
  "Consume the `values` port ONLY — an array of per-symbol snapshots "
168
- "[{symbol, current_price, volume, change_percent, bid, ask, per, eps, 52w_high, 52w_low}, ...]. "
168
+ "[{symbol, exchange, name, price, change, change_pct, volume, open, high, low, close, per, pbr}, ...]. "
169
169
  "⚠️ The `value` (singular) port is NOT populated at runtime — the executor emits `values` only; "
170
- "binding `{{ nodes.market.value }}` (or `.value.current_price`) silently resolves to None. "
170
+ "binding `{{ nodes.market.value }}` (or `.value.price`) silently resolves to None. "
171
171
  "TableDisplayNode.data ← `{{ nodes.market.values }}`; PositionSizingNode.market_data ← `{{ nodes.market.values }}` "
172
- "(the array carries each symbol's current_price, which PositionSizingNode resolves internally)."
172
+ "(the array carries each symbol's price, which PositionSizingNode resolves internally)."
173
173
  ),
174
174
  "common_combinations": [
175
175
  "KoreaStockMarketDataNode.values → TableDisplayNode.data (KRX price monitor)",
@@ -189,7 +189,12 @@ class KoreaStockMarketDataNode(BaseNode):
189
189
  ]
190
190
  _outputs: List[OutputPort] = [
191
191
  OutputPort(name="value", type="market_data", description="i18n:ports.market_data_value", fields=KOREA_STOCK_PRICE_DATA_FIELDS),
192
- OutputPort(name="values", type="array", description="Array of per-symbol market quotes"),
192
+ OutputPort(
193
+ name="values",
194
+ type="array",
195
+ description="Array of per-symbol market quotes — [{symbol, exchange, name, price, change, change_pct, ...}, ...]",
196
+ fields=KOREA_STOCK_PRICE_DATA_FIELDS,
197
+ ),
193
198
  ]
194
199
 
195
200
  _version: ClassVar[str] = "1.0.0"
@@ -74,7 +74,7 @@ class OverseasStockMarketDataNode(BaseNode):
74
74
  ],
75
75
  }
76
76
  _features: ClassVar[List[str]] = [
77
- "Returns a single symbol's snapshot: current_price, volume, change_percent, bid, ask, per, eps, 52w_high, 52w_low",
77
+ "Returns a single symbol's snapshot: symbol, exchange, price, change, change_pct, volume, open, high, low, close, per, eps",
78
78
  "Item-based execution: receives one {exchange, symbol} dict per call and emits one value dict — pair with SplitNode for multi-symbol queries",
79
79
  "is_tool_enabled=True — AI Agent can call this node to look up live prices autonomously",
80
80
  "Broker connection is auto-injected via DAG traversal — no explicit binding needed",
@@ -121,7 +121,7 @@ class OverseasStockMarketDataNode(BaseNode):
121
121
  }
122
122
  ],
123
123
  },
124
- "expected_output": "values port: array of {symbol, exchange, current_price, volume, change_percent, bid, ask, per, eps} for each symbol.",
124
+ "expected_output": "values port: array of {symbol, exchange, price, change, change_pct, volume, open, high, low, close, per, eps} for each symbol.",
125
125
  },
126
126
  {
127
127
  "title": "Price lookup feeding PositionSizingNode",
@@ -164,12 +164,11 @@ class OverseasStockMarketDataNode(BaseNode):
164
164
  "The node auto-receives the broker connection via DAG traversal from OverseasStockBrokerNode."
165
165
  ),
166
166
  "output_consumption": (
167
- "Consume the `values` port ONLY — an array of {symbol, exchange, current_price, volume, change_percent, "
168
- "bid, ask, per, eps, 52w_high, 52w_low}. "
167
+ "Consume the `values` port ONLY — an array of {symbol, exchange, price, change, change_pct, volume, open, high, low, close, per, eps}. "
169
168
  "⚠️ The `value` (singular) port is NOT populated at runtime — the executor emits `values` only; "
170
- "binding `{{ nodes.market.value }}` (or `.value.current_price`) silently resolves to None. "
169
+ "binding `{{ nodes.market.value }}` (or `.value.price`) silently resolves to None. "
171
170
  "TableDisplayNode.data ← `{{ nodes.market.values }}`; PositionSizingNode.market_data ← `{{ nodes.market.values }}` "
172
- "(the array carries each symbol's current_price, resolved internally)."
171
+ "(the array carries each symbol's price, resolved internally)."
173
172
  ),
174
173
  "common_combinations": [
175
174
  "WatchlistNode/SymbolQueryNode → OverseasStockMarketDataNode.symbols (multi-symbol price fetch)",
@@ -194,11 +193,16 @@ class OverseasStockMarketDataNode(BaseNode):
194
193
  description="i18n:ports.market_data_value",
195
194
  fields=PRICE_DATA_FIELDS,
196
195
  example={
197
- "exchange": "NASDAQ",
198
196
  "symbol": "AAPL",
199
- "current_price": 187.45,
197
+ "exchange": "NASDAQ",
198
+ "price": 187.45,
199
+ "change": -2.34,
200
+ "change_pct": -1.23,
200
201
  "volume": 12_345_678,
201
- "change_percent": -1.23,
202
+ "open": 189.10,
203
+ "high": 190.02,
204
+ "low": 186.90,
205
+ "close": 187.45,
202
206
  "per": 28.5,
203
207
  "eps": 6.57,
204
208
  },
@@ -206,7 +210,8 @@ class OverseasStockMarketDataNode(BaseNode):
206
210
  OutputPort(
207
211
  name="values",
208
212
  type="array",
209
- description="Array of per-symbol market quotes — [{symbol, exchange, current_price, ...}, ...]",
213
+ description="Array of per-symbol market quotes — [{symbol, exchange, price, change, change_pct, ...}, ...]",
214
+ fields=PRICE_DATA_FIELDS,
210
215
  ),
211
216
  ]
212
217
 
@@ -21,11 +21,10 @@ from programgarden_core.nodes.base import (
21
21
  ProductScope,
22
22
  BrokerProvider,
23
23
  OVERSEAS_FUTURES_REAL_BALANCE_FIELDS,
24
- MARKET_DATA_FULL_FIELDS,
25
24
  OHLCV_DATA_FIELDS,
26
25
  ORDER_EVENT_FIELDS,
27
26
  ORDER_LIST_FIELDS,
28
- POSITION_FIELDS,
27
+ OVERSEAS_FUTURES_REAL_POSITION_FIELDS,
29
28
  SYMBOL_LIST_FIELDS,
30
29
  PRICE_DATA_FIELDS,
31
30
  )
@@ -65,7 +64,7 @@ class OverseasFuturesRealMarketDataNode(BaseNode):
65
64
  ],
66
65
  }
67
66
  _features: ClassVar[List[str]] = [
68
- "Streams real-time trade events from LS Securities WebSocket — ohlcv_data port emits aggregated candles; data port emits raw tick dicts",
67
+ "Streams real-time trade events from LS Securities WebSocket — ohlcv_data 포트가 집계된 봉을 내보낸다 (data 포트는 같은 객체의 별칭)",
69
68
  "stay_connected=True keeps the WebSocket subscription live between cycles (recommended for realtime strategies)",
70
69
  "Supports CME, EUREX, SGX, HKEX contracts — symbol must include contract month code (e.g., ESH26)",
71
70
  "Item-based execution: one subscription per node; use multiple nodes to watch multiple contracts",
@@ -179,8 +178,11 @@ class OverseasFuturesRealMarketDataNode(BaseNode):
179
178
  "Use OverseasFuturesBrokerNode as the upstream broker."
180
179
  ),
181
180
  "output_consumption": (
182
- "The `ohlcv_data` port emits aggregated candle dicts: {symbol, exchange, open, high, low, close, volume}. "
183
- "The `data` port emits the raw full tick dict. "
181
+ "`ohlcv_data` `data` **같은 객체**다 `data` 별칭이다. "
182
+ "(예전 문서는 `data` bid/ask 담은 raw tick dict 이라고 했지만, 그 값을 만드는 코드는 "
183
+ "아무도 호출하지 않는 죽은 코드였다. 런타임에 bid/ask/current_price/change_percent 는 없다.) "
184
+ "값의 모양: {\"AAPL\": [{date, open, high, low, close, volume}, ...]} — 종목코드로 키를 잡은 dict 이고, "
185
+ "봉 한 줄에는 exchange/symbol/timestamp 가 없다. "
184
186
  "ALWAYS insert ThrottleNode before any order or AI node downstream."
185
187
  ),
186
188
  "common_combinations": [
@@ -210,7 +212,8 @@ class OverseasFuturesRealMarketDataNode(BaseNode):
210
212
  ]
211
213
  _outputs: List[OutputPort] = [
212
214
  OutputPort(name="ohlcv_data", type="ohlcv_data", description="i18n:ports.ohlcv_data", fields=OHLCV_DATA_FIELDS),
213
- OutputPort(name="data", type="market_data_full", description="i18n:ports.market_data_full", fields=MARKET_DATA_FULL_FIELDS),
215
+ OutputPort(name="data", type="ohlcv_data", description="i18n:ports.market_data_full", fields=OHLCV_DATA_FIELDS),
216
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SYMBOL_LIST_FIELDS),
214
217
  ]
215
218
 
216
219
  _version: ClassVar[str] = "1.0.0"
@@ -401,7 +404,7 @@ class OverseasFuturesRealAccountNode(BaseNode):
401
404
  OutputPort(name="held_symbols", type="symbol_list", description="i18n:ports.held_symbols", fields=SYMBOL_LIST_FIELDS),
402
405
  OutputPort(name="balance", type="balance_data", description="i18n:ports.balance", fields=OVERSEAS_FUTURES_REAL_BALANCE_FIELDS),
403
406
  OutputPort(name="open_orders", type="order_list", description="i18n:ports.open_orders", fields=ORDER_LIST_FIELDS),
404
- OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=POSITION_FIELDS),
407
+ OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=OVERSEAS_FUTURES_REAL_POSITION_FIELDS),
405
408
  ]
406
409
 
407
410
  _version: ClassVar[str] = "1.0.0"
@@ -21,11 +21,10 @@ from programgarden_core.nodes.base import (
21
21
  ProductScope,
22
22
  BrokerProvider,
23
23
  KOREA_STOCK_REAL_BALANCE_FIELDS,
24
- MARKET_DATA_FULL_FIELDS,
25
24
  OHLCV_DATA_FIELDS,
26
25
  ORDER_EVENT_FIELDS,
27
26
  ORDER_LIST_FIELDS,
28
- POSITION_FIELDS,
27
+ KOREA_STOCK_REAL_POSITION_FIELDS,
29
28
  SYMBOL_LIST_FIELDS,
30
29
  )
31
30
 
@@ -64,7 +63,7 @@ class KoreaStockRealMarketDataNode(BaseNode):
64
63
  ],
65
64
  }
66
65
  _features: ClassVar[List[str]] = [
67
- "Streams real-time trade events from LS Securities WebSocket for KRX stocks — ohlcv_data port emits aggregated candles; data port emits raw tick dicts",
66
+ "Streams real-time trade events from LS Securities WebSocket for KRX stocks — ohlcv_data 포트가 집계된 봉을 내보낸다 (data 포트는 같은 객체의 별칭)",
68
67
  "stay_connected=True keeps the WebSocket subscription live between cycles (recommended for realtime strategies)",
69
68
  "Symbol format: 6-digit KRX code (e.g., '005930') without exchange field — domestic market implied",
70
69
  "Item-based execution: one subscription per node; use multiple nodes to watch multiple domestic stocks",
@@ -178,8 +177,11 @@ class KoreaStockRealMarketDataNode(BaseNode):
178
177
  "Use KoreaStockBrokerNode as the upstream broker."
179
178
  ),
180
179
  "output_consumption": (
181
- "The `ohlcv_data` port emits aggregated candle dicts: {symbol, open, high, low, close, volume}. "
182
- "The `data` port emits the raw full tick dict including bid/ask. "
180
+ "`ohlcv_data` `data` **같은 객체**다 `data` 별칭이다. "
181
+ "(예전 문서는 `data` bid/ask 담은 raw tick dict 이라고 했지만, 그 값을 만드는 코드는 "
182
+ "아무도 호출하지 않는 죽은 코드였다. 런타임에 bid/ask/current_price/change_percent 는 없다.) "
183
+ "값의 모양: {\"AAPL\": [{date, open, high, low, close, volume}, ...]} — 종목코드로 키를 잡은 dict 이고, "
184
+ "봉 한 줄에는 exchange/symbol/timestamp 가 없다. "
183
185
  "ALWAYS insert ThrottleNode before any order or AI node downstream."
184
186
  ),
185
187
  "common_combinations": [
@@ -209,7 +211,8 @@ class KoreaStockRealMarketDataNode(BaseNode):
209
211
  ]
210
212
  _outputs: List[OutputPort] = [
211
213
  OutputPort(name="ohlcv_data", type="ohlcv_data", description="i18n:ports.ohlcv_data", fields=OHLCV_DATA_FIELDS),
212
- OutputPort(name="data", type="market_data_full", description="i18n:ports.market_data_full", fields=MARKET_DATA_FULL_FIELDS),
214
+ OutputPort(name="data", type="ohlcv_data", description="i18n:ports.market_data_full", fields=OHLCV_DATA_FIELDS),
215
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SYMBOL_LIST_FIELDS),
213
216
  ]
214
217
 
215
218
  _version: ClassVar[str] = "1.0.0"
@@ -404,7 +407,7 @@ class KoreaStockRealAccountNode(BaseNode):
404
407
  OutputPort(name="held_symbols", type="symbol_list", description="i18n:ports.held_symbols", fields=SYMBOL_LIST_FIELDS),
405
408
  OutputPort(name="balance", type="balance_data", description="i18n:ports.balance", fields=KOREA_STOCK_REAL_BALANCE_FIELDS),
406
409
  OutputPort(name="open_orders", type="order_list", description="i18n:ports.open_orders", fields=ORDER_LIST_FIELDS),
407
- OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=POSITION_FIELDS),
410
+ OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=KOREA_STOCK_REAL_POSITION_FIELDS),
408
411
  ]
409
412
 
410
413
  _version: ClassVar[str] = "1.0.0"
@@ -21,11 +21,10 @@ from programgarden_core.nodes.base import (
21
21
  ProductScope,
22
22
  BrokerProvider,
23
23
  OVERSEAS_STOCK_REAL_BALANCE_FIELDS,
24
- MARKET_DATA_FULL_FIELDS,
25
24
  OHLCV_DATA_FIELDS,
26
25
  ORDER_EVENT_FIELDS,
27
26
  ORDER_LIST_FIELDS,
28
- POSITION_FIELDS,
27
+ OVERSEAS_STOCK_REAL_POSITION_FIELDS,
29
28
  SYMBOL_LIST_FIELDS,
30
29
  PRICE_DATA_FIELDS,
31
30
  )
@@ -66,7 +65,7 @@ class OverseasStockRealMarketDataNode(BaseNode):
66
65
  ],
67
66
  }
68
67
  _features: ClassVar[List[str]] = [
69
- "Streams GSC (trade/tick) events from LS Securities WebSocket — ohlcv_data port emits aggregated candles; data port emits raw tick dicts",
68
+ "Streams GSC (trade/tick) events from LS Securities WebSocket — ohlcv_data 포트가 집계된 봉을 내보낸다 (data 포트는 같은 객체의 별칭)",
70
69
  "stay_connected=True keeps the WebSocket subscription live between cycles (recommended for realtime strategies)",
71
70
  "Item-based execution: one subscription per node; use multiple nodes or SplitNode to watch several symbols",
72
71
  "Automatically re-subscribes after WebSocket reconnection events",
@@ -180,8 +179,11 @@ class OverseasStockRealMarketDataNode(BaseNode):
180
179
  "Set stay_connected=True (default) to avoid reconnection overhead on every cycle."
181
180
  ),
182
181
  "output_consumption": (
183
- "The `ohlcv_data` port emits aggregated candle dicts: {symbol, exchange, open, high, low, close, volume}. "
184
- "The `data` port emits the raw full tick dict including bid/ask fields. "
182
+ "`ohlcv_data` `data` **같은 객체**다 `data` 별칭이다. "
183
+ "(예전 문서는 `data` bid/ask 담은 raw tick dict 이라고 했지만, 값을 만드는 코드는 "
184
+ "아무도 호출하지 않는 죽은 코드였다. 런타임에 bid/ask/current_price/change_percent 는 없다.) "
185
+ "값의 모양: {\"AAPL\": [{date, open, high, low, close, volume}, ...]} — 종목코드로 키를 잡은 dict 이고, "
186
+ "봉 한 줄에는 exchange/symbol/timestamp 가 없다. "
185
187
  "ALWAYS insert ThrottleNode before any order or AI node downstream."
186
188
  ),
187
189
  "common_combinations": [
@@ -211,7 +213,8 @@ class OverseasStockRealMarketDataNode(BaseNode):
211
213
  ]
212
214
  _outputs: List[OutputPort] = [
213
215
  OutputPort(name="ohlcv_data", type="ohlcv_data", description="i18n:ports.ohlcv_data", fields=OHLCV_DATA_FIELDS),
214
- OutputPort(name="data", type="market_data_full", description="i18n:ports.market_data_full", fields=MARKET_DATA_FULL_FIELDS),
216
+ OutputPort(name="data", type="ohlcv_data", description="i18n:ports.market_data_full", fields=OHLCV_DATA_FIELDS),
217
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SYMBOL_LIST_FIELDS),
215
218
  ]
216
219
 
217
220
  _version: ClassVar[str] = "1.0.0"
@@ -406,7 +409,7 @@ class OverseasStockRealAccountNode(BaseNode):
406
409
  OutputPort(name="held_symbols", type="symbol_list", description="i18n:ports.held_symbols", fields=SYMBOL_LIST_FIELDS),
407
410
  OutputPort(name="balance", type="balance_data", description="i18n:ports.balance", fields=OVERSEAS_STOCK_REAL_BALANCE_FIELDS),
408
411
  OutputPort(name="open_orders", type="order_list", description="i18n:ports.open_orders", fields=ORDER_LIST_FIELDS),
409
- OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=POSITION_FIELDS),
412
+ OutputPort(name="positions", type="position_data", description="i18n:ports.positions", fields=OVERSEAS_STOCK_REAL_POSITION_FIELDS),
410
413
  ]
411
414
 
412
415
  _version: ClassVar[str] = "1.0.0"
@@ -24,6 +24,8 @@ from programgarden_core.nodes.base import (
24
24
  InputPort,
25
25
  OutputPort,
26
26
  SYMBOL_LIST_FIELDS,
27
+ SCREENER_SYMBOL_FIELDS,
28
+ MARKET_UNIVERSE_SYMBOL_FIELDS,
27
29
  )
28
30
  from programgarden_core.models.exchange import SymbolEntry, ProductType
29
31
 
@@ -251,7 +253,7 @@ class MarketUniverseNode(BaseNode):
251
253
 
252
254
  _inputs: List[InputPort] = []
253
255
  _outputs: List[OutputPort] = [
254
- OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SYMBOL_LIST_FIELDS),
256
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=MARKET_UNIVERSE_SYMBOL_FIELDS),
255
257
  OutputPort(name="count", type="integer", description="종목 수"),
256
258
  ]
257
259
 
@@ -453,7 +455,7 @@ class ScreenerNode(BaseNode):
453
455
  ),
454
456
  ]
455
457
  _outputs: List[OutputPort] = [
456
- OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SYMBOL_LIST_FIELDS),
458
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SCREENER_SYMBOL_FIELDS),
457
459
  OutputPort(name="count", type="integer", description="결과 종목 수"),
458
460
  ]
459
461
 
@@ -20,6 +20,7 @@ from programgarden_core.nodes.base import (
20
20
  ProductScope,
21
21
  BrokerProvider,
22
22
  SYMBOL_LIST_FIELDS,
23
+ OVERSEAS_FUTURES_SYMBOL_QUERY_FIELDS,
23
24
  )
24
25
 
25
26
 
@@ -182,7 +183,7 @@ class OverseasFuturesSymbolQueryNode(BaseNode):
182
183
 
183
184
  _inputs: List[InputPort] = []
184
185
  _outputs: List[OutputPort] = [
185
- OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SYMBOL_LIST_FIELDS),
186
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=OVERSEAS_FUTURES_SYMBOL_QUERY_FIELDS),
186
187
  OutputPort(name="count", type="integer", description="Total symbol count"),
187
188
  ]
188
189
 
@@ -18,7 +18,7 @@ from programgarden_core.nodes.base import (
18
18
  OutputPort,
19
19
  ProductScope,
20
20
  BrokerProvider,
21
- SYMBOL_LIST_FIELDS,
21
+ KOREA_STOCK_SYMBOL_QUERY_FIELDS,
22
22
  )
23
23
 
24
24
 
@@ -169,7 +169,7 @@ class KoreaStockSymbolQueryNode(BaseNode):
169
169
 
170
170
  _inputs: List[InputPort] = []
171
171
  _outputs: List[OutputPort] = [
172
- OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SYMBOL_LIST_FIELDS),
172
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=KOREA_STOCK_SYMBOL_QUERY_FIELDS),
173
173
  OutputPort(name="count", type="integer", description="Total symbol count"),
174
174
  ]
175
175
 
@@ -18,7 +18,7 @@ from programgarden_core.nodes.base import (
18
18
  OutputPort,
19
19
  ProductScope,
20
20
  BrokerProvider,
21
- SYMBOL_LIST_FIELDS,
21
+ OVERSEAS_STOCK_SYMBOL_QUERY_FIELDS,
22
22
  )
23
23
 
24
24
 
@@ -177,7 +177,7 @@ class OverseasStockSymbolQueryNode(BaseNode):
177
177
 
178
178
  _inputs: List[InputPort] = []
179
179
  _outputs: List[OutputPort] = [
180
- OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=SYMBOL_LIST_FIELDS),
180
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=OVERSEAS_STOCK_SYMBOL_QUERY_FIELDS),
181
181
  OutputPort(name="count", type="integer", description="Total symbol count"),
182
182
  ]
183
183
 
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden-core"
8
- version = "1.19.0"
8
+ version = "1.20.0"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의"
11
11
  readme = "README.md"