programgarden-core 2.6.2__tar.gz → 2.6.3__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 (89) hide show
  1. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/PKG-INFO +4 -4
  2. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/README.md +3 -3
  3. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/__init__.py +2 -0
  4. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/i18n/locales/en.json +29 -0
  5. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/i18n/locales/ko.json +29 -0
  6. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/__init__.py +4 -0
  7. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/base.py +66 -0
  8. programgarden_core-2.6.3/programgarden_core/nodes/ranking_korea_stock.py +277 -0
  9. programgarden_core-2.6.3/programgarden_core/nodes/ranking_stock.py +248 -0
  10. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/registry/node_registry.py +2 -0
  11. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/pyproject.toml +1 -1
  12. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/bases/__init__.py +0 -0
  13. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/bases/client.py +0 -0
  14. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/bases/components.py +0 -0
  15. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/bases/listener.py +0 -0
  16. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/bases/mixins.py +0 -0
  17. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/bases/products.py +0 -0
  18. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/bases/sql.py +0 -0
  19. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/bases/storage.py +0 -0
  20. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/code_node.py +0 -0
  21. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/exceptions.py +0 -0
  22. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/expression/__init__.py +0 -0
  23. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/expression/evaluator.py +0 -0
  24. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/expression/reference.py +0 -0
  25. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/i18n/__init__.py +0 -0
  26. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/i18n/translator.py +0 -0
  27. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/korea_alias.py +0 -0
  28. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/market_calendar.py +0 -0
  29. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/__init__.py +0 -0
  30. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/connection_rule.py +0 -0
  31. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/credential.py +0 -0
  32. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/edge.py +0 -0
  33. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/event.py +0 -0
  34. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/exchange.py +0 -0
  35. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/field_binding.py +0 -0
  36. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/job.py +0 -0
  37. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/order_diagnostics.py +0 -0
  38. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/plugin_resource.py +0 -0
  39. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/resilience.py +0 -0
  40. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/resource.py +0 -0
  41. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/validation.py +0 -0
  42. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/models/workflow.py +0 -0
  43. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/account_futures.py +0 -0
  44. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/account_korea_stock.py +0 -0
  45. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/account_stock.py +0 -0
  46. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/ai.py +0 -0
  47. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/backtest.py +0 -0
  48. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/backtest_futures.py +0 -0
  49. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/backtest_korea_stock.py +0 -0
  50. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/backtest_stock.py +0 -0
  51. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/broker.py +0 -0
  52. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/calculation.py +0 -0
  53. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/code.py +0 -0
  54. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/condition.py +0 -0
  55. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/contract_info_futures.py +0 -0
  56. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/data.py +0 -0
  57. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/data_futures.py +0 -0
  58. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/data_korea_stock.py +0 -0
  59. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/data_stock.py +0 -0
  60. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/display.py +0 -0
  61. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/event.py +0 -0
  62. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
  63. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/fundamental_stock.py +0 -0
  64. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/infra.py +0 -0
  65. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/open_orders_futures.py +0 -0
  66. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
  67. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/open_orders_stock.py +0 -0
  68. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/order.py +0 -0
  69. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/orderable_quantity_futures.py +0 -0
  70. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/portfolio.py +0 -0
  71. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/realtime_futures.py +0 -0
  72. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/realtime_korea_stock.py +0 -0
  73. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/realtime_stock.py +0 -0
  74. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/risk.py +0 -0
  75. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/session_gate.py +0 -0
  76. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/symbol.py +0 -0
  77. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/symbol_futures.py +0 -0
  78. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
  79. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/symbol_stock.py +0 -0
  80. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/nodes/trigger.py +0 -0
  81. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/presets/__init__.py +0 -0
  82. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/presets/news_analyst.json +0 -0
  83. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/presets/risk_manager.json +0 -0
  84. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/presets/strategist.json +0 -0
  85. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/presets/technical_analyst.json +0 -0
  86. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/registry/__init__.py +0 -0
  87. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/registry/credential_registry.py +0 -0
  88. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/registry/plugin_registry.py +0 -0
  89. {programgarden_core-2.6.2 → programgarden_core-2.6.3}/programgarden_core/retry_executor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden-core
3
- Version: 2.6.2
3
+ Version: 2.6.3
4
4
  Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -60,13 +60,13 @@ poetry add programgarden-core
60
60
 
61
61
  요구 사항: Python 3.12+
62
62
 
63
- ## Node categories (11 categories, 71 core nodes)
63
+ ## Node categories (11 categories, 74 core nodes)
64
64
 
65
65
  | 카테고리 | 노드 수 | 대표 노드 |
66
66
  |----------|---------|----------|
67
67
  | infra | 8 | StartNode, BrokerNode, ThrottleNode, SplitNode, AggregateNode, IfNode, KoreaStockBrokerNode |
68
68
  | account | 13 | AccountNode, OpenOrdersNode, RealAccountNode, RealOrderEventNode (해외주식/선물 + 국내주식) |
69
- | market | 20 | MarketDataNode, HistoricalDataNode, RealMarketDataNode, WatchlistNode, ExclusionListNode, KoreaStock* |
69
+ | market | 23 | MarketDataNode, HistoricalDataNode, RealMarketDataNode, WatchlistNode, ExclusionListNode, KoreaStockRankingNode, OverseasStockRankingNode, KoreaStock* |
70
70
  | condition | 2 | ConditionNode, LogicNode |
71
71
  | order | 10 | NewOrderNode, ModifyOrderNode, CancelOrderNode, PositionSizingNode (해외주식/선물 + 국내주식) |
72
72
  | risk | 1 | PortfolioNode |
@@ -76,7 +76,7 @@ poetry add programgarden-core
76
76
  | analysis | 2 | BacktestEngineNode, BenchmarkCompareNode |
77
77
  | ai | 2 | LLMModelNode, AIAgentNode |
78
78
 
79
- > Community adds two nodes: TelegramNode and PerformanceReportNode, for 73 total.
79
+ > Community adds two nodes: TelegramNode and PerformanceReportNode, for 76 total.
80
80
 
81
81
  ## 사용 예시
82
82
 
@@ -43,13 +43,13 @@ poetry add programgarden-core
43
43
 
44
44
  요구 사항: Python 3.12+
45
45
 
46
- ## Node categories (11 categories, 71 core nodes)
46
+ ## Node categories (11 categories, 74 core nodes)
47
47
 
48
48
  | 카테고리 | 노드 수 | 대표 노드 |
49
49
  |----------|---------|----------|
50
50
  | infra | 8 | StartNode, BrokerNode, ThrottleNode, SplitNode, AggregateNode, IfNode, KoreaStockBrokerNode |
51
51
  | account | 13 | AccountNode, OpenOrdersNode, RealAccountNode, RealOrderEventNode (해외주식/선물 + 국내주식) |
52
- | market | 20 | MarketDataNode, HistoricalDataNode, RealMarketDataNode, WatchlistNode, ExclusionListNode, KoreaStock* |
52
+ | market | 23 | MarketDataNode, HistoricalDataNode, RealMarketDataNode, WatchlistNode, ExclusionListNode, KoreaStockRankingNode, OverseasStockRankingNode, KoreaStock* |
53
53
  | condition | 2 | ConditionNode, LogicNode |
54
54
  | order | 10 | NewOrderNode, ModifyOrderNode, CancelOrderNode, PositionSizingNode (해외주식/선물 + 국내주식) |
55
55
  | risk | 1 | PortfolioNode |
@@ -59,7 +59,7 @@ poetry add programgarden-core
59
59
  | analysis | 2 | BacktestEngineNode, BenchmarkCompareNode |
60
60
  | ai | 2 | LLMModelNode, AIAgentNode |
61
61
 
62
- > Community adds two nodes: TelegramNode and PerformanceReportNode, for 73 total.
62
+ > Community adds two nodes: TelegramNode and PerformanceReportNode, for 76 total.
63
63
 
