programgarden 2.7.2__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {programgarden-2.7.2 → programgarden-3.0.0}/PKG-INFO +5 -134
  2. {programgarden-2.7.2 → programgarden-3.0.0}/README.md +1 -130
  3. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/__init__.py +5 -0
  4. programgarden-3.0.0/programgarden/_build_info.py +3 -0
  5. programgarden-3.0.0/programgarden/code_worker.py +3 -0
  6. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/context.py +13 -4
  7. programgarden-3.0.0/programgarden/database/broker_evidence.py +52 -0
  8. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/broker_snapshot.py +9 -2
  9. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/position_reconciliation.py +82 -21
  10. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/workflow_position_tracker.py +2 -2
  11. programgarden-3.0.0/programgarden/deep_fixtures.py +46 -0
  12. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/executor.py +410 -70
  13. programgarden-3.0.0/programgarden/paper_futures_business_date.py +355 -0
  14. programgarden-3.0.0/programgarden/replay_contracts.py +7 -0
  15. programgarden-3.0.0/programgarden/replay_semantics.py +3 -0
  16. programgarden-3.0.0/programgarden/replay_triggers.py +3 -0
  17. programgarden-3.0.0/programgarden/semantic_rules.py +12 -0
  18. {programgarden-2.7.2 → programgarden-3.0.0}/pyproject.toml +4 -4
  19. programgarden-2.7.2/programgarden/code_worker.py +0 -499
  20. programgarden-2.7.2/programgarden/database/broker_evidence.py +0 -189
  21. programgarden-2.7.2/programgarden/deep_fixtures.py +0 -561
  22. programgarden-2.7.2/programgarden/incremental_build.py +0 -430
  23. programgarden-2.7.2/programgarden/paper_futures_business_date.py +0 -151
  24. programgarden-2.7.2/programgarden/replay_contracts.py +0 -261
  25. programgarden-2.7.2/programgarden/replay_events.py +0 -196
  26. programgarden-2.7.2/programgarden/replay_external.py +0 -279
  27. programgarden-2.7.2/programgarden/replay_order_adapter.py +0 -291
  28. programgarden-2.7.2/programgarden/replay_orders.py +0 -359
  29. programgarden-2.7.2/programgarden/replay_responders.py +0 -148
  30. programgarden-2.7.2/programgarden/replay_scenarios.py +0 -127
  31. programgarden-2.7.2/programgarden/replay_semantics.py +0 -674
  32. programgarden-2.7.2/programgarden/replay_sources.py +0 -161
  33. programgarden-2.7.2/programgarden/replay_sqlite.py +0 -53
  34. programgarden-2.7.2/programgarden/replay_tape.py +0 -182
  35. programgarden-2.7.2/programgarden/replay_triggers.py +0 -76
  36. programgarden-2.7.2/programgarden/semantic_rules.py +0 -501
  37. programgarden-2.7.2/programgarden/validation_replay.py +0 -492
  38. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/binding_provenance.py +0 -0
  39. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/binding_validator.py +0 -0
  40. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/client.py +0 -0
  41. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/__init__.py +0 -0
  42. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/adjustment_delivery.py +0 -0
  43. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/broker_order_totals.py +0 -0
  44. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/checkpoint_manager.py +0 -0
  45. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/db_naming.py +0 -0
  46. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/execution_storage.py +0 -0
  47. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/fill_reconciler.py +0 -0
  48. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/order_cancellation.py +0 -0
  49. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/order_recovery.py +0 -0
  50. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/owned_order_cancellation.py +0 -0
  51. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/query_builder.py +0 -0
  52. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/sqlite_scope.py +0 -0
  53. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/workflow_risk_tracker.py +0 -0
  54. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_contract_info.py +0 -0
  55. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_exchange.py +0 -0
  56. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_history.py +0 -0
  57. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_orderable.py +0 -0
  58. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_pnl.py +0 -0
  59. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_read_evidence.py +0 -0
  60. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/managed_order_control.py +0 -0
  61. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/node_runner.py +0 -0
  62. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/order_lifecycle.py +0 -0
  63. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/plugin/__init__.py +0 -0
  64. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/plugin/sandbox.py +0 -0
  65. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/providers/__init__.py +0 -0
  66. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/providers/llm_errors.py +0 -0
  67. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/providers/llm_provider.py +0 -0
  68. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/ranking_nodes.py +0 -0
  69. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/reconnect_handler.py +0 -0
  70. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resolver.py +0 -0
  71. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/__init__.py +0 -0
  72. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/context.py +0 -0
  73. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/limiter.py +0 -0
  74. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/monitor.py +0 -0
  75. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/throttle.py +0 -0
  76. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/__init__.py +0 -0
  77. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/credential_tools.py +0 -0
  78. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/definition_tools.py +0 -0
  79. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/event_tools.py +0 -0
  80. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/job_tools.py +0 -0
  81. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/registry_tools.py +0 -0
  82. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/sqlite_tools.py +0 -0
  83. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/validation_recommender.py +0 -0
  84. {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/valuation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden
3
- Version: 2.7.2
3
+ Version: 3.0.0
4
4
  Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -15,9 +15,9 @@ Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
15
15
  Requires-Dist: croniter (>=6.0.0,<7.0.0)
16
16
  Requires-Dist: litellm (>=1.40.0)
17
17
  Requires-Dist: lxml (>=6.0.2,<7.0.0)
18
- Requires-Dist: programgarden-community (>=2.3.2,<3.0.0)
19
- Requires-Dist: programgarden-core (>=2.7.0,<3.0.0)
20
- Requires-Dist: programgarden-finance (>=2.0.6,<3.0.0)
18
+ Requires-Dist: programgarden-community (==3.0.0)
19
+ Requires-Dist: programgarden-core (==3.0.0)
20
+ Requires-Dist: programgarden-finance (==3.0.0)
21
21
  Requires-Dist: psutil (>=6.0.0,<7.0.0)
22
22
  Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
23
23
  Requires-Dist: pydantic (>=2.0.1,<3.0.0)
@@ -27,58 +27,8 @@ Requires-Dist: tzdata (>=2025.2,<2026.0)
27
27
  Requires-Dist: yfinance (>=0.2.0,<0.3.0)
28
28
  Description-Content-Type: text/markdown
29
29
 
30
- The 2.6.2 release candidate [futures minute alias correction](../../docs/futures-minute-alias-contract.md) aligns canonical minute periods with the existing one-page o3103 path and rejects unsupported hourly/adjustment claims. Complete-range coverage and consumer adoption are not implied.
31
-
32
30
  # ProgramGarden
33
31
 
34
- The 2.6.7 release [futures TC2 confirmation handling](../../docs/release-2.6.7-validation.md)
35
- requires programgarden-finance 2.0.5, which reads overseas futures modify and
36
- cancel confirmations (TC2) as LS sends them and re-registers account events
37
- after a reconnect. The engine finds the workflow's order through
38
- `orgn_ordr_no`, uses the guide's side codes and skips frames the SDK could
39
- not read.
40
-
41
- The 2.6.6 release candidate [ranking nodes and runtime-values-as-data correction](../../docs/release-2.6.6-validation.md)
42
- adds KoreaStockRankingNode and OverseasStockRankingNode, evaluates only
43
- expression templates authored in the workflow, routes Gemini and DeepSeek
44
- credentials to their own APIs, and reads an unresolved futures position
45
- exchange again at most once per 5 minutes per contract instead of keeping it
46
- empty for the whole run.
47
-
48
- The 2.6.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
49
- reads each futures position's and pending order's exchange from o3105 instead
50
- of a fixed `HKEX`; an unconfirmed exchange stays empty.
51
-
52
- The 2.6.4 release candidate [paper futures account correction](../../docs/paper-futures-business-date.md#account-node-and-rejection-messages-264-candidate)
53
- returns an empty futures account for the paper empty-account answer and names
54
- the broker message in unaccepted response errors. Live behavior is unchanged.
55
-
56
- The 2.6.3 release candidate [paper futures business-date correction](../../docs/paper-futures-business-date.md)
57
- uses a fresh account-scoped broker date for startup pending orders, OpenOrdersNode
58
- and legacy fill observation. Missing or mismatched evidence remains unavailable.
59
-
60
- An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
61
- rejects unsupported intraday periods instead of silently querying daily bars.
62
- Supported g3204 periods retain their existing requests and output contract.
63
-
64
- An unreleased [futures detail and durable state candidate](../../docs/futures-contract-details-and-rollover-state.md)
65
- adds exact o3105 metadata, opt-in execution-scoped SQLite and XHKG calendar
66
- intersection. Missing evidence remains explicit; this does not authorize orders.
67
-
68
- An unreleased [offline temporal replay extension](../../docs/offline-temporal-replay.md)
69
- supports explicit bounded multi-day acceptance without changing the default
70
- 32-event worker/save path. Public package release remains pending; the server
71
- team has validated a private DEV candidate with the same runtime changes.
72
-
73
- The REST overseas-stock account output preserves broker-reported `sellable_qty`
74
- separately from holdings; absent capacity remains null. See
75
- [the sell-capacity contract](../../docs/overseas-stock-sellable-quantity.md).
76
-
77
- Unreleased execution reconciliation work is documented in
78
- [execution-reconciliation.md](../../docs/execution-reconciliation.md). Its
79
- optional host storage identity is not yet enabled in deployed worker/tray hosts;
80
- historical recovery policy and host migration remain open.
81
-
82
32
  ProgramGarden은 AI 시대에 맞춰 파이썬을 모르는 투자자도 개인화된 시스템 트레이딩을 자동으로 수행할 수 있게 돕는 노드 기반 자동매매 DSL(Domain Specific Language) 오픈소스입니다.
83
33
 
84
34
  노드를 조합하여 워크플로우를 정의하고, 실행 엔진이 이를 자동으로 처리합니다. LS증권 OpenAPI를 메인으로 해외 주식/선물 거래 자동화를 지원합니다.
@@ -155,14 +105,6 @@ pending count and cancels those tasks, allowing another 0.25 seconds for cancell
155
105
  A listener that suppresses cancellation is reported and remains tracked; it cannot
156
106
  hold job shutdown indefinitely. Delivery beyond this bounded grace is not guaranteed.
157
107
 
158
- Overseas-stock OpenOrdersNode treats the observed terminal COSAQ00102 `02679`
159
- empty envelope as `open_orders=[]` / `count=0` without an error. The echoed date,
160
- market and filters must match its current-day pending query, the empty detail
161
- array must be explicitly present, and continuation must be terminal. This follows
162
- the finance package's observed-response reference and StockAccountTracker;
163
- `02679` is not a generic success code. Missing, malformed, mismatched and failed
164
- reads retain `error` / `reason=fetch_failed` so workflows can block new entries.
165
-
166
108
  Overseas-stock historical queries preserve the exchange supplied by each symbol,
167
109
  including when the account has no holdings. Item-bound consumers of an explicitly
168
110
  empty selected upstream list are skipped before expression evaluation; this also
@@ -185,7 +127,7 @@ the catalog's `result` port as a list of outcome rows: `{{ nodes.order.result }}
185
127
  can feed TableDisplayNode directly. Each row copies the final `order_result`
186
128
  and includes `order_id` from the existing top-level or nested order number.
187
129
  Legacy `order_result` and `order_id` outputs are unchanged. The projection occurs
188
- after fill confirmation, fractional-remainder annotation, and replay marking;
130
+ after fill confirmation, and fractional-remainder annotation;
189
131
  an accepted order is not promoted to a fill without confirmation. Rejections and
190
132
  `no_signal` keep their original diagnostics/reason as an outcome row, with no
191
133
  fabricated order number. Dry runs add a `simulated` row while retaining the legacy
@@ -195,73 +137,6 @@ of a new-order result retains its legacy `order_result` row shape; explicit
195
137
  `nodes.order.result` bindings always read the list. Modify/cancel nodes retain their
196
138
  separately declared `modify_result`/`cancel_result` ports and are outside this change.
197
139
 
198
- ## Explicit execution identities in local ledgers
199
-
200
- `WorkflowPositionTracker.record_fill(..., execution_id=...)` can preserve a
201
- broker-provided execution identity. Within one ledger/product/provider/mode,
202
- order date and normalized order number, an identical replay returns the first
203
- classification without changing FIFO or history. Conflicting facts raise
204
- `ExecutionIdentityConflictError`. Positive numeric identifiers ignore padding;
205
- opaque identifiers retain case. Missing, blank and zero identifiers retain legacy
206
- behavior and are never inferred from time, price or quantity. Every pending
207
- partial fill is retained while its order acknowledgement is being recorded.
208
-
209
- The migration is additive and does not invent identities for existing rows.
210
- Callers must establish consistent identifier and timestamp semantics first;
211
- exchange execution numbers and broker execution numbers are not interchangeable.
212
- This ledger API alone does not reconcile finite workflows after their shutdown.
213
-
214
- Standalone futures TC3 callbacks interpret `s_b_ccd="1"` as sell and `"2"` as buy,
215
- matching the SDK contract. TC2 confirmations use the same codes and find the
216
- workflow's order through `orgn_ordr_no`: a futures modify or cancel gets its own
217
- new order number (LS paper, 2026-10-15), so the cancel confirmation cancels the
218
- original order in the ledger and a modify confirmation updates the successor row
219
- recorded by ModifyOrderNodeExecutor. Unknown or blank side codes are logged and skipped
220
- before any inventory write is scheduled. With an app order lifecycle handler,
221
- TC3 still bypasses this legacy fill path: canonical REST reconciliation remains
222
- the sole writer. This side correction does not establish an alias between TC3
223
- and REST execution identities, dates, or times.
224
-
225
- ## Futures monetary evidence
226
-
227
- Futures workflow PnL events preserve native gross estimates separately from
228
- accounting results. `workflow_*`, `other_*`, `total_*`, account and competition
229
- monetary/rate scalars remain null when accounting evidence is unavailable.
230
- `pnl_by_currency` contains gross price-change subtotals by contract currency;
231
- `monetary_positions` retains their basis/status and unmodified, unconfirmed
232
- `broker_pnl_amount`. No fee, FX, margin/equity return or verified contest score
233
- is inferred from these estimates. Actual zero and negative amounts are retained.
234
- `currency` is null for mixed or unavailable currencies. Consumers must handle
235
- nullable monetary fields and must not coerce them to zero.
236
-
237
- ## Personal workflow execution metrics
238
-
239
- `WorkflowPositionTracker.personal_metrics()` reads retained SQLite `trade_history`
240
- for the current product, provider and paper/live mode. Its independent
241
- `WorkflowPnLEvent.personal_metrics` envelope retains stock realized PnL after all
242
- positions close and counts distinct positive executed orders by valid stored
243
- order date and normalized order number. Partial fills of one dated order count
244
- once. This is the reporting runtime's retained workflow ledger, not a complete
245
- account history or verified contest score. No credential/account generation is
246
- claimed by this legacy local storage.
247
-
248
- Stored stock realized PnL is gross long-only FIFO before fees. The reader
249
- validates sufficient recorded buys and consistency with stored results; mixed
250
- manual/product/provider/exchange FIFO ownership makes the affected symbol
251
- unavailable. Amounts remain separate by symbol/exchange, with currency null
252
- because the historical table has no currency evidence. No FX conversion or return
253
- denominator is invented. Futures executed-order counts are available under the
254
- same identity rules, but futures monetary results and portfolio MDD remain null:
255
- the retained FIFO lacks contract accounting and risk price windows are not equity
256
- curves. Invalid order identities also yield a null count with its reason.
257
-
258
- The listener refreshes this evidence at most once per 10 seconds to avoid scanning
259
- retained history on every price tick; reused observations retain their original
260
- `as_of`. The cache is isolated to the actual tracker/product/provider/mode, and
261
- other broker-node callbacks do not receive it. A newly recorded fill may appear
262
- at the next refresh. Existing open-position PnL fields are unchanged. Core and
263
- engine consumers must ship together to support the additive event field.
264
-
265
140
  ## Stock read failures
266
141
 
267
142
  Stock open-order queries include current-day orders (`ThdayBnsAppYn="1"`).
@@ -365,7 +240,3 @@ job = await pg.run_async(
365
240
 
366
241
  자세한 변경 사항은 `CHANGELOG.md`를 참고하세요.
367
242
 
368
- See [Session gates and guarded Split](../../docs/session-gates-and-guarded-split.md)
369
- for immediate timezone decisions, per-item conditional routing, effective sequential
370
- execution of guarded/order branches, and finite CodeNode input bindings.
371
-
@@ -1,55 +1,5 @@
1
- The 2.6.2 release candidate [futures minute alias correction](../../docs/futures-minute-alias-contract.md) aligns canonical minute periods with the existing one-page o3103 path and rejects unsupported hourly/adjustment claims. Complete-range coverage and consumer adoption are not implied.
2
-
3
1
  # ProgramGarden
4
2
 
5
- The 2.6.7 release [futures TC2 confirmation handling](../../docs/release-2.6.7-validation.md)
6
- requires programgarden-finance 2.0.5, which reads overseas futures modify and
7
- cancel confirmations (TC2) as LS sends them and re-registers account events
8
- after a reconnect. The engine finds the workflow's order through
9
- `orgn_ordr_no`, uses the guide's side codes and skips frames the SDK could
10
- not read.
11
-
12
- The 2.6.6 release candidate [ranking nodes and runtime-values-as-data correction](../../docs/release-2.6.6-validation.md)
13
- adds KoreaStockRankingNode and OverseasStockRankingNode, evaluates only
14
- expression templates authored in the workflow, routes Gemini and DeepSeek
15
- credentials to their own APIs, and reads an unresolved futures position
16
- exchange again at most once per 5 minutes per contract instead of keeping it
17
- empty for the whole run.
18
-
19
- The 2.6.5 release candidate [futures position exchange correction](../../docs/futures-position-exchange.md)
20
- reads each futures position's and pending order's exchange from o3105 instead
21
- of a fixed `HKEX`; an unconfirmed exchange stays empty.
22
-
23
- The 2.6.4 release candidate [paper futures account correction](../../docs/paper-futures-business-date.md#account-node-and-rejection-messages-264-candidate)
24
- returns an empty futures account for the paper empty-account answer and names
25
- the broker message in unaccepted response errors. Live behavior is unchanged.
26
-
27
- The 2.6.3 release candidate [paper futures business-date correction](../../docs/paper-futures-business-date.md)
28
- uses a fresh account-scoped broker date for startup pending orders, OpenOrdersNode
29
- and legacy fill observation. Missing or mismatched evidence remains unavailable.
30
-
31
- An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
32
- rejects unsupported intraday periods instead of silently querying daily bars.
33
- Supported g3204 periods retain their existing requests and output contract.
34
-
35
- An unreleased [futures detail and durable state candidate](../../docs/futures-contract-details-and-rollover-state.md)
36
- adds exact o3105 metadata, opt-in execution-scoped SQLite and XHKG calendar
37
- intersection. Missing evidence remains explicit; this does not authorize orders.
38
-
39
- An unreleased [offline temporal replay extension](../../docs/offline-temporal-replay.md)
40
- supports explicit bounded multi-day acceptance without changing the default
41
- 32-event worker/save path. Public package release remains pending; the server
42
- team has validated a private DEV candidate with the same runtime changes.
43
-
44
- The REST overseas-stock account output preserves broker-reported `sellable_qty`
45
- separately from holdings; absent capacity remains null. See
46
- [the sell-capacity contract](../../docs/overseas-stock-sellable-quantity.md).
47
-
48
- Unreleased execution reconciliation work is documented in
49
- [execution-reconciliation.md](../../docs/execution-reconciliation.md). Its
50
- optional host storage identity is not yet enabled in deployed worker/tray hosts;
51
- historical recovery policy and host migration remain open.
52
-
53
3
  ProgramGarden은 AI 시대에 맞춰 파이썬을 모르는 투자자도 개인화된 시스템 트레이딩을 자동으로 수행할 수 있게 돕는 노드 기반 자동매매 DSL(Domain Specific Language) 오픈소스입니다.
54
4
 
55
5
  노드를 조합하여 워크플로우를 정의하고, 실행 엔진이 이를 자동으로 처리합니다. LS증권 OpenAPI를 메인으로 해외 주식/선물 거래 자동화를 지원합니다.
@@ -126,14 +76,6 @@ pending count and cancels those tasks, allowing another 0.25 seconds for cancell
126
76
  A listener that suppresses cancellation is reported and remains tracked; it cannot
127
77
  hold job shutdown indefinitely. Delivery beyond this bounded grace is not guaranteed.
128
78
 
129
- Overseas-stock OpenOrdersNode treats the observed terminal COSAQ00102 `02679`
130
- empty envelope as `open_orders=[]` / `count=0` without an error. The echoed date,
131
- market and filters must match its current-day pending query, the empty detail
132
- array must be explicitly present, and continuation must be terminal. This follows
133
- the finance package's observed-response reference and StockAccountTracker;
134
- `02679` is not a generic success code. Missing, malformed, mismatched and failed
135
- reads retain `error` / `reason=fetch_failed` so workflows can block new entries.
136
-
137
79
  Overseas-stock historical queries preserve the exchange supplied by each symbol,
138
80
  including when the account has no holdings. Item-bound consumers of an explicitly
139
81
  empty selected upstream list are skipped before expression evaluation; this also
@@ -156,7 +98,7 @@ the catalog's `result` port as a list of outcome rows: `{{ nodes.order.result }}
156
98
  can feed TableDisplayNode directly. Each row copies the final `order_result`
157
99
  and includes `order_id` from the existing top-level or nested order number.
158
100
  Legacy `order_result` and `order_id` outputs are unchanged. The projection occurs
159
- after fill confirmation, fractional-remainder annotation, and replay marking;
101
+ after fill confirmation, and fractional-remainder annotation;
160
102
  an accepted order is not promoted to a fill without confirmation. Rejections and
161
103
  `no_signal` keep their original diagnostics/reason as an outcome row, with no
162
104
  fabricated order number. Dry runs add a `simulated` row while retaining the legacy
@@ -166,73 +108,6 @@ of a new-order result retains its legacy `order_result` row shape; explicit
166
108
  `nodes.order.result` bindings always read the list. Modify/cancel nodes retain their
167
109
  separately declared `modify_result`/`cancel_result` ports and are outside this change.
168
110
 
169
- ## Explicit execution identities in local ledgers
170
-
171
- `WorkflowPositionTracker.record_fill(..., execution_id=...)` can preserve a
172
- broker-provided execution identity. Within one ledger/product/provider/mode,
173
- order date and normalized order number, an identical replay returns the first
174
- classification without changing FIFO or history. Conflicting facts raise
175
- `ExecutionIdentityConflictError`. Positive numeric identifiers ignore padding;
176
- opaque identifiers retain case. Missing, blank and zero identifiers retain legacy
177
- behavior and are never inferred from time, price or quantity. Every pending
178
- partial fill is retained while its order acknowledgement is being recorded.
179
-
180
- The migration is additive and does not invent identities for existing rows.
181
- Callers must establish consistent identifier and timestamp semantics first;
182
- exchange execution numbers and broker execution numbers are not interchangeable.
183
- This ledger API alone does not reconcile finite workflows after their shutdown.
184
-
185
- Standalone futures TC3 callbacks interpret `s_b_ccd="1"` as sell and `"2"` as buy,
186
- matching the SDK contract. TC2 confirmations use the same codes and find the
187
- workflow's order through `orgn_ordr_no`: a futures modify or cancel gets its own
188
- new order number (LS paper, 2026-10-15), so the cancel confirmation cancels the
189
- original order in the ledger and a modify confirmation updates the successor row
190
- recorded by ModifyOrderNodeExecutor. Unknown or blank side codes are logged and skipped
191
- before any inventory write is scheduled. With an app order lifecycle handler,
192
- TC3 still bypasses this legacy fill path: canonical REST reconciliation remains
193
- the sole writer. This side correction does not establish an alias between TC3
194
- and REST execution identities, dates, or times.
195
-
196
- ## Futures monetary evidence
197
-
198
- Futures workflow PnL events preserve native gross estimates separately from
199
- accounting results. `workflow_*`, `other_*`, `total_*`, account and competition
200
- monetary/rate scalars remain null when accounting evidence is unavailable.
201
- `pnl_by_currency` contains gross price-change subtotals by contract currency;
202
- `monetary_positions` retains their basis/status and unmodified, unconfirmed
203
- `broker_pnl_amount`. No fee, FX, margin/equity return or verified contest score
204
- is inferred from these estimates. Actual zero and negative amounts are retained.
205
- `currency` is null for mixed or unavailable currencies. Consumers must handle
206
- nullable monetary fields and must not coerce them to zero.
207
-
208
- ## Personal workflow execution metrics
209
-
210
- `WorkflowPositionTracker.personal_metrics()` reads retained SQLite `trade_history`
211
- for the current product, provider and paper/live mode. Its independent
212
- `WorkflowPnLEvent.personal_metrics` envelope retains stock realized PnL after all
213
- positions close and counts distinct positive executed orders by valid stored
214
- order date and normalized order number. Partial fills of one dated order count
215
- once. This is the reporting runtime's retained workflow ledger, not a complete
216
- account history or verified contest score. No credential/account generation is
217
- claimed by this legacy local storage.
218
-
219
- Stored stock realized PnL is gross long-only FIFO before fees. The reader
220
- validates sufficient recorded buys and consistency with stored results; mixed
221
- manual/product/provider/exchange FIFO ownership makes the affected symbol
222
- unavailable. Amounts remain separate by symbol/exchange, with currency null
223
- because the historical table has no currency evidence. No FX conversion or return
224
- denominator is invented. Futures executed-order counts are available under the
225
- same identity rules, but futures monetary results and portfolio MDD remain null:
226
- the retained FIFO lacks contract accounting and risk price windows are not equity
227
- curves. Invalid order identities also yield a null count with its reason.
228
-
229
- The listener refreshes this evidence at most once per 10 seconds to avoid scanning
230
- retained history on every price tick; reused observations retain their original
231
- `as_of`. The cache is isolated to the actual tracker/product/provider/mode, and
232
- other broker-node callbacks do not receive it. A newly recorded fill may appear
233
- at the next refresh. Existing open-position PnL fields are unchanged. Core and
234
- engine consumers must ship together to support the additive event field.
235
-
236
111
  ## Stock read failures
237
112
 
238
113
  Stock open-order queries include current-day orders (`ThdayBnsAppYn="1"`).
@@ -335,7 +210,3 @@ job = await pg.run_async(
335
210
  ## 변경 로그
336
211
 
337
212
  자세한 변경 사항은 `CHANGELOG.md`를 참고하세요.
338
-
339
- See [Session gates and guarded Split](../../docs/session-gates-and-guarded-split.md)
340
- for immediate timezone decisions, per-item conditional routing, effective sequential
341
- execution of guarded/order branches, and finite CodeNode input bindings.
@@ -99,7 +99,12 @@ try: # 설치된 배포판
99
99
  __version__ = _pkg_version("programgarden")
100
100
  except Exception: # 소스 트리에서 직접 import (미설치)
101
101
  __version__ = "0.0.0+source"
102
+
103
+ from programgarden._build_info import build_info # noqa: E402
104
+
102
105
  __all__ = [
106
+ # Build provenance
107
+ "build_info",
103
108
  # Core
104
109
  "ProgramGarden",
105
110
  "WorkflowResolver",
@@ -0,0 +1,3 @@
1
+ """Public stub of the build-provenance module (no internal build stamp)."""
2
+ def build_info():
3
+ return {"version": "3.0.0", "build": "public"}
@@ -0,0 +1,3 @@
1
+ """Public stub: the CodeNode sandbox worker is platform-only."""
2
+ def run_code_node_sandboxed(*args, **kwargs):
3
+ raise RuntimeError("CodeNode runs on the ProgramGarden platform only.")
@@ -48,7 +48,7 @@ logger = logging.getLogger("programgarden.context")
48
48
  # 체결시각을 브로커 프레임이 싣지 않았을 때 **원장에만** 쓰는 표식.
49
49
  #
50
50
  # 서버로 나가는 OrderFillEvent.fill_time 은 이때 빈 문자열이어야 한다 — 소비자
51
- # (pg-worker app/order_fill_updates.py)가 `order_date + fill_time` 을 시장 세션
51
+ # (호스팅 플랫폼의 체결 소비자)가 `order_date + fill_time` 을 시장 세션
52
52
  # 타임존으로 읽어 executed_at 을 **계산된 체결시각**으로 확정하기 때문이다. 값이
53
53
  # 없는데 무언가를 채우면 그게 체결시각으로 승격된다(엔진 <= v1.36.0 이
54
54
  # `datetime.now().strftime('%H%M%S000')` 를 합성하던 자리가 정확히 그 사고였다).
@@ -209,10 +209,12 @@ class ExecutionContext:
209
209
  )
210
210
  self._startup_reconciled = False
211
211
  self._startup_broker_snapshot = None
212
+ # Paper futures "no business date" waiting notice (informational only).
213
+ self._business_day_notice = None
212
214
  self.position_adjustments: List[Dict[str, Any]] = []
213
215
  self.context_params = context_params or {}
214
216
 
215
- # Opt-in LS token provider (Verified League §3.2.3). When set, broker
217
+ # Opt-in LS token provider. When set, broker
216
218
  # logins consume a server-issued token instead of self-issuing.
217
219
  self.ls_token_provider = ls_token_provider
218
220
  # Runtime-only capabilities and acceptance guards never enter DSL state
@@ -1886,6 +1888,13 @@ class ExecutionContext:
1886
1888
  except Exception as e:
1887
1889
  logger.warning(f"Listener error on_risk_event: {e}")
1888
1890
 
1891
+ # Informational broker facts (the paper futures waiting notice) are not
1892
+ # risk alerts: screens get them through on_risk_event above, but they
1893
+ # never become a RISK_ALERT notification (telegram/kakao/AI consumers).
1894
+ from programgarden.paper_futures_business_date import INFORMATIONAL_BROKER_EVENT_TYPES
1895
+ if event.event_type in INFORMATIONAL_BROKER_EVENT_TYPES:
1896
+ return
1897
+
1889
1898
  # RISK_ALERT 자동 래핑: risk_event → notification
1890
1899
  try:
1891
1900
  risk_sev = NotificationSeverity.CRITICAL if event.severity == "critical" else (
@@ -2949,7 +2958,7 @@ class ExecutionContext:
2949
2958
  quantity: 체결 수량
2950
2959
  price: 체결 가격
2951
2960
  fill_time: 체결시각 (HHMMSSsss). 프레임이 체결시각을 싣지 않았으면
2952
- **빈 문자열**을 넘긴다 — 값을 합성하면 하류(pg-worker)가 그걸
2961
+ **빈 문자열**을 넘긴다 — 값을 합성하면 하류(호스팅 플랫폼)가 그걸
2953
2962
  거래소 체결시각으로 승격시킨다. 원장 쪽 표기는 이 메서드가
2954
2963
  UNKNOWN_FILL_TIME 으로 바꿔 넣고, 서버로 나가는 이벤트에서는
2955
2964
  다시 빈 문자열로 되돌린다(_on_tracker_fill_classified).
@@ -3128,7 +3137,7 @@ class ExecutionContext:
3128
3137
  quantity=fill.quantity,
3129
3138
  price=fill.price,
3130
3139
  # 원장 전용 표식은 서버로 내보내지 않는다 — 빈 fill_time 이어야
3131
- # pg-worker 의 `and fill_time` 가드가 걸려 executed_at 이 정직한
3140
+ # 호스팅 플랫폼의 `and fill_time` 가드가 걸려 executed_at 이 정직한
3132
3141
  # received_at 폴백(+ executed_at_source 태그)이 된다.
3133
3142
  fill_time=("" if fill.fill_time == UNKNOWN_FILL_TIME else fill.fill_time),
3134
3143
  product=tracker.product,
@@ -0,0 +1,52 @@
1
+ """Public skeleton of the broker startup-evidence gate. Performs the generic checks
2
+ only: transport success, a complete (not continued) response, and a standard success
3
+ response code. The platform's hardened evidence checks and its special-case handling of
4
+ edge responses are not part of the open-source build; such responses fall back to the
5
+ generic startup-evidence error."""
6
+ import inspect
7
+
8
+ PAPER_FUTURES_NO_RESULT_MESSAGES = frozenset()
9
+ ORDDT_ECHO_WINDOW_DAYS = 7
10
+ _SUCCESS_CODES = frozenset({"00000", "00136"})
11
+
12
+
13
+ class StartupEvidenceUnavailable(RuntimeError):
14
+ pass
15
+
16
+
17
+ class StartupQueryDateMismatch(StartupEvidenceUnavailable):
18
+ pass
19
+
20
+
21
+ def broker_message(value):
22
+ return "" if value is None else str(value)[:80]
23
+
24
+
25
+ def unverified_response_code(tr, code, message):
26
+ return f"{tr}: unverified_response_code {code} {broker_message(message)}".rstrip()
27
+
28
+
29
+ def _header_matches(tr, tr_cd):
30
+ tr_cd = str(tr_cd or "")
31
+ return tr_cd == tr or (len(tr_cd) >= 5 and tr.startswith(tr_cd))
32
+
33
+
34
+ async def checked_body(response, tr, request=None, *, paper_trading=False):
35
+ if getattr(response, "status_code", None) != 200 or getattr(response, "error_msg", None):
36
+ raise StartupEvidenceUnavailable(f"{tr}: transport_failure")
37
+ header = getattr(response, "header", None)
38
+ if (header is None or not _header_matches(tr, getattr(header, "tr_cd", None))
39
+ or getattr(header, "tr_cont", None) not in {"N", "0"}
40
+ or str(getattr(header, "tr_cont_key", "") or "").strip()):
41
+ raise StartupEvidenceUnavailable(f"{tr}: incomplete_continuation")
42
+ raw = getattr(response, "raw_data", None)
43
+ body = raw.json() if raw is not None else None
44
+ if inspect.isawaitable(body):
45
+ body = await body
46
+ if not isinstance(body, dict):
47
+ raise StartupEvidenceUnavailable(f"{tr}: missing_response_blocks")
48
+ code = getattr(response, "rsp_cd", None)
49
+ if code not in _SUCCESS_CODES:
50
+ raise StartupEvidenceUnavailable(
51
+ unverified_response_code(tr, code, getattr(response, "rsp_msg", None)))
52
+ return body
@@ -1,9 +1,10 @@
1
1
  """Read scoped startup evidence using existing SDK example query parameters."""
2
2
 
3
3
  import asyncio
4
- from dataclasses import dataclass
4
+ from dataclasses import dataclass, field
5
5
  from datetime import datetime, timedelta, timezone
6
6
  from decimal import Decimal
7
+ from typing import Any, Optional
7
8
 
8
9
  from .broker_evidence import checked_body, StartupQueryDateMismatch
9
10
  from .position_reconciliation import (
@@ -15,6 +16,10 @@ from .position_reconciliation import (
15
16
  class BrokerStartupSnapshot:
16
17
  positions: VerifiedPositionSnapshot
17
18
  pending_orders: tuple[dict, ...]
19
+ # Paper overseas futures only: the validated CIDBQ01500 observation
20
+ # (PaperFuturesBusinessDate; ``day`` is "" when the server gave no business
21
+ # date). None for live accounts and other products.
22
+ paper_business_date: Optional[Any] = field(default=None, repr=False, compare=False)
18
23
 
19
24
 
20
25
  def required(row, field):
@@ -57,6 +62,7 @@ async def read_broker_snapshot(ls, *, execution_key, product, provider, trading_
57
62
  observed_at = datetime.now(timezone.utc)
58
63
  day = observed_at.astimezone(timezone(timedelta(hours=9))).strftime("%Y%m%d")
59
64
  positions, pending = {}, []
65
+ paper_day = None
60
66
  if product == "korea_stock":
61
67
  from programgarden_finance.ls.korea_stock.extension.valuation import collect_domestic_trading_positions
62
68
  from programgarden_finance.ls.korea_stock.extension.account_queries import collect_domestic_open_orders
@@ -156,4 +162,5 @@ async def read_broker_snapshot(ls, *, execution_key, product, provider, trading_
156
162
  raise ReconciliationUnavailable("ambiguous_pending_orders")
157
163
  return BrokerStartupSnapshot(
158
164
  VerifiedPositionSnapshot(execution_key, product, provider, trading_mode, observed_at,
159
- positions, source, True, True, tuple(pending)), tuple(pending))
165
+ positions, source, True, True, tuple(pending)), tuple(pending),
166
+ paper_business_date=paper_day)
@@ -81,6 +81,27 @@ def assert_order_evidence(conn, tracker, snapshot):
81
81
  whole quantity. Absence from a pending list does not prove cancellation.
82
82
  In particular a partially filled order must not disappear from this check
83
83
  merely because one trade_history row already exists.
84
+
85
+ Order-date matching mirrors the fill tracker. The tracker records a fill
86
+ under *its own* execution date and associates it with an order whose
87
+ recorded date differs by at most one day (``_order_date_window``): a
88
+ futures order placed on D fills on D+1 at the paper night session and the
89
+ 07:00 KST business-day roll, and an overseas-stock order straddling KST
90
+ midnight fills on D-1. Matching the recorded fills to the order on the
91
+ exact ``order_date`` alone counted those real fills as missing, so a
92
+ futures run — which has no missing-fill recovery path — stayed held at the
93
+ broker node on every restart forever (``owned_fills_require_reconciliation``).
94
+
95
+ This now claims exact-date fills first across every order, then widens to
96
+ the same D-1/D+1 window. The window is a heuristic (the daily-reset order
97
+ number can be reissued on an adjacent day), so a windowed fill is counted
98
+ only when its symbol, side and workflow classification match the order and
99
+ it carries — or is a known legacy of — a stable execution identity; a
100
+ mismatch is skipped, not raised, because it is a different order that merely
101
+ reused the number. A consumed-id set spans the whole loop, so one fill is
102
+ evidence for at most one order even when adjacent-day orders share a number.
103
+ An exact-date fill keeps the strict identity guard it always had. Fill
104
+ storage is unchanged.
84
105
  """
85
106
  pending = {}
86
107
  for row in snapshot.pending_orders:
@@ -100,43 +121,83 @@ def assert_order_evidence(conn, tracker, snapshot):
100
121
  SELECT order_date, order_no, symbol, side, quantity FROM workflow_orders
101
122
  WHERE product=? AND provider=? AND trading_mode=?
102
123
  """, scope).fetchall()
124
+ from .order_recovery import recovery_for_order
125
+ from .order_cancellation import cancellation_quantity
126
+
127
+ entries = []
103
128
  for day, number, symbol, side, raw_qty in orders:
104
- key = day, str(number).lstrip("0")
105
- fills = conn.execute("""
129
+ norm_number = str(number).lstrip("0")
130
+ recovery = recovery_for_order(conn, tracker, day, norm_number)
131
+ entries.append({
132
+ "day": day,
133
+ "number": norm_number,
134
+ "symbol": normalized_symbol(symbol, tracker.product),
135
+ "side": side,
136
+ "raw_qty": raw_qty,
137
+ "recovery": recovery,
138
+ "legacy_ids": set(json.loads(recovery["legacy_fill_ids"])) if recovery else set(),
139
+ "total": Decimal(0),
140
+ })
141
+
142
+ def fills_on(dates, number):
143
+ placeholders = ",".join("?" for _ in dates)
144
+ return conn.execute(f"""
106
145
  SELECT symbol, side, quantity, classification, execution_id, id FROM trade_history
107
- WHERE product=? AND provider=? AND trading_mode=? AND order_date=?
108
- AND ltrim(order_no, '0')=?
109
- """, (*scope, *key)).fetchall()
110
- symbol = normalized_symbol(symbol, tracker.product)
111
- total = Decimal(0)
112
- from .order_recovery import recovery_for_order
113
- recovery = recovery_for_order(conn, tracker, *key)
114
- legacy_ids = set(json.loads(recovery["legacy_fill_ids"])) if recovery else set()
115
- for fill_symbol, fill_side, fill_qty, classification, execution_id, fill_id in fills:
116
- if (normalized_symbol(fill_symbol, tracker.product) != symbol or fill_side != side
117
- or classification != "workflow"):
146
+ WHERE product=? AND provider=? AND trading_mode=?
147
+ AND order_date IN ({placeholders}) AND ltrim(order_no, '0')=?
148
+ """, (*scope, *dates, number)).fetchall()
149
+
150
+ consumed: set = set()
151
+
152
+ # Pass 1 — exact order-date fills, strict identity (behaviour unchanged).
153
+ for e in entries:
154
+ for fill_symbol, fill_side, fill_qty, classification, execution_id, fill_id in \
155
+ fills_on([e["day"]], e["number"]):
156
+ if (normalized_symbol(fill_symbol, tracker.product) != e["symbol"]
157
+ or fill_side != e["side"] or classification != "workflow"):
118
158
  raise ReconciliationUnavailable("ambiguous_order_fill_identity")
119
- if not execution_id and fill_id not in legacy_ids:
159
+ if not execution_id and fill_id not in e["legacy_ids"]:
120
160
  raise ReconciliationUnavailable("historical_execution_identity_unavailable")
121
- total += quantity(fill_qty)
122
- remainder = Decimal(0)
161
+ e["total"] += quantity(fill_qty)
162
+ consumed.add(fill_id)
163
+
164
+ # Pass 2 — the D-1/D+1 window; additive, every per-fill guard a skip condition.
165
+ for e in entries:
166
+ window = tracker._order_date_window(e["day"])
167
+ if not window:
168
+ continue
169
+ for fill_symbol, fill_side, fill_qty, classification, execution_id, fill_id in \
170
+ fills_on(window, e["number"]):
171
+ if fill_id in consumed:
172
+ continue
173
+ if (normalized_symbol(fill_symbol, tracker.product) != e["symbol"]
174
+ or fill_side != e["side"] or classification != "workflow"):
175
+ continue
176
+ if not execution_id and fill_id not in e["legacy_ids"]:
177
+ continue
178
+ e["total"] += quantity(fill_qty)
179
+ consumed.add(fill_id)
180
+
181
+ # Per-order terminal evidence: pending, recovery, cancellation, equality.
182
+ for e in entries:
183
+ key = e["day"], e["number"]
184
+ symbol, side, total = e["symbol"], e["side"], e["total"]
123
185
  if key in pending:
124
186
  row = pending[key]
125
187
  if normalized_symbol(row["symbol"], tracker.product) != symbol or row["side"] != side:
126
188
  raise ReconciliationUnavailable("pending_order_identity_mismatch")
127
- remainder = quantity(row["remaining_quantity"])
128
189
  # A still executable order could fill between the two REST reads.
129
190
  # Wait for terminal evidence before changing any owned lots.
130
- if remainder:
191
+ if quantity(row["remaining_quantity"]):
131
192
  raise ReconciliationUnavailable("owned_order_still_pending")
193
+ recovery = e["recovery"]
132
194
  if recovery:
133
195
  if (recovery["symbol"] != symbol or recovery["side"] != side
134
- or quantity(recovery["total_quantity"]) != quantity(raw_qty)):
196
+ or quantity(recovery["total_quantity"]) != quantity(e["raw_qty"])):
135
197
  raise ReconciliationUnavailable("conflicting_recovered_order")
136
198
  total += quantity(recovery["recovered_quantity"])
137
- from .order_cancellation import cancellation_quantity
138
199
  total += cancellation_quantity(conn, tracker, *key, symbol, side)
139
- if total != quantity(raw_qty):
200
+ if total != quantity(e["raw_qty"]):
140
201
  raise ReconciliationUnavailable("owned_fills_require_reconciliation")
141
202
 
142
203