programgarden 1.4.0__tar.gz → 1.5.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.4.0 → programgarden-1.5.0}/PKG-INFO +4 -4
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/executor.py +142 -1
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/resolver.py +3 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/pyproject.toml +4 -4
- {programgarden-1.4.0 → programgarden-1.5.0}/README.md +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/__init__.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/binding_validator.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/client.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/context.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/database/__init__.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/resource/context.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.0}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.4.0 → programgarden-1.5.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.5.0
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
Author: 프로그램동산
|
|
6
6
|
Author-email: coding@programgarden.com
|
|
@@ -13,9 +13,9 @@ Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
|
|
|
13
13
|
Requires-Dist: croniter (>=6.0.0,<7.0.0)
|
|
14
14
|
Requires-Dist: litellm (>=1.40.0)
|
|
15
15
|
Requires-Dist: lxml (>=6.0.2,<7.0.0)
|
|
16
|
-
Requires-Dist: programgarden-community (>=1.
|
|
17
|
-
Requires-Dist: programgarden-core (>=1.
|
|
18
|
-
Requires-Dist: programgarden-finance (>=1.
|
|
16
|
+
Requires-Dist: programgarden-community (>=1.5.0,<2.0.0)
|
|
17
|
+
Requires-Dist: programgarden-core (>=1.3.0,<2.0.0)
|
|
18
|
+
Requires-Dist: programgarden-finance (>=1.3.0,<2.0.0)
|
|
19
19
|
Requires-Dist: psutil (>=6.0.0,<7.0.0)
|
|
20
20
|
Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
|
|
21
21
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
@@ -6181,6 +6181,68 @@ class LogicNodeExecutor(NodeExecutorBase):
|
|
|
6181
6181
|
return final_result, final_passed
|
|
6182
6182
|
|
|
6183
6183
|
|
|
6184
|
+
class IfNodeExecutor(NodeExecutorBase):
|
|
6185
|
+
"""
|
|
6186
|
+
IfNode executor - 조건 분기
|
|
6187
|
+
|
|
6188
|
+
left op right를 평가하여 true/false 포트로 흐름 분기.
|
|
6189
|
+
- true 포트: 조건 충족 시 upstream 데이터 pass-through
|
|
6190
|
+
- false 포트: 조건 미충족 시 upstream 데이터 pass-through
|
|
6191
|
+
- result 포트: 조건 평가 결과 (boolean)
|
|
6192
|
+
"""
|
|
6193
|
+
|
|
6194
|
+
async def execute(
|
|
6195
|
+
self,
|
|
6196
|
+
node_id: str,
|
|
6197
|
+
node_type: str,
|
|
6198
|
+
config: Dict[str, Any],
|
|
6199
|
+
context: ExecutionContext,
|
|
6200
|
+
**kwargs,
|
|
6201
|
+
) -> Dict[str, Any]:
|
|
6202
|
+
config = evaluate_all_bindings(config, context, node_id)
|
|
6203
|
+
|
|
6204
|
+
left = config.get("left")
|
|
6205
|
+
operator = config.get("operator", "==")
|
|
6206
|
+
right = config.get("right")
|
|
6207
|
+
|
|
6208
|
+
result = self._evaluate(left, operator, right)
|
|
6209
|
+
context.log(
|
|
6210
|
+
"info",
|
|
6211
|
+
f"IfNode: {left!r} {operator} {right!r} → {result}",
|
|
6212
|
+
node_id,
|
|
6213
|
+
)
|
|
6214
|
+
|
|
6215
|
+
# upstream에서 전달된 데이터 (pass-through)
|
|
6216
|
+
passthrough = kwargs.get("input_data") or {}
|
|
6217
|
+
|
|
6218
|
+
return {
|
|
6219
|
+
"true": passthrough if result else None,
|
|
6220
|
+
"false": passthrough if not result else None,
|
|
6221
|
+
"result": result,
|
|
6222
|
+
"_if_branch": "true" if result else "false",
|
|
6223
|
+
}
|
|
6224
|
+
|
|
6225
|
+
@staticmethod
|
|
6226
|
+
def _evaluate(left: Any, operator: str, right: Any) -> bool:
|
|
6227
|
+
"""비교 연산 수행"""
|
|
6228
|
+
try:
|
|
6229
|
+
if operator == "==": return left == right
|
|
6230
|
+
if operator == "!=": return left != right
|
|
6231
|
+
if operator == ">": return float(left) > float(right)
|
|
6232
|
+
if operator == ">=": return float(left) >= float(right)
|
|
6233
|
+
if operator == "<": return float(left) < float(right)
|
|
6234
|
+
if operator == "<=": return float(left) <= float(right)
|
|
6235
|
+
if operator == "in": return left in right
|
|
6236
|
+
if operator == "not_in": return left not in right
|
|
6237
|
+
if operator == "contains": return right in left
|
|
6238
|
+
if operator == "not_contains": return right not in left
|
|
6239
|
+
if operator == "is_empty": return not left
|
|
6240
|
+
if operator == "is_not_empty": return bool(left)
|
|
6241
|
+
except (TypeError, ValueError):
|
|
6242
|
+
return False
|
|
6243
|
+
return False
|
|
6244
|
+
|
|
6245
|
+
|
|
6184
6246
|
class MarketDataNodeExecutor(NodeExecutorBase):
|
|
6185
6247
|
"""
|
|
6186
6248
|
MarketDataNode executor - REST API 현재가 조회 (당일 데이터만)
|
|
@@ -11511,6 +11573,7 @@ class WorkflowExecutor:
|
|
|
11511
11573
|
"DisplayNode": DisplayNodeExecutor(),
|
|
11512
11574
|
"ConditionNode": ConditionNodeExecutor(),
|
|
11513
11575
|
"LogicNode": LogicNodeExecutor(), # 조건 조합
|
|
11576
|
+
"IfNode": IfNodeExecutor(), # 조건 분기
|
|
11514
11577
|
# Backtest nodes
|
|
11515
11578
|
"HistoricalDataNode": HistoricalDataNodeExecutor(),
|
|
11516
11579
|
"OverseasStockHistoricalDataNode": HistoricalDataNodeExecutor(),
|
|
@@ -12052,6 +12115,9 @@ class WorkflowJob:
|
|
|
12052
12115
|
# Track which SplitNodes have been processed
|
|
12053
12116
|
processed_splits: Dict[str, bool] = {}
|
|
12054
12117
|
|
|
12118
|
+
# IfNode: 비활성 브랜치 스킵 집합 (IfNode 실행 후 동적 갱신)
|
|
12119
|
+
if_skipped_nodes: Set[str] = set()
|
|
12120
|
+
|
|
12055
12121
|
# 🆕 실행 시작 전 모든 노드 상태를 pending으로 리셋 (UI 깜빡임 효과)
|
|
12056
12122
|
for node_id in self.workflow.execution_order:
|
|
12057
12123
|
node = self.workflow.nodes.get(node_id)
|
|
@@ -12090,6 +12156,16 @@ class WorkflowJob:
|
|
|
12090
12156
|
print(f" ⏭ Skipping AggregateNode: {node_id} (already executed by SplitNode)")
|
|
12091
12157
|
continue
|
|
12092
12158
|
|
|
12159
|
+
# === IfNode: 비활성 브랜치 스킵 ===
|
|
12160
|
+
if node_id in if_skipped_nodes:
|
|
12161
|
+
print(f" ⏭ Skipping node: {node_id} (IfNode branch not taken)")
|
|
12162
|
+
await self.context.notify_node_state(
|
|
12163
|
+
node_id=node_id,
|
|
12164
|
+
node_type=node.node_type,
|
|
12165
|
+
state=NodeState.COMPLETED,
|
|
12166
|
+
)
|
|
12167
|
+
continue
|
|
12168
|
+
|
|
12093
12169
|
print(f" ▶ Executing node: {node_id} ({node.node_type})")
|
|
12094
12170
|
|
|
12095
12171
|
# 🆕 노드 실행 시작 알림
|
|
@@ -12216,6 +12292,14 @@ class WorkflowJob:
|
|
|
12216
12292
|
workflow=self.workflow,
|
|
12217
12293
|
)
|
|
12218
12294
|
|
|
12295
|
+
# IfNode: 스킵 노드 계산 후 내부 키 제거
|
|
12296
|
+
if node.node_type == "IfNode" and outputs:
|
|
12297
|
+
taken = outputs.pop("_if_branch", "true")
|
|
12298
|
+
new_skips = self._compute_if_skip_nodes(node_id, taken)
|
|
12299
|
+
if_skipped_nodes.update(new_skips)
|
|
12300
|
+
if new_skips:
|
|
12301
|
+
print(f" 🔀 IfNode {node_id}: branch={taken}, skipping {new_skips}")
|
|
12302
|
+
|
|
12219
12303
|
# Store outputs
|
|
12220
12304
|
for out_port_name, value in outputs.items():
|
|
12221
12305
|
self.context.set_output(node_id, out_port_name, value)
|
|
@@ -12252,7 +12336,7 @@ class WorkflowJob:
|
|
|
12252
12336
|
# 자동 반복 실행에서 제외할 노드 타입 (배열을 그대로 처리하는 노드)
|
|
12253
12337
|
NO_AUTO_ITERATE_NODE_TYPES = {
|
|
12254
12338
|
"SplitNode", "AggregateNode", # 명시적 반복 제어 노드
|
|
12255
|
-
"StartNode", "ThrottleNode",
|
|
12339
|
+
"StartNode", "ThrottleNode", "IfNode", # 인프라 노드
|
|
12256
12340
|
"TableDisplayNode", "LineChartNode", "MultiLineChartNode", # 디스플레이 노드 (배열 표시)
|
|
12257
12341
|
"CandlestickChartNode", "BarChartNode", "SummaryDisplayNode",
|
|
12258
12342
|
"WatchlistNode", "MarketUniverseNode", "ScreenerNode", # 배열 생성 노드
|
|
@@ -12386,6 +12470,63 @@ class WorkflowJob:
|
|
|
12386
12470
|
|
|
12387
12471
|
return merged
|
|
12388
12472
|
|
|
12473
|
+
def _compute_if_skip_nodes(self, if_node_id: str, taken_branch: str) -> Set[str]:
|
|
12474
|
+
"""IfNode 실행 결과에 따라 스킵할 노드 집합 계산 (캐스케이딩)
|
|
12475
|
+
|
|
12476
|
+
비활성 브랜치(from_port)의 하위 노드를 BFS로 탐색하고,
|
|
12477
|
+
다른 활성 경로가 없는 노드만 스킵합니다.
|
|
12478
|
+
|
|
12479
|
+
Args:
|
|
12480
|
+
if_node_id: IfNode ID
|
|
12481
|
+
taken_branch: 선택된 브랜치 ("true" or "false")
|
|
12482
|
+
|
|
12483
|
+
Returns:
|
|
12484
|
+
스킵할 노드 ID 집합
|
|
12485
|
+
"""
|
|
12486
|
+
skipped_port = "false" if taken_branch == "true" else "true"
|
|
12487
|
+
|
|
12488
|
+
# 1. 비활성 브랜치의 직접 하위 노드 찾기
|
|
12489
|
+
initial_skip = set()
|
|
12490
|
+
for edge in self.workflow.edges:
|
|
12491
|
+
if (edge.from_node_id == if_node_id
|
|
12492
|
+
and edge.is_dag_edge
|
|
12493
|
+
and edge.from_port == skipped_port):
|
|
12494
|
+
initial_skip.add(edge.to_node_id)
|
|
12495
|
+
|
|
12496
|
+
# 2. BFS로 캐스케이딩 스킵 전파
|
|
12497
|
+
skip_nodes: Set[str] = set()
|
|
12498
|
+
queue = list(initial_skip)
|
|
12499
|
+
|
|
12500
|
+
while queue:
|
|
12501
|
+
current = queue.pop(0)
|
|
12502
|
+
if current in skip_nodes:
|
|
12503
|
+
continue
|
|
12504
|
+
|
|
12505
|
+
# 이 노드의 모든 incoming main edge 확인
|
|
12506
|
+
incoming = [e for e in self.workflow.edges
|
|
12507
|
+
if e.to_node_id == current and e.is_dag_edge]
|
|
12508
|
+
|
|
12509
|
+
all_inactive = True
|
|
12510
|
+
for edge in incoming:
|
|
12511
|
+
if edge.from_node_id == if_node_id:
|
|
12512
|
+
# IfNode에서 오는 edge: 활성 브랜치면 스킵 안 함
|
|
12513
|
+
if edge.from_port == taken_branch or edge.from_port is None:
|
|
12514
|
+
all_inactive = False
|
|
12515
|
+
break
|
|
12516
|
+
elif edge.from_node_id not in skip_nodes:
|
|
12517
|
+
# 다른 활성 노드에서 오는 edge가 있으면 스킵 안 함
|
|
12518
|
+
all_inactive = False
|
|
12519
|
+
break
|
|
12520
|
+
|
|
12521
|
+
if all_inactive:
|
|
12522
|
+
skip_nodes.add(current)
|
|
12523
|
+
# 하위 노드도 확인 대상에 추가
|
|
12524
|
+
for edge in self.workflow.edges:
|
|
12525
|
+
if edge.from_node_id == current and edge.is_dag_edge:
|
|
12526
|
+
queue.append(edge.to_node_id)
|
|
12527
|
+
|
|
12528
|
+
return skip_nodes
|
|
12529
|
+
|
|
12389
12530
|
def _find_split_aggregate_pairs(self) -> Dict[str, str]:
|
|
12390
12531
|
"""Find pairs of SplitNode → AggregateNode in the workflow
|
|
12391
12532
|
|
|
@@ -64,10 +64,12 @@ class ResolvedEdge:
|
|
|
64
64
|
from_node_id: str,
|
|
65
65
|
to_node_id: str,
|
|
66
66
|
edge_type: str = "main",
|
|
67
|
+
from_port: str = None,
|
|
67
68
|
):
|
|
68
69
|
self.from_node_id = from_node_id
|
|
69
70
|
self.to_node_id = to_node_id
|
|
70
71
|
self.edge_type = edge_type
|
|
72
|
+
self.from_port = from_port
|
|
71
73
|
|
|
72
74
|
@property
|
|
73
75
|
def is_dag_edge(self) -> bool:
|
|
@@ -512,6 +514,7 @@ class WorkflowResolver:
|
|
|
512
514
|
from_node_id=edge.from_node_id,
|
|
513
515
|
to_node_id=edge.to_node_id,
|
|
514
516
|
edge_type=edge_type_str,
|
|
517
|
+
from_port=getattr(edge, 'from_port', None),
|
|
515
518
|
)
|
|
516
519
|
if edge.is_dag_edge:
|
|
517
520
|
dag_edges.append(resolved_edge)
|
|
@@ -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.5.0"
|
|
9
9
|
description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
|
|
@@ -28,9 +28,9 @@ lxml = "^6.0.2"
|
|
|
28
28
|
pytickersymbols = {version = ">=1.17.5", python = ">=3.12,<4.0"}
|
|
29
29
|
aiosqlite = "^0.20.0"
|
|
30
30
|
litellm = ">=1.40.0"
|
|
31
|
-
programgarden-core = "^1.
|
|
32
|
-
programgarden-finance = "^1.
|
|
33
|
-
programgarden-community = "^1.
|
|
31
|
+
programgarden-core = "^1.3.0"
|
|
32
|
+
programgarden-finance = "^1.3.0"
|
|
33
|
+
programgarden-community = "^1.5.0"
|
|
34
34
|
|
|
35
35
|
[tool.poetry.group.dev.dependencies]
|
|
36
36
|
pytest = "^8.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-1.4.0 → programgarden-1.5.0}/programgarden/database/workflow_position_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
|