programgarden 2.3.0__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {programgarden-2.3.0 → programgarden-2.4.0}/PKG-INFO +2 -2
  2. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/executor.py +10 -6
  3. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_events.py +7 -1
  4. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_semantics.py +7 -4
  5. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_triggers.py +7 -2
  6. {programgarden-2.3.0 → programgarden-2.4.0}/pyproject.toml +2 -2
  7. {programgarden-2.3.0 → programgarden-2.4.0}/README.md +0 -0
  8. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/__init__.py +0 -0
  9. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/binding_validator.py +0 -0
  10. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/client.py +0 -0
  11. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/code_worker.py +0 -0
  12. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/context.py +0 -0
  13. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/__init__.py +0 -0
  14. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/adjustment_delivery.py +0 -0
  15. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/broker_evidence.py +0 -0
  16. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/broker_order_totals.py +0 -0
  17. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/broker_snapshot.py +0 -0
  18. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/checkpoint_manager.py +0 -0
  19. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/db_naming.py +0 -0
  20. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/execution_storage.py +0 -0
  21. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/fill_reconciler.py +0 -0
  22. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/order_cancellation.py +0 -0
  23. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/order_recovery.py +0 -0
  24. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/owned_order_cancellation.py +0 -0
  25. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/position_reconciliation.py +0 -0
  26. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/query_builder.py +0 -0
  27. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/workflow_position_tracker.py +0 -0
  28. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/database/workflow_risk_tracker.py +0 -0
  29. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/deep_fixtures.py +0 -0
  30. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/futures_orderable.py +0 -0
  31. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/futures_pnl.py +0 -0
  32. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/futures_read_evidence.py +0 -0
  33. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/incremental_build.py +0 -0
  34. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/managed_order_control.py +0 -0
  35. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/node_runner.py +0 -0
  36. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/order_lifecycle.py +0 -0
  37. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/plugin/__init__.py +0 -0
  38. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/plugin/sandbox.py +0 -0
  39. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/providers/__init__.py +0 -0
  40. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/providers/llm_errors.py +0 -0
  41. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/providers/llm_provider.py +0 -0
  42. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/reconnect_handler.py +0 -0
  43. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_contracts.py +0 -0
  44. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_external.py +0 -0
  45. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_order_adapter.py +0 -0
  46. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_orders.py +0 -0
  47. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_scenarios.py +0 -0
  48. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_sources.py +0 -0
  49. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/replay_sqlite.py +0 -0
  50. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/resolver.py +0 -0
  51. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/resource/__init__.py +0 -0
  52. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/resource/context.py +0 -0
  53. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/resource/limiter.py +0 -0
  54. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/resource/monitor.py +0 -0
  55. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/resource/throttle.py +0 -0
  56. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/semantic_rules.py +0 -0
  57. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/tools/__init__.py +0 -0
  58. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/tools/credential_tools.py +0 -0
  59. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/tools/definition_tools.py +0 -0
  60. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/tools/event_tools.py +0 -0
  61. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/tools/job_tools.py +0 -0
  62. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/tools/registry_tools.py +0 -0
  63. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/tools/sqlite_tools.py +0 -0
  64. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/validation_recommender.py +0 -0
  65. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/validation_replay.py +0 -0
  66. {programgarden-2.3.0 → programgarden-2.4.0}/programgarden/valuation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -16,7 +16,7 @@ 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
18
  Requires-Dist: programgarden-community (>=2.2.0,<3.0.0)
19
- Requires-Dist: programgarden-core (>=2.3.0,<3.0.0)
19
+ Requires-Dist: programgarden-core (>=2.4.0,<3.0.0)
20
20
  Requires-Dist: programgarden-finance (>=2.0.1,<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)
@@ -2253,8 +2253,10 @@ class ScheduleNodeExecutor(NodeExecutorBase):
2253
2253
  cron_expr = cron_expr.strip()
2254
2254
  tz_name = config.get("timezone", "America/New_York")
2255
2255
  enabled = config.get("enabled", True)
2256
- count = config.get("count", 1000) # 최대 반복 횟수 (M-6: 기본값 축소; 모델 필드로 편입)
2257
- max_duration_hours = config.get("max_duration_hours", 24.0)
2256
+ # None/absent = unbounded: run until the workflow is stopped (owner
2257
+ # 2026-09-26). A provided value is enforced as a cap in the loop below.
2258
+ count = config.get("count") # None → no cycle cap
2259
+ max_duration_hours = config.get("max_duration_hours") # None → no wall-clock cap
2258
2260
 
2259
2261
  if not enabled:
2260
2262
  context.log("info", f"Schedule disabled: {cron_expr}", node_id)
@@ -2289,7 +2291,8 @@ class ScheduleNodeExecutor(NodeExecutorBase):
2289
2291
  import time as _time
2290
2292
  cnt = 0
2291
2293
  start_mono = _time.monotonic()
2292
- max_duration_sec = max_duration_hours * 3600
2294
+ # None = unbounded; only compute a wall-clock cap when a bound was set.
2295
+ max_duration_sec = max_duration_hours * 3600 if max_duration_hours is not None else None
2293
2296
  try:
2294
2297
  # second_at_beginning=True로 초 단위 cron도 지원
2295
2298
  try:
@@ -2297,7 +2300,7 @@ class ScheduleNodeExecutor(NodeExecutorBase):
2297
2300
  except TypeError:
2298
2301
  itr = croniter(cron_expr, datetime.now(tz))
2299
2302
 
2300
- while cnt < count and context.is_running:
2303
+ while (count is None or cnt < count) and context.is_running:
2301
2304
  # dry_run: 루프 진입 즉시 1회 emit 후 종료
2302
2305
  if context.is_dry_run:
2303
2306
  cnt += 1
@@ -2317,8 +2320,9 @@ class ScheduleNodeExecutor(NodeExecutorBase):
2317
2320
  },
2318
2321
  )
2319
2322
  break
2320
- # M-6: max_duration_hours 초과 시 스케줄 종료
2321
- if (_time.monotonic() - start_mono) >= max_duration_sec:
2323
+ # M-6: max_duration_hours 초과 시 스케줄 종료 (한도가 설정된 경우에만;
2324
+ # None 이면 사용자가 워크플로우를 멈출 때까지 계속 실행)
2325
+ if max_duration_sec is not None and (_time.monotonic() - start_mono) >= max_duration_sec:
2322
2326
  context.log(
2323
2327
  "warning",
2324
2328
  f"Schedule max_duration_hours={max_duration_hours}h 초과 - "
@@ -128,7 +128,13 @@ async def replay_events(job, runner, fixture, outcome):
128
128
  if now != expected:
129
129
  raise ContractViolation(source_id, "Recorded tick is not the next configured cron instant")
130
130
  count = schedule_counts.get(source_id, 0) + 1
131
- if count > config.get("count", 1000) or (now - origin).total_seconds() > config.get("max_duration_hours", 24.0) * 3600:
131
+ # count / max_duration_hours are optional (None/absent = unbounded,
132
+ # run until the workflow is stopped). Only enforce a bound that was set.
133
+ cfg_count = config.get("count")
134
+ cfg_max_dur = config.get("max_duration_hours")
135
+ if (cfg_count is not None and count > cfg_count) or (
136
+ cfg_max_dur is not None and (now - origin).total_seconds() > cfg_max_dur * 3600
137
+ ):
132
138
  raise ContractViolation(source_id, "Recorded ticks exceed schedule safety limits")
133
139
  schedule_instants[source_id], schedule_counts[source_id] = now, count
134
140
  else:
@@ -349,8 +349,9 @@ def _time_rules(node_type: str, role: str) -> List[Dict[str, Any]]:
349
349
  {"id": "timezone_iana", "field": "timezone",
350
350
  "default": "America/New_York", "invalid": "replay_rejects"},
351
351
  {"id": "disabled_emits_nothing", "field": "enabled"},
352
- {"id": "limits", "count": {"default": 1000, "min": 1},
353
- "max_duration_hours": {"default": 24.0, "gt": 0}, "pin": "range_not_const"},
352
+ {"id": "limits", "optional": True, "absent_means": "unbounded",
353
+ "count": {"default": None, "min": 1},
354
+ "max_duration_hours": {"default": None, "gt": 0}, "pin": "range_not_const"},
354
355
  ]
355
356
  if node_type == "SessionGateNode":
356
357
  return [{"id": "evaluate_at_fixture_instant"}] # validation_replay.py:276-283
@@ -607,8 +608,10 @@ def _schedule_time_lines(lines: List[str]) -> None:
607
608
  lines.append("A recorded tick must equal the cron's next firing instant after "
608
609
  "the prior frame in this timezone: a whole minute, at least one "
609
610
  "minute later.")
610
- lines.append("count and max_duration_hours are safety limits (defaults 1000 and "
611
- "24.0); a contract bounds them with a range, never a const.")
611
+ lines.append("count and max_duration_hours are optional safety limits; omit both "
612
+ "and the schedule runs until the workflow is stopped. When provided, "
613
+ "count must be >= 1 and max_duration_hours > 0, and a contract bounds "
614
+ "them with a range, never a const.")
612
615
  lines.append("Set the cron time in the target market's timezone. Market "
613
616
  "operating hours by product:")
614
617
  pe = product_execution()
@@ -49,8 +49,13 @@ async def schedule_startup(node, config, context):
49
49
  ZoneInfo(node.timezone)
50
50
  except (ValueError, KeyError):
51
51
  raise ContractViolation(node.id, "Invalid schedule timezone") from None
52
- if node.count < 1 or node.max_duration_hours <= 0:
53
- raise ContractViolation(node.id, "Schedule safety limits must be positive")
52
+ # count / max_duration_hours are optional (None = unbounded, run until the
53
+ # workflow is stopped). Only a provided value is bounded (count >= 1,
54
+ # max_duration_hours > 0).
55
+ if (node.count is not None and node.count < 1) or (
56
+ node.max_duration_hours is not None and node.max_duration_hours <= 0
57
+ ):
58
+ raise ContractViolation(node.id, "Schedule safety limits must be positive when set")
54
59
  proxy = _StartupContext(context)
55
60
  try:
56
61
  return await ScheduleNodeExecutor().execute(node.id, node.type, config, proxy)
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden"
8
- version = "2.3.0"
8
+ version = "2.4.0"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
11
11
  readme = "README.md"
@@ -36,7 +36,7 @@ litellm = ">=1.40.0"
36
36
  # 로 통째 실패한다(1.35.1 의 조용한 TypeError 보다 강한 모듈 로드 실패).
37
37
  # (1.26.0 이후로도 context.py 는 WorkflowPnLEvent(**event_data) 에 personal_metrics
38
38
  # 를 무조건 실으므로 그 하한 근거도 이 상한에 포함된다.)
39
- programgarden-core = "^2.3.0"
39
+ programgarden-core = "^2.4.0"
40
40
  programgarden-finance = "^2.0.1"
41
41
  programgarden-community = "^2.2.0"
42
42
 
File without changes