programgarden 2.5.4__tar.gz → 2.5.6__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.5.4 → programgarden-2.5.6}/PKG-INFO +1 -1
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/context.py +1 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/order_recovery.py +29 -8
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/workflow_position_tracker.py +11 -1
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/executor.py +23 -6
- {programgarden-2.5.4 → programgarden-2.5.6}/pyproject.toml +1 -1
- {programgarden-2.5.4 → programgarden-2.5.6}/README.md +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/__init__.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/binding_validator.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/client.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/code_worker.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/__init__.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/adjustment_delivery.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/broker_evidence.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/broker_order_totals.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/broker_snapshot.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/db_naming.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/execution_storage.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/fill_reconciler.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/order_cancellation.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/owned_order_cancellation.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/position_reconciliation.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/query_builder.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/deep_fixtures.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/futures_orderable.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/futures_pnl.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/futures_read_evidence.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/incremental_build.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/managed_order_control.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/node_runner.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/order_lifecycle.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/plugin/__init__.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/providers/__init__.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/reconnect_handler.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_contracts.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_events.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_external.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_order_adapter.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_orders.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_scenarios.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_semantics.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_sources.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_sqlite.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/replay_triggers.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/resolver.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/resource/__init__.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/resource/context.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/resource/limiter.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/resource/monitor.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/resource/throttle.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/semantic_rules.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/tools/__init__.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/tools/event_tools.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/tools/job_tools.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/validation_recommender.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/validation_replay.py +0 -0
- {programgarden-2.5.4 → programgarden-2.5.6}/programgarden/valuation.py +0 -0
|
@@ -260,6 +260,7 @@ class ExecutionContext:
|
|
|
260
260
|
# Stores trackers/connections that should stay alive between flow executions
|
|
261
261
|
self._persistent_nodes: Dict[str, Any] = {} # node_id -> tracker/connection
|
|
262
262
|
self._persistent_tasks: Dict[str, asyncio.Task] = {} # node_id -> background task
|
|
263
|
+
self._schedule_tick_source: Optional[str] = None # Only this timer may trigger the current cycle.
|
|
263
264
|
self._persistent_metadata: Dict[str, Dict[str, Any]] = {} # node_id -> {key: value}
|
|
264
265
|
|
|
265
266
|
# === New: Order Event Dispatcher ===
|
|
@@ -97,6 +97,9 @@ def initialize_recovery(conn):
|
|
|
97
97
|
columns = {row[1] for row in conn.execute("PRAGMA table_info(workflow_position_lots)")}
|
|
98
98
|
if "recovery_id" not in columns:
|
|
99
99
|
conn.execute("ALTER TABLE workflow_position_lots ADD COLUMN recovery_id INTEGER")
|
|
100
|
+
recovery_columns = {row[1] for row in conn.execute("PRAGMA table_info(workflow_order_recoveries)")}
|
|
101
|
+
if "unpriced_quantity" not in recovery_columns:
|
|
102
|
+
conn.execute("ALTER TABLE workflow_order_recoveries ADD COLUMN unpriced_quantity TEXT NOT NULL DEFAULT '0'")
|
|
100
103
|
|
|
101
104
|
|
|
102
105
|
def recovery_for_order(conn, tracker, day, number):
|
|
@@ -159,7 +162,8 @@ def recover_totals(tracker, totals):
|
|
|
159
162
|
with sqlite3.connect(tracker.db_path) as conn:
|
|
160
163
|
conn.execute("BEGIN IMMEDIATE")
|
|
161
164
|
# Aggregate execution times are only a recovery ordering, never invented
|
|
162
|
-
# timestamps for individual executions. Missing basis
|
|
165
|
+
# timestamps for individual executions. Missing basis invalidates PnL,
|
|
166
|
+
# not a broker-verified execution or the later fresh holdings check.
|
|
163
167
|
for total, (number, symbol, total_qty, price) in sorted(
|
|
164
168
|
validated, key=lambda item: (item[0].order_date, item[0].fill_time, int(item[1][0]))):
|
|
165
169
|
scope = tracker.product, tracker.provider, tracker.trading_mode
|
|
@@ -232,13 +236,18 @@ def recover_totals(tracker, totals):
|
|
|
232
236
|
SELECT 1 FROM trade_history WHERE product=? AND provider=? AND trading_mode=?
|
|
233
237
|
AND symbol=? AND classification='workflow' AND side='sell' AND fill_datetime>=? LIMIT 1
|
|
234
238
|
""", (*scope, symbol, fill_datetime)).fetchone()
|
|
239
|
+
later_recovery = conn.execute("""
|
|
240
|
+
SELECT 1 FROM workflow_order_recoveries WHERE product=? AND provider=? AND trading_mode=?
|
|
241
|
+
AND symbol=? AND side='sell' AND fill_datetime>=? LIMIT 1
|
|
242
|
+
""", (*scope, symbol, fill_datetime)).fetchone()
|
|
235
243
|
adjusted = conn.execute("SELECT 1 FROM sqlite_master WHERE name='position_adjustments'").fetchone()
|
|
236
244
|
if adjusted:
|
|
237
245
|
adjusted = conn.execute("""SELECT 1 FROM position_adjustments WHERE product=?
|
|
238
246
|
AND provider=? AND trading_mode=? AND symbol=? LIMIT 1""", (*scope, symbol)).fetchone()
|
|
239
|
-
if later_sell or adjusted:
|
|
247
|
+
if later_sell or later_recovery or adjusted:
|
|
240
248
|
raise ReconciliationUnavailable("historical_buy_requires_full_replay")
|
|
241
249
|
pnl = Decimal(0)
|
|
250
|
+
unpriced = Decimal(0)
|
|
242
251
|
lots = []
|
|
243
252
|
if total.side == "sell" and missing:
|
|
244
253
|
lots = conn.execute("""
|
|
@@ -246,9 +255,12 @@ def recover_totals(tracker, totals):
|
|
|
246
255
|
AND provider=? AND trading_mode=? AND symbol=? AND classification='workflow'
|
|
247
256
|
AND remaining_qty>0 AND fill_datetime<=? ORDER BY fill_datetime,id
|
|
248
257
|
""", (*scope, symbol, fill_datetime)).fetchall()
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
# Account-wide strategies can sell holdings acquired before this
|
|
259
|
+
# workflow. Never invent their purchase lots or a zero-cost basis.
|
|
260
|
+
# Validate stored numbers even when there are too few owned lots.
|
|
261
|
+
for _, basis, available in lots:
|
|
262
|
+
quantity(basis)
|
|
263
|
+
quantity(available)
|
|
252
264
|
cursor = conn.execute("""
|
|
253
265
|
INSERT INTO workflow_order_recoveries
|
|
254
266
|
(product,provider,trading_mode,order_date,order_no,symbol,exchange,side,
|
|
@@ -273,16 +285,25 @@ def recover_totals(tracker, totals):
|
|
|
273
285
|
remainder = missing
|
|
274
286
|
for lot_id, basis, available in lots:
|
|
275
287
|
used = min(remainder, quantity(available))
|
|
276
|
-
|
|
288
|
+
if quantity(basis) > 0:
|
|
289
|
+
pnl += (recovered_price - quantity(basis)) * used
|
|
290
|
+
else:
|
|
291
|
+
unpriced += used
|
|
277
292
|
conn.execute("UPDATE workflow_position_lots SET remaining_qty=? WHERE id=?",
|
|
278
293
|
(float(quantity(available) - used), lot_id))
|
|
279
294
|
remainder -= used
|
|
280
295
|
if not remainder:
|
|
281
296
|
break
|
|
282
|
-
|
|
297
|
+
unpriced += remainder
|
|
298
|
+
# estimated_pnl stores only the matched portion for audit. Public
|
|
299
|
+
# readers must return null whenever any quantity lacks a basis.
|
|
300
|
+
conn.execute("UPDATE workflow_order_recoveries SET estimated_pnl=?, unpriced_quantity=? WHERE id=?",
|
|
301
|
+
(str(pnl), str(unpriced), recovery_id))
|
|
283
302
|
results.append({"id": recovery_id, "order_date": total.order_date, "order_no": number,
|
|
284
303
|
"symbol": symbol, "side": total.side, "quantity": str(missing),
|
|
285
|
-
"estimated_pnl": str(pnl), "currency": total.currency,
|
|
304
|
+
"estimated_pnl": None if unpriced else str(pnl), "currency": total.currency,
|
|
305
|
+
"unpriced_quantity": str(unpriced),
|
|
306
|
+
"cost_basis_status": "unavailable" if unpriced else "estimated",
|
|
286
307
|
"basis": "broker_order_total", "is_estimated": True, "is_trade": False})
|
|
287
308
|
return results
|
|
288
309
|
|
{programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/workflow_position_tracker.py
RENAMED
|
@@ -1818,10 +1818,16 @@ class WorkflowPositionTracker:
|
|
|
1818
1818
|
"""Read the immutable recovery audit; this is not individual fill history."""
|
|
1819
1819
|
with sqlite3.connect(self.db_path) as conn:
|
|
1820
1820
|
conn.row_factory = sqlite3.Row
|
|
1821
|
-
|
|
1821
|
+
rows = [dict(row) for row in conn.execute(
|
|
1822
1822
|
"SELECT * FROM workflow_order_recoveries WHERE product=? AND provider=? "
|
|
1823
1823
|
"AND trading_mode=? ORDER BY id",
|
|
1824
1824
|
(self.product, self.provider, self.trading_mode))]
|
|
1825
|
+
for row in rows:
|
|
1826
|
+
unknown = Decimal(row["unpriced_quantity"]) > 0
|
|
1827
|
+
row["cost_basis_status"] = "unavailable" if unknown else "estimated"
|
|
1828
|
+
if unknown:
|
|
1829
|
+
row["estimated_pnl"] = None
|
|
1830
|
+
return rows
|
|
1825
1831
|
|
|
1826
1832
|
def get_position_adjustments(self, limit: int = 50) -> List[Dict[str, Any]]:
|
|
1827
1833
|
"""Return the current product/mode audit without creating synthetic trades."""
|
|
@@ -2249,6 +2255,10 @@ class WorkflowPositionTracker:
|
|
|
2249
2255
|
reason = "futures_fifo_not_monetary"
|
|
2250
2256
|
elif not symbol:
|
|
2251
2257
|
reason = "missing_symbol"
|
|
2258
|
+
elif any(fill.get("cost_basis_status") == "unavailable" for fill in fills):
|
|
2259
|
+
# Verified order quantity can permit startup without establishing
|
|
2260
|
+
# historical profit. Preserve the existing unavailable contract.
|
|
2261
|
+
reason = "incomplete_fifo_basis"
|
|
2252
2262
|
elif len(units - {None}) > 1 or unit_conflict:
|
|
2253
2263
|
# Contradictory units cannot establish a comparable FIFO basis.
|
|
2254
2264
|
reason = "incomplete_fifo_basis"
|
|
@@ -2227,12 +2227,12 @@ class ScheduleNodeExecutor(NodeExecutorBase):
|
|
|
2227
2227
|
) -> Dict[str, Any]:
|
|
2228
2228
|
from datetime import datetime
|
|
2229
2229
|
from zoneinfo import ZoneInfo
|
|
2230
|
-
from croniter import croniter
|
|
2230
|
+
from croniter import croniter, CroniterBadDateError
|
|
2231
2231
|
|
|
2232
2232
|
# 이미 스케줄러가 실행 중이면 재등록하지 않음 (schedule_tick 이벤트로 인한 재실행 시)
|
|
2233
2233
|
if node_id in context._persistent_tasks:
|
|
2234
2234
|
context.log("debug", f"Scheduler already running for {node_id}, skip", node_id)
|
|
2235
|
-
return {"trigger":
|
|
2235
|
+
return {"trigger": bool(context.is_dry_run or context._schedule_tick_source == node_id)}
|
|
2236
2236
|
|
|
2237
2237
|
# ⑭ cron is required — NO silent "*/5 * * * *" fallback. A missing cron
|
|
2238
2238
|
# used to make the schedule quietly run every 5 minutes ("looks fine,
|
|
@@ -2305,7 +2305,7 @@ class ScheduleNodeExecutor(NodeExecutorBase):
|
|
|
2305
2305
|
try:
|
|
2306
2306
|
# second_at_beginning=True로 초 단위 cron도 지원
|
|
2307
2307
|
try:
|
|
2308
|
-
itr = croniter(cron_expr, datetime.now(tz), second_at_beginning=True)
|
|
2308
|
+
itr = croniter(cron_expr, datetime.now(tz), second_at_beginning=True, max_years_between_matches=130)
|
|
2309
2309
|
except TypeError:
|
|
2310
2310
|
itr = croniter(cron_expr, datetime.now(tz))
|
|
2311
2311
|
|
|
@@ -2340,7 +2340,12 @@ class ScheduleNodeExecutor(NodeExecutorBase):
|
|
|
2340
2340
|
)
|
|
2341
2341
|
break
|
|
2342
2342
|
# 다음 실행 시간 계산
|
|
2343
|
-
|
|
2343
|
+
try:
|
|
2344
|
+
next_dt = itr.get_next(datetime)
|
|
2345
|
+
except CroniterBadDateError:
|
|
2346
|
+
# A calendar date/year can be exhausted. It is not an
|
|
2347
|
+
# execution failure and must never roll into another year.
|
|
2348
|
+
break
|
|
2344
2349
|
now = datetime.now(tz)
|
|
2345
2350
|
delay = (next_dt - now).total_seconds()
|
|
2346
2351
|
|
|
@@ -2421,8 +2426,9 @@ class ScheduleNodeExecutor(NodeExecutorBase):
|
|
|
2421
2426
|
task = asyncio.create_task(scheduler_task())
|
|
2422
2427
|
context.register_persistent_task(node_id, task)
|
|
2423
2428
|
|
|
2424
|
-
#
|
|
2425
|
-
|
|
2429
|
+
# Register the timer without executing downstream trading work early.
|
|
2430
|
+
# Explicit dry-run simulation still exercises one synthetic cycle.
|
|
2431
|
+
return {"trigger": bool(context.is_dry_run)}
|
|
2426
2432
|
|
|
2427
2433
|
|
|
2428
2434
|
class WatchlistNodeExecutor(NodeExecutorBase):
|
|
@@ -23557,6 +23563,14 @@ class WorkflowJob:
|
|
|
23557
23563
|
event = await self.context.wait_for_event(timeout=1.0)
|
|
23558
23564
|
|
|
23559
23565
|
if event is None:
|
|
23566
|
+
has_realtime_source = any(
|
|
23567
|
+
self.workflow.nodes[n].node_type in REALTIME_NODE_TYPES
|
|
23568
|
+
for n in self._stay_connected_nodes
|
|
23569
|
+
)
|
|
23570
|
+
if (self._has_schedule_node and not has_realtime_source
|
|
23571
|
+
and all(task.done() for task in self.context._persistent_tasks.values())):
|
|
23572
|
+
# Finite calendar/count schedules finish after queued ticks drain.
|
|
23573
|
+
break
|
|
23560
23574
|
continue
|
|
23561
23575
|
|
|
23562
23576
|
# Wait if paused
|
|
@@ -23595,6 +23609,7 @@ class WorkflowJob:
|
|
|
23595
23609
|
# RuntimeError 등)가 스케줄 잡 전체를 종료시키지 않도록 한다.
|
|
23596
23610
|
# 실패 사이클은 silent 하지 않게 'cycle_failed' 로 통지하고
|
|
23597
23611
|
# 다음 tick 에서 재시도한다 (24시간 무인 운영 보호).
|
|
23612
|
+
self.context._schedule_tick_source = event.source_node_id
|
|
23598
23613
|
try:
|
|
23599
23614
|
await self._execute_main_flow()
|
|
23600
23615
|
except Exception as cycle_err:
|
|
@@ -23606,6 +23621,8 @@ class WorkflowJob:
|
|
|
23606
23621
|
await self.context.notify_job_state("cycle_failed", self.stats)
|
|
23607
23622
|
else:
|
|
23608
23623
|
await self.context.notify_job_state("cycle_completed", self.stats)
|
|
23624
|
+
finally:
|
|
23625
|
+
self.context._schedule_tick_source = None
|
|
23609
23626
|
|
|
23610
23627
|
logger.info("Event loop ended")
|
|
23611
23628
|
|
|
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
|
{programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/owned_order_cancellation.py
RENAMED
|
File without changes
|
{programgarden-2.5.4 → programgarden-2.5.6}/programgarden/database/position_reconciliation.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
|
|
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
|