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.
- {programgarden-1.16.2 → programgarden-1.18.0}/PKG-INFO +21 -3
- {programgarden-1.16.2 → programgarden-1.18.0}/README.md +18 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/__init__.py +1 -1
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/client.py +8 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/context.py +13 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/executor.py +118 -7
- {programgarden-1.16.2 → programgarden-1.18.0}/pyproject.toml +3 -8
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/binding_validator.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/__init__.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/node_runner.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resolver.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/context.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.16.2 → programgarden-1.18.0}/programgarden/tools/registry_tools.py +0 -0
- {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.
|
|
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.
|
|
18
|
-
Requires-Dist: programgarden-core (>=1.
|
|
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
|
|
@@ -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.
|
|
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.
|
|
32
|
+
programgarden-core = "^1.10.0"
|
|
33
33
|
programgarden-finance = "^1.4.4"
|
|
34
|
-
programgarden-community = "^1.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-1.16.2 → programgarden-1.18.0}/programgarden/database/workflow_position_tracker.py
RENAMED
|
File without changes
|
{programgarden-1.16.2 → programgarden-1.18.0}/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
|