programgarden 2.6.4__tar.gz → 2.6.5__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 (71) hide show
  1. {programgarden-2.6.4 → programgarden-2.6.5}/PKG-INFO +5 -1
  2. {programgarden-2.6.4 → programgarden-2.6.5}/README.md +4 -0
  3. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/context.py +3 -0
  4. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/executor.py +31 -4
  5. programgarden-2.6.5/programgarden/futures_exchange.py +102 -0
  6. {programgarden-2.6.4 → programgarden-2.6.5}/pyproject.toml +1 -1
  7. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/__init__.py +0 -0
  8. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/binding_validator.py +0 -0
  9. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/client.py +0 -0
  10. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/code_worker.py +0 -0
  11. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/__init__.py +0 -0
  12. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/adjustment_delivery.py +0 -0
  13. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/broker_evidence.py +0 -0
  14. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/broker_order_totals.py +0 -0
  15. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/broker_snapshot.py +0 -0
  16. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/checkpoint_manager.py +0 -0
  17. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/db_naming.py +0 -0
  18. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/execution_storage.py +0 -0
  19. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/fill_reconciler.py +0 -0
  20. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/order_cancellation.py +0 -0
  21. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/order_recovery.py +0 -0
  22. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/owned_order_cancellation.py +0 -0
  23. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/position_reconciliation.py +0 -0
  24. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/query_builder.py +0 -0
  25. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/sqlite_scope.py +0 -0
  26. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/workflow_position_tracker.py +0 -0
  27. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/database/workflow_risk_tracker.py +0 -0
  28. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/deep_fixtures.py +0 -0
  29. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/futures_contract_info.py +0 -0
  30. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/futures_history.py +0 -0
  31. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/futures_orderable.py +0 -0
  32. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/futures_pnl.py +0 -0
  33. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/futures_read_evidence.py +0 -0
  34. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/incremental_build.py +0 -0
  35. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/managed_order_control.py +0 -0
  36. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/node_runner.py +0 -0
  37. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/order_lifecycle.py +0 -0
  38. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/paper_futures_business_date.py +0 -0
  39. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/plugin/__init__.py +0 -0
  40. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/plugin/sandbox.py +0 -0
  41. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/providers/__init__.py +0 -0
  42. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/providers/llm_errors.py +0 -0
  43. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/providers/llm_provider.py +0 -0
  44. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/reconnect_handler.py +0 -0
  45. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_contracts.py +0 -0
  46. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_events.py +0 -0
  47. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_external.py +0 -0
  48. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_order_adapter.py +0 -0
  49. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_orders.py +0 -0
  50. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_scenarios.py +0 -0
  51. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_semantics.py +0 -0
  52. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_sources.py +0 -0
  53. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_sqlite.py +0 -0
  54. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/replay_triggers.py +0 -0
  55. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/resolver.py +0 -0
  56. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/resource/__init__.py +0 -0
  57. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/resource/context.py +0 -0
  58. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/resource/limiter.py +0 -0
  59. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/resource/monitor.py +0 -0
  60. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/resource/throttle.py +0 -0
  61. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/semantic_rules.py +0 -0
  62. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/tools/__init__.py +0 -0
  63. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/tools/credential_tools.py +0 -0
  64. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/tools/definition_tools.py +0 -0
  65. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/tools/event_tools.py +0 -0
  66. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/tools/job_tools.py +0 -0
  67. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/tools/registry_tools.py +0 -0
  68. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/tools/sqlite_tools.py +0 -0
  69. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/validation_recommender.py +0 -0
  70. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/validation_replay.py +0 -0
  71. {programgarden-2.6.4 → programgarden-2.6.5}/programgarden/valuation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden
3
- Version: 2.6.4
3
+ Version: 2.6.5
4
4
  Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -31,6 +31,10 @@ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures
31
31
 
32
32
  # ProgramGarden
33
33
 
