programgarden 1.37.6__tar.gz → 1.37.8__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-1.37.6 → programgarden-1.37.8}/PKG-INFO +12 -1
- {programgarden-1.37.6 → programgarden-1.37.8}/README.md +11 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/context.py +17 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/database/fill_reconciler.py +1 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/database/workflow_position_tracker.py +85 -7
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/executor.py +82 -7
- {programgarden-1.37.6 → programgarden-1.37.8}/pyproject.toml +1 -1
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/__init__.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/binding_validator.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/client.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/code_worker.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/database/__init__.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/deep_fixtures.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/futures_pnl.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/node_runner.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/order_lifecycle.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/resolver.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/resource/context.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/semantic_rules.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.37.6 → programgarden-1.37.8}/programgarden/validation_recommender.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 1.37.
|
|
3
|
+
Version: 1.37.8
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Author: 프로그램동산
|
|
@@ -96,12 +96,23 @@ Broker account trackers belong to the workflow job. Normal completion, `stop()`,
|
|
|
96
96
|
account polling, and close each tracker's dedicated WebSocket. Late account PnL
|
|
97
97
|
callbacks do not restart work after shutdown. This cleanup also covers partially
|
|
98
98
|
initialized trackers and leaves other jobs running.
|
|
99
|
+
Scheduled cycles reuse the same broker account tracker, including pending startup.
|
|
100
|
+
A failed startup stops its tracker and closes its connection before a later cycle
|
|
101
|
+
may try again; no immediate retry or extra workflow node is introduced.
|
|
99
102
|
Already queued or in-flight PnL notifications have up to one second to finish
|
|
100
103
|
after account trackers stop, before listeners close. On timeout, cleanup logs the
|
|
101
104
|
pending count and cancels those tasks, allowing another 0.25 seconds for cancellation.
|
|
102
105
|
A listener that suppresses cancellation is reported and remains tracked; it cannot
|
|
103
106
|
hold job shutdown indefinitely. Delivery beyond this bounded grace is not guaranteed.
|
|
104
107
|
|
|
108
|
+
Overseas-stock OpenOrdersNode treats the observed terminal COSAQ00102 `02679`
|
|
109
|
+
empty envelope as `open_orders=[]` / `count=0` without an error. The echoed date,
|
|
110
|
+
market and filters must match its current-day pending query, the empty detail
|
|
111
|
+
array must be explicitly present, and continuation must be terminal. This follows
|
|
112
|
+
the finance package's observed-response reference and StockAccountTracker;
|
|
113
|
+
`02679` is not a generic success code. Missing, malformed, mismatched and failed
|
|
114
|
+
reads retain `error` / `reason=fetch_failed` so workflows can block new entries.
|
|
115
|
+
|
|
105
116
|
For one-shot workflows, an unhandled `order_result.success=False` returned directly
|
|
106
117
|
by a main-flow node makes the final job status `failed` and emits `WORKFLOW_FAILED`.
|
|
107
118
|
The original broker message remains in node diagnostics and the failure statistics.
|
|
@@ -67,12 +67,23 @@ Broker account trackers belong to the workflow job. Normal completion, `stop()`,
|
|
|
67
67
|
account polling, and close each tracker's dedicated WebSocket. Late account PnL
|
|
68
68
|
callbacks do not restart work after shutdown. This cleanup also covers partially
|
|
69
69
|
initialized trackers and leaves other jobs running.
|
|
70
|
+
Scheduled cycles reuse the same broker account tracker, including pending startup.
|
|
71
|
+
A failed startup stops its tracker and closes its connection before a later cycle
|
|
72
|
+
may try again; no immediate retry or extra workflow node is introduced.
|
|
70
73
|
Already queued or in-flight PnL notifications have up to one second to finish
|
|
71
74
|
after account trackers stop, before listeners close. On timeout, cleanup logs the
|
|
72
75
|
pending count and cancels those tasks, allowing another 0.25 seconds for cancellation.
|
|
73
76
|
A listener that suppresses cancellation is reported and remains tracked; it cannot
|
|
74
77
|
hold job shutdown indefinitely. Delivery beyond this bounded grace is not guaranteed.
|
|
75
78
|
|
|
79
|
+
Overseas-stock OpenOrdersNode treats the observed terminal COSAQ00102 `02679`
|
|
80
|
+
empty envelope as `open_orders=[]` / `count=0` without an error. The echoed date,
|
|
81
|
+
market and filters must match its current-day pending query, the empty detail
|
|
82
|
+
array must be explicitly present, and continuation must be terminal. This follows
|
|
83
|
+
the finance package's observed-response reference and StockAccountTracker;
|
|
84
|
+
`02679` is not a generic success code. Missing, malformed, mismatched and failed
|
|
85
|
+
reads retain `error` / `reason=fetch_failed` so workflows can block new entries.
|
|
86
|
+
|
|
76
87
|
For one-shot workflows, an unhandled `order_result.success=False` returned directly
|
|
77
88
|
by a main-flow node makes the final job status `failed` and emits `WORKFLOW_FAILED`.
|
|
78
89
|
The original broker message remains in node diagnostics and the failure statistics.
|
|
@@ -2865,6 +2865,19 @@ class ExecutionContext:
|
|
|
2865
2865
|
logger.warning(f"Failed to update order fill price: {e}")
|
|
2866
2866
|
return False
|
|
2867
2867
|
|
|
2868
|
+
async def _annotate_workflow_fill_currency(self, *, currency: str, **fill_kwargs: Any) -> bool:
|
|
2869
|
+
"""Preserve post-refresh currency without replaying the AS1 execution."""
|
|
2870
|
+
tracker = self._workflow_position_tracker
|
|
2871
|
+
if tracker is None:
|
|
2872
|
+
return False
|
|
2873
|
+
fill_kwargs["fill_time"] = fill_kwargs.get("fill_time") or UNKNOWN_FILL_TIME
|
|
2874
|
+
try:
|
|
2875
|
+
return await tracker.annotate_fill_currency(currency=currency, **fill_kwargs)
|
|
2876
|
+
except ValueError:
|
|
2877
|
+
# Unusable/conflicting identity must never cause another fill write.
|
|
2878
|
+
logger.warning("Cannot attach currency evidence to the retained execution")
|
|
2879
|
+
return False
|
|
2880
|
+
|
|
2868
2881
|
async def record_workflow_fill(
|
|
2869
2882
|
self,
|
|
2870
2883
|
order_no: str,
|
|
@@ -2879,6 +2892,7 @@ class ExecutionContext:
|
|
|
2879
2892
|
*,
|
|
2880
2893
|
execution_id: Optional[str | int] = None,
|
|
2881
2894
|
account_avg_price: Optional[float] = None,
|
|
2895
|
+
currency: Optional[str] = None,
|
|
2882
2896
|
) -> str:
|
|
2883
2897
|
"""Record fill event for FIFO position tracking.
|
|
2884
2898
|
|
|
@@ -2904,6 +2918,7 @@ class ExecutionContext:
|
|
|
2904
2918
|
execution_id: Optional broker execution number, preserved for durable replay detection.
|
|
2905
2919
|
account_avg_price: Optional account average purchase price for this
|
|
2906
2920
|
symbol at fill time; only a workflow sell's residual tail uses it.
|
|
2921
|
+
currency: Explicit broker unit at fill time; no product default.
|
|
2907
2922
|
|
|
2908
2923
|
Returns:
|
|
2909
2924
|
분류 결과: "workflow" | "manual" | "unknown_api" | "pending"
|
|
@@ -2976,6 +2991,8 @@ class ExecutionContext:
|
|
|
2976
2991
|
# 재시도에도 유지한다 — identity 축이 아니고, 매도 잔량 추정의
|
|
2977
2992
|
# 유일한 근거다.
|
|
2978
2993
|
base_kwargs["account_avg_price"] = account_avg_price
|
|
2994
|
+
if currency is not None:
|
|
2995
|
+
base_kwargs["currency"] = currency
|
|
2979
2996
|
|
|
2980
2997
|
try:
|
|
2981
2998
|
result = await self._workflow_position_tracker.record_fill(
|
|
@@ -227,6 +227,7 @@ async def reconcile_workflow_fills(
|
|
|
227
227
|
# 빈 값이면 트래커가 '우리 주문과 일치하는가' 로만 분류한다.
|
|
228
228
|
commda_code="",
|
|
229
229
|
account_avg_price=avg_price,
|
|
230
|
+
currency=hit.get("currency"),
|
|
230
231
|
)
|
|
231
232
|
except Exception as exc:
|
|
232
233
|
report.errors.append(f"record_fill_failed[{order_no}]: {exc}")
|
{programgarden-1.37.6 → programgarden-1.37.8}/programgarden/database/workflow_position_tracker.py
RENAMED
|
@@ -183,6 +183,9 @@ class PendingFill:
|
|
|
183
183
|
# read from the broker snapshot before it refreshes. Used only to estimate a
|
|
184
184
|
# workflow sell's residual tail (see _process_sell_fifo); None → no estimate.
|
|
185
185
|
account_avg_price: Optional[float] = None
|
|
186
|
+
# An explicit broker unit, never inferred from the product or exchange.
|
|
187
|
+
currency: Optional[str] = None
|
|
188
|
+
currency_conflict: bool = False
|
|
186
189
|
|
|
187
190
|
|
|
188
191
|
class ExecutionIdentityError(ValueError):
|
|
@@ -535,6 +538,12 @@ class WorkflowPositionTracker:
|
|
|
535
538
|
for column in ("execution_id", "normalized_order_no", "execution_payload"):
|
|
536
539
|
if column not in columns:
|
|
537
540
|
cursor.execute(f"ALTER TABLE trade_history ADD COLUMN {column} TEXT")
|
|
541
|
+
if "currency" not in columns:
|
|
542
|
+
cursor.execute("ALTER TABLE trade_history ADD COLUMN currency TEXT")
|
|
543
|
+
if "currency_conflict" not in columns:
|
|
544
|
+
cursor.execute(
|
|
545
|
+
"ALTER TABLE trade_history ADD COLUMN currency_conflict INTEGER NOT NULL DEFAULT 0"
|
|
546
|
+
)
|
|
538
547
|
# Additive columns for the account-average-price sell estimate. A
|
|
539
548
|
# workflow sell that empties the strategy's own lots and still has a
|
|
540
549
|
# tail records that tail here (the residual quantity, the account
|
|
@@ -722,6 +731,7 @@ class WorkflowPositionTracker:
|
|
|
722
731
|
*,
|
|
723
732
|
execution_id: Optional[str | int] = None,
|
|
724
733
|
account_avg_price: Optional[float] = None,
|
|
734
|
+
currency: Optional[str] = None,
|
|
725
735
|
) -> str:
|
|
726
736
|
"""
|
|
727
737
|
체결 기록 및 FIFO 처리
|
|
@@ -743,6 +753,8 @@ class WorkflowPositionTracker:
|
|
|
743
753
|
account_avg_price: Optional account average purchase price for this
|
|
744
754
|
symbol at fill time. Only a workflow sell whose own lots run out
|
|
745
755
|
uses it, to estimate the residual tail. None → no estimate.
|
|
756
|
+
currency: Explicit broker currency for this fill. Missing/invalid
|
|
757
|
+
values remain unknown and never change fill identity or amounts.
|
|
746
758
|
execution_id: Optional broker execution identity. Positive numeric
|
|
747
759
|
strings/integers ignore padding; opaque strings retain case.
|
|
748
760
|
None, blank, and zero mean no identity and retain legacy replay
|
|
@@ -765,6 +777,7 @@ class WorkflowPositionTracker:
|
|
|
765
777
|
exchange=exchange, side=side, quantity=quantity, price=price,
|
|
766
778
|
fill_time=fill_time, commda_code=commda_code, received_at=datetime.now(),
|
|
767
779
|
execution_id=execution_id, account_avg_price=account_avg_price,
|
|
780
|
+
currency=self._currency_code(currency),
|
|
768
781
|
)
|
|
769
782
|
identity = self._execution_key(fill)
|
|
770
783
|
notifications: list = []
|
|
@@ -879,6 +892,59 @@ class WorkflowPositionTracker:
|
|
|
879
892
|
"commda_code": fill.commda_code,
|
|
880
893
|
}
|
|
881
894
|
|
|
895
|
+
@staticmethod
|
|
896
|
+
def _currency_code(value: Any) -> Optional[str]:
|
|
897
|
+
if not isinstance(value, str):
|
|
898
|
+
return None
|
|
899
|
+
value = value.strip().upper()
|
|
900
|
+
return value if re.fullmatch(r"[A-Z]{3}", value) else None
|
|
901
|
+
|
|
902
|
+
async def annotate_fill_currency(self, *, currency: str, **fill_kwargs: Any) -> bool:
|
|
903
|
+
"""Attach a contemporaneous broker unit to an exact retained execution.
|
|
904
|
+
|
|
905
|
+
Called after the existing AS1 account refresh, never to reconstruct old
|
|
906
|
+
history. No identity means no annotation. Check all canonical facts and
|
|
907
|
+
hold the ledger write lock, but do not create a fill, consume a lot, emit
|
|
908
|
+
another fill notification or alter the deduplication payload. Conflicting
|
|
909
|
+
units permanently make the unit unknown until separately investigated.
|
|
910
|
+
"""
|
|
911
|
+
unit = self._currency_code(currency)
|
|
912
|
+
if unit is None or self.product != "overseas_stock":
|
|
913
|
+
return False
|
|
914
|
+
fill = PendingFill(**fill_kwargs, received_at=datetime.now(), currency=unit)
|
|
915
|
+
identity = self._execution_key(fill)
|
|
916
|
+
if identity is None:
|
|
917
|
+
return False
|
|
918
|
+
changed = False
|
|
919
|
+
async with self._buffer_lock:
|
|
920
|
+
for pending in self._pending_fills.values():
|
|
921
|
+
if self._execution_key(pending) == identity:
|
|
922
|
+
self._assert_same_execution(pending, fill)
|
|
923
|
+
if pending.currency_conflict or pending.currency == unit:
|
|
924
|
+
return False
|
|
925
|
+
pending.currency_conflict = pending.currency is not None
|
|
926
|
+
pending.currency = None if pending.currency_conflict else unit
|
|
927
|
+
return True
|
|
928
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
929
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
930
|
+
cursor = conn.cursor()
|
|
931
|
+
if self._find_execution(cursor, fill) is None:
|
|
932
|
+
return False
|
|
933
|
+
where = """product=? AND provider=? AND trading_mode=? AND order_date=?
|
|
934
|
+
AND normalized_order_no=? AND execution_id=?"""
|
|
935
|
+
stored, conflict = cursor.execute(
|
|
936
|
+
f"SELECT currency, currency_conflict FROM trade_history WHERE {where}", identity
|
|
937
|
+
).fetchone()
|
|
938
|
+
if not conflict and stored != unit:
|
|
939
|
+
cursor.execute(
|
|
940
|
+
f"UPDATE trade_history SET currency=?, currency_conflict=? WHERE {where}",
|
|
941
|
+
(unit if stored is None else None, int(stored is not None), *identity),
|
|
942
|
+
)
|
|
943
|
+
changed = True
|
|
944
|
+
if changed:
|
|
945
|
+
self.fill_revision += 1
|
|
946
|
+
return changed
|
|
947
|
+
|
|
882
948
|
def _assert_same_execution(self, previous: PendingFill, fill: PendingFill) -> None:
|
|
883
949
|
if self._execution_facts(previous) != self._execution_facts(fill):
|
|
884
950
|
raise ExecutionIdentityConflictError("Conflicting fill facts for an existing execution identity")
|
|
@@ -984,8 +1050,9 @@ class WorkflowPositionTracker:
|
|
|
984
1050
|
(product, provider, order_no, order_date, symbol, exchange, side, quantity, price,
|
|
985
1051
|
fill_datetime, classification, commda_code, realized_pnl, trading_mode, created_at,
|
|
986
1052
|
execution_id, normalized_order_no, execution_payload,
|
|
987
|
-
unmatched_qty, estimate_basis_price, estimate_source, estimated_pnl
|
|
988
|
-
|
|
1053
|
+
unmatched_qty, estimate_basis_price, estimate_source, estimated_pnl,
|
|
1054
|
+
currency, currency_conflict)
|
|
1055
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
989
1056
|
""", (
|
|
990
1057
|
self.product, self.provider,
|
|
991
1058
|
fill.order_no, fill.order_date, fill.symbol, fill.exchange,
|
|
@@ -996,6 +1063,8 @@ class WorkflowPositionTracker:
|
|
|
996
1063
|
estimate["estimate_basis_price"] if estimate else None,
|
|
997
1064
|
estimate["estimate_source"] if estimate else None,
|
|
998
1065
|
estimate["estimated_pnl"] if estimate else None,
|
|
1066
|
+
fill.currency,
|
|
1067
|
+
int(fill.currency_conflict),
|
|
999
1068
|
))
|
|
1000
1069
|
|
|
1001
1070
|
conn.commit()
|
|
@@ -1960,8 +2029,9 @@ class WorkflowPositionTracker:
|
|
|
1960
2029
|
def personal_metrics(self) -> Dict[str, Any]:
|
|
1961
2030
|
"""Read retained workflow executions without claiming account performance.
|
|
1962
2031
|
|
|
1963
|
-
Historical FIFO rows have no currency or fee evidence.
|
|
1964
|
-
|
|
2032
|
+
Historical FIFO rows have no currency or fee evidence. New rows can carry
|
|
2033
|
+
an explicit broker unit; old units are never backfilled. Keep amounts
|
|
2034
|
+
per symbol/exchange and reject contradictory units or an incomplete basis.
|
|
1965
2035
|
Futures FIFO is not a monetary ledger. No equity/MDD basis is inferred.
|
|
1966
2036
|
|
|
1967
2037
|
Version 2 adds closed-trade outcomes. One closed trade = one sell fill,
|
|
@@ -1986,8 +2056,8 @@ class WorkflowPositionTracker:
|
|
|
1986
2056
|
plus the estimated tail). Any other residual is an incomplete basis.
|
|
1987
2057
|
|
|
1988
2058
|
Counts aggregate across symbols because a count carries no currency.
|
|
1989
|
-
Amounts do not:
|
|
1990
|
-
|
|
2059
|
+
Amounts do not: the API may sum groups only when every group carries the
|
|
2060
|
+
same explicit currency. Missing evidence never inherits an account unit.
|
|
1991
2061
|
So gross amounts stay inside each group, and the top-level profit/loss
|
|
1992
2062
|
ratio is published only when a single group carries the whole ledger —
|
|
1993
2063
|
the one case where "the currency is the same" needs no evidence.
|
|
@@ -2025,6 +2095,8 @@ class WorkflowPositionTracker:
|
|
|
2025
2095
|
|
|
2026
2096
|
realized = []
|
|
2027
2097
|
for (symbol, exchange), fills in sorted(groups.items()):
|
|
2098
|
+
units = {self._currency_code(fill.get("currency")) for fill in fills}
|
|
2099
|
+
unit_conflict = any(fill.get("currency_conflict") for fill in fills)
|
|
2028
2100
|
reason = None
|
|
2029
2101
|
amount = None
|
|
2030
2102
|
outcome = None
|
|
@@ -2036,6 +2108,9 @@ class WorkflowPositionTracker:
|
|
|
2036
2108
|
reason = "futures_fifo_not_monetary"
|
|
2037
2109
|
elif not symbol:
|
|
2038
2110
|
reason = "missing_symbol"
|
|
2111
|
+
elif len(units - {None}) > 1 or unit_conflict:
|
|
2112
|
+
# Contradictory units cannot establish a comparable FIFO basis.
|
|
2113
|
+
reason = "incomplete_fifo_basis"
|
|
2039
2114
|
else:
|
|
2040
2115
|
# The old mixed_fifo_ownership pre-check is gone: a workflow sell
|
|
2041
2116
|
# now consumes only workflow lots, so it can never have realized a
|
|
@@ -2137,7 +2212,10 @@ class WorkflowPositionTracker:
|
|
|
2137
2212
|
status = "available"
|
|
2138
2213
|
basis = "fifo"
|
|
2139
2214
|
estimated_quantity = 0
|
|
2140
|
-
|
|
2215
|
+
unit = (next(iter(units)) if len(units) == 1 and None not in units
|
|
2216
|
+
and not unit_conflict
|
|
2217
|
+
and self.product != "overseas_futures" else None)
|
|
2218
|
+
entry = {"symbol": symbol, "exchange": exchange, "currency": unit,
|
|
2141
2219
|
"amount": amount, "status": status, "reason": reason,
|
|
2142
2220
|
"basis": basis, "estimated_quantity": estimated_quantity}
|
|
2143
2221
|
# A rejected group publishes no outcome: its trades are unknown, and
|
|
@@ -165,6 +165,24 @@ def _whole_unit_quantity(value: Any) -> Optional[int]:
|
|
|
165
165
|
return int(number) if number.is_integer() else None
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def _stock_position_fill_currency(position: Any, symbol: str, market_code: str) -> Optional[str]:
|
|
169
|
+
"""Use only explicitly populated COSOQ00201 position fields for this symbol.
|
|
170
|
+
|
|
171
|
+
AS1 has no currency. StockPositionItem defaults to USD, so a model default
|
|
172
|
+
is insufficient evidence. No quote currency or account-wide default is used.
|
|
173
|
+
"""
|
|
174
|
+
from programgarden_finance.ls.overseas_stock.extension.models import StockPositionItem
|
|
175
|
+
|
|
176
|
+
if not isinstance(position, StockPositionItem):
|
|
177
|
+
return None
|
|
178
|
+
if not {"symbol", "market_code", "currency_code"} <= position.model_fields_set:
|
|
179
|
+
return None
|
|
180
|
+
if position.symbol != symbol or not market_code or position.market_code != market_code:
|
|
181
|
+
return None
|
|
182
|
+
unit = position.currency_code.strip().upper()
|
|
183
|
+
return unit if re.fullmatch(r"[A-Z]{3}", unit) else None
|
|
184
|
+
|
|
185
|
+
|
|
168
186
|
def _usable_execution_identity(order_no: Any, order_date: Any) -> bool:
|
|
169
187
|
"""체결 프레임의 (주문번호, 주문일자)가 원장 execution identity 키로 쓸 수 있나.
|
|
170
188
|
|
|
@@ -4881,10 +4899,12 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
4881
4899
|
# 읽는다. 캐시는 아직 이번 매도가 반영되기 전이라 '매도 직전 평단'.
|
|
4882
4900
|
# 캐시에 없으면 None → 추정 없음(오늘처럼 unavailable).
|
|
4883
4901
|
account_avg_price = None
|
|
4902
|
+
currency = None
|
|
4884
4903
|
if tracker_info and "tracker" in tracker_info:
|
|
4885
4904
|
acct_tracker = tracker_info["tracker"]
|
|
4886
4905
|
positions = getattr(acct_tracker, "_positions", None)
|
|
4887
4906
|
pos = positions.get(symbol) if isinstance(positions, dict) else None
|
|
4907
|
+
currency = _stock_position_fill_currency(pos, symbol, market_code)
|
|
4888
4908
|
buy_price = getattr(pos, "buy_price", None) if pos is not None else None
|
|
4889
4909
|
if buy_price is not None:
|
|
4890
4910
|
try:
|
|
@@ -4892,7 +4912,7 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
4892
4912
|
except (TypeError, ValueError):
|
|
4893
4913
|
account_avg_price = None
|
|
4894
4914
|
|
|
4895
|
-
|
|
4915
|
+
fill_kwargs = dict(
|
|
4896
4916
|
order_no=order_no,
|
|
4897
4917
|
order_date=order_date,
|
|
4898
4918
|
symbol=symbol,
|
|
@@ -4912,12 +4932,20 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
4912
4932
|
),
|
|
4913
4933
|
account_avg_price=account_avg_price,
|
|
4914
4934
|
)
|
|
4935
|
+
await context.record_workflow_fill(currency=currency, **fill_kwargs)
|
|
4915
4936
|
|
|
4916
4937
|
# AccountTracker refresh로 PnL 이벤트 강제 트리거
|
|
4917
4938
|
if tracker_info and "tracker" in tracker_info:
|
|
4918
4939
|
tracker = tracker_info["tracker"]
|
|
4919
4940
|
if hasattr(tracker, 'refresh_now'):
|
|
4920
4941
|
await tracker.refresh_now()
|
|
4942
|
+
positions = getattr(tracker, "_positions", None)
|
|
4943
|
+
pos = positions.get(symbol) if isinstance(positions, dict) else None
|
|
4944
|
+
refreshed_currency = _stock_position_fill_currency(pos, symbol, market_code)
|
|
4945
|
+
if refreshed_currency is not None:
|
|
4946
|
+
await context._annotate_workflow_fill_currency(
|
|
4947
|
+
currency=refreshed_currency, **fill_kwargs
|
|
4948
|
+
)
|
|
4921
4949
|
logger.debug(f"📊 AccountTracker refreshed after fill")
|
|
4922
4950
|
|
|
4923
4951
|
asyncio.run_coroutine_threadsafe(record_and_refresh(), loop)
|
|
@@ -5491,6 +5519,11 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
5491
5519
|
로그인이라, 더미 secret 으로 도는 기기(트레이앱 검증 잡·서버 발급 모드)의 실제 실행에서
|
|
5492
5520
|
추적기만 403 으로 죽고 error 로그 뒤 추적 없이 진행됐다.
|
|
5493
5521
|
"""
|
|
5522
|
+
tracker_key = f"{context.job_id}_{node_id}"
|
|
5523
|
+
# Scheduled cycles revisit the broker. Keep its existing tracker (also
|
|
5524
|
+
# while startup awaits connect) instead of orphaning polling/WS resources.
|
|
5525
|
+
if context.is_shutdown or tracker_key in self._active_trackers:
|
|
5526
|
+
return
|
|
5494
5527
|
try:
|
|
5495
5528
|
from programgarden_finance import LS
|
|
5496
5529
|
|
|
@@ -5527,6 +5560,18 @@ class BrokerNodeExecutor(NodeExecutorBase):
|
|
|
5527
5560
|
|
|
5528
5561
|
except Exception as e:
|
|
5529
5562
|
context.log("error", f"Failed to start account tracking: {e}", node_id)
|
|
5563
|
+
# Failed startup must not leave an unusable entry that prevents a
|
|
5564
|
+
# later scheduled cycle from trying again. No immediate retry.
|
|
5565
|
+
info = self._active_trackers.get(tracker_key)
|
|
5566
|
+
if info is not None:
|
|
5567
|
+
for resource, method in ((info.get("tracker"), "stop"), (info.get("real"), "close")):
|
|
5568
|
+
if resource is not None:
|
|
5569
|
+
try:
|
|
5570
|
+
await getattr(resource, method)()
|
|
5571
|
+
except Exception as cleanup_error:
|
|
5572
|
+
logger.warning("Account tracker startup cleanup failed: %s", type(cleanup_error).__name__)
|
|
5573
|
+
if self._active_trackers.get(tracker_key) is info:
|
|
5574
|
+
self._active_trackers.pop(tracker_key)
|
|
5530
5575
|
|
|
5531
5576
|
async def _start_overseas_stock_tracker(
|
|
5532
5577
|
self,
|
|
@@ -6541,14 +6586,39 @@ class OpenOrdersNodeExecutor(NodeExecutorBase):
|
|
|
6541
6586
|
|
|
6542
6587
|
rows = response.block3 or []
|
|
6543
6588
|
usable_rows = [item for item in rows if item.OrdNo and item.OrdNo > 0]
|
|
6544
|
-
#
|
|
6545
|
-
#
|
|
6546
|
-
|
|
6589
|
+
# Match StockAccountTracker's observed pending-query envelope. The SDK
|
|
6590
|
+
# defaults missing details to []; only an explicit empty array is evidence.
|
|
6591
|
+
complete_empty = (
|
|
6547
6592
|
not rows
|
|
6548
|
-
and
|
|
6549
|
-
and
|
|
6550
|
-
and
|
|
6593
|
+
and "block3" in response.model_fields_set
|
|
6594
|
+
and response.block1 is not None
|
|
6595
|
+
and response.block2 is not None
|
|
6551
6596
|
)
|
|
6597
|
+
echo = response.block1
|
|
6598
|
+
header = response.header
|
|
6599
|
+
# 02679 is not a generic success code. This exact terminal response was
|
|
6600
|
+
# observed for market 00 (echoed as %) and this current-day pending query;
|
|
6601
|
+
# see finance/docs/observed_broker_responses.md (2026-09-09).
|
|
6602
|
+
observed_no_data = (
|
|
6603
|
+
complete_empty and status == 200 and diagnostics["rsp_cd"] == "02679"
|
|
6604
|
+
and echo.OrdDt == today and echo.OrdMktCode == "%"
|
|
6605
|
+
and echo.ThdayBnsAppYn == "1" and echo.BnsTpCode == "0"
|
|
6606
|
+
and echo.CrcyCode == "000"
|
|
6607
|
+
and all(
|
|
6608
|
+
str(getattr(echo, name)) == expected
|
|
6609
|
+
for name, expected in (
|
|
6610
|
+
("QryTpCode", "1"), ("BkseqTpCode", "1"),
|
|
6611
|
+
("IsuNo", ""), ("SrtOrdNo", "999999999"),
|
|
6612
|
+
("ExecYn", "2"), ("LoanBalHldYn", "0"),
|
|
6613
|
+
)
|
|
6614
|
+
if name in echo.model_fields_set
|
|
6615
|
+
)
|
|
6616
|
+
and header is not None and header.tr_cont == "N"
|
|
6617
|
+
and not header.tr_cont_key
|
|
6618
|
+
)
|
|
6619
|
+
valid_empty = complete_empty and (diagnostics["rsp_cd"] == "00000" or observed_no_data)
|
|
6620
|
+
if diagnostics["rsp_cd"] == "02679" and not observed_no_data:
|
|
6621
|
+
return unavailable()
|
|
6552
6622
|
if not usable_rows and not valid_empty:
|
|
6553
6623
|
return unavailable()
|
|
6554
6624
|
|
|
@@ -16963,9 +17033,11 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
16963
17033
|
p = float(getattr(item, "OvrsExecPrc", 0) or getattr(item, "OvrsOrdPrc", 0) or 0)
|
|
16964
17034
|
row = out.setdefault(order_no, {
|
|
16965
17035
|
"filled_qty": 0, "_amount": 0.0, "symbol": "", "fill_time": "",
|
|
17036
|
+
"_currencies": set(),
|
|
16966
17037
|
})
|
|
16967
17038
|
row["filled_qty"] += q
|
|
16968
17039
|
row["_amount"] += q * p
|
|
17040
|
+
row["_currencies"].add(str(item.CrcyCode or "").strip().upper())
|
|
16969
17041
|
# 🔴 LS 는 TR 마다 같은 뜻의 필드 이름이 다르고, 이 블록에는 `ShtnIsuNo`
|
|
16970
17042
|
# (단축종목번호)와 `IsuNo`(종목번호)가 **둘 다** 있다. 이 저장소의 관행은
|
|
16971
17043
|
# ShtnIsuNo 우선·IsuNo 폴백이다(잔고·체결 파싱이 전부 그렇게 한다).
|
|
@@ -16981,6 +17053,9 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
16981
17053
|
for row in out.values():
|
|
16982
17054
|
qty = row["filled_qty"]
|
|
16983
17055
|
row["avg_price"] = (row.pop("_amount") / qty) if qty > 0 else 0.0
|
|
17056
|
+
units = row.pop("_currencies")
|
|
17057
|
+
unit = next(iter(units)) if len(units) == 1 else ""
|
|
17058
|
+
row["currency"] = unit if re.fullmatch(r"[A-Z]{3}", unit) else None
|
|
16984
17059
|
return out
|
|
16985
17060
|
except Exception as e:
|
|
16986
17061
|
context.log("debug", f"COSAQ00102 batch fill query exception: {e}", node_id)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-1.37.6 → programgarden-1.37.8}/programgarden/database/workflow_risk_tracker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|