programgarden-core 1.28.0__tar.gz → 1.28.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/PKG-INFO +1 -1
  2. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/bases/listener.py +3 -0
  3. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/__init__.py +10 -0
  4. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/order_diagnostics.py +84 -2
  5. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/order.py +13 -1
  6. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/pyproject.toml +1 -1
  7. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/README.md +0 -0
  8. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/__init__.py +0 -0
  9. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/bases/__init__.py +0 -0
  10. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/bases/client.py +0 -0
  11. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/bases/components.py +0 -0
  12. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/bases/mixins.py +0 -0
  13. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/bases/products.py +0 -0
  14. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/bases/sql.py +0 -0
  15. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/bases/storage.py +0 -0
  16. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/code_node.py +0 -0
  17. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/exceptions.py +0 -0
  18. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/expression/__init__.py +0 -0
  19. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/expression/evaluator.py +0 -0
  20. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/i18n/__init__.py +0 -0
  21. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/i18n/locales/en.json +0 -0
  22. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/i18n/locales/ko.json +0 -0
  23. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/i18n/translator.py +0 -0
  24. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/korea_alias.py +0 -0
  25. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/connection_rule.py +0 -0
  26. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/credential.py +0 -0
  27. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/edge.py +0 -0
  28. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/event.py +0 -0
  29. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/exchange.py +0 -0
  30. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/field_binding.py +0 -0
  31. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/job.py +0 -0
  32. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/plugin_resource.py +0 -0
  33. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/resilience.py +0 -0
  34. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/resource.py +0 -0
  35. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/validation.py +0 -0
  36. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/models/workflow.py +0 -0
  37. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/__init__.py +0 -0
  38. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/account_futures.py +0 -0
  39. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/account_korea_stock.py +0 -0
  40. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/account_stock.py +0 -0
  41. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/ai.py +0 -0
  42. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/backtest.py +0 -0
  43. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/backtest_futures.py +0 -0
  44. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/backtest_korea_stock.py +0 -0
  45. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/backtest_stock.py +0 -0
  46. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/base.py +0 -0
  47. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/broker.py +0 -0
  48. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/calculation.py +0 -0
  49. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/code.py +0 -0
  50. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/condition.py +0 -0
  51. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/data.py +0 -0
  52. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/data_futures.py +0 -0
  53. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/data_korea_stock.py +0 -0
  54. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/data_stock.py +0 -0
  55. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/display.py +0 -0
  56. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/event.py +0 -0
  57. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
  58. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/fundamental_stock.py +0 -0
  59. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/infra.py +0 -0
  60. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/market_external.py +0 -0
  61. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/market_status.py +0 -0
  62. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/open_orders_futures.py +0 -0
  63. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
  64. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/open_orders_stock.py +0 -0
  65. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/portfolio.py +0 -0
  66. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/realtime_futures.py +0 -0
  67. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/realtime_korea_stock.py +0 -0
  68. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/realtime_stock.py +0 -0
  69. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/risk.py +0 -0
  70. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/symbol.py +0 -0
  71. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/symbol_futures.py +0 -0
  72. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
  73. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/symbol_stock.py +0 -0
  74. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/nodes/trigger.py +0 -0
  75. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/presets/__init__.py +0 -0
  76. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/presets/news_analyst.json +0 -0
  77. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/presets/risk_manager.json +0 -0
  78. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/presets/strategist.json +0 -0
  79. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/presets/technical_analyst.json +0 -0
  80. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/registry/__init__.py +0 -0
  81. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/registry/credential_registry.py +0 -0
  82. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/registry/node_registry.py +0 -0
  83. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/registry/plugin_registry.py +0 -0
  84. {programgarden_core-1.28.0 → programgarden_core-1.28.2}/programgarden_core/retry_executor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden-core
3
- Version: 1.28.0
3
+ Version: 1.28.2
4
4
  Summary: ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -409,6 +409,9 @@ class WorkflowPnLEvent:
409
409
  competition_account_overseas_futures_pnl_amount: Optional[Union[Decimal, float]] = None
