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.
- {programgarden-2.7.2 → programgarden-3.0.0}/PKG-INFO +5 -134
- {programgarden-2.7.2 → programgarden-3.0.0}/README.md +1 -130
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/__init__.py +5 -0
- programgarden-3.0.0/programgarden/_build_info.py +3 -0
- programgarden-3.0.0/programgarden/code_worker.py +3 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/context.py +13 -4
- programgarden-3.0.0/programgarden/database/broker_evidence.py +52 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/broker_snapshot.py +9 -2
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/position_reconciliation.py +82 -21
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/workflow_position_tracker.py +2 -2
- programgarden-3.0.0/programgarden/deep_fixtures.py +46 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/executor.py +410 -70
- programgarden-3.0.0/programgarden/paper_futures_business_date.py +355 -0
- programgarden-3.0.0/programgarden/replay_contracts.py +7 -0
- programgarden-3.0.0/programgarden/replay_semantics.py +3 -0
- programgarden-3.0.0/programgarden/replay_triggers.py +3 -0
- programgarden-3.0.0/programgarden/semantic_rules.py +12 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/pyproject.toml +4 -4
- programgarden-2.7.2/programgarden/code_worker.py +0 -499
- programgarden-2.7.2/programgarden/database/broker_evidence.py +0 -189
- programgarden-2.7.2/programgarden/deep_fixtures.py +0 -561
- programgarden-2.7.2/programgarden/incremental_build.py +0 -430
- programgarden-2.7.2/programgarden/paper_futures_business_date.py +0 -151
- programgarden-2.7.2/programgarden/replay_contracts.py +0 -261
- programgarden-2.7.2/programgarden/replay_events.py +0 -196
- programgarden-2.7.2/programgarden/replay_external.py +0 -279
- programgarden-2.7.2/programgarden/replay_order_adapter.py +0 -291
- programgarden-2.7.2/programgarden/replay_orders.py +0 -359
- programgarden-2.7.2/programgarden/replay_responders.py +0 -148
- programgarden-2.7.2/programgarden/replay_scenarios.py +0 -127
- programgarden-2.7.2/programgarden/replay_semantics.py +0 -674
- programgarden-2.7.2/programgarden/replay_sources.py +0 -161
- programgarden-2.7.2/programgarden/replay_sqlite.py +0 -53
- programgarden-2.7.2/programgarden/replay_tape.py +0 -182
- programgarden-2.7.2/programgarden/replay_triggers.py +0 -76
- programgarden-2.7.2/programgarden/semantic_rules.py +0 -501
- programgarden-2.7.2/programgarden/validation_replay.py +0 -492
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/binding_provenance.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/binding_validator.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/client.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/__init__.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/adjustment_delivery.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/broker_order_totals.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/db_naming.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/execution_storage.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/fill_reconciler.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/order_cancellation.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/order_recovery.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/owned_order_cancellation.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/query_builder.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/sqlite_scope.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_contract_info.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_exchange.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_history.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_orderable.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_pnl.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/futures_read_evidence.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/managed_order_control.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/node_runner.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/order_lifecycle.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/plugin/__init__.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/providers/__init__.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/ranking_nodes.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/reconnect_handler.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resolver.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/__init__.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/context.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/limiter.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/monitor.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/resource/throttle.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/__init__.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/event_tools.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/job_tools.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-2.7.2 → programgarden-3.0.0}/programgarden/validation_recommender.py +0 -0
- {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:
|
|
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 (
|
|
19
|
-
Requires-Dist: programgarden-core (
|
|
20
|
-
Requires-Dist: programgarden-finance (
|
|
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
|
|
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
|
|
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",
|
|
@@ -48,7 +48,7 @@ logger = logging.getLogger("programgarden.context")
|
|
|
48
48
|
# 체결시각을 브로커 프레임이 싣지 않았을 때 **원장에만** 쓰는 표식.
|
|
49
49
|
#
|
|
50
50
|
# 서버로 나가는 OrderFillEvent.fill_time 은 이때 빈 문자열이어야 한다 — 소비자
|
|
51
|
-
# (
|
|
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
|
|
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
|
-
**빈 문자열**을 넘긴다 — 값을 합성하면 하류(
|
|
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
|
-
#
|
|
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)
|
{programgarden-2.7.2 → programgarden-3.0.0}/programgarden/database/position_reconciliation.py
RENAMED
|
@@ -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
|
-
|
|
105
|
-
|
|
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=?
|
|
108
|
-
AND ltrim(order_no, '0')=?
|
|
109
|
-
""", (*scope, *
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for fill_symbol, fill_side, fill_qty, classification, execution_id, fill_id in
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|