programgarden-core 1.17.0__tar.gz → 1.19.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.
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/PKG-INFO +4 -4
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/__init__.py +17 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/i18n/locales/en.json +9 -4
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/i18n/locales/ko.json +10 -2
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/credential.py +34 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/exchange.py +22 -1
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/__init__.py +2 -1
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/ai.py +5 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/data_futures.py +19 -18
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/data_korea_stock.py +15 -17
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/data_stock.py +17 -20
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/display.py +14 -4
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/infra.py +6 -1
- programgarden_core-1.19.0/programgarden_core/nodes/symbol_futures.py +497 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/registry/node_registry.py +2 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/registry/plugin_registry.py +1 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/pyproject.toml +1 -1
- programgarden_core-1.17.0/programgarden_core/nodes/symbol_futures.py +0 -237
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/README.md +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/bases/__init__.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/bases/client.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/bases/components.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/bases/listener.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/bases/mixins.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/bases/products.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/bases/sql.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/bases/storage.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/code_node.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/exceptions.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/expression/__init__.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/expression/evaluator.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/i18n/__init__.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/i18n/translator.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/korea_alias.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/__init__.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/connection_rule.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/edge.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/event.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/field_binding.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/job.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/order_diagnostics.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/plugin_resource.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/resilience.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/resource.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/validation.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/workflow.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/account_futures.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/account_korea_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/account_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/backtest.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/backtest_futures.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/backtest_korea_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/backtest_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/base.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/broker.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/calculation.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/code.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/condition.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/data.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/event.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/fundamental_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/market_external.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/market_status.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/open_orders_futures.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/open_orders_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/order.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/portfolio.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/realtime_futures.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/realtime_korea_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/realtime_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/risk.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/symbol.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/symbol_stock.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/trigger.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/presets/__init__.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/presets/news_analyst.json +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/presets/risk_manager.json +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/presets/strategist.json +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/presets/technical_analyst.json +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/registry/__init__.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/registry/credential_registry.py +0 -0
- {programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/retry_executor.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden-core
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.19.0
|
|
4
4
|
Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
|
|
5
|
-
License: AGPL-3.0-or-later
|
|
5
|
+
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
7
7
|
Author-email: coding@programgarden.com
|
|
8
8
|
Requires-Python: >=3.12
|
|
9
|
-
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
10
9
|
Classifier: Programming Language :: Python :: 3
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
|
|
@@ -35,6 +35,7 @@ __all__ = [
|
|
|
35
35
|
# Nodes - Broker (상품별 분리)
|
|
36
36
|
"OverseasStockBrokerNode",
|
|
37
37
|
"OverseasFuturesBrokerNode",
|
|
38
|
+
"KoreaStockBrokerNode",
|
|
38
39
|
# Nodes - Market (해외주식)
|
|
39
40
|
"OverseasStockMarketDataNode",
|
|
40
41
|
"OverseasStockHistoricalDataNode",
|
|
@@ -45,6 +46,13 @@ __all__ = [
|
|
|
45
46
|
"OverseasFuturesHistoricalDataNode",
|
|
46
47
|
"OverseasFuturesRealMarketDataNode",
|
|
47
48
|
"OverseasFuturesSymbolQueryNode",
|
|
49
|
+
"FuturesContractNode",
|
|
50
|
+
# Nodes - Market (국내주식)
|
|
51
|
+
"KoreaStockMarketDataNode",
|
|
52
|
+
"KoreaStockHistoricalDataNode",
|
|
53
|
+
"KoreaStockRealMarketDataNode",
|
|
54
|
+
"KoreaStockSymbolQueryNode",
|
|
55
|
+
"KoreaStockFundamentalNode",
|
|
48
56
|
# Nodes - Account (해외주식)
|
|
49
57
|
"OverseasStockAccountNode",
|
|
50
58
|
"OverseasStockRealAccountNode",
|
|
@@ -53,6 +61,11 @@ __all__ = [
|
|
|
53
61
|
"OverseasFuturesAccountNode",
|
|
54
62
|
"OverseasFuturesRealAccountNode",
|
|
55
63
|
"OverseasFuturesRealOrderEventNode",
|
|
64
|
+
# Nodes - Account (국내주식)
|
|
65
|
+
"KoreaStockAccountNode",
|
|
66
|
+
"KoreaStockOpenOrdersNode",
|
|
67
|
+
"KoreaStockRealAccountNode",
|
|
68
|
+
"KoreaStockRealOrderEventNode",
|
|
56
69
|
# Nodes - Symbol (상품 무관)
|
|
57
70
|
"WatchlistNode",
|
|
58
71
|
"MarketUniverseNode",
|
|
@@ -79,6 +92,10 @@ __all__ = [
|
|
|
79
92
|
"OverseasFuturesNewOrderNode",
|
|
80
93
|
"OverseasFuturesModifyOrderNode",
|
|
81
94
|
"OverseasFuturesCancelOrderNode",
|
|
95
|
+
# Nodes - Order (국내주식)
|
|
96
|
+
"KoreaStockNewOrderNode",
|
|
97
|
+
"KoreaStockModifyOrderNode",
|
|
98
|
+
"KoreaStockCancelOrderNode",
|
|
82
99
|
# Nodes - Display (6개)
|
|
83
100
|
"TableDisplayNode",
|
|
84
101
|
"LineChartNode",
|
{programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/i18n/locales/en.json
RENAMED
|
@@ -1277,7 +1277,6 @@
|
|
|
1277
1277
|
"ui.no_tables_create_first": "Create a table first",
|
|
1278
1278
|
"ui.no_tables_found": "No tables found",
|
|
1279
1279
|
"ui.select_table_first": "Select a table first",
|
|
1280
|
-
|
|
1281
1280
|
"connection_rules.realtime_to_order.reason": "Direct connection from realtime node to order node is blocked. Each tick would trigger an order, potentially causing unintended mass orders.",
|
|
1282
1281
|
"connection_rules.realtime_to_order.suggestion": "Place a ThrottleNode between the realtime node and order node to control execution frequency.",
|
|
1283
1282
|
"connection_rules.realtime_to_ai_agent.reason": "Direct connection from realtime node to AIAgentNode is blocked. Each tick would trigger an LLM call, potentially causing excessive costs.",
|
|
@@ -1286,12 +1285,10 @@
|
|
|
1286
1285
|
"connection_rules.realtime_to_external_api.suggestion": "Place a ThrottleNode between the realtime node and external API node to control request frequency.",
|
|
1287
1286
|
"connection_rules.realtime_to_http.reason": "Direct connection from realtime node to HTTPRequestNode is not recommended. Each tick would trigger an HTTP request, potentially hitting external API rate limits.",
|
|
1288
1287
|
"connection_rules.realtime_to_http.suggestion": "Place a ThrottleNode between the realtime node and HTTP request node to control request frequency.",
|
|
1289
|
-
|
|
1290
1288
|
"fields.BaseOrderNode.rate_limit_interval": "Minimum order interval (seconds). Default 5 seconds. The next order will only execute after this time has elapsed since the last order.",
|
|
1291
1289
|
"fields.BaseOrderNode.rate_limit_action": "Action when order interval is not met. skip: silently skip, error: raise error",
|
|
1292
1290
|
"enums.rate_limit_action.skip": "Skip (default)",
|
|
1293
1291
|
"enums.rate_limit_action.error": "Raise error",
|
|
1294
|
-
|
|
1295
1292
|
"plugins.TimeSeriesMomentum.name": "Time Series Momentum",
|
|
1296
1293
|
"plugins.TimeSeriesMomentum.description": "Determines trading direction based on past returns of individual assets (Moskowitz et al. 2012)",
|
|
1297
1294
|
"plugins.ConnorsRSI.name": "Connors RSI",
|
|
@@ -1321,5 +1318,13 @@
|
|
|
1321
1318
|
"events.restart.data_gap_warning": "Data gap warning during restore: data may be missing for {gap_sec} seconds since checkpoint",
|
|
1322
1319
|
"events.restart.skipped_nodes": "Restore: skipping {count} completed nodes",
|
|
1323
1320
|
"events.restart.checkpoint_expired": "Checkpoint expired ({age_sec}s > 600s limit)",
|
|
1324
|
-
"events.restart.workflow_changed": "Workflow definition changed, cannot restore (hash mismatch)"
|
|
1321
|
+
"events.restart.workflow_changed": "Workflow definition changed, cannot restore (hash mismatch)",
|
|
1322
|
+
"nodes.FuturesContractNode.name": "Futures Contract",
|
|
1323
|
+
"nodes.FuturesContractNode.description": "Resolves an underlying futures product (e.g. Mini Hang Seng) to its currently listed contract symbol at execution time, so the workflow never breaks when a contract expires. Pick the front month, the next month (roll target), or the nearest quarterly. Outputs a symbol list identical in shape to WatchlistNode, so downstream futures nodes wire unchanged. Requires an upstream OverseasFuturesBrokerNode. Typical connection: FuturesContractNode → OverseasFuturesHistoricalDataNode / OverseasFuturesMarketDataNode.",
|
|
1324
|
+
"fieldNames.FuturesContractNode.base_products": "Underlying Products",
|
|
1325
|
+
"fieldNames.FuturesContractNode.contract_selection": "Contract Selection",
|
|
1326
|
+
"fieldNames.FuturesContractNode.futures_exchange": "Exchange",
|
|
1327
|
+
"enums.contract_selection.front": "Front month (nearest expiry)",
|
|
1328
|
+
"enums.contract_selection.next": "Next month (roll target)",
|
|
1329
|
+
"enums.contract_selection.quarterly": "Quarterly (Mar/Jun/Sep/Dec)"
|
|
1325
1330
|
}
|
{programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/i18n/locales/ko.json
RENAMED
|
@@ -1318,5 +1318,13 @@
|
|
|
1318
1318
|
"events.restart.data_gap_warning": "복구 중 데이터 갭 경고: 체크포인트 이후 {gap_sec}초 동안의 데이터가 누락될 수 있습니다",
|
|
1319
1319
|
"events.restart.skipped_nodes": "복구: {count}개 완료 노드 스킵",
|
|
1320
1320
|
"events.restart.checkpoint_expired": "체크포인트가 만료됨 ({age_sec}초 > 600초 제한)",
|
|
1321
|
-
"events.restart.workflow_changed": "워크플로우 정의가 변경되어 복구 불가 (해시 불일치)"
|
|
1322
|
-
|
|
1321
|
+
"events.restart.workflow_changed": "워크플로우 정의가 변경되어 복구 불가 (해시 불일치)",
|
|
1322
|
+
"nodes.FuturesContractNode.name": "선물 월물 선택",
|
|
1323
|
+
"nodes.FuturesContractNode.description": "기초자산(예: 미니 항셍)을 실행 시점에 **현재 상장된 월물 종목코드**로 바꿔줍니다. 월물을 직접 적어두면 만기가 지나는 순간 시세가 조용히 끊기는데, 이 노드를 쓰면 시간이 지나도 자동으로 다음 월물로 넘어갑니다. 근월물/차월물/분기월물 중에서 고를 수 있습니다. 출력 형식이 관심종목 노드와 같아서 아래쪽 배선을 그대로 두면 됩니다. 위쪽에 해외선물 브로커 노드가 필요합니다.",
|
|
1324
|
+
"fieldNames.FuturesContractNode.base_products": "기초자산",
|
|
1325
|
+
"fieldNames.FuturesContractNode.contract_selection": "월물 선택",
|
|
1326
|
+
"fieldNames.FuturesContractNode.futures_exchange": "거래소",
|
|
1327
|
+
"enums.contract_selection.front": "근월물 (만기 가장 가까움)",
|
|
1328
|
+
"enums.contract_selection.next": "차월물 (롤오버 대상)",
|
|
1329
|
+
"enums.contract_selection.quarterly": "분기월물 (3·6·9·12월)"
|
|
1330
|
+
}
|
{programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/credential.py
RENAMED
|
@@ -160,6 +160,40 @@ BUILTIN_CREDENTIAL_SCHEMAS: Dict[str, CredentialTypeSchema] = {
|
|
|
160
160
|
}
|
|
161
161
|
),
|
|
162
162
|
# ============================================================
|
|
163
|
+
# LS증권 국내주식 (korea_stock) - 모의투자 미지원 (실전 전용)
|
|
164
|
+
# ============================================================
|
|
165
|
+
"broker_ls_korea_stock": CredentialTypeSchema(
|
|
166
|
+
type_id="broker_ls_korea_stock",
|
|
167
|
+
name="LS증권 국내주식",
|
|
168
|
+
description="국내주식 OpenAPI 인증 정보 (실전 전용 — 모의투자 미지원)",
|
|
169
|
+
widget_schema={
|
|
170
|
+
"fields": [
|
|
171
|
+
{
|
|
172
|
+
"key": "name",
|
|
173
|
+
"type": "text",
|
|
174
|
+
"label": "Credential 이름",
|
|
175
|
+
"hint": "my-ls-korea-cred",
|
|
176
|
+
"description": "이 인증 정보를 식별할 이름",
|
|
177
|
+
"required": True
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"key": "appkey",
|
|
181
|
+
"type": "password",
|
|
182
|
+
"label": "App Key",
|
|
183
|
+
"description": "LS증권에서 발급받은 App Key",
|
|
184
|
+
"required": True
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"key": "appsecret",
|
|
188
|
+
"type": "password",
|
|
189
|
+
"label": "App Secret",
|
|
190
|
+
"description": "LS증권에서 발급받은 App Secret",
|
|
191
|
+
"required": True
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
# ============================================================
|
|
163
197
|
# Telegram Bot
|
|
164
198
|
# ============================================================
|
|
165
199
|
"telegram": CredentialTypeSchema(
|
{programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/models/exchange.py
RENAMED
|
@@ -15,6 +15,7 @@ class ProductType(str, Enum):
|
|
|
15
15
|
"""상품 타입"""
|
|
16
16
|
OVERSEAS_STOCK = "overseas_stock"
|
|
17
17
|
OVERSEAS_FUTURES = "overseas_futures"
|
|
18
|
+
KOREA_STOCK = "korea_stock" # 국내주식 (KOSPI/KOSDAQ, KRX 단일)
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class ExchangeInfo(BaseModel):
|
|
@@ -136,8 +137,20 @@ class ExchangeRegistry:
|
|
|
136
137
|
),
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
# 국내주식 거래소 (KRX 단일 — KOSPI/KOSDAQ 통합)
|
|
141
|
+
ls_korea_stock = {
|
|
142
|
+
"KRX": ExchangeInfo(
|
|
143
|
+
code="KRX",
|
|
144
|
+
name="KRX",
|
|
145
|
+
full_name="Korea Exchange",
|
|
146
|
+
country="KR",
|
|
147
|
+
currency="KRW",
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
self.register_exchanges("ls", ProductType.OVERSEAS_STOCK, ls_overseas_stock)
|
|
140
152
|
self.register_exchanges("ls", ProductType.OVERSEAS_FUTURES, ls_overseas_futures)
|
|
153
|
+
self.register_exchanges("ls", ProductType.KOREA_STOCK, ls_korea_stock)
|
|
141
154
|
|
|
142
155
|
def register_exchanges(
|
|
143
156
|
self,
|
|
@@ -209,7 +222,11 @@ class ExchangeRegistry:
|
|
|
209
222
|
# 해외선물: CME 기본
|
|
210
223
|
if product == ProductType.OVERSEAS_FUTURES:
|
|
211
224
|
return "CME" if "CME" in exchanges else exchanges[0]
|
|
212
|
-
|
|
225
|
+
|
|
226
|
+
# 국내주식: KRX 단일
|
|
227
|
+
if product == ProductType.KOREA_STOCK:
|
|
228
|
+
return "KRX" if "KRX" in exchanges else exchanges[0]
|
|
229
|
+
|
|
213
230
|
return exchanges[0]
|
|
214
231
|
|
|
215
232
|
|
|
@@ -229,6 +246,10 @@ class SymbolEntry(BaseModel):
|
|
|
229
246
|
|
|
230
247
|
def to_api_symbol(self, broker: str, product: ProductType) -> str:
|
|
231
248
|
"""API용 종목코드 생성 (예: 82AAPL)"""
|
|
249
|
+
# 국내주식: 6자리 KRX 종목코드를 그대로 사용 (거래소 접두어 없음)
|
|
250
|
+
if product == ProductType.KOREA_STOCK:
|
|
251
|
+
return self.symbol
|
|
252
|
+
|
|
232
253
|
code = exchange_registry.name_to_code(broker, product, self.exchange)
|
|
233
254
|
if code is None:
|
|
234
255
|
raise ValueError(f"Unknown exchange: {self.exchange}")
|
{programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/__init__.py
RENAMED
|
@@ -45,7 +45,7 @@ from programgarden_core.nodes.account_futures import OverseasFuturesAccountNode
|
|
|
45
45
|
from programgarden_core.nodes.open_orders_stock import OverseasStockOpenOrdersNode
|
|
46
46
|
from programgarden_core.nodes.open_orders_futures import OverseasFuturesOpenOrdersNode
|
|
47
47
|
from programgarden_core.nodes.symbol_stock import OverseasStockSymbolQueryNode
|
|
48
|
-
from programgarden_core.nodes.symbol_futures import OverseasFuturesSymbolQueryNode
|
|
48
|
+
from programgarden_core.nodes.symbol_futures import OverseasFuturesSymbolQueryNode, FuturesContractNode
|
|
49
49
|
# Data (상품 무관)
|
|
50
50
|
from programgarden_core.nodes.data import SQLiteNode, HTTPRequestNode, FieldMappingNode
|
|
51
51
|
from programgarden_core.nodes.code import CodeNode
|
|
@@ -132,6 +132,7 @@ __all__ = [
|
|
|
132
132
|
"OverseasFuturesHistoricalDataNode",
|
|
133
133
|
"OverseasFuturesRealMarketDataNode",
|
|
134
134
|
"OverseasFuturesSymbolQueryNode",
|
|
135
|
+
"FuturesContractNode",
|
|
135
136
|
# Account - Stock (해외주식)
|
|
136
137
|
"OverseasStockAccountNode",
|
|
137
138
|
"OverseasStockRealAccountNode",
|
|
@@ -360,6 +360,11 @@ class AIAgentNode(BaseNode):
|
|
|
360
360
|
"max_total_tokens (default 100,000) caps total token spend per execution cycle",
|
|
361
361
|
]
|
|
362
362
|
_anti_patterns: ClassVar[List[Dict[str, str]]] = [
|
|
363
|
+
{
|
|
364
|
+
"pattern": "Adding an AIAgentNode without connecting an LLMModelNode via an 'ai_model' edge",
|
|
365
|
+
"reason": "AIAgentNode CANNOT run without an LLM — the ai_model edge injects the LLM connection. With no LLMModelNode wired via an ai_model edge, the node fails at runtime (raises). The workflow validator now blocks this with an ERROR at save time.",
|
|
366
|
+
"alternative": "Always add an LLMModelNode and wire it to the AIAgentNode with an ai_model edge: {\"from\": \"<llm_node_id>\", \"to\": \"<agent_id>\", \"type\": \"ai_model\"}. A plain 'main' edge does NOT inject the LLM.",
|
|
367
|
+
},
|
|
363
368
|
{
|
|
364
369
|
"pattern": "Connecting a real-time node (RealMarketDataNode) directly to AIAgentNode without ThrottleNode",
|
|
365
370
|
"reason": "Real-time nodes fire on every tick. Without throttling, AIAgentNode would call the LLM API thousands of times per minute, causing massive cost and rate limit errors. The workflow validator blocks this with an ERROR.",
|
{programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/data_futures.py
RENAMED
|
@@ -102,14 +102,11 @@ class OverseasFuturesMarketDataNode(BaseNode):
|
|
|
102
102
|
"nodes": [
|
|
103
103
|
{"id": "start", "type": "StartNode"},
|
|
104
104
|
{"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
105
|
-
{"id": "
|
|
106
|
-
{"id": "
|
|
107
|
-
{"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.market.value }}"},
|
|
105
|
+
{"id": "market", "type": "OverseasFuturesMarketDataNode", "symbols": [{"symbol": "ESH26", "exchange": "CME"}]},
|
|
106
|
+
{"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.market.values }}"},
|
|
108
107
|
],
|
|
109
108
|
"edges": [
|
|
110
109
|
{"from": "start", "to": "broker"},
|
|
111
|
-
{"from": "broker", "to": "split"},
|
|
112
|
-
{"from": "split", "to": "market"},
|
|
113
110
|
{"from": "broker", "to": "market"},
|
|
114
111
|
{"from": "market", "to": "display"},
|
|
115
112
|
],
|
|
@@ -124,7 +121,7 @@ class OverseasFuturesMarketDataNode(BaseNode):
|
|
|
124
121
|
}
|
|
125
122
|
],
|
|
126
123
|
},
|
|
127
|
-
"expected_output": "
|
|
124
|
+
"expected_output": "values port: array of {symbol, exchange, current_price, volume, open_interest, change_percent, bid, ask}.",
|
|
128
125
|
},
|
|
129
126
|
{
|
|
130
127
|
"title": "Multi-contract scan — compare HKEX futures prices",
|
|
@@ -135,14 +132,11 @@ class OverseasFuturesMarketDataNode(BaseNode):
|
|
|
135
132
|
"nodes": [
|
|
136
133
|
{"id": "start", "type": "StartNode"},
|
|
137
134
|
{"id": "broker", "type": "OverseasFuturesBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
138
|
-
{"id": "
|
|
139
|
-
{"id": "
|
|
140
|
-
{"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.market.value }}"},
|
|
135
|
+
{"id": "market", "type": "OverseasFuturesMarketDataNode", "symbols": [{"symbol": "MHIH26", "exchange": "HKEX"}, {"symbol": "MHIK26", "exchange": "HKEX"}]},
|
|
136
|
+
{"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.market.values }}"},
|
|
141
137
|
],
|
|
142
138
|
"edges": [
|
|
143
139
|
{"from": "start", "to": "broker"},
|
|
144
|
-
{"from": "broker", "to": "split"},
|
|
145
|
-
{"from": "split", "to": "market"},
|
|
146
140
|
{"from": "broker", "to": "market"},
|
|
147
141
|
{"from": "market", "to": "display"},
|
|
148
142
|
],
|
|
@@ -157,23 +151,25 @@ class OverseasFuturesMarketDataNode(BaseNode):
|
|
|
157
151
|
}
|
|
158
152
|
],
|
|
159
153
|
},
|
|
160
|
-
"expected_output": "market.
|
|
154
|
+
"expected_output": "market.values — array with one {price, volume, ...} entry per HKEX mini-futures contract.",
|
|
161
155
|
},
|
|
162
156
|
]
|
|
163
157
|
_node_guide: ClassVar[Dict[str, Any]] = {
|
|
164
158
|
"input_handling": (
|
|
165
|
-
"The `
|
|
159
|
+
"The `symbols` field takes an array of {exchange, symbol} dicts — or bind a list source. "
|
|
166
160
|
"Exchange values: CME, EUREX, SGX, HKEX. Symbol includes contract month code (e.g., ESH26 for March 2026). "
|
|
167
161
|
"Broker connection auto-injected from OverseasFuturesBrokerNode."
|
|
168
162
|
),
|
|
169
163
|
"output_consumption": (
|
|
170
|
-
"
|
|
171
|
-
"
|
|
164
|
+
"Consume the `values` port ONLY — an array of {symbol, exchange, current_price, volume, open_interest, change_percent, bid, ask}. "
|
|
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. "
|
|
167
|
+
"TableDisplayNode.data ← `{{ nodes.market.values }}`; PositionSizingNode.market_data ← `{{ nodes.market.values }}`."
|
|
172
168
|
),
|
|
173
169
|
"common_combinations": [
|
|
174
|
-
"
|
|
175
|
-
"OverseasFuturesMarketDataNode.
|
|
176
|
-
"OverseasFuturesMarketDataNode.
|
|
170
|
+
"OverseasFuturesSymbolQueryNode → OverseasFuturesMarketDataNode.symbols (multi-contract fetch)",
|
|
171
|
+
"OverseasFuturesMarketDataNode.values → TableDisplayNode.data (futures monitor)",
|
|
172
|
+
"OverseasFuturesMarketDataNode.values → PositionSizingNode.market_data (with symbols + balance)",
|
|
177
173
|
],
|
|
178
174
|
"pitfalls": [
|
|
179
175
|
"Symbol must include contract month code (e.g., ESH26 not ES) — check OverseasFuturesSymbolQueryNode for valid codes",
|
|
@@ -188,6 +184,11 @@ class OverseasFuturesMarketDataNode(BaseNode):
|
|
|
188
184
|
]
|
|
189
185
|
_outputs: List[OutputPort] = [
|
|
190
186
|
OutputPort(name="value", type="market_data", description="i18n:ports.market_data_value", fields=PRICE_DATA_FIELDS),
|
|
187
|
+
OutputPort(
|
|
188
|
+
name="values",
|
|
189
|
+
type="array",
|
|
190
|
+
description="Array of per-contract market quotes — [{symbol, exchange, current_price, ...}, ...]",
|
|
191
|
+
),
|
|
191
192
|
]
|
|
192
193
|
|
|
193
194
|
_version: ClassVar[str] = "1.0.0"
|
{programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/data_korea_stock.py
RENAMED
|
@@ -102,14 +102,11 @@ class KoreaStockMarketDataNode(BaseNode):
|
|
|
102
102
|
"nodes": [
|
|
103
103
|
{"id": "start", "type": "StartNode"},
|
|
104
104
|
{"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
|
|
105
|
-
{"id": "
|
|
106
|
-
{"id": "
|
|
107
|
-
{"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.market.value }}"},
|
|
105
|
+
{"id": "market", "type": "KoreaStockMarketDataNode", "symbols": [{"symbol": "005930"}, {"symbol": "000660"}]},
|
|
106
|
+
{"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.market.values }}"},
|
|
108
107
|
],
|
|
109
108
|
"edges": [
|
|
110
109
|
{"from": "start", "to": "broker"},
|
|
111
|
-
{"from": "broker", "to": "split"},
|
|
112
|
-
{"from": "split", "to": "market"},
|
|
113
110
|
{"from": "broker", "to": "market"},
|
|
114
111
|
{"from": "market", "to": "display"},
|
|
115
112
|
],
|
|
@@ -124,7 +121,7 @@ class KoreaStockMarketDataNode(BaseNode):
|
|
|
124
121
|
}
|
|
125
122
|
],
|
|
126
123
|
},
|
|
127
|
-
"expected_output": "
|
|
124
|
+
"expected_output": "values port: array of {symbol, current_price, volume, change_percent, bid, ask, per, eps} per domestic stock.",
|
|
128
125
|
},
|
|
129
126
|
{
|
|
130
127
|
"title": "Price lookup for domestic PositionSizingNode",
|
|
@@ -136,15 +133,12 @@ class KoreaStockMarketDataNode(BaseNode):
|
|
|
136
133
|
{"id": "start", "type": "StartNode"},
|
|
137
134
|
{"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
|
|
138
135
|
{"id": "account", "type": "KoreaStockAccountNode"},
|
|
139
|
-
{"id": "
|
|
140
|
-
{"id": "
|
|
141
|
-
{"id": "sizing", "type": "PositionSizingNode", "method": "fixed_percent", "max_percent": 5, "balance": "{{ nodes.account.balance }}", "price": "{{ nodes.market.value.current_price }}", "symbol": "{{ nodes.split.item }}"},
|
|
136
|
+
{"id": "market", "type": "KoreaStockMarketDataNode", "symbols": [{"symbol": "005930"}]},
|
|
137
|
+
{"id": "sizing", "type": "PositionSizingNode", "method": "fixed_percent", "max_percent": 5, "balance": "{{ nodes.account.balance.orderable_amount }}", "market_data": "{{ nodes.market.values }}", "symbols": "{{ nodes.market.values }}"},
|
|
142
138
|
],
|
|
143
139
|
"edges": [
|
|
144
140
|
{"from": "start", "to": "broker"},
|
|
145
141
|
{"from": "broker", "to": "account"},
|
|
146
|
-
{"from": "broker", "to": "split"},
|
|
147
|
-
{"from": "split", "to": "market"},
|
|
148
142
|
{"from": "broker", "to": "market"},
|
|
149
143
|
{"from": "market", "to": "sizing"},
|
|
150
144
|
{"from": "account", "to": "sizing"},
|
|
@@ -160,7 +154,7 @@ class KoreaStockMarketDataNode(BaseNode):
|
|
|
160
154
|
}
|
|
161
155
|
],
|
|
162
156
|
},
|
|
163
|
-
"expected_output": "market.
|
|
157
|
+
"expected_output": "market.values (per-symbol price array) + account.balance.orderable_amount feed PositionSizingNode to compute risk-adjusted domestic stock order quantity (orders port).",
|
|
164
158
|
},
|
|
165
159
|
]
|
|
166
160
|
_node_guide: ClassVar[Dict[str, Any]] = {
|
|
@@ -170,13 +164,17 @@ class KoreaStockMarketDataNode(BaseNode):
|
|
|
170
164
|
"Broker connection is auto-injected from KoreaStockBrokerNode."
|
|
171
165
|
),
|
|
172
166
|
"output_consumption": (
|
|
173
|
-
"
|
|
174
|
-
"
|
|
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}, ...]. "
|
|
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. "
|
|
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)."
|
|
175
173
|
),
|
|
176
174
|
"common_combinations": [
|
|
177
|
-
"
|
|
178
|
-
"KoreaStockMarketDataNode.
|
|
179
|
-
"
|
|
175
|
+
"KoreaStockMarketDataNode.values → TableDisplayNode.data (KRX price monitor)",
|
|
176
|
+
"KoreaStockMarketDataNode.values → PositionSizingNode.market_data (with symbols + balance=KoreaStockAccountNode.balance.orderable_amount)",
|
|
177
|
+
"KoreaStockSymbolQueryNode/ConditionNode.passed_symbols → PositionSizingNode.symbols (canonical symbol list)",
|
|
180
178
|
],
|
|
181
179
|
"pitfalls": [
|
|
182
180
|
"KoreaStock does not support paper trading — KoreaStockBrokerNode always uses a live session",
|
{programgarden_core-1.17.0 → programgarden_core-1.19.0}/programgarden_core/nodes/data_stock.py
RENAMED
|
@@ -102,14 +102,11 @@ class OverseasStockMarketDataNode(BaseNode):
|
|
|
102
102
|
"nodes": [
|
|
103
103
|
{"id": "start", "type": "StartNode"},
|
|
104
104
|
{"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
105
|
-
{"id": "
|
|
106
|
-
{"id": "
|
|
107
|
-
{"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.market.value }}"},
|
|
105
|
+
{"id": "market", "type": "OverseasStockMarketDataNode", "symbols": [{"symbol": "AAPL", "exchange": "NASDAQ"}, {"symbol": "MSFT", "exchange": "NASDAQ"}]},
|
|
106
|
+
{"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.market.values }}"},
|
|
108
107
|
],
|
|
109
108
|
"edges": [
|
|
110
109
|
{"from": "start", "to": "broker"},
|
|
111
|
-
{"from": "broker", "to": "split"},
|
|
112
|
-
{"from": "split", "to": "market"},
|
|
113
110
|
{"from": "broker", "to": "market"},
|
|
114
111
|
{"from": "market", "to": "display"},
|
|
115
112
|
],
|
|
@@ -124,7 +121,7 @@ class OverseasStockMarketDataNode(BaseNode):
|
|
|
124
121
|
}
|
|
125
122
|
],
|
|
126
123
|
},
|
|
127
|
-
"expected_output": "
|
|
124
|
+
"expected_output": "values port: array of {symbol, exchange, current_price, volume, change_percent, bid, ask, per, eps} for each symbol.",
|
|
128
125
|
},
|
|
129
126
|
{
|
|
130
127
|
"title": "Price lookup feeding PositionSizingNode",
|
|
@@ -136,15 +133,12 @@ class OverseasStockMarketDataNode(BaseNode):
|
|
|
136
133
|
{"id": "start", "type": "StartNode"},
|
|
137
134
|
{"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred", "paper_trading": False},
|
|
138
135
|
{"id": "account", "type": "OverseasStockAccountNode"},
|
|
139
|
-
{"id": "
|
|
140
|
-
{"id": "
|
|
141
|
-
{"id": "sizing", "type": "PositionSizingNode", "method": "fixed_percent", "max_percent": 5, "balance": "{{ nodes.account.balance }}", "price": "{{ nodes.market.value.current_price }}", "symbol": "{{ nodes.split.item }}"},
|
|
136
|
+
{"id": "market", "type": "OverseasStockMarketDataNode", "symbols": [{"symbol": "NVDA", "exchange": "NASDAQ"}]},
|
|
137
|
+
{"id": "sizing", "type": "PositionSizingNode", "method": "fixed_percent", "max_percent": 5, "balance": "{{ nodes.account.balance }}", "market_data": "{{ nodes.market.values }}", "symbols": "{{ nodes.market.values }}"},
|
|
142
138
|
],
|
|
143
139
|
"edges": [
|
|
144
140
|
{"from": "start", "to": "broker"},
|
|
145
141
|
{"from": "broker", "to": "account"},
|
|
146
|
-
{"from": "broker", "to": "split"},
|
|
147
|
-
{"from": "split", "to": "market"},
|
|
148
142
|
{"from": "broker", "to": "market"},
|
|
149
143
|
{"from": "market", "to": "sizing"},
|
|
150
144
|
{"from": "account", "to": "sizing"},
|
|
@@ -160,24 +154,27 @@ class OverseasStockMarketDataNode(BaseNode):
|
|
|
160
154
|
}
|
|
161
155
|
],
|
|
162
156
|
},
|
|
163
|
-
"expected_output": "market.
|
|
157
|
+
"expected_output": "market.values (per-symbol price array) fed into PositionSizingNode.market_data to compute risk-adjusted order quantity (orders port).",
|
|
164
158
|
},
|
|
165
159
|
]
|
|
166
160
|
_node_guide: ClassVar[Dict[str, Any]] = {
|
|
167
161
|
"input_handling": (
|
|
168
|
-
"The `
|
|
169
|
-
"
|
|
162
|
+
"The `symbols` field takes an array of {exchange, symbol} dicts (e.g. [{\"symbol\": \"AAPL\", \"exchange\": \"NASDAQ\"}]) "
|
|
163
|
+
"— or bind a list source `{{ nodes.watchlist.symbols }}` / `{{ nodes.split_symbol.items }}`. Exchange codes: NASDAQ, NYSE, AMEX. "
|
|
170
164
|
"The node auto-receives the broker connection via DAG traversal from OverseasStockBrokerNode."
|
|
171
165
|
),
|
|
172
166
|
"output_consumption": (
|
|
173
|
-
"
|
|
174
|
-
"bid, ask, per, eps, 52w_high, 52w_low.
|
|
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}. "
|
|
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. "
|
|
171
|
+
"TableDisplayNode.data ← `{{ nodes.market.values }}`; PositionSizingNode.market_data ← `{{ nodes.market.values }}` "
|
|
172
|
+
"(the array carries each symbol's current_price, resolved internally)."
|
|
175
173
|
),
|
|
176
174
|
"common_combinations": [
|
|
177
|
-
"
|
|
178
|
-
"OverseasStockMarketDataNode.
|
|
179
|
-
"OverseasStockMarketDataNode.
|
|
180
|
-
"OverseasStockMarketDataNode.value → TableDisplayNode (price monitoring display)",
|
|
175
|
+
"WatchlistNode/SymbolQueryNode → OverseasStockMarketDataNode.symbols (multi-symbol price fetch)",
|
|
176
|
+
"OverseasStockMarketDataNode.values → TableDisplayNode.data (price monitoring display)",
|
|
177
|
+
"OverseasStockMarketDataNode.values → PositionSizingNode.market_data (with symbols + balance)",
|
|
181
178
|
],
|
|
182
179
|
"pitfalls": [
|
|
183
180
|
"The symbol field must be a single dict — not a list. Use SplitNode for multi-symbol scenarios",
|
|
@@ -10,7 +10,7 @@ ProgramGarden Core - Display Nodes
|
|
|
10
10
|
- SummaryDisplayNode: JSON/요약 데이터 표시
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from typing import Optional, List, Literal, Dict, Any, ClassVar, TYPE_CHECKING
|
|
13
|
+
from typing import Optional, List, Literal, Dict, Any, Union, ClassVar, TYPE_CHECKING
|
|
14
14
|
from pydantic import Field
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
@@ -304,9 +304,16 @@ class TableDisplayNode(BaseDisplayNode):
|
|
|
304
304
|
_updated_at: ClassVar[str] = "2026-05-19"
|
|
305
305
|
_change_note: ClassVar[Optional[str]] = None
|
|
306
306
|
|
|
307
|
-
columns
|
|
307
|
+
# columns 는 두 형태를 모두 받는다(스키마=코드=카탈로그 정합; 렌더러 _normalize_columns):
|
|
308
|
+
# 1) 필드명 문자열 리스트 — ["symbol", "close"]
|
|
309
|
+
# 2) {key, label} 객체 리스트 — [{"key": "close", "label": "종가"}] (현지화 헤더용)
|
|
310
|
+
# 옛 선언은 List[str] 뿐이라 객체형을 코드만 몰래 허용하는 드리프트였다.
|
|
311
|
+
columns: Optional[List[Union[str, Dict[str, Any]]]] = Field(
|
|
308
312
|
default=None,
|
|
309
|
-
description=
|
|
313
|
+
description=(
|
|
314
|
+
"표시할 컬럼 (미지정시 전체). 필드명 문자열 리스트 [\"symbol\",\"close\"] 또는 "
|
|
315
|
+
"현지화 헤더용 객체 리스트 [{\"key\":\"close\",\"label\":\"종가\"}] 둘 다 허용."
|
|
316
|
+
),
|
|
310
317
|
)
|
|
311
318
|
|
|
312
319
|
limit: Optional[int] = Field(
|
|
@@ -336,7 +343,10 @@ class TableDisplayNode(BaseDisplayNode):
|
|
|
336
343
|
"columns": FieldSchema(
|
|
337
344
|
name="columns",
|
|
338
345
|
type=FieldType.ARRAY,
|
|
339
|
-
description=
|
|
346
|
+
description=(
|
|
347
|
+
"표시할 컬럼. 필드명 문자열 리스트 [\"symbol\",\"close\"] 또는 "
|
|
348
|
+
"{key,label} 객체 리스트 [{\"key\":\"close\",\"label\":\"종가\"}](현지화 헤더) 허용"
|
|
349
|
+
),
|
|
340
350
|
category=FieldCategory.SETTINGS,
|
|
341
351
|
expression_mode=ExpressionMode.FIXED_ONLY,
|
|
342
352
|
ui_options={"multiple": True},
|
|
@@ -457,6 +457,11 @@ class SplitNode(BaseNode):
|
|
|
457
457
|
"reason": "Auto-iterate is implicit and handled by the executor — adding SplitNode duplicates the iteration and doubles the workload.",
|
|
458
458
|
"alternative": "Bind `{{ item }}` directly on the downstream node and delete SplitNode.",
|
|
459
459
|
},
|
|
460
|
+
{
|
|
461
|
+
"pattern": "Adding SplitNode without wiring its `array` input to an upstream list producer",
|
|
462
|
+
"reason": "SplitNode with no array source (no WatchlistNode/MarketUniverseNode/AccountNode/ConditionNode feeding a list, and no `array` binding) emits zero items — downstream `{{ nodes.split.item }}` resolves empty and the workflow produces silent empty results.",
|
|
463
|
+
"alternative": "Wire an upstream node that outputs an array into SplitNode, or — for a single known symbol — delete SplitNode and bind that symbol directly on the downstream node.",
|
|
464
|
+
},
|
|
460
465
|
{
|
|
461
466
|
"pattern": "parallel=True for order-placement loops",
|
|
462
467
|
"reason": "Placing orders concurrently without delay_ms will trip LS-Sec TR rate limits and cause spurious cancellations.",
|
|
@@ -530,7 +535,7 @@ class SplitNode(BaseNode):
|
|
|
530
535
|
},
|
|
531
536
|
]
|
|
532
537
|
_node_guide: ClassVar[Dict[str, Any]] = {
|
|
533
|
-
"input_handling": "`array` input
|
|
538
|
+
"input_handling": "`array` input is REQUIRED and MUST have a source: either an incoming edge from an upstream node that outputs an ARRAY (WatchlistNode/MarketUniverseNode → symbols, AccountNode → positions/held_symbols, ConditionNode → values, etc.), or an explicit `array` config binding. SplitNode has NOTHING to split if no upstream produces a list — it emits zero items and every downstream `{{ nodes.split.item }}` silently resolves empty. If you already know a single concrete symbol (e.g. 'AAPL'), do NOT add SplitNode — bind that one symbol directly on the downstream node.",
|
|
534
539
|
"output_consumption": "Downstream binds `{{ nodes.split.item }}` for the current element, `{{ nodes.split.index }}` for the 0-based position, and `{{ nodes.split.total }}` for the count.",
|
|
535
540
|
"common_combinations": [
|
|
536
541
|
"WatchlistNode → SplitNode → OverseasStockFundamentalNode",
|