410
410
  competition_account_korea_stock_pnl_rate: Optional[Union[Decimal, float]] = None
411
411
  competition_account_korea_stock_pnl_amount: Optional[Union[Decimal, float]] = None
412
+ # Open-position money, independent of closed-trade personal metrics.
413
+ account_valuation: Optional[Dict[str, Any]] = None
414
+ workflow_valuation: Optional[Dict[str, Any]] = None
412
415
 
413
416
 
414
417
  # ============================================================
@@ -99,6 +99,11 @@ from programgarden_core.models.order_diagnostics import (
99
99
  EmptyOrderReason,
100
100
  map_reject_code,
101
101
  diagnose_missing_order_no,
102
+ unsupported_market_reject,
103
+ looks_like_otc_ticker,
104
+ LS_OVERSEAS_DESK_PHONE,
105
+ OVERSEAS_STOCK_ORDER_MARKET_CODES,
106
+ UNSUPPORTED_MARKET_RSP_CD,
102
107
  ORDER_ACCEPTED_RSP_CDS,
103
108
  OVERSEAS_STOCK_REJECT_CODES,
104
109
  OVERSEAS_FUTURES_REJECT_CODES,
@@ -196,6 +201,11 @@ __all__ = [
196
201
  "EmptyOrderReason",
197
202
  "map_reject_code",
198
203
  "diagnose_missing_order_no",
204
+ "unsupported_market_reject",
205
+ "looks_like_otc_ticker",
206
+ "LS_OVERSEAS_DESK_PHONE",
207
+ "OVERSEAS_STOCK_ORDER_MARKET_CODES",
208
+ "UNSUPPORTED_MARKET_RSP_CD",
199
209
  "ORDER_ACCEPTED_RSP_CDS",
200
210
  "OVERSEAS_STOCK_REJECT_CODES",
201
211
  "OVERSEAS_FUTURES_REJECT_CODES",
@@ -124,6 +124,12 @@ class EmptyOrderReason(str, Enum):
124
124
  (fractional balances measured on a real LS account, prod 2026-08-24)."""
125
125
 
126
126
 
127
+ LS_OVERSEAS_DESK_PHONE = "02-3779-8888"
128
+ """LS Securities overseas-stock desk. An OTC holding can only be sold by calling this
129
+ number: LS finds a local broker to take the shares during US regular hours
130
+ (confirmed by LS support, relayed by the account owner 2026-09-14)."""
131
+
132
+
127
133
  # ---------------------------------------------------------------------------
128
134
  # Market-specific reject-code tables.
129
135
  #
@@ -160,10 +166,26 @@ OVERSEAS_STOCK_REJECT_CODES: Dict[str, Dict[str, str]] = {
160
166
  "the number may be wrong or the order may already be gone."
161
167
  ),
162
168
  },
169
+ # Live-verified 2026-09-14 on a real LS overseas-stock account. A held OTC
170
+ # issue (ZOMDF) was sent as a limit sell under BOTH accepted market codes —
171
+ # "82" (NASDAQ) and "81" (NYSE/AMEX) — and each came back 03053. LS support
172
+ # confirmed the reason by phone: **OTC issues cannot be traded through the
173
+ # API at all; they must be ordered by calling LS Securities.** So this code
174
+ # is not only "typo in the ticker" — for a symbol the account actually holds,
175
+ # it means the symbol is not tradable on either supported market.
163
176
  "03053": {
164
177
  "retry": RetryAdvice.DO_NOT_RETRY.value,
165
- "cause": "The broker does not recognize the symbol code.",
166
- "tip": "Check the symbol spelling and that it is listed on the given exchange.",
178
+ "cause": (
179
+ "The broker does not recognize the symbol on the market the order was sent to. "
180
+ "For a symbol the account already holds, this means it is not listed on either "
181
+ "market the order API accepts (NYSE/AMEX or NASDAQ) — an OTC issue, for example."
182
+ ),
183
+ "tip": (
184
+ "If the account does not hold this symbol, check the ticker spelling and its exchange. "
185
+ "If the account does hold it, the symbol is most likely OTC and the API cannot trade it "
186
+ f"at all — call the LS Securities overseas desk at {LS_OVERSEAS_DESK_PHONE} and ask them "
187
+ "to sell it; LS finds a local broker during US regular hours."
188
+ ),
167
189
  },
168
190
  "03759": {
169
191
  "retry": RetryAdvice.DO_NOT_RETRY.value,
@@ -211,6 +233,66 @@ KOREA_STOCK_REJECT_CODES: Dict[str, Dict[str, str]] = {
211
233
  },
212
234
  }
213
235
 
236
+ # Markets the LS overseas-stock order TRs accept. COSAT00301's ``OrdMktCode`` and
237
+ # g3101's ``exchcd`` are both ``Literal["81", "82"]`` — there is no third value to
238
+ # try, so a holding on any other market cannot be ordered through this API.
239
+ OVERSEAS_STOCK_ORDER_MARKET_CODES: Dict[str, str] = {
240
+ "NYSE": "81", "AMEX": "81", "NASDAQ": "82", "81": "81", "82": "82",
241
+ }
242
+
243
+ UNSUPPORTED_MARKET_RSP_CD = "PG_UNSUPPORTED_MARKET"
244
+ """Sentinel used in place of a broker response code.
245
+
246
+ This rejection is raised by us **before** any request reaches LS, so there is no
247
+ ``rsp_cd`` to report. The sentinel keeps the field populated (never a bare "")
248
+ and is prefixed ``PG_`` so a consumer can tell at a glance that it did not come
249
+ from the broker.
250
+ """
251
+
252
+
253
+ def looks_like_otc_ticker(symbol: object) -> bool:
254
+ """Heuristic from LS support: a 5-letter ticker ending in ``F`` usually means the
255
+ issue has moved to OTC (e.g. ``ZOMDF``).
256
+
257
+ ⚠️ LS explicitly said this is **not always true**, so it must never gate an order.
258
+ Use it only to make a message more helpful — the authoritative signal is the market
259
+ code, which the order API rejects on its own.
260
+ """
261
+ s = str(symbol or "").strip().upper()
262
+ return len(s) == 5 and s.endswith("F") and s.isalpha()
263
+
264
+
265
+ def unsupported_market_reject(exchange: object, symbol: str = "") -> OrderRejectInfo:
266
+ """Diagnostic for a symbol whose market the order API cannot address.
267
+
268
+ Live-verified 2026-09-14: a held OTC issue is reported by the balance TR with
269
+ a market code outside {81, 82}, and both accepted codes are rejected with
270
+ ``03053``. Blocking before the request keeps the real cause visible — sending
271
+ a substituted code instead makes the broker answer "해당 종목번호가 없습니다",
272
+ which reads like a mistyped ticker and hides that the symbol is untradable.
273
+ """
274
+ shown = str(exchange) if exchange not in (None, "") else "unknown"
275
+ otc_hint = (
276
+ " The ticker is 5 letters ending in F, which usually means the issue moved to OTC."
277
+ if looks_like_otc_ticker(symbol) else ""
278
+ )
279
+ return OrderRejectInfo(
280
+ rsp_cd=UNSUPPORTED_MARKET_RSP_CD,
281
+ cause=(
282
+ f"This symbol's market ({shown}) is not one the order API can address — "
283
+ f"it accepts NYSE/AMEX and NASDAQ only. OTC issues arrive this way.{otc_hint}"
284
+ ),
285
+ tip=(
286
+ "ProgramGarden cannot place this order. Call the LS Securities overseas desk at "
287
+ f"{LS_OVERSEAS_DESK_PHONE} and ask them to sell it — LS finds a local broker to "
288
+ "take the shares during US regular hours. Automation cannot reach this symbol."
289
+ ),
290
+ raw_msg="",
291
+ retry=RetryAdvice.DO_NOT_RETRY,
292
+ known=True,
293
+ )
294
+
295
+
214
296
  _MARKET_TABLES: Dict[str, Dict[str, Dict[str, str]]] = {
215
297
  "overseas_stock": OVERSEAS_STOCK_REJECT_CODES,
216
298
  "overseas_futures": OVERSEAS_FUTURES_REJECT_CODES,
@@ -367,7 +367,7 @@ class OverseasStockNewOrderNode(BaseOrderNode):
367
367
  ],
368
368
  "typical_scenarios": [
369
369
  "ConditionNode.result → PositionSizingNode → OverseasStockNewOrderNode (signal-driven buy)",
370
- "OverseasStockAccountNode.held_symbols → ConditionNode (stop-loss) → OverseasStockNewOrderNode (sell)",
370
+ "OverseasStockAccountNode.positions → ConditionNode (stop-loss) → SymbolFilterNode (intersection with positions) → OverseasStockNewOrderNode (sell, limit, price={{ item.current_price }})",
371
371
  "ScreenerNode.symbols → SplitNode → PositionSizingNode → OverseasStockNewOrderNode (basket order)",
372
372
  ],
373
373
  }
@@ -377,6 +377,8 @@ class OverseasStockNewOrderNode(BaseOrderNode):
377
377
  "Supports extended price_type options: LOO, LOC, MOO, MOC for open/close auction orders",
378
378
  "is_tool_enabled=True — AI Agent can call this node as a tool to place orders autonomously",
379
379
  "retry is disabled by default (resilience.retry.enabled=False); only pure network-connection failures may be retried, never a submitted order",
380
+ "US daytime (Blue Ocean) session accepts LIMIT orders only — a market order is refused at intake with rsp_cd=00891 and no order number. A workflow scheduled across both the daytime and the regular overnight session must therefore use order_type='limit' for it to work in both.",
381
+ "A limit order needs a price. Buy orders fall back to a current-price lookup, and so do sell orders since engine 1.37.3, but that lookup (g3101) only accepts exchange codes 81/82 — it fails for a holding listed on any other market. Prefer feeding the price the account balance already carries: price='{{ item.current_price }}'.",
380
382
  ]
381
383
  _anti_patterns: ClassVar[List[Dict[str, str]]] = [
382
384
  {
@@ -394,6 +396,16 @@ class OverseasStockNewOrderNode(BaseOrderNode):
394
396
  "reason": "Every tick would trigger an order attempt; rate-limit rules block this and raise a connection error at validation time.",
395
397
  "alternative": "Insert a ThrottleNode between the realtime source and the order node to control firing rate.",
396
398
  },
399
+ {
400
+ "pattern": "order_type='market' on a stop-loss / take-profit sell that can run during US daytime (Blue Ocean) hours",
401
+ "reason": "That session refuses market orders at intake (rsp_cd=00891, empty order number), so the stop-loss silently never executes while the schedule covers daytime hours.",
402
+ "alternative": "Use order_type='limit' and supply the price from the account snapshot: price='{{ item.current_price }}'. Note a limit order is not guaranteed to fill.",
403
+ },
404
+ {
405
+ "pattern": "Wiring an order node to ConditionNode.symbols or AccountNode.held_symbols for a close/sell order",
406
+ "reason": "`symbols` lists every evaluated symbol, not the ones that passed, and neither port carries `quantity` or `current_price` — the order node cannot build an order and skips the item.",
407
+ "alternative": "Use `passed_symbols` for the pass list, and to recover quantity and price put a SymbolFilterNode (operation='intersection', input_a='{{ nodes.account.positions }}', input_b='{{ nodes.cond.passed_symbols }}') in between — intersection preserves input_a's full position dicts.",
408
+ },
397
409
  {
398
410
  "pattern": "Using a dict-keyed positions object {symbol: {...}} from a RealAccountNode output as order input",
399
411
  "reason": "All position outputs in ProgramGarden are list[dict] with `symbol`/`exchange` keys. Dict-keyed access will fail at runtime.",
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden-core"
8
- version = "1.28.0"
8
+ version = "1.28.2"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden Core - 노드 기반 DSL 핵심 타입 정의"
11
11
  readme = "README.md"