34
+ The 2.6.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
35
+ reads each futures position's and pending order's exchange from o3105 instead
36
+ of a fixed `HKEX`; an unconfirmed exchange stays empty.
37
+
34
38
  The 2.6.4 release candidate [paper futures account correction](../../docs/paper-futures-business-date.md#account-node-and-rejection-messages-264-candidate)
35
39
  returns an empty futures account for the paper empty-account answer and names
36
40
  the broker message in unaccepted response errors. Live behavior is unchanged.
@@ -2,6 +2,10 @@ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures
2
2
 
3
3
  # ProgramGarden
4
4
 
5
+ The 2.6.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
6
+ reads each futures position's and pending order's exchange from o3105 instead
7
+ of a fixed `HKEX`; an unconfirmed exchange stays empty.
8
+
5
9
  The 2.6.4 release candidate [paper futures account correction](../../docs/paper-futures-business-date.md#account-node-and-rejection-messages-264-candidate)
6
10
  returns an empty futures account for the paper empty-account answer and names
7
11
  the broker message in unaccepted response errors. Live behavior is unchanged.
@@ -285,6 +285,9 @@ class ExecutionContext:
285
285
  # Tracks workflow positions separately using FIFO for competition ranking
286
286
  self._workflow_position_tracker: Optional[Any] = None # WorkflowPositionTracker (lazy init)
287
287
  self._personal_metrics_cache: Optional[Tuple[tuple, float, dict]] = None
288
+ # Per-run o3105 exchange answers for futures account/pending rows
289
+ # (programgarden.futures_exchange creates it on first use).
290
+ self._futures_exchange_cache: Optional[Any] = None
288
291
  self._workflow_risk_tracker: Optional[Any] = None # WorkflowRiskTracker (lazy init)
289
292
  self._workflow_broker_node_id: Optional[str] = None # BrokerNode ID for PnL refresh
290
293
  self._workflow_product: str = "overseas_stock" # Product type for PnL refresh
@@ -6141,6 +6141,7 @@ class AccountNodeExecutor(NodeExecutorBase):
6141
6141
  """
6142
6142
  from datetime import datetime
6143
6143
  from zoneinfo import ZoneInfo
6144
+ from programgarden.futures_exchange import resolve_contract_exchanges
6144
6145
  from programgarden.futures_read_evidence import require_complete_rows, require_position_identity
6145
6146
  from programgarden.paper_futures_business_date import is_paper_client
6146
6147
 
@@ -6166,7 +6167,6 @@ class AccountNodeExecutor(NodeExecutorBase):
6166
6167
 
6167
6168
  # block2 = 종목별 잔고 (list 형태로 변환, NewOrderNode 호환)
6168
6169
  positions = []
6169
- held_symbols = []
6170
6170
  for item in position_rows:
6171
6171
  require_position_identity(item)
6172
6172
  symbol = item.IsuCodeVal.strip() if item.IsuCodeVal else ""
@@ -6187,7 +6187,9 @@ class AccountNodeExecutor(NodeExecutorBase):
6187
6187
 
6188
6188
  positions.append({
6189
6189
  "symbol": symbol,
6190
- "exchange": "HKEX", # 해외선물 기본 거래소
6190
+ # CIDBQ01500 carries no exchange. Filled below from o3105
6191
+ # ExchCd; None when that evidence is unavailable.
6192
+ "exchange": None,
6191
6193
  "name": item.IsuNm.strip() if hasattr(item, 'IsuNm') and item.IsuNm else symbol,
6192
6194
  "direction": position_side, # long/short
6193
6195
  "close_side": close_side, # 청산 시 주문 방향: sell/buy
@@ -6213,7 +6215,6 @@ class AccountNodeExecutor(NodeExecutorBase):
6213
6215
  "pnl_rate": pnl_rate, # 명목가 대비 수익률(%) — StopLoss/ProfitTarget 소비
6214
6216
  "currency": item.CrcyCodeVal.strip() if item.CrcyCodeVal else "",
6215
6217
  })
6216
- held_symbols.append({"exchange": "HKEX", "symbol": symbol})
6217
6218
 
6218
6219
  # 2. CIDBQ05300: 예탁자산 조회 (CIDBQ03000 상위호환).
6219
6220
  # Wrapped in its own try so a balance failure no longer drops
@@ -6288,6 +6289,15 @@ class AccountNodeExecutor(NodeExecutorBase):
6288
6289
  balance_info["_failure_codes"] = ["CIDBQ05300"]
6289
6290
  balance_info["_failure_reason"] = cidbq05300_failure_reason or "CIDBQ05300 fetch failed"
6290
6291
 
6292
+ # 3. o3105: the exchange of each held contract (at most one read per
6293
+ # symbol per run; unresolved stays None, never a default exchange).
6294
+ exchanges = await resolve_contract_exchanges(
6295
+ ls, [row["symbol"] for row in positions], context, node_id,
6296
+ )
6297
+ for row in positions:
6298
+ row["exchange"] = exchanges.get(row["symbol"])
6299
+ held_symbols = [{"exchange": row["exchange"], "symbol": row["symbol"]} for row in positions]
6300
+
6291
6301
  context.log("info", f"AccountNode (futures): {len(positions)} positions, {len(balance_by_currency)} currencies", node_id)
6292
6302
  return {
6293
6303
  "held_symbols": held_symbols,
@@ -6663,6 +6673,7 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
6663
6673
  """해외선물 미체결 조회 (CIDBQ02400)"""
6664
6674
  from datetime import datetime
6665
6675
  from zoneinfo import ZoneInfo
6676
+ from programgarden.futures_exchange import resolve_contract_exchanges
6666
6677
  from programgarden.futures_read_evidence import require_complete_rows, require_pending_identity
6667
6678
  from programgarden.paper_futures_business_date import is_paper_client, read_current_business_date
6668
6679
  from programgarden.database.broker_evidence import StartupEvidenceUnavailable
@@ -6709,7 +6720,9 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
6709
6720
 
6710
6721
  open_orders.append({
6711
6722
  "order_id": order_id,
6712
- "exchange": "HKEX", # 해외선물 기본
6723
+ # CIDBQ02400 carries no exchange. Filled below from o3105
6724
+ # ExchCd; None when that evidence is unavailable.
6725
+ "exchange": None,
6713
6726
  "symbol": item.IsuCodeVal.strip() if hasattr(item, 'IsuCodeVal') and item.IsuCodeVal else "",
6714
6727
  "name": item.IsuNm.strip() if hasattr(item, 'IsuNm') and item.IsuNm else "",
6715
6728
  "side": side,
@@ -6721,6 +6734,12 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
6721
6734
  "order_time": item.OrdSndDttm if hasattr(item, 'OrdSndDttm') else "",
6722
6735
  })
6723
6736
 
6737
+ exchanges = await resolve_contract_exchanges(
6738
+ ls, [row["symbol"] for row in open_orders], context, node_id,
6739
+ )
6740
+ for row in open_orders:
6741
+ row["exchange"] = exchanges.get(row["symbol"])
6742
+
6724
6743
  context.log("info", f"OpenOrdersNode (futures): {len(open_orders)} open orders", node_id)
6725
6744
  return {
6726
6745
  "open_orders": open_orders,
@@ -16927,6 +16946,14 @@ class NewOrderNodeExecutor(NodeExecutorBase):
16927
16946
  from datetime import date
16928
16947
  from decimal import Decimal
16929
16948
 
16949
+ if not isinstance(order.get("exchange"), str):
16950
+ # A futures position or pending row whose exchange o3105 could not
16951
+ # confirm carries exchange None. Stop before any lifecycle entry is
16952
+ # recorded, so nothing is sent and a later retry is not blocked.
16953
+ raise ValueError(
16954
+ "Futures order exchange is unavailable (exchange=None, e.g. a position "
16955
+ "whose exchange o3105 could not confirm); no order was sent"
16956
+ )
16930
16957
  cycle = getattr(context._workflow_job, "_order_cycle", 0)
16931
16958
  iteration = iteration_index
16932
16959
  if iteration is None:
@@ -0,0 +1,102 @@
1
+ """Listing exchange of the overseas-futures contracts an account reports.
2
+
3
+ CIDBQ01500 (open positions) and CIDBQ02400 (pending orders) name a contract
4
+ but carry no exchange or market field. The exchange code is read from o3105
5
+ ``ExchCd``, the contract-detail TR that FuturesContractInfoNode already uses,
6
+ under the same identity rules as ``futures_contract_info.read_contract_info``:
7
+ HTTP 200, no error, ``rsp_cd`` 00000, an echoed ``Symbol`` equal to the
8
+ requested contract and a non-empty ``ExchCd``. Anything else leaves the
9
+ exchange unknown (``None``). No default exchange is ever substituted.
10
+
11
+ Each execution context (one workflow run) asks o3105 at most once per symbol.
12
+ The answer is cached on the context, including an unknown answer. Reads are
13
+ serialized and spaced like the startup broker reads so the shared app key is
14
+ not burst. Request fields follow
15
+ ``finance/example/overseas_futureoption/run_o3105.py`` (symbol only).
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ from typing import Any, Dict, Iterable, Optional
21
+
22
+ # Same gap the startup broker reads keep between consecutive TRs on one app key
23
+ # (database/broker_snapshot.py). o3105 is limited to 2 requests per second.
24
+ READ_SPACING_SECONDS = 2.0
25
+ READ_TIMEOUT_SECONDS = 20.0
26
+ SOURCE_TR = "o3105"
27
+
28
+ _sleep = asyncio.sleep
29
+
30
+
31
+ class _RunExchangeCache:
32
+ """Per-run symbol -> exchange answers; ``None`` records an unresolved read."""
33
+
34
+ __slots__ = ("exchanges", "lock")
35
+
36
+ def __init__(self) -> None:
37
+ self.exchanges: Dict[str, Optional[str]] = {}
38
+ self.lock = asyncio.Lock()
39
+
40
+
41
+ def _run_cache(context: Any) -> _RunExchangeCache:
42
+ cache = getattr(context, "_futures_exchange_cache", None)
43
+ if not isinstance(cache, _RunExchangeCache):
44
+ cache = _RunExchangeCache()
45
+ setattr(context, "_futures_exchange_cache", cache)
46
+ return cache
47
+
48
+
49
+ def exchange_from_contract_detail(response: Any, symbol: str) -> Optional[str]:
50
+ """Return o3105 ``ExchCd`` for ``symbol``, or ``None`` without exact evidence."""
51
+ if (
52
+ getattr(response, "status_code", None) != 200
53
+ or getattr(response, "error_msg", None)
54
+ or getattr(response, "rsp_cd", None) != "00000"
55
+ ):
56
+ return None
57
+ block = getattr(response, "block", None)
58
+ fields = getattr(block, "model_fields_set", None)
59
+ if block is None or not isinstance(fields, (set, frozenset)) or not {"Symbol", "ExchCd"} <= fields:
60
+ return None
61
+ echoed, exchange = getattr(block, "Symbol", None), getattr(block, "ExchCd", None)
62
+ if not isinstance(echoed, str) or echoed.strip() != symbol or not isinstance(exchange, str):
63
+ return None
64
+ return exchange.strip() or None
65
+
66
+
67
+ async def _read_exchange(ls: Any, symbol: str) -> Optional[str]:
68
+ from programgarden_finance.ls.overseas_futureoption.market.o3105.blocks import O3105InBlock
69
+
70
+ try:
71
+ response = await asyncio.wait_for(
72
+ ls.overseas_futureoption().market().o3105(body=O3105InBlock(symbol=symbol)).req_async(),
73
+ timeout=READ_TIMEOUT_SECONDS,
74
+ )
75
+ except Exception: # noqa: BLE001 - an unavailable read leaves the exchange unknown
76
+ return None
77
+ return exchange_from_contract_detail(response, symbol)
78
+
79
+
80
+ async def resolve_contract_exchanges(
81
+ ls: Any, symbols: Iterable[str], context: Any, node_id: str,
82
+ ) -> Dict[str, Optional[str]]:
83
+ """Map each distinct contract symbol to its o3105 exchange code or ``None``."""
84
+ wanted = [symbol for symbol in dict.fromkeys(symbols) if isinstance(symbol, str) and symbol]
85
+ if not wanted:
86
+ return {}
87
+ cache = _run_cache(context)
88
+ async with cache.lock:
89
+ for symbol in wanted:
90
+ if symbol in cache.exchanges:
91
+ continue
92
+ await _sleep(READ_SPACING_SECONDS)
93
+ exchange = await _read_exchange(ls, symbol)
94
+ cache.exchanges[symbol] = exchange
95
+ if exchange is None:
96
+ context.log(
97
+ "warning",
98
+ f"Futures exchange for {symbol} is unavailable ({SOURCE_TR} gave no matching "
99
+ "exchange code); the row's exchange is left empty for this run",
100
+ node_id,
101
+ )
102
+ return {symbol: cache.exchanges[symbol] for symbol in wanted}
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden"
8
- version = "2.6.4"
8
+ version = "2.6.5"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
11
11
  readme = "README.md"