programgarden 1.37.7__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.7 → programgarden-1.37.8}/PKG-INFO +1 -1
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/context.py +17 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/database/fill_reconciler.py +1 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/database/workflow_position_tracker.py +85 -7
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/executor.py +34 -1
- {programgarden-1.37.7 → programgarden-1.37.8}/pyproject.toml +1 -1
- {programgarden-1.37.7 → programgarden-1.37.8}/README.md +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/__init__.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/binding_validator.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/client.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/code_worker.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/database/__init__.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/deep_fixtures.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/futures_pnl.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/node_runner.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/order_lifecycle.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/resolver.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/resource/context.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/semantic_rules.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.37.7 → programgarden-1.37.8}/programgarden/validation_recommender.py +0 -0
|
@@ -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.7 → 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)
|
|
@@ -17005,9 +17033,11 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
17005
17033
|
p = float(getattr(item, "OvrsExecPrc", 0) or getattr(item, "OvrsOrdPrc", 0) or 0)
|
|
17006
17034
|
row = out.setdefault(order_no, {
|
|
17007
17035
|
"filled_qty": 0, "_amount": 0.0, "symbol": "", "fill_time": "",
|
|
17036
|
+
"_currencies": set(),
|
|
17008
17037
|
})
|
|
17009
17038
|
row["filled_qty"] += q
|
|
17010
17039
|
row["_amount"] += q * p
|
|
17040
|
+
row["_currencies"].add(str(item.CrcyCode or "").strip().upper())
|
|
17011
17041
|
# 🔴 LS 는 TR 마다 같은 뜻의 필드 이름이 다르고, 이 블록에는 `ShtnIsuNo`
|
|
17012
17042
|
# (단축종목번호)와 `IsuNo`(종목번호)가 **둘 다** 있다. 이 저장소의 관행은
|
|
17013
17043
|
# ShtnIsuNo 우선·IsuNo 폴백이다(잔고·체결 파싱이 전부 그렇게 한다).
|
|
@@ -17023,6 +17053,9 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
17023
17053
|
for row in out.values():
|
|
17024
17054
|
qty = row["filled_qty"]
|
|
17025
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
|
|
17026
17059
|
return out
|
|
17027
17060
|
except Exception as e:
|
|
17028
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
|
|
File without changes
|
{programgarden-1.37.7 → 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
|