programgarden 1.16.2__tar.gz → 1.18.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 (33) hide show
  1. {programgarden-1.16.2 → programgarden-1.18.0}/PKG-INFO +21 -3
  2. {programgarden-1.16.2 → programgarden-1.18.0}/README.md +18 -0
  3. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/__init__.py +1 -1
  4. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/client.py +8 -0
  5. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/context.py +13 -0
  6. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/executor.py +118 -7
  7. {programgarden-1.16.2 → programgarden-1.18.0}/pyproject.toml +3 -8
  8. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/binding_validator.py +0 -0
  9. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/__init__.py +0 -0
  10. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/checkpoint_manager.py +0 -0
  11. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/query_builder.py +0 -0
  12. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/workflow_position_tracker.py +0 -0
  13. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/workflow_risk_tracker.py +0 -0
  14. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/node_runner.py +0 -0
  15. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/plugin/__init__.py +0 -0
  16. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/plugin/sandbox.py +0 -0
  17. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/providers/__init__.py +0 -0
  18. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/providers/llm_errors.py +0 -0
  19. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/providers/llm_provider.py +0 -0
  20. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/reconnect_handler.py +0 -0
  21. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resolver.py +0 -0
  22. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/__init__.py +0 -0
  23. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/context.py +0 -0
  24. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/limiter.py +0 -0
  25. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/monitor.py +0 -0
  26. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/throttle.py +0 -0
  27. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/__init__.py +0 -0
  28. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/credential_tools.py +0 -0
  29. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/definition_tools.py +0 -0
  30. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/event_tools.py +0 -0
  31. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/job_tools.py +0 -0
  32. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/registry_tools.py +0 -0
  33. {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/sqlite_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: programgarden
3
- Version: 1.16.2
3
+ Version: 1.18.0
4
4
  Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
5
5
  Author: 프로그램동산
6
6
  Author-email: coding@programgarden.com
@@ -14,8 +14,8 @@ Requires-Dist: croniter (>=6.0.0,<7.0.0)
14
14
  Requires-Dist: fastembed (>=0.4.0)
15
15
  Requires-Dist: litellm (>=1.40.0)
16
16
  Requires-Dist: lxml (>=6.0.2,<7.0.0)
17
- Requires-Dist: programgarden-community (>=1.10.5,<2.0.0)
18
- Requires-Dist: programgarden-core (>=1.9.9,<2.0.0)
17
+ Requires-Dist: programgarden-community (>=1.11.0,<2.0.0)
18
+ Requires-Dist: programgarden-core (>=1.10.0,<2.0.0)
19
19
  Requires-Dist: programgarden-finance (>=1.4.4,<2.0.0)
20
20
  Requires-Dist: psutil (>=6.0.0,<7.0.0)
21
21
  Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
@@ -90,6 +90,24 @@ job = await pg.run_async(
90
90
  await job.stop()
91
91
  ```
92
92
 
93
+ ### Dry Run (워크플로우 검증용 모의 실행)
94
+
95
+ 실제 주문/알림/Realtime WebSocket 연결 없이 워크플로우를 검증합니다.
96
+
97
+ - ScheduleNode / TradingHoursFilterNode → 1 cycle 후 즉시 종료
98
+ - 주문 노드 → LS API 미호출, `{"order_id": "DRYRUN-<uuid>", "status": "simulated", ...}` 반환
99
+ - Realtime 노드 → WebSocket 미개방, `{"status": "skipped_dry_run"}` 반환
100
+ - Messaging 노드(Telegram 등) → no-op, `{"status": "simulated"}` 반환
101
+ - 조회/백테스트 노드 → 기존 동작 유지 (실제 API 경로)
102
+
103
+ ```python
104
+ job = await pg.run_async(
105
+ definition=workflow_definition,
106
+ context={"dry_run": True},
107
+ secrets={...},
108
+ )
109
+ ```
110
+
93
111
  ### 워크플로우 정의 (JSON)
94
112
 
95
113
  ```json
@@ -62,6 +62,24 @@ job = await pg.run_async(
62
62
  await job.stop()
63
63
  ```
64
64
 
65
+ ### Dry Run (워크플로우 검증용 모의 실행)
66
+
67
+ 실제 주문/알림/Realtime WebSocket 연결 없이 워크플로우를 검증합니다.
68
+
69
+ - ScheduleNode / TradingHoursFilterNode → 1 cycle 후 즉시 종료
70
+ - 주문 노드 → LS API 미호출, `{"order_id": "DRYRUN-<uuid>", "status": "simulated", ...}` 반환
71
+ - Realtime 노드 → WebSocket 미개방, `{"status": "skipped_dry_run"}` 반환
72
+ - Messaging 노드(Telegram 등) → no-op, `{"status": "simulated"}` 반환
73
+ - 조회/백테스트 노드 → 기존 동작 유지 (실제 API 경로)
74
+
75
+ ```python
76
+ job = await pg.run_async(
77
+ definition=workflow_definition,
78
+ context={"dry_run": True},
79
+ secrets={...},
80
+ )
81
+ ```
82
+
65
83
  ### 워크플로우 정의 (JSON)
66
84
 
67
85
  ```json
@@ -80,7 +80,7 @@ except ImportError:
80
80
  # Community package not installed
81
81
  pass
82
82
 
83
- __version__ = "1.7.0"
83
+ __version__ = "1.18.0"
84
84
  __all__ = [
85
85
  # Core
86
86
  "ProgramGarden",
@@ -83,6 +83,14 @@ class ProgramGarden:
83
83
  ... secrets={"credential_id": {"appkey": "...", "appsecret": "..."}},
84
84
  ... storage_dir="./my_data",
85
85
  ... )
86
+
87
+ # Dry run (검증용): 주문/Realtime/알림 노드 실제 호출 없이 시뮬레이션
88
+ >>> job = pg.run(
89
+ ... workflow,
90
+ ... context={"dry_run": True},
91
+ ... wait=True,
92
+ ... timeout=60,
93
+ ... )
86
94
  """
87
95
  # Parse resource_limits if provided
88
96
  limits = None
@@ -278,6 +278,19 @@ class ExecutionContext:
278
278
  db_dir.mkdir(parents=True, exist_ok=True)
279
279
  return str(db_dir / db_filename)
280
280
 
281
+ # === Dry Run ===
282
+
283
+ @property
284
+ def is_dry_run(self) -> bool:
285
+ """Workflow is running in dry_run mode.
286
+
287
+ When True, side-effectful nodes (주문/Realtime/알림) skip external calls
288
+ and return simulated responses. Query/백테스트 nodes still execute normally.
289
+
290
+ Enable via ``context_params={"dry_run": True}`` in ``pg.run_async``.
291
+ """
292
+ return bool(self.context_params.get("dry_run", False))
293
+
281
294
  # === DAG Index Building ===
282
295
 
283
296
  def _build_dag_index(
@@ -364,7 +364,23 @@ class GenericNodeExecutor(NodeExecutorBase):
364
364
  except Exception as e:
365
365
  context.log("error", f"Failed to create node instance: {e}", node_id)
366
366
  return {"error": str(e)}
367
-
367
+
368
+ # dry_run 가드: Messaging/Notification 노드는 no-op 반환
369
+ if context.is_dry_run:
370
+ try:
371
+ from programgarden_core.nodes.base import NodeCategory
372
+ node_category = getattr(node_instance, "category", None)
373
+ if node_category == NodeCategory.MESSAGING:
374
+ context.log(
375
+ "info",
376
+ f"[dry_run] {node_type} skipped (messaging node)",
377
+ node_id,
378
+ )
379
+ return {"status": "simulated", "dry_run": True}
380
+ except Exception:
381
+ # NodeCategory import 실패는 무시 — dry_run 가드가 동작 안 할 뿐
382
+ pass
383
+
368
384
  # execute() 메서드 호출
369
385
  if hasattr(node_instance, "execute"):
370
386
  try:
@@ -916,6 +932,25 @@ class ScheduleNodeExecutor(NodeExecutorBase):
916
932
  itr = croniter(cron_expr, datetime.now(tz))
917
933
 
918
934
  while cnt < count and context.is_running:
935
+ # dry_run: 루프 진입 즉시 1회 emit 후 종료
936
+ if context.is_dry_run:
937
+ cnt += 1
938
+ context.log(
939
+ "info",
940
+ f"[dry_run] Schedule tick #{cnt} (single cycle), exiting scheduler",
941
+ node_id,
942
+ )
943
+ await context.emit_event(
944
+ event_type="schedule_tick",
945
+ source_node_id=node_id,
946
+ data={
947
+ "cron": cron_expr,
948
+ "count": cnt,
949
+ "triggered_at": datetime.now(tz).isoformat(),
950
+ "dry_run": True,
951
+ },
952
+ )
953
+ break
919
954
  # M-6: max_duration_hours 초과 시 스케줄 종료
920
955
  if (_time.monotonic() - start_mono) >= max_duration_sec:
921
956
  context.log(
@@ -3800,14 +3835,24 @@ class RealAccountNodeExecutor(NodeExecutorBase):
3800
3835
  **kwargs,
3801
3836
  ) -> Dict[str, Any]:
3802
3837
  import sys
3838
+
3839
+ # dry_run: WebSocket 미개방, skip 반환
3840
+ if context.is_dry_run:
3841
+ context.log(
3842
+ "warning",
3843
+ f"[dry_run] {node_type} skipped (realtime WebSocket disabled)",
3844
+ node_id,
3845
+ )
3846
+ return {"status": "skipped_dry_run", "dry_run": True}
3847
+
3803
3848
  # 옵션 확인
3804
3849
  stay_connected = config.get("stay_connected", True)
3805
3850
  sync_interval_sec = config.get("sync_interval_sec", 60)
3806
-
3807
-
3851
+
3852
+
3808
3853
  # config에서 connection 확인 (자동 주입 또는 명시적 바인딩)
3809
3854
  broker_connection = config.get("connection")
3810
-
3855
+
3811
3856
  # connection 없으면 에러 - 자동 주입 또는 명시적 바인딩 필요
3812
3857
  if not broker_connection:
3813
3858
  context.log("error", "RealAccountNode: connection이 자동 주입되지 않았습니다. 매칭되는 BrokerNode가 워크플로우에 있는지 확인하세요.", node_id)
@@ -4682,12 +4727,21 @@ class RealMarketDataNodeExecutor(NodeExecutorBase):
4682
4727
  **kwargs,
4683
4728
  ) -> Dict[str, Any]:
4684
4729
  from programgarden_core.exceptions import ValidationError, ConnectionError
4685
-
4730
+
4731
+ # dry_run: WebSocket 미개방, skip 반환
4732
+ if context.is_dry_run:
4733
+ context.log(
4734
+ "warning",
4735
+ f"[dry_run] {node_type} skipped (realtime WebSocket disabled)",
4736
+ node_id,
4737
+ )
4738
+ return {"status": "skipped_dry_run", "dry_run": True}
4739
+
4686
4740
  # ========================================
4687
4741
  # 1. BrokerNode connection 획득 (config 바인딩 필수)
4688
4742
  # ========================================
4689
4743
  broker_connection = config.get("connection")
4690
-
4744
+
4691
4745
  # connection 없으면 에러 - 자동 주입 또는 명시적 바인딩 필요
4692
4746
  if not broker_connection:
4693
4747
  error_msg = (
@@ -5343,9 +5397,18 @@ class RealOrderEventNodeExecutor(NodeExecutorBase):
5343
5397
  context: ExecutionContext,
5344
5398
  **kwargs,
5345
5399
  ) -> Dict[str, Any]:
5400
+ # dry_run: WebSocket 미개방, skip 반환
5401
+ if context.is_dry_run:
5402
+ context.log(
5403
+ "warning",
5404
+ f"[dry_run] {node_type} skipped (realtime WebSocket disabled)",
5405
+ node_id,
5406
+ )
5407
+ return {"status": "skipped_dry_run", "dry_run": True}
5408
+
5346
5409
  # 옵션 확인
5347
5410
  stay_connected = config.get("stay_connected", True)
5348
-
5411
+
5349
5412
  # config에서 connection 확인 (자동 주입 또는 명시적 바인딩)
5350
5413
  broker_connection = config.get("connection")
5351
5414
 
@@ -10830,6 +10893,22 @@ class NewOrderNodeExecutor(NodeExecutorBase):
10830
10893
  ) -> Dict[str, Any]:
10831
10894
  """신규 주문 실행"""
10832
10895
 
10896
+ # dry_run: LS API 미호출, 모의 응답 반환
10897
+ if context.is_dry_run:
10898
+ import uuid
10899
+ order_id = f"DRYRUN-{uuid.uuid4()}"
10900
+ context.log(
10901
+ "info",
10902
+ f"[dry_run] {node_type} simulated order → {order_id}",
10903
+ node_id,
10904
+ )
10905
+ return {
10906
+ "order_id": order_id,
10907
+ "status": "simulated",
10908
+ "dry_run": True,
10909
+ "requested": config,
10910
+ }
10911
+
10833
10912
  # M-10: Risk halt 체크 - critical risk event로 주문 중단
10834
10913
  if context.is_risk_halted:
10835
10914
  context.log(
@@ -11566,6 +11645,22 @@ class ModifyOrderNodeExecutor(NodeExecutorBase):
11566
11645
  ) -> Dict[str, Any]:
11567
11646
  """주문 정정 실행"""
11568
11647
 
11648
+ # dry_run: LS API 미호출, 모의 응답 반환
11649
+ if context.is_dry_run:
11650
+ import uuid
11651
+ order_id = f"DRYRUN-{uuid.uuid4()}"
11652
+ context.log(
11653
+ "info",
11654
+ f"[dry_run] {node_type} simulated modify → {order_id}",
11655
+ node_id,
11656
+ )
11657
+ return {
11658
+ "order_id": order_id,
11659
+ "status": "simulated",
11660
+ "dry_run": True,
11661
+ "requested": config,
11662
+ }
11663
+
11569
11664
  # M-10: Risk halt 체크
11570
11665
  if context.is_risk_halted:
11571
11666
  context.log("warning", f"{node_type}: 위험 이벤트로 인해 주문 정정이 중단되었습니다", node_id)
@@ -11975,6 +12070,22 @@ class CancelOrderNodeExecutor(NodeExecutorBase):
11975
12070
  ) -> Dict[str, Any]:
11976
12071
  """주문 취소 실행"""
11977
12072
 
12073
+ # dry_run: LS API 미호출, 모의 응답 반환
12074
+ if context.is_dry_run:
12075
+ import uuid
12076
+ order_id = f"DRYRUN-{uuid.uuid4()}"
12077
+ context.log(
12078
+ "info",
12079
+ f"[dry_run] {node_type} simulated cancel → {order_id}",
12080
+ node_id,
12081
+ )
12082
+ return {
12083
+ "order_id": order_id,
12084
+ "status": "simulated",
12085
+ "dry_run": True,
12086
+ "requested": config,
12087
+ }
12088
+
11978
12089
  # === 1. Connection 확인 ===
11979
12090
  broker_connection = config.get("connection")
11980
12091
  if not broker_connection:
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden"
8
- version = "1.16.2"
8
+ version = "1.18.0"
9
9
  description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
10
10
  readme = "README.md"
11
11
 
@@ -29,9 +29,9 @@ pytickersymbols = {version = ">=1.17.5", python = ">=3.12,<4.0"}
29
29
  aiosqlite = "^0.20.0"
30
30
  litellm = ">=1.40.0"
31
31
  fastembed = ">=0.4.0"
32
- programgarden-core = "^1.9.9"
32
+ programgarden-core = "^1.10.0"
33
33
  programgarden-finance = "^1.4.4"
34
- programgarden-community = "^1.10.5"
34
+ programgarden-community = "^1.11.0"
35
35
 
36
36
  [tool.poetry.group.dev.dependencies]
37
37
  pytest = "^8.0.0"
@@ -42,11 +42,6 @@ fastapi = "^0.128.0"
42
42
  uvicorn = "^0.40.0"
43
43
 
44
44
 
45
- [[tool.poetry.source]]
46
- name = "testpypi"
47
- url = "https://test.pypi.org/simple/"
48
- priority = "supplemental"
49
-
50
45
  [build-system]
51
46
  requires = ["poetry-core>=2.0.0,<3.0.0"]
52
47
  build-backend = "poetry.core.masonry.api"