64
64
  ## 사용 예시
65
65
 
@@ -51,6 +51,7 @@ __all__ = [
51
51
  "OverseasStockHistoricalDataNode",
52
52
  "OverseasStockRealMarketDataNode",
53
53
  "OverseasStockSymbolQueryNode",
54
+ "OverseasStockRankingNode",
54
55
  # Nodes - Market (해외선물)
55
56
  "OverseasFuturesMarketDataNode",
56
57
  "OverseasFuturesHistoricalDataNode",
@@ -63,6 +64,7 @@ __all__ = [
63
64
  "KoreaStockHistoricalDataNode",
64
65
  "KoreaStockRealMarketDataNode",
65
66
  "KoreaStockSymbolQueryNode",
67
+ "KoreaStockRankingNode",
66
68
  "KoreaStockFundamentalNode",
67
69
  # Nodes - Account (해외주식)
68
70
  "OverseasStockAccountNode",
@@ -94,6 +94,10 @@
94
94
  "nodes.KoreaStockRealOrderEventNode.name": "Korea Stock Real Order Event",
95
95
  "nodes.KoreaStockSymbolQueryNode.description": "Queries all tradable symbols on KOSPI/KOSDAQ markets. Uses t9945 API to fetch symbol names and codes. Filter by market type (All, KOSPI, KOSDAQ). Outputs symbol list and count. Requires an upstream KoreaStockBrokerNode. Typical connection: KoreaStockSymbolQueryNode → ConditionNode or → KoreaStockMarketDataNode.",
96
96
  "nodes.KoreaStockSymbolQueryNode.name": "Korea Stock Symbols",
97
+ "nodes.KoreaStockRankingNode.description": "Lists today's top domestic stocks by gain, loss, volume, trading value or market cap (LS t1441, t1452, t1463, t1444). Outputs ranked rows, a KoreaStock symbol list and the count. Read-only, real account only. Requires an upstream KoreaStockBrokerNode. Typical connection: KoreaStockRankingNode → CodeNode (pick) → KoreaStockMarketDataNode.",
98
+ "nodes.KoreaStockRankingNode.name": "Korea Stock Ranking",
99
+ "nodes.OverseasStockRankingNode.description": "Lists today's top US stocks by gain, loss, volume, trading value, turnover, market cap or 52-week high/low (LS g3112). Outputs ranked rows, an overseas-stock symbol list and the count. Read-only. Requires an upstream OverseasStockBrokerNode. Typical connection: OverseasStockRankingNode → CodeNode (pick) → OverseasStockMarketDataNode.",
100
+ "nodes.OverseasStockRankingNode.name": "Overseas Stock Ranking",
97
101
  "nodes.LineChartNode.description": "Renders a time-series line chart on the workflow canvas. Configure x-axis field (typically date) and y-axis field (price, equity, etc.), with optional signal and side markers for buy/sell visualization. Bind data from HistoricalDataNode, BacktestEngineNode equity curves, or any time-series output. Outputs a 'rendered' signal when display is complete. Typical connection: BacktestEngineNode.equity_curve → LineChartNode.",
98
102
  "nodes.LineChartNode.name": "Line Chart",
99
103
  "nodes.LLMModelNode.description": "Provides LLM (Large Language Model) API connection for AI Agent nodes. Acts as the authentication gateway for LLM services — similar to BrokerNode pattern. Supports OpenAI, Anthropic, and Google credentials. Configure model ID (e.g., gpt-4o, claude-sonnet-4-5-20250929), temperature, max tokens, seed for reproducibility, and streaming mode. Connect to AIAgentNode via ai_model edge type. Does not execute independently — only provides the connection. Typical connection: LLMModelNode →(ai_model edge)→ AIAgentNode.",
@@ -345,6 +349,12 @@
345
349
  "fields.KoreaStockRealOrderEventNode.event_filter": "Select which Korea stock order notifications to receive. Choose 'All' to get all order events.",
346
350
  "fields.KoreaStockRealOrderEventNode.stay_connected": "✅ ON: Continuously receives order fill/reject/cancel notifications.\n❌ OFF: Receives events once and disconnects.",
347
351
  "fields.KoreaStockSymbolQueryNode.market": "Select the market to query. Choose All, KOSPI, or KOSDAQ.",
352
+ "fields.KoreaStockRankingNode.ranking_type": "Choose what to rank by: top gainers, top losers, volume, trading value or market cap.",
353
+ "fields.KoreaStockRankingNode.market": "Choose All, KOSPI or KOSDAQ. Market cap ranking needs KOSPI or KOSDAQ.",
354
+ "fields.KoreaStockRankingNode.limit": "Maximum number of ranked stocks to return (1-100).",
355
+ "fields.OverseasStockRankingNode.ranking_type": "Choose what to rank by: top gainers, top losers, volume, trading value, turnover, market cap, 52-week high or 52-week low.",
356
+ "fields.OverseasStockRankingNode.market": "Choose the US segment: All, NYSE, NASDAQ or ETF.",
357
+ "fields.OverseasStockRankingNode.limit": "Maximum number of ranked stocks to return (1-100).",
348
358
  "fields.LLMModelNode.credential_id": "LLM API Credential",
349
359
  "fields.LLMModelNode.model": "Model",
350
360
  "fields.LLMModelNode.model_help": "LLM model ID to use. e.g. gpt-4o, claude-sonnet-4-5-20250929, llama3.1",
@@ -638,6 +648,12 @@
638
648
  "fieldNames.KoreaStockRealOrderEventNode.event_filter": "Event Filter",
639
649
  "fieldNames.KoreaStockRealOrderEventNode.stay_connected": "Stay Connected",
640
650
  "fieldNames.KoreaStockSymbolQueryNode.market": "Market",
651
+ "fieldNames.KoreaStockRankingNode.ranking_type": "Ranking",
652
+ "fieldNames.KoreaStockRankingNode.market": "Market",
653
+ "fieldNames.KoreaStockRankingNode.limit": "Count",
654
+ "fieldNames.OverseasStockRankingNode.ranking_type": "Ranking",
655
+ "fieldNames.OverseasStockRankingNode.market": "Market",
656
+ "fieldNames.OverseasStockRankingNode.limit": "Count",
641
657
  "fieldNames.LineChartNode.data": "Data",
642
658
  "fieldNames.LineChartNode.side_field": "Side Field",
643
659
  "fieldNames.LineChartNode.signal_field": "Signal Field",
@@ -956,6 +972,7 @@
956
972
  "ports.summary": "Performance summary",
957
973
  "ports.symbol": "Symbol code",
958
974
  "ports.symbols": "List of symbols",
975
+ "ports.rankings": "Ranked rows",
959
976
  "ports.target_orders": "Target orders",
960
977
  "ports.throttle_stats": "Throttle stats (skip count, countdown, etc.)",
961
978
  "ports.tick_data": "Tick data",
@@ -1027,6 +1044,18 @@
1027
1044
  "enums.kr_market.KOSDAQ": "KOSDAQ",
1028
1045
  "enums.kr_market.KOSPI": "KOSPI",
1029
1046
  "enums.kr_market.all": "All",
1047
+ "enums.ranking_type.change_rate_up": "Top gainers",
1048
+ "enums.ranking_type.change_rate_down": "Top losers",
1049
+ "enums.ranking_type.volume": "Volume",
1050
+ "enums.ranking_type.trading_value": "Trading value",
1051
+ "enums.ranking_type.turnover": "Turnover",
1052
+ "enums.ranking_type.market_cap": "Market cap",
1053
+ "enums.ranking_type.high_52w": "52-week high",
1054
+ "enums.ranking_type.low_52w": "52-week low",
1055
+ "enums.us_ranking_market.all": "All",
1056
+ "enums.us_ranking_market.NYSE": "NYSE",
1057
+ "enums.us_ranking_market.NASDAQ": "NASDAQ",
1058
+ "enums.us_ranking_market.ETF": "ETF",
1030
1059
  "enums.kr_price_type.conditional_limit": "Conditional Limit",
1031
1060
  "enums.kr_price_type.limit": "Limit",
1032
1061
  "enums.kr_price_type.market": "Market",
@@ -94,6 +94,10 @@
94
94
  "nodes.KoreaStockRealOrderEventNode.name": "국내주식 실시간 주문 이벤트",
95
95
  "nodes.KoreaStockSymbolQueryNode.description": "KOSPI/KOSDAQ 전체 거래 가능 종목을 조회합니다. t9945 API로 종목명, 종목코드를 가져옵니다. 시장 구분(전체, KOSPI, KOSDAQ)으로 필터링 가능. 출력: 종목 목록, 종목 수. 상위 KoreaStockBrokerNode 연결 필수. 일반적인 연결: KoreaStockSymbolQueryNode → ConditionNode 또는 → KoreaStockMarketDataNode.",
96
96
  "nodes.KoreaStockSymbolQueryNode.name": "국내주식 종목 조회",
97
+ "nodes.KoreaStockRankingNode.description": "오늘의 국내주식 상위 종목을 등락률(상승·하락), 거래량, 거래대금, 시가총액 기준으로 조회합니다 (LS t1441, t1452, t1463, t1444). 출력: 순위 행, 국내주식 종목 리스트, 종목 수. 읽기 전용이며 실계좌 전용입니다. 상위 KoreaStockBrokerNode 연결 필수. 일반적인 연결: KoreaStockRankingNode → CodeNode(선별) → KoreaStockMarketDataNode.",
98
+ "nodes.KoreaStockRankingNode.name": "국내주식 순위",
99
+ "nodes.OverseasStockRankingNode.description": "오늘의 미국 주식 상위 종목을 등락률(상승·하락), 거래량, 거래대금, 회전율, 시가총액, 52주 신고가·신저가 기준으로 조회합니다 (LS g3112). 출력: 순위 행, 해외주식 종목 리스트, 종목 수. 읽기 전용입니다. 상위 OverseasStockBrokerNode 연결 필수. 일반적인 연결: OverseasStockRankingNode → CodeNode(선별) → OverseasStockMarketDataNode.",
100
+ "nodes.OverseasStockRankingNode.name": "해외주식 순위",
97
101
  "nodes.LineChartNode.description": "워크플로우 캔버스에 시계열 라인 차트를 렌더링합니다. X축 필드(보통 날짜)와 Y축 필드(가격, 자산 등)를 설정하고, 매수/매도 시각화를 위한 signal/side 마커를 선택적으로 추가합니다. HistoricalDataNode, BacktestEngineNode equity curve 등 시계열 데이터를 바인딩합니다. 표시 완료 시 'rendered' 신호 출력. 일반적인 연결: BacktestEngineNode.equity_curve → LineChartNode.",
98
102
  "nodes.LineChartNode.name": "라인차트",
99
103
  "nodes.LLMModelNode.description": "AI Agent 노드에 LLM(대규모 언어 모델) API 연결을 제공합니다. BrokerNode와 유사한 인증 게이트웨이 패턴입니다. OpenAI, Anthropic, Google 인증 지원. 모델 ID(예: gpt-4o, claude-sonnet-4-5-20250929), temperature, max tokens, 재현성을 위한 seed, 스트리밍 모드를 설정합니다. ai_model 엣지로 AIAgentNode에 연결합니다. 독립 실행 불가 — 연결만 제공합니다. 일반적인 연결: LLMModelNode →(ai_model 엣지)→ AIAgentNode.",
@@ -345,6 +349,12 @@
345
349
  "fields.KoreaStockRealOrderEventNode.event_filter": "받고 싶은 국내주식 주문 알림을 선택하세요. '전체'를 선택하면 모든 주문 이벤트를 받습니다.",
346
350
  "fields.KoreaStockRealOrderEventNode.stay_connected": "✅ 켜면: 주문 체결/거부/취소 알림을 계속 받습니다.\n❌ 끄면: 이벤트를 한 번만 받고 끊깁니다.",
347
351
  "fields.KoreaStockSymbolQueryNode.market": "조회할 시장을 선택하세요. 전체, KOSPI(유가증권), KOSDAQ(코스닥) 중 선택합니다.",
352
+ "fields.KoreaStockRankingNode.ranking_type": "순위 기준을 고르세요. 상승률, 하락률, 거래량, 거래대금, 시가총액 중 선택합니다.",
353
+ "fields.KoreaStockRankingNode.market": "전체, KOSPI(유가증권), KOSDAQ(코스닥) 중 선택합니다. 시가총액 순위는 KOSPI 또는 KOSDAQ 을 골라야 합니다.",
354
+ "fields.KoreaStockRankingNode.limit": "가져올 순위 종목 수의 최대값입니다 (1~100).",
355
+ "fields.OverseasStockRankingNode.ranking_type": "순위 기준을 고르세요. 상승률, 하락률, 거래량, 거래대금, 회전율, 시가총액, 52주 신고가, 52주 신저가 중 선택합니다.",
356
+ "fields.OverseasStockRankingNode.market": "미국 시장 구분을 고르세요. 전체, 뉴욕(NYSE), 나스닥(NASDAQ), ETF 중 선택합니다.",
357
+ "fields.OverseasStockRankingNode.limit": "가져올 순위 종목 수의 최대값입니다 (1~100).",
348
358
  "fields.LLMModelNode.credential_id": "LLM API 인증정보",
349
359
  "fields.LLMModelNode.model": "모델",
350
360
  "fields.LLMModelNode.model_help": "사용할 LLM 모델 ID입니다. 예: gpt-4o, claude-sonnet-4-5-20250929, llama3.1",
@@ -638,6 +648,12 @@
638
648
  "fieldNames.KoreaStockRealOrderEventNode.event_filter": "이벤트필터",
639
649
  "fieldNames.KoreaStockRealOrderEventNode.stay_connected": "연결유지",
640
650
  "fieldNames.KoreaStockSymbolQueryNode.market": "시장구분",
651
+ "fieldNames.KoreaStockRankingNode.ranking_type": "순위 기준",
652
+ "fieldNames.KoreaStockRankingNode.market": "시장구분",
653
+ "fieldNames.KoreaStockRankingNode.limit": "종목 수",
654
+ "fieldNames.OverseasStockRankingNode.ranking_type": "순위 기준",
655
+ "fieldNames.OverseasStockRankingNode.market": "시장구분",
656
+ "fieldNames.OverseasStockRankingNode.limit": "종목 수",
641
657
  "fieldNames.LineChartNode.data": "데이터",
642
658
  "fieldNames.LineChartNode.side_field": "방향필드",
643
659
  "fieldNames.LineChartNode.signal_field": "시그널필드",
@@ -956,6 +972,7 @@
956
972
  "ports.summary": "성과 요약",
957
973
  "ports.symbol": "종목 코드",
958
974
  "ports.symbols": "종목 리스트",
975
+ "ports.rankings": "순위 목록",
959
976
  "ports.target_orders": "대상 주문",
960
977
  "ports.throttle_stats": "쓰로틀 통계 (스킵 횟수, 카운트다운 등)",
961
978
  "ports.tick_data": "틱 데이터",
@@ -1027,6 +1044,18 @@
1027
1044
  "enums.kr_market.KOSDAQ": "KOSDAQ (코스닥)",
1028
1045
  "enums.kr_market.KOSPI": "KOSPI (유가증권)",
1029
1046
  "enums.kr_market.all": "전체",
1047
+ "enums.ranking_type.change_rate_up": "상승률 상위",
1048
+ "enums.ranking_type.change_rate_down": "하락률 상위",
1049
+ "enums.ranking_type.volume": "거래량 상위",
1050
+ "enums.ranking_type.trading_value": "거래대금 상위",
1051
+ "enums.ranking_type.turnover": "회전율 상위",
1052
+ "enums.ranking_type.market_cap": "시가총액 상위",
1053
+ "enums.ranking_type.high_52w": "52주 신고가",
1054
+ "enums.ranking_type.low_52w": "52주 신저가",
1055
+ "enums.us_ranking_market.all": "전체",
1056
+ "enums.us_ranking_market.NYSE": "뉴욕(NYSE)",
1057
+ "enums.us_ranking_market.NASDAQ": "나스닥(NASDAQ)",
1058
+ "enums.us_ranking_market.ETF": "ETF",
1030
1059
  "enums.kr_price_type.conditional_limit": "조건부지정가 (Conditional Limit)",
1031
1060
  "enums.kr_price_type.limit": "지정가 (Limit)",
1032
1061
  "enums.kr_price_type.market": "시장가 (Market)",
@@ -100,6 +100,8 @@ from programgarden_core.nodes.data_korea_stock import KoreaStockMarketDataNode
100
100
  from programgarden_core.nodes.fundamental_korea_stock import KoreaStockFundamentalNode
101
101
  from programgarden_core.nodes.backtest_korea_stock import KoreaStockHistoricalDataNode
102
102
  from programgarden_core.nodes.symbol_korea_stock import KoreaStockSymbolQueryNode
103
+ from programgarden_core.nodes.ranking_korea_stock import KoreaStockRankingNode
104
+ from programgarden_core.nodes.ranking_stock import OverseasStockRankingNode
103
105
  from programgarden_core.nodes.realtime_korea_stock import (
104
106
  KoreaStockRealMarketDataNode,
105
107
  KoreaStockRealAccountNode,
@@ -196,6 +198,8 @@ __all__ = [
196
198
  "KoreaStockHistoricalDataNode",
197
199
  "KoreaStockRealMarketDataNode",
198
200
  "KoreaStockSymbolQueryNode",
201
+ "KoreaStockRankingNode",
202
+ "OverseasStockRankingNode",
199
203
  # Account - Korea Stock (국내주식)
200
204
  "KoreaStockAccountNode",
201
205
  "KoreaStockOpenOrdersNode",
@@ -185,6 +185,72 @@ KOREA_STOCK_SYMBOL_QUERY_FIELDS: List[Dict[str, str]] = [
185
185
  {"name": "product", "type": "string", "description": "상품 구분 (항상 korea_stock)"},
186
186
  ]
187
187
 
188
+ # ── KoreaStockRankingNode.rankings (t1441 / t1452 / t1463 / t1444) ──
189
+ # Values are consumed as returned by LS; units are named, never converted.
190
+ KOREA_STOCK_RANKING_FIELDS: List[Dict[str, str]] = [
191
+ {"name": "rank", "type": "integer", "description": "1-based rank in this result"},
192
+ {"name": "symbol", "type": "string", "description": "6-digit KRX code (shcode)"},
193
+ {"name": "exchange", "type": "string", "description": "Always KRX"},
194
+ {"name": "market", "type": "string", "description": "KOSPI/KOSDAQ when the request named a market; null for market=all"},
195
+ {"name": "name", "type": "string", "description": "Issue name (hname)"},
196
+ {"name": "price", "type": "number", "description": "Current price"},
197
+ {"name": "open", "type": "number", "description": "Session open (t1441 only; null otherwise)"},
198
+ {"name": "high", "type": "number", "description": "Session high (t1441 only; null otherwise)"},
199
+ {"name": "low", "type": "number", "description": "Session low (t1441 only; null otherwise)"},
200
+ {"name": "sign", "type": "string", "description": "Raw LS direction code (1 upper limit, 2 up, 3 unchanged, 4 lower limit, 5 down)"},
201
+ {"name": "change", "type": "number", "description": "Change vs previous close, signed from the sign code"},
202
+ {"name": "change_pct", "type": "number", "description": "Change percent vs previous close, signed from the sign code"},
203
+ {"name": "volume", "type": "number", "description": "Cumulative volume (shares)"},
204
+ {"name": "trading_value", "type": "number", "description": "Trading value as returned (t1441/t1463; null otherwise)"},
205
+ {"name": "trading_value_unit", "type": "string", "description": "as_returned (t1441) or KRW_million (t1463); null when trading_value is null"},
206
+ {"name": "market_cap", "type": "number", "description": "Market capitalisation as returned (t1441/t1463/t1444; null for t1452)"},
207
+ {"name": "market_cap_unit", "type": "string", "description": "as_returned (t1441), KRW_million (t1463) or KRW_100million (t1444); null when market_cap is null"},
208
+ {"name": "product", "type": "string", "description": "Always korea_stock"},
209
+ {"name": "source_tr", "type": "string", "description": "LS TR that produced the row (t1441/t1452/t1463/t1444)"},
210
+ ]
211
+
212
+ # ── KoreaStockRankingNode.symbols ──
213
+ KOREA_STOCK_RANKING_SYMBOL_FIELDS: List[Dict[str, str]] = [
214
+ {"name": "exchange", "type": "string", "description": "Always KRX"},
215
+ {"name": "market", "type": "string", "description": "KOSPI/KOSDAQ when the request named a market; null for market=all"},
216
+ {"name": "symbol", "type": "string", "description": "6-digit KRX code"},
217
+ {"name": "name", "type": "string", "description": "Issue name"},
218
+ {"name": "product", "type": "string", "description": "Always korea_stock"},
219
+ ]
220
+
221
+ # ── OverseasStockRankingNode.rankings (g3112) ──
222
+ OVERSEAS_STOCK_RANKING_FIELDS: List[Dict[str, str]] = [
223
+ {"name": "rank", "type": "integer", "description": "1-based rank in this result"},
224
+ {"name": "symbol", "type": "string", "description": "Ticker symbol"},
225
+ {"name": "exchange", "type": "string", "description": "NYSE or NASDAQ from the LS key symbol prefix (81/82); null when unavailable"},
226
+ {"name": "exchange_source", "type": "string", "description": "keysymbol when derived from the LS key symbol, otherwise unavailable"},
227
+ {"name": "name", "type": "string", "description": "Korean issue name (korname)"},
228
+ {"name": "exchange_name", "type": "string", "description": "LS exchange label and symbol as returned (exsymbol, e.g. '나스닥 AEHL')"},
229
+ {"name": "keysymbol", "type": "string", "description": "LS key symbol as returned"},
230
+ {"name": "price", "type": "number", "description": "Current price"},
231
+ {"name": "sign", "type": "string", "description": "Raw LS direction code"},
232
+ {"name": "change", "type": "number", "description": "Change vs previous close, signed from the sign code"},
233
+ {"name": "change_pct", "type": "number", "description": "Change percent vs previous close, signed from the sign code"},
234
+ {"name": "volume", "type": "number", "description": "Volume"},
235
+ {"name": "trading_value", "type": "number", "description": "Trading value as returned by LS (unit not declared)"},
236
+ {"name": "trading_value_unit", "type": "string", "description": "as_returned; null when trading_value is null"},
237
+ {"name": "turnover", "type": "number", "description": "Turnover as returned by LS"},
238
+ {"name": "market_cap", "type": "number", "description": "Market capitalisation as returned by LS (unit not declared)"},
239
+ {"name": "market_cap_unit", "type": "string", "description": "as_returned; null when market_cap is null"},
240
+ {"name": "high_52w", "type": "number", "description": "52-week high"},
241
+ {"name": "low_52w", "type": "number", "description": "52-week low"},
242
+ {"name": "product", "type": "string", "description": "Always overseas_stock"},
243
+ {"name": "source_tr", "type": "string", "description": "Always g3112"},
244
+ ]
245
+
246
+ # ── OverseasStockRankingNode.symbols ── (rows without a known exchange are omitted)
247
+ OVERSEAS_STOCK_RANKING_SYMBOL_FIELDS: List[Dict[str, str]] = [
248
+ {"name": "exchange", "type": "string", "description": "NYSE or NASDAQ"},
249
+ {"name": "symbol", "type": "string", "description": "Ticker symbol"},
250
+ {"name": "name", "type": "string", "description": "Korean issue name"},
251
+ {"name": "product", "type": "string", "description": "Always overseas_stock"},
252
+ ]
253
+
188
254
  # ── OverseasFuturesSymbolQueryNode.symbols (o3101 마스터) ──
189
255
  OVERSEAS_FUTURES_SYMBOL_QUERY_FIELDS: List[Dict[str, str]] = [
190
256
  {"name": "exchange", "type": "string", "description": "거래소 코드 (CME, HKEX 등 — 주문에 쓰는 값)"},
@@ -0,0 +1,277 @@
1
+ """
2
+ ProgramGarden Core - Korea Stock Ranking Node
3
+
4
+ 국내주식 순위 조회:
5
+ - KoreaStockRankingNode: 등락률·거래량·거래대금·시가총액 상위 종목 (LS t1441 / t1452 / t1463 / t1444)
6
+ """
7
+
8
+ from typing import Any, ClassVar, Dict, List, Literal, Optional, TYPE_CHECKING
9
+
10
+ from pydantic import Field
11
+
12
+ if TYPE_CHECKING:
13
+ from programgarden_core.models.field_binding import FieldSchema
14
+
15
+ from programgarden_core.nodes.base import (
16
+ BaseNode,
17
+ BrokerProvider,
18
+ InputPort,
19
+ KOREA_STOCK_RANKING_FIELDS,
20
+ KOREA_STOCK_RANKING_SYMBOL_FIELDS,
21
+ NodeCategory,
22
+ OutputPort,
23
+ ProductScope,
24
+ )
25
+
26
+
27
+ KOREA_RANKING_TYPES = ["change_rate_up", "change_rate_down", "volume", "trading_value", "market_cap"]
28
+ KOREA_RANKING_MARKETS = ["all", "KOSPI", "KOSDAQ"]
29
+ RANKING_LIMIT_MIN = 1
30
+ RANKING_LIMIT_MAX = 100
31
+
32
+ # The programgarden engine runs both ranking nodes with its own executor from this
33
+ # version. An older engine paired with this core falls back to its generic executor,
34
+ # which calls the node's ``execute``; without the method below it would report the
35
+ # node as executed with no rankings and no error.
36
+ RANKING_MIN_ENGINE = "2.6.6"
37
+
38
+
39
+ def ranking_engine_required(node_type: str) -> RuntimeError:
40
+ """Error raised when an engine without the ranking executor runs a ranking node."""
41
+ return RuntimeError(
42
+ f"{node_type} requires the programgarden engine {RANKING_MIN_ENGINE} or later; "
43
+ "this engine has no ranking executor, so no ranking was read. "
44
+ "Update the programgarden package (updating programgarden-core alone is not enough)."
45
+ )
46
+
47
+
48
+ _BROKER_CREDENTIAL = {
49
+ "credential_id": "broker_cred",
50
+ "type": "broker_ls_korea_stock",
51
+ "data": [
52
+ {"key": "appkey", "value": "", "type": "password", "label": "App Key"},
53
+ {"key": "appsecret", "value": "", "type": "password", "label": "App Secret"},
54
+ ],
55
+ }
56
+
57
+
58
+ class KoreaStockRankingNode(BaseNode):
59
+ """
60
+ 국내주식 순위 노드
61
+
62
+ KOSPI/KOSDAQ 의 등락률·거래량·거래대금·시가총액 상위 종목을 조회합니다.
63
+ LS t1441(등락율상위) / t1452(거래량상위) / t1463(거래대금상위) / t1444(시가총액상위).
64
+ 읽기 전용 노드이며 주문을 내지 않습니다.
65
+ """
66
+
67
+ type: Literal["KoreaStockRankingNode"] = "KoreaStockRankingNode"
68
+ category: NodeCategory = NodeCategory.MARKET
69
+ description: str = "i18n:nodes.KoreaStockRankingNode.description"
70
+ _img_url: ClassVar[str] = ""
71
+ _product_scope: ClassVar[ProductScope] = ProductScope.KOREA_STOCK
72
+ _broker_provider: ClassVar[BrokerProvider] = BrokerProvider.LS
73
+
74
+ ranking_type: str = Field(
75
+ default="change_rate_up",
76
+ description="Ranking: change_rate_up, change_rate_down, volume, trading_value, market_cap",
77
+ )
78
+ market: str = Field(
79
+ default="all",
80
+ description="Market: all, KOSPI, KOSDAQ (market_cap needs KOSPI or KOSDAQ)",
81
+ )
82
+ limit: int = Field(
83
+ default=20,
84
+ ge=RANKING_LIMIT_MIN,
85
+ le=RANKING_LIMIT_MAX,
86
+ description="Maximum number of ranked rows (1-100)",
87
+ )
88
+
89
+ _usage: ClassVar[Dict[str, Any]] = {
90
+ "when_to_use": [
91
+ "Pick today's top domestic gainers, losers, most traded or largest stocks as a candidate list",
92
+ "Start an intraday screen from the whole KOSPI/KOSDAQ market instead of a fixed watchlist",
93
+ ],
94
+ "when_not_to_use": [
95
+ "For the full listed universe — use KoreaStockSymbolQueryNode",
96
+ "For overseas stocks — use OverseasStockRankingNode",
97
+ "For a fixed list of symbols — use WatchlistNode",
98
+ ],
99
+ "typical_scenarios": [
100
+ "KoreaStockRankingNode(change_rate_up) → CodeNode (filter and pick) → KoreaStockMarketDataNode → order decision",
101
+ "KoreaStockRankingNode(trading_value) → TableDisplayNode (market overview)",
102
+ "KoreaStockRankingNode.symbols → SplitNode → KoreaStockHistoricalDataNode (per-symbol history)",
103
+ ],
104
+ }
105
+ _features: ClassVar[List[str]] = [
106
+ "One node, four LS ranking TRs: change_rate_up/change_rate_down → t1441, volume → t1452, trading_value → t1463, market_cap → t1444",
107
+ "Outputs `rankings` (full rows), `symbols` (KoreaStock symbol list) and `count`",
108
+ "Change and change percent are signed from the LS sign code; units are named (trading_value_unit, market_cap_unit) and never converted",
109
+ "Pages with the LS cursor up to `limit` rows, at most 5 requests per run, at the SDK rate limit of each TR",
110
+ "Read-only and real-account only — KoreaStock has no paper trading; no order is ever placed",
111
+ "Deep validation and dry runs return 3 synthetic rows with the same shape and make no broker call",
112
+ ]
113
+ _anti_patterns: ClassVar[List[Dict[str, str]]] = [
114
+ {
115
+ "pattern": "Wiring `symbols` or `rankings` straight into KoreaStockNewOrderNode",
116
+ "reason": "A ranking is a moving market snapshot, not a trading decision. Top gainers often include limit-up, halted-adjacent or illiquid issues.",
117
+ "alternative": "Filter in a CodeNode or ConditionNode (price, change range, holdings, open orders, budget) and order at most the chosen symbol.",
118
+ },
119
+ {
120
+ "pattern": "Adding the five TRs together or comparing trading_value across ranking types",
121
+ "reason": "Each TR reports in its own unit (as_returned, KRW_million, KRW_100million).",
122
+ "alternative": "Read trading_value_unit / market_cap_unit before comparing values.",
123
+ },
124
+ {
125
+ "pattern": "Using market_cap with market='all'",
126
+ "reason": "t1444 ranks one market index at a time (001 KOSPI, 301 KOSDAQ).",
127
+ "alternative": "Choose market='KOSPI' or market='KOSDAQ', or use two nodes.",
128
+ },
129
+ ]
130
+ _examples: ClassVar[List[Dict[str, Any]]] = [
131
+ {
132
+ "title": "Top 30 domestic gainers",
133
+ "description": "List today's 30 biggest gainers across KOSPI and KOSDAQ.",
134
+ "workflow_snippet": {
135
+ "id": "korea_stock_ranking_gainers",
136
+ "name": "Domestic Top Gainers",
137
+ "nodes": [
138
+ {"id": "start", "type": "StartNode"},
139
+ {"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
140
+ {"id": "ranking", "type": "KoreaStockRankingNode", "ranking_type": "change_rate_up", "market": "all", "limit": 30},
141
+ {"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.ranking.rankings }}"},
142
+ ],
143
+ "edges": [
144
+ {"from": "start", "to": "broker"},
145
+ {"from": "broker", "to": "ranking"},
146
+ {"from": "ranking", "to": "display"},
147
+ ],
148
+ "credentials": [_BROKER_CREDENTIAL],
149
+ },
150
+ "expected_output": "rankings: [{rank, symbol, name, price, change_pct, volume, ...}] (30 rows); count: 30.",
151
+ },
152
+ {
153
+ "title": "KOSPI market-cap leaders into a screen",
154
+ "description": "Take the 20 largest KOSPI issues and pass the symbol list to a CodeNode for screening.",
155
+ "workflow_snippet": {
156
+ "id": "korea_stock_ranking_market_cap",
157
+ "name": "KOSPI Market Cap Leaders",
158
+ "nodes": [
159
+ {"id": "start", "type": "StartNode"},
160
+ {"id": "broker", "type": "KoreaStockBrokerNode", "credential_id": "broker_cred"},
161
+ {"id": "ranking", "type": "KoreaStockRankingNode", "ranking_type": "market_cap", "market": "KOSPI", "limit": 20},
162
+ {
163
+ "id": "screen",
164
+ "type": "CodeNode",
165
+ "data": "{{ nodes.ranking.rankings }}",
166
+ "code": (
167
+ "async def execute(data, params, context):\n"
168
+ " rows = [r for r in (data or []) if (r.get('change_pct') or 0) > 0]\n"
169
+ " return {'picked': rows[:5]}"
170
+ ),
171
+ "outputs": [{"name": "picked", "type": "array"}],
172
+ },
173
+ ],
174
+ "edges": [
175
+ {"from": "start", "to": "broker"},
176
+ {"from": "broker", "to": "ranking"},
177
+ {"from": "ranking", "to": "screen"},
178
+ ],
179
+ "credentials": [_BROKER_CREDENTIAL],
180
+ },
181
+ "expected_output": "screen.picked: up to 5 KOSPI market-cap leaders that are up today.",
182
+ },
183
+ ]
184
+ _node_guide: ClassVar[Dict[str, Any]] = {
185
+ "input_handling": (
186
+ "No symbol input. Choose ranking_type (change_rate_up, change_rate_down, volume, trading_value, market_cap), "
187
+ "market (all, KOSPI, KOSDAQ; market_cap needs KOSPI or KOSDAQ) and limit (1-100). "
188
+ "A connected KoreaStockBrokerNode supplies the real-account connection automatically."
189
+ ),
190
+ "output_consumption": (
191
+ "`rankings` rows: rank, symbol (6 digits), exchange (KRX), market (KOSPI/KOSDAQ or null for all), name, price, "
192
+ "open/high/low (t1441 only, else null), sign (raw code), change and change_pct (signed), volume, "
193
+ "trading_value + trading_value_unit, market_cap + market_cap_unit, product (korea_stock), source_tr. "
194
+ "`symbols` items: {exchange, market, symbol, name, product} — the KoreaStock symbol-list shape. "
195
+ "`count`: number of rows. An empty successful result is valid (rankings=[], count=0). "
196
+ "Deep validation and dry runs emit 3 synthetic rows (005930, 000660, 035420) with this exact shape and no broker call."
197
+ ),
198
+ "common_combinations": [
199
+ "KoreaStockRankingNode.rankings → CodeNode (decision) → KoreaStockNewOrderNode",
200
+ "KoreaStockRankingNode.symbols → SplitNode → KoreaStockMarketDataNode",
201
+ "KoreaStockRankingNode.rankings → TableDisplayNode",
202
+ ],
203
+ "pitfalls": [
204
+ "Rankings move every second; re-check price and holdings right before ordering",
205
+ "Units differ by ranking type — read trading_value_unit / market_cap_unit",
206
+ "LS failures raise with the TR code first (e.g. 't1441: <code> <message>'); missing credentials fail the node",
207
+ "KoreaStock is real-account only",
208
+ ],
209
+ }
210
+
211
+ _inputs: List[InputPort] = []
212
+ _outputs: List[OutputPort] = [
213
+ OutputPort(name="rankings", type="array", description="i18n:ports.rankings", fields=KOREA_STOCK_RANKING_FIELDS),
214
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=KOREA_STOCK_RANKING_SYMBOL_FIELDS),
215
+ OutputPort(name="count", type="integer", description="i18n:ports.count"),
216
+ ]
217
+
218
+ _version: ClassVar[str] = "1.0.0"
219
+ _updated_at: ClassVar[str] = "2026-10-15"
220
+ _change_note: ClassVar[Optional[str]] = "New: domestic gainers/losers/volume/trading value/market cap rankings (LS)."
221
+
222
+ @classmethod
223
+ def get_field_schema(cls) -> Dict[str, "FieldSchema"]:
224
+ from programgarden_core.models.field_binding import ExpressionMode, FieldCategory, FieldSchema, FieldType
225
+ return {
226
+ "ranking_type": FieldSchema(
227
+ name="ranking_type",
228
+ type=FieldType.ENUM,
229
+ description="i18n:fields.KoreaStockRankingNode.ranking_type",
230
+ default="change_rate_up",
231
+ enum_values=list(KOREA_RANKING_TYPES),
232
+ enum_labels={value: f"i18n:enums.ranking_type.{value}" for value in KOREA_RANKING_TYPES},
233
+ required=False,
234
+ category=FieldCategory.PARAMETERS,
235
+ expression_mode=ExpressionMode.FIXED_ONLY,
236
+ example="change_rate_up",
237
+ expected_type="str",
238
+ ),
239
+ "market": FieldSchema(
240
+ name="market",
241
+ type=FieldType.ENUM,
242
+ description="i18n:fields.KoreaStockRankingNode.market",
243
+ default="all",
244
+ enum_values=list(KOREA_RANKING_MARKETS),
245
+ enum_labels={
246
+ "all": "i18n:enums.kr_market.all",
247
+ "KOSPI": "i18n:enums.kr_market.KOSPI",
248
+ "KOSDAQ": "i18n:enums.kr_market.KOSDAQ",
249
+ },
250
+ required=False,
251
+ category=FieldCategory.PARAMETERS,
252
+ expression_mode=ExpressionMode.FIXED_ONLY,
253
+ example="all",
254
+ expected_type="str",
255
+ ),
256
+ "limit": FieldSchema(
257
+ name="limit",
258
+ type=FieldType.INTEGER,
259
+ description="i18n:fields.KoreaStockRankingNode.limit",
260
+ default=20,
261
+ min_value=RANKING_LIMIT_MIN,
262
+ max_value=RANKING_LIMIT_MAX,
263
+ required=False,
264
+ category=FieldCategory.PARAMETERS,
265
+ expression_mode=ExpressionMode.FIXED_ONLY,
266
+ example=20,
267
+ expected_type="int",
268
+ ),
269
+ }
270
+
271
+ async def execute(self, context: Any) -> Dict[str, Any]:
272
+ """Fail closed on an engine without the ranking executor.
273
+
274
+ programgarden 2.6.6+ executes this node with its ranking
275
+ executor and never calls this method (see ``RANKING_MIN_ENGINE``).
276
+ """
277
+ raise ranking_engine_required(self.type)
@@ -0,0 +1,248 @@
1
+ """
2
+ ProgramGarden Core - Overseas Stock Ranking Node
3
+
4
+ 해외주식 순위 조회:
5
+ - OverseasStockRankingNode: 미국 주식 등락률·거래량·거래대금·회전율·시가총액·52주 신고가/신저가 순위 (LS g3112)
6
+ """
7
+
8
+ from typing import Any, ClassVar, Dict, List, Literal, Optional, TYPE_CHECKING
9
+
10
+ from pydantic import Field
11
+
12
+ if TYPE_CHECKING:
13
+ from programgarden_core.models.field_binding import FieldSchema
14
+
15
+ from programgarden_core.nodes.base import (
16
+ BaseNode,
17
+ BrokerProvider,
18
+ InputPort,
19
+ NodeCategory,
20
+ OVERSEAS_STOCK_RANKING_FIELDS,
21
+ OVERSEAS_STOCK_RANKING_SYMBOL_FIELDS,
22
+ OutputPort,
23
+ ProductScope,
24
+ )
25
+ from programgarden_core.nodes.ranking_korea_stock import RANKING_LIMIT_MAX, RANKING_LIMIT_MIN, ranking_engine_required
26
+
27
+
28
+ # Node enum → g3112 rankgubun (LS guide: 0 상승률, 1 하락률, 2 거래량, 3 거래대금, 4 회전율,
29
+ # 5 시가총액, 6 52주신고가, 7 52주신저가). '8' 상한가 / '9' 하한가 are intentionally not exposed.
30
+ OVERSEAS_RANKING_TYPES = [
31
+ "change_rate_up", "change_rate_down", "volume", "trading_value",
32
+ "turnover", "market_cap", "high_52w", "low_52w",
33
+ ]
34
+ # Node enum → g3112 exgubun (LS guide: 1 전체, 2 뉴욕, 3 나스닥, 4 ETF).
35
+ OVERSEAS_RANKING_MARKETS = ["all", "NYSE", "NASDAQ", "ETF"]
36
+
37
+
38
+ _BROKER_CREDENTIAL = {
39
+ "credential_id": "broker_cred",
40
+ "type": "broker_ls_overseas_stock",
41
+ "data": [
42
+ {"key": "appkey", "value": "", "type": "password", "label": "App Key"},
43
+ {"key": "appsecret", "value": "", "type": "password", "label": "App Secret"},
44
+ ],
45
+ }
46
+
47
+
48
+ class OverseasStockRankingNode(BaseNode):
49
+ """
50
+ 해외주식 순위 노드
51
+
52
+ 미국 주식의 등락률·거래량·거래대금·회전율·시가총액·52주 신고가/신저가 순위를
53
+ LS g3112(해외주식API시세순위)로 조회합니다. 읽기 전용이며 주문을 내지 않습니다.
54
+ """
55
+
56
+ type: Literal["OverseasStockRankingNode"] = "OverseasStockRankingNode"
57
+ category: NodeCategory = NodeCategory.MARKET
58
+ description: str = "i18n:nodes.OverseasStockRankingNode.description"
59
+ _img_url: ClassVar[str] = ""
60
+ _product_scope: ClassVar[ProductScope] = ProductScope.STOCK
61
+ _broker_provider: ClassVar[BrokerProvider] = BrokerProvider.LS
62
+
63
+ ranking_type: str = Field(
64
+ default="change_rate_up",
65
+ description="Ranking: change_rate_up, change_rate_down, volume, trading_value, turnover, market_cap, high_52w, low_52w",
66
+ )
67
+ market: str = Field(
68
+ default="all",
69
+ description="US segment: all, NYSE, NASDAQ, ETF",
70
+ )
71
+ limit: int = Field(
72
+ default=20,
73
+ ge=RANKING_LIMIT_MIN,
74
+ le=RANKING_LIMIT_MAX,
75
+ description="Maximum number of ranked rows (1-100)",
76
+ )
77
+
78
+ _usage: ClassVar[Dict[str, Any]] = {
79
+ "when_to_use": [
80
+ "Pick today's top US gainers, losers, most traded, largest or 52-week-high stocks as a candidate list",
81
+ "Start a US screen from the whole market or one segment (NYSE, NASDAQ, ETF) instead of a fixed watchlist",
82
+ ],
83
+ "when_not_to_use": [
84
+ "For the full listed US universe — use OverseasStockSymbolQueryNode",
85
+ "For domestic stocks — use KoreaStockRankingNode",
86
+ "For overseas futures — there is no LS ranking query; pick a contract with FuturesContractNode",
87
+ ],
88
+ "typical_scenarios": [
89
+ "OverseasStockRankingNode(volume) → CodeNode (filter) → OverseasStockMarketDataNode",
90
+ "OverseasStockRankingNode(high_52w, NASDAQ) → TableDisplayNode",
91
+ "OverseasStockRankingNode.symbols → SplitNode → OverseasStockHistoricalDataNode",
92
+ ],
93
+ }
94
+ _features: ClassVar[List[str]] = [
95
+ "LS g3112 (해외주식API시세순위), US only; ranking types map to the LS rank codes 0-7",
96
+ "Outputs `rankings` (full rows), `symbols` (OverseasStock symbol list) and `count`",
97
+ "Exchange comes from the LS key symbol prefix (81 NYSE/AMEX shown as NYSE, 82 NASDAQ); rows without it keep exchange=null and are left out of `symbols`",
98
+ "Numbers are parsed exactly from LS strings; units of trading value and market cap are not declared by LS and are not converted",
99
+ "Pages with cts_value up to `limit` rows, at most 5 requests at 1 request per second",
100
+ "Read-only; overseas stock has no paper trading. Deep validation and dry runs return 3 synthetic rows and make no broker call",
101
+ ]
102
+ _anti_patterns: ClassVar[List[Dict[str, str]]] = [
103
+ {
104
+ "pattern": "Wiring `symbols` or `rankings` straight into OverseasStockNewOrderNode",
105
+ "reason": "A ranking is a moving market snapshot, not a trading decision; top movers include thin or halted-adjacent issues.",
106
+ "alternative": "Filter in a CodeNode or ConditionNode (price, change, holdings, budget) and order at most the chosen symbol.",
107
+ },
108
+ {
109
+ "pattern": "Treating a row with exchange=null as NYSE or NASDAQ",
110
+ "reason": "The exchange could not be read from the LS key symbol; guessing routes orders to the wrong market.",
111
+ "alternative": "Use the `symbols` output, which keeps only rows with a known exchange.",
112
+ },
113
+ ]
114
+ _examples: ClassVar[List[Dict[str, Any]]] = [
115
+ {
116
+ "title": "Top 20 US gainers",
117
+ "description": "List today's 20 biggest US gainers across all segments.",
118
+ "workflow_snippet": {
119
+ "id": "overseas_stock_ranking_gainers",
120
+ "name": "US Top Gainers",
121
+ "nodes": [
122
+ {"id": "start", "type": "StartNode"},
123
+ {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred"},
124
+ {"id": "ranking", "type": "OverseasStockRankingNode", "ranking_type": "change_rate_up", "market": "all", "limit": 20},
125
+ {"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.ranking.rankings }}"},
126
+ ],
127
+ "edges": [
128
+ {"from": "start", "to": "broker"},
129
+ {"from": "broker", "to": "ranking"},
130
+ {"from": "ranking", "to": "display"},
131
+ ],
132
+ "credentials": [_BROKER_CREDENTIAL],
133
+ },
134
+ "expected_output": "rankings: [{rank, symbol, exchange, name, price, change_pct, volume, ...}] (up to 20 rows).",
135
+ },
136
+ {
137
+ "title": "NASDAQ volume leaders to quotes",
138
+ "description": "Take the 10 most traded NASDAQ stocks and read their current quotes.",
139
+ "workflow_snippet": {
140
+ "id": "overseas_stock_ranking_volume",
141
+ "name": "NASDAQ Volume Leaders",
142
+ "nodes": [
143
+ {"id": "start", "type": "StartNode"},
144
+ {"id": "broker", "type": "OverseasStockBrokerNode", "credential_id": "broker_cred"},
145
+ {"id": "ranking", "type": "OverseasStockRankingNode", "ranking_type": "volume", "market": "NASDAQ", "limit": 10},
146
+ {"id": "quotes", "type": "OverseasStockMarketDataNode", "symbols": "{{ nodes.ranking.symbols }}"},
147
+ {"id": "display", "type": "TableDisplayNode", "data": "{{ nodes.quotes.values }}"},
148
+ ],
149
+ "edges": [
150
+ {"from": "start", "to": "broker"},
151
+ {"from": "broker", "to": "ranking"},
152
+ {"from": "ranking", "to": "quotes"},
153
+ {"from": "quotes", "to": "display"},
154
+ ],
155
+ "credentials": [_BROKER_CREDENTIAL],
156
+ },
157
+ "expected_output": "quotes.values: current quotes for up to 10 NASDAQ volume leaders.",
158
+ },
159
+ ]
160
+ _node_guide: ClassVar[Dict[str, Any]] = {
161
+ "input_handling": (
162
+ "No symbol input. Choose ranking_type (change_rate_up, change_rate_down, volume, trading_value, turnover, "
163
+ "market_cap, high_52w, low_52w), market (all, NYSE, NASDAQ, ETF) and limit (1-100). "
164
+ "A connected OverseasStockBrokerNode supplies the connection automatically."
165
+ ),
166
+ "output_consumption": (
167
+ "`rankings` rows: rank, symbol, exchange (NYSE/NASDAQ or null), exchange_source (keysymbol/unavailable), "
168
+ "name (Korean name), exchange_name, keysymbol, price, sign (raw), change and change_pct (signed), volume, "
169
+ "trading_value + trading_value_unit (as_returned), turnover, market_cap + market_cap_unit (as_returned), "
170
+ "high_52w, low_52w, product (overseas_stock), source_tr (g3112). Empty LS strings become null. "
171
+ "`symbols` items: {exchange, symbol, name, product} for rows with a known exchange. `count`: number of rankings rows. "
172
+ "Deep validation and dry runs emit 3 synthetic rows (AAPL, MSFT, NVDA) with this exact shape and no broker call."
173
+ ),
174
+ "common_combinations": [
175
+ "OverseasStockRankingNode.symbols → OverseasStockMarketDataNode",
176
+ "OverseasStockRankingNode.rankings → CodeNode (decision)",
177
+ "OverseasStockRankingNode.rankings → TableDisplayNode",
178
+ ],
179
+ "pitfalls": [
180
+ "The exchange code 81 covers both NYSE and AMEX; it is shown as NYSE",
181
+ "Trading value and market cap units are not declared by LS; compare only within one result",
182
+ "LS failures raise with the TR code first (e.g. 'g3112: <code> <message>')",
183
+ ],
184
+ }
185
+
186
+ _inputs: List[InputPort] = []
187
+ _outputs: List[OutputPort] = [
188
+ OutputPort(name="rankings", type="array", description="i18n:ports.rankings", fields=OVERSEAS_STOCK_RANKING_FIELDS),
189
+ OutputPort(name="symbols", type="symbol_list", description="i18n:ports.symbols", fields=OVERSEAS_STOCK_RANKING_SYMBOL_FIELDS),
190
+ OutputPort(name="count", type="integer", description="i18n:ports.count"),
191
+ ]
192
+
193
+ _version: ClassVar[str] = "1.0.0"
194
+ _updated_at: ClassVar[str] = "2026-10-15"
195
+ _change_note: ClassVar[Optional[str]] = "New: US stock rankings by change, volume, value, turnover, market cap, 52-week (LS g3112)."
196
+
197
+ @classmethod
198
+ def get_field_schema(cls) -> Dict[str, "FieldSchema"]:
199
+ from programgarden_core.models.field_binding import ExpressionMode, FieldCategory, FieldSchema, FieldType
200
+ return {
201
+ "ranking_type": FieldSchema(
202
+ name="ranking_type",
203
+ type=FieldType.ENUM,
204
+ description="i18n:fields.OverseasStockRankingNode.ranking_type",
205
+ default="change_rate_up",
206
+ enum_values=list(OVERSEAS_RANKING_TYPES),
207
+ enum_labels={value: f"i18n:enums.ranking_type.{value}" for value in OVERSEAS_RANKING_TYPES},
208
+ required=False,
209
+ category=FieldCategory.PARAMETERS,
210
+ expression_mode=ExpressionMode.FIXED_ONLY,
211
+ example="change_rate_up",
212
+ expected_type="str",
213
+ ),
214
+ "market": FieldSchema(
215
+ name="market",
216
+ type=FieldType.ENUM,
217
+ description="i18n:fields.OverseasStockRankingNode.market",
218
+ default="all",
219
+ enum_values=list(OVERSEAS_RANKING_MARKETS),
220
+ enum_labels={value: f"i18n:enums.us_ranking_market.{value}" for value in OVERSEAS_RANKING_MARKETS},
221
+ required=False,
222
+ category=FieldCategory.PARAMETERS,
223
+ expression_mode=ExpressionMode.FIXED_ONLY,
224
+ example="all",
225
+ expected_type="str",
226
+ ),
227
+ "limit": FieldSchema(
228
+ name="limit",
229
+ type=FieldType.INTEGER,
230
+ description="i18n:fields.OverseasStockRankingNode.limit",
231
+ default=20,
232
+ min_value=RANKING_LIMIT_MIN,
233
+ max_value=RANKING_LIMIT_MAX,
234
+ required=False,
235
+ category=FieldCategory.PARAMETERS,
236
+ expression_mode=ExpressionMode.FIXED_ONLY,
237
+ example=20,
238
+ expected_type="int",
239
+ ),
240
+ }
241
+
242
+ async def execute(self, context: Any) -> Dict[str, Any]:
243
+ """Fail closed on an engine without the ranking executor.
244
+
245
+ programgarden 2.6.6+ executes this node with its ranking executor and
246
+ never calls this method (see ``ranking_korea_stock.RANKING_MIN_ENGINE``).
247
+ """
248
+ raise ranking_engine_required(self.type)
@@ -207,6 +207,7 @@ class NodeTypeRegistry:
207
207
  KoreaStockAccountNode, KoreaStockOpenOrdersNode,
208
208
  KoreaStockMarketDataNode, KoreaStockFundamentalNode,
209
209
  KoreaStockHistoricalDataNode, KoreaStockSymbolQueryNode,
210
+ KoreaStockRankingNode, OverseasStockRankingNode,
210
211
  KoreaStockRealMarketDataNode, KoreaStockRealAccountNode, KoreaStockRealOrderEventNode,
211
212
  KoreaStockNewOrderNode, KoreaStockModifyOrderNode, KoreaStockCancelOrderNode,
212
213
  # Data (상품 무관)
@@ -251,6 +252,7 @@ class NodeTypeRegistry:
251
252
  KoreaStockAccountNode, KoreaStockOpenOrdersNode,
252
253
  KoreaStockMarketDataNode, KoreaStockFundamentalNode,
253
254
  KoreaStockHistoricalDataNode, KoreaStockSymbolQueryNode,
255
+ KoreaStockRankingNode, OverseasStockRankingNode,
254
256
  KoreaStockRealMarketDataNode, KoreaStockRealAccountNode, KoreaStockRealOrderEventNode,
255
257
  KoreaStockNewOrderNode, KoreaStockModifyOrderNode, KoreaStockCancelOrderNode,
256
258
  # Data (상품 무관)
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden-core"
8
- version = "2.6.2"
8
+ version = "2.6.3"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의"
11
11
  readme = "README.md"