programgarden 1.29.4__tar.gz → 1.30.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.29.4 → programgarden-1.30.0}/PKG-INFO +2 -2
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/executor.py +65 -7
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/semantic_rules.py +99 -4
- {programgarden-1.29.4 → programgarden-1.30.0}/pyproject.toml +2 -2
- {programgarden-1.29.4 → programgarden-1.30.0}/README.md +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/__init__.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/binding_validator.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/client.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/code_worker.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/context.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/database/__init__.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/deep_fixtures.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/node_runner.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/resolver.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/resource/context.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.29.4 → programgarden-1.30.0}/programgarden/validation_recommender.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.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 (>=1.13.11,<2.0.0)
|
|
19
|
-
Requires-Dist: programgarden-core (>=1.
|
|
19
|
+
Requires-Dist: programgarden-core (>=1.22.0,<2.0.0)
|
|
20
20
|
Requires-Dist: programgarden-finance (>=1.6.14,<2.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)
|
|
@@ -1125,10 +1125,22 @@ def _pick_split_array(upstream: Dict[str, Any], *, source: str) -> List[Any]:
|
|
|
1125
1125
|
if len(candidates) == 1:
|
|
1126
1126
|
return upstream[candidates[0]]
|
|
1127
1127
|
if not candidates:
|
|
1128
|
+
# ⑰ Diagnostic: a singular alias (value → values, item → items) present
|
|
1129
|
+
# where the plural array port was meant is a common wrong-key mistake;
|
|
1130
|
+
# surface it in the error so the fix is obvious (report-only — resolution
|
|
1131
|
+
# is unchanged, the two ports legitimately co-exist).
|
|
1132
|
+
_alias_hint = ""
|
|
1133
|
+
for _sing, _plur in (("value", "values"), ("item", "items")):
|
|
1134
|
+
if _sing in upstream and _plur not in upstream:
|
|
1135
|
+
_alias_hint = (
|
|
1136
|
+
f" Upstream has '{_sing}' (singular) but no '{_plur}' (array) "
|
|
1137
|
+
f"— did you mean '{_plur}'?"
|
|
1138
|
+
)
|
|
1139
|
+
break
|
|
1128
1140
|
raise RuntimeError(
|
|
1129
1141
|
f"SplitNode has no array to split: upstream ({source}) exposes no "
|
|
1130
|
-
f"array output among {_SPLIT_ARRAY_KEYS}. Bind split.array
|
|
1131
|
-
"(e.g. array: '{{ nodes.<id>.held_symbols }}') or wire an "
|
|
1142
|
+
f"array output among {_SPLIT_ARRAY_KEYS}.{_alias_hint} Bind split.array "
|
|
1143
|
+
"explicitly (e.g. array: '{{ nodes.<id>.held_symbols }}') or wire an "
|
|
1132
1144
|
"array-producing node upstream."
|
|
1133
1145
|
)
|
|
1134
1146
|
raise RuntimeError(
|
|
@@ -1548,10 +1560,26 @@ class ScheduleNodeExecutor(NodeExecutorBase):
|
|
|
1548
1560
|
context.log("debug", f"Scheduler already running for {node_id}, skip", node_id)
|
|
1549
1561
|
return {"trigger": True}
|
|
1550
1562
|
|
|
1551
|
-
|
|
1563
|
+
# ⑭ cron is required — NO silent "*/5 * * * *" fallback. A missing cron
|
|
1564
|
+
# used to make the schedule quietly run every 5 minutes ("looks fine,
|
|
1565
|
+
# behaves differently"). Fail loudly instead so the real cause surfaces.
|
|
1566
|
+
cron_expr = config.get("cron")
|
|
1567
|
+
if not isinstance(cron_expr, str) or not cron_expr.strip():
|
|
1568
|
+
context.log(
|
|
1569
|
+
"error",
|
|
1570
|
+
"ScheduleNode requires a 'cron' expression; there is no default schedule fallback",
|
|
1571
|
+
node_id,
|
|
1572
|
+
)
|
|
1573
|
+
raise ValueError(
|
|
1574
|
+
f"ScheduleNode '{node_id}' requires a 'cron' expression; there is no "
|
|
1575
|
+
f"default schedule fallback. Set cron explicitly, e.g. '*/5 * * * *' "
|
|
1576
|
+
f"(every 5 min) or '30 9 * * 1-5' (09:30 weekdays). Keys like "
|
|
1577
|
+
f"'mode'/'schedule' are not read by this node."
|
|
1578
|
+
)
|
|
1579
|
+
cron_expr = cron_expr.strip()
|
|
1552
1580
|
tz_name = config.get("timezone", "America/New_York")
|
|
1553
1581
|
enabled = config.get("enabled", True)
|
|
1554
|
-
count = config.get("count", 1000) # 최대 반복 횟수 (M-6: 기본값
|
|
1582
|
+
count = config.get("count", 1000) # 최대 반복 횟수 (M-6: 기본값 축소; 모델 필드로 편입)
|
|
1555
1583
|
max_duration_hours = config.get("max_duration_hours", 24.0)
|
|
1556
1584
|
|
|
1557
1585
|
if not enabled:
|
|
@@ -10594,7 +10622,7 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
|
|
|
10594
10622
|
except (ValueError, _json.JSONDecodeError):
|
|
10595
10623
|
pass
|
|
10596
10624
|
|
|
10597
|
-
# 우선순위: config.symbol > input.symbol
|
|
10625
|
+
# 우선순위: config.symbol > input.symbol > config.symbols (수동 목록 폴백)
|
|
10598
10626
|
if config_symbol:
|
|
10599
10627
|
symbols_raw = [config_symbol] if isinstance(config_symbol, dict) else []
|
|
10600
10628
|
context.log("debug", f"Using config.symbol: {config_symbol}", node_id)
|
|
@@ -10602,7 +10630,23 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
|
|
|
10602
10630
|
symbols_raw = [input_symbol] if isinstance(input_symbol, dict) else []
|
|
10603
10631
|
context.log("debug", f"Using input port symbol: {input_symbol}", node_id)
|
|
10604
10632
|
else:
|
|
10605
|
-
|
|
10633
|
+
# 마지막 폴백: config.symbols (복수 리터럴 목록) — MarketDataNodeExecutor 의
|
|
10634
|
+
# config_symbols 폴백과 대칭. 상류 배열 입력은 메인 루프 auto-iterate 가
|
|
10635
|
+
# 단수 input.symbol 로 풀어 위 분기에서 소화되므로, 여기 도달하는 건
|
|
10636
|
+
# 리터럴 목록뿐이라 N² 재조회 우려가 없다. deep_validate 픽스처는 이미
|
|
10637
|
+
# _config_symbols() 로 plural 을 읽어 라이브만 조용히 무시하던 비대칭이었다
|
|
10638
|
+
# (실측: code-node 예제 87~93 이 심볼 3개를 적고도 빈 ohlcv 로 사망).
|
|
10639
|
+
config_symbols = config.get("symbols")
|
|
10640
|
+
if isinstance(config_symbols, list):
|
|
10641
|
+
symbols_raw = [e for e in config_symbols if isinstance(e, (dict, str))]
|
|
10642
|
+
if symbols_raw:
|
|
10643
|
+
context.log(
|
|
10644
|
+
"debug",
|
|
10645
|
+
f"Using config.symbols list: {len(symbols_raw)} entries",
|
|
10646
|
+
node_id,
|
|
10647
|
+
)
|
|
10648
|
+
else:
|
|
10649
|
+
symbols_raw = []
|
|
10606
10650
|
|
|
10607
10651
|
# symbols 정규화: [{exchange, symbol}] → 문자열 리스트 + exchange 정보 보존
|
|
10608
10652
|
symbols = []
|
|
@@ -12386,7 +12430,21 @@ class PositionSizingNodeExecutor(NodeExecutorBase):
|
|
|
12386
12430
|
price_data = evaluated.get("price_data") or evaluated.get("prices") or evaluated.get("market_data") or {}
|
|
12387
12431
|
|
|
12388
12432
|
# 설정 추출
|
|
12389
|
-
|
|
12433
|
+
# ⑰ method is required — NO silent "fixed_percent" fallback. The old
|
|
12434
|
+
# default quietly confirmed a hallucinated key (e.g. size_type:"fixed"
|
|
12435
|
+
# left method unset → sized as fixed_percent). Fail loudly instead.
|
|
12436
|
+
method = evaluated.get("method")
|
|
12437
|
+
if not method:
|
|
12438
|
+
context.log(
|
|
12439
|
+
"error",
|
|
12440
|
+
"PositionSizingNode requires 'method'; there is no default fallback",
|
|
12441
|
+
node_id,
|
|
12442
|
+
)
|
|
12443
|
+
raise ValueError(
|
|
12444
|
+
f"PositionSizingNode '{node_id}' requires 'method' (one of "
|
|
12445
|
+
f"fixed_percent, fixed_amount, fixed_quantity, kelly, atr_based); "
|
|
12446
|
+
f"there is no default. A key like 'size_type' is not read by this node."
|
|
12447
|
+
)
|
|
12390
12448
|
max_percent = float(evaluated.get("max_percent", 10.0))
|
|
12391
12449
|
fixed_amount = evaluated.get("fixed_amount")
|
|
12392
12450
|
fixed_quantity = int(evaluated.get("fixed_quantity", 1))
|
|
@@ -7,8 +7,8 @@ workflows that are still *semantically* wrong or *unsafe* — they validate
|
|
|
7
7
|
*executability*, never *intent* or *safety*.
|
|
8
8
|
|
|
9
9
|
The four ground-truth anti-patterns (verified against the canonical example
|
|
10
|
-
|
|
11
|
-
registry):
|
|
10
|
+
corpus — maintained internally since the 2026-08 privatization — and the live
|
|
11
|
+
node registry):
|
|
12
12
|
|
|
13
13
|
* **R1 — order quantity bound to an AIAgent response.** An AIAgent
|
|
14
14
|
sentiment/decision output wired straight into an order quantity risks a
|
|
@@ -27,6 +27,15 @@ registry):
|
|
|
27
27
|
Always trades the same size regardless of balance/risk. (Advisory.)
|
|
28
28
|
* **R4 — order node carrying ``paper_trading``.** That field is a *broker*
|
|
29
29
|
field; on an order node it is silently ignored. (Advisory.)
|
|
30
|
+
* **R5 — unknown node config key.** A key that is not in the node's
|
|
31
|
+
``get_field_schema()`` nor its declared model fields. ``BaseNode`` sets
|
|
32
|
+
``extra="allow"`` so Pydantic accepts it, and the executor never reads it —
|
|
33
|
+
the workflow *looks* configured but behaves differently (e.g. ScheduleNode
|
|
34
|
+
``{mode, schedule}`` instead of ``cron`` → silent 5-min fallback;
|
|
35
|
+
PositionSizingNode ``size_type`` instead of ``method``). (Blocking when
|
|
36
|
+
enabled.) Structural, not keyword-based: the allowed set is derived from the
|
|
37
|
+
registry node class, and unknown / community / dynamic types (no resolvable
|
|
38
|
+
class) are skipped rather than false-flagged.
|
|
30
39
|
|
|
31
40
|
Design contract
|
|
32
41
|
---------------
|
|
@@ -58,6 +67,7 @@ from programgarden_core import (
|
|
|
58
67
|
ErrorLocation,
|
|
59
68
|
ErrorSeverity,
|
|
60
69
|
build_error,
|
|
70
|
+
suggest_close_match,
|
|
61
71
|
)
|
|
62
72
|
|
|
63
73
|
# ---------------------------------------------------------------------------
|
|
@@ -68,12 +78,14 @@ RULE_ORDER_QTY_FROM_AI = "order_qty_from_ai_response" # R1
|
|
|
68
78
|
RULE_STRUCTURED_OUTPUT_NO_SCHEMA = "structured_output_missing_schema" # R2
|
|
69
79
|
RULE_HARDCODED_ORDER_QTY = "hardcoded_order_quantity" # R3
|
|
70
80
|
RULE_ORDER_IGNORED_FIELD = "order_ignored_field" # R4
|
|
81
|
+
RULE_UNKNOWN_NODE_FIELD = "unknown_node_field" # R5
|
|
71
82
|
|
|
72
83
|
ALL_RULES: tuple[str, ...] = (
|
|
73
84
|
RULE_ORDER_QTY_FROM_AI,
|
|
74
85
|
RULE_STRUCTURED_OUTPUT_NO_SCHEMA,
|
|
75
86
|
RULE_HARDCODED_ORDER_QTY,
|
|
76
87
|
RULE_ORDER_IGNORED_FIELD,
|
|
88
|
+
RULE_UNKNOWN_NODE_FIELD,
|
|
77
89
|
)
|
|
78
90
|
|
|
79
91
|
_RULE_CODE: Dict[str, ErrorCode] = {
|
|
@@ -81,18 +93,23 @@ _RULE_CODE: Dict[str, ErrorCode] = {
|
|
|
81
93
|
RULE_STRUCTURED_OUTPUT_NO_SCHEMA: ErrorCode.SEMANTIC_STRUCTURED_OUTPUT_NO_SCHEMA,
|
|
82
94
|
RULE_HARDCODED_ORDER_QTY: ErrorCode.SEMANTIC_HARDCODED_ORDER_QTY,
|
|
83
95
|
RULE_ORDER_IGNORED_FIELD: ErrorCode.SEMANTIC_ORDER_IGNORED_FIELD,
|
|
96
|
+
RULE_UNKNOWN_NODE_FIELD: ErrorCode.UNKNOWN_NODE_FIELD,
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
# Everything off — the default when no config is supplied.
|
|
87
100
|
DEFAULT_SEMANTIC_SEVERITIES: Dict[str, str] = {r: "off" for r in ALL_RULES}
|
|
88
101
|
|
|
89
|
-
# Chatbot strict preset —
|
|
90
|
-
#
|
|
102
|
+
# Chatbot strict preset — the save-chokepoint anti-pattern gate (R1/R2/R5
|
|
103
|
+
# block, R3/R4 advise). R5 blocks hallucinated / typo'd node config keys
|
|
104
|
+
# (e.g. ScheduleNode {mode,schedule} instead of cron, PositionSizingNode
|
|
105
|
+
# size_type instead of method) that ``extra="allow"`` silently accepts — the
|
|
106
|
+
# workflow looks configured but the extra keys are ignored at runtime.
|
|
91
107
|
STRICT_SEMANTIC_SEVERITIES: Dict[str, str] = {
|
|
92
108
|
RULE_ORDER_QTY_FROM_AI: "error",
|
|
93
109
|
RULE_STRUCTURED_OUTPUT_NO_SCHEMA: "error",
|
|
94
110
|
RULE_HARDCODED_ORDER_QTY: "warning",
|
|
95
111
|
RULE_ORDER_IGNORED_FIELD: "warning",
|
|
112
|
+
RULE_UNKNOWN_NODE_FIELD: "error",
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
_SEVERITY_MAP: Dict[str, ErrorSeverity] = {
|
|
@@ -171,6 +188,47 @@ def _is_aiagent_node(node_type: Any) -> bool:
|
|
|
171
188
|
return False
|
|
172
189
|
|
|
173
190
|
|
|
191
|
+
# Structural keys present on every node dict but not always declared as a schema
|
|
192
|
+
# / model field. ``config`` / ``description`` are BaseNode fields; ``plugin`` /
|
|
193
|
+
# ``fields`` are the ConditionNode plugin-binding structure carried at node level.
|
|
194
|
+
# ``credential_id`` is the canonical credential-binding key (``nodes[].credential_id``
|
|
195
|
+
# + top-level ``credentials[]``): the executor's universal node-creation layer reads
|
|
196
|
+
# it (``config.get("credential_id")`` → ``_inject_credentials``) for every node type,
|
|
197
|
+
# so no per-node schema declares it — flagging it would block every credential-bound
|
|
198
|
+
# workflow at the strict save gate.
|
|
199
|
+
_RESERVED_NODE_KEYS: frozenset = frozenset(
|
|
200
|
+
{"id", "type", "category", "position", "config", "description", "plugin",
|
|
201
|
+
"fields", "credential_id"}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _node_allowed_keys(node_type: Any) -> Optional[frozenset]:
|
|
206
|
+
"""Allowed top-level config keys for a registered node type, or None.
|
|
207
|
+
|
|
208
|
+
The allowed set is the union of the node's declared Pydantic model fields,
|
|
209
|
+
its ``get_field_schema()`` keys (the AI/UI-facing schema), and the reserved
|
|
210
|
+
structural keys. Returns None when ``node_type`` does not resolve to a
|
|
211
|
+
registered class (unknown / community / dynamic) — R5 skips those, since
|
|
212
|
+
their schema is unknown and any key would otherwise be false-flagged.
|
|
213
|
+
Never raises.
|
|
214
|
+
"""
|
|
215
|
+
cls = _node_class(node_type)
|
|
216
|
+
if cls is None:
|
|
217
|
+
return None
|
|
218
|
+
allowed = set(_RESERVED_NODE_KEYS)
|
|
219
|
+
try:
|
|
220
|
+
allowed |= set(cls.model_fields.keys())
|
|
221
|
+
except Exception: # pragma: no cover - defensive
|
|
222
|
+
pass
|
|
223
|
+
try:
|
|
224
|
+
fs = cls.get_field_schema()
|
|
225
|
+
if isinstance(fs, dict):
|
|
226
|
+
allowed |= set(fs.keys())
|
|
227
|
+
except Exception: # pragma: no cover - defensive
|
|
228
|
+
pass
|
|
229
|
+
return frozenset(allowed)
|
|
230
|
+
|
|
231
|
+
|
|
174
232
|
# ---------------------------------------------------------------------------
|
|
175
233
|
# Binding helpers
|
|
176
234
|
# ---------------------------------------------------------------------------
|
|
@@ -261,6 +319,7 @@ def analyze_workflow_semantics(
|
|
|
261
319
|
sev_r2 = _enabled(severities, RULE_STRUCTURED_OUTPUT_NO_SCHEMA)
|
|
262
320
|
sev_r3 = _enabled(severities, RULE_HARDCODED_ORDER_QTY)
|
|
263
321
|
sev_r4 = _enabled(severities, RULE_ORDER_IGNORED_FIELD)
|
|
322
|
+
sev_r5 = _enabled(severities, RULE_UNKNOWN_NODE_FIELD)
|
|
264
323
|
|
|
265
324
|
# Index node id → node, and whether any PositionSizingNode exists (R3).
|
|
266
325
|
nodes_by_id: Dict[str, Dict[str, Any]] = {}
|
|
@@ -282,6 +341,41 @@ def analyze_workflow_semantics(
|
|
|
282
341
|
node_type = node.get("type")
|
|
283
342
|
node_id = node.get("id") if isinstance(node.get("id"), str) else "?"
|
|
284
343
|
|
|
344
|
+
# ── R5: unknown node config key (hallucinated / typo'd field) ──────
|
|
345
|
+
# Runs for every node. Structural: the allowed set comes from the
|
|
346
|
+
# registry node class; unknown / dynamic types (allowed is None) are
|
|
347
|
+
# skipped rather than false-flagged.
|
|
348
|
+
if sev_r5 is not None:
|
|
349
|
+
allowed = _node_allowed_keys(node_type)
|
|
350
|
+
if allowed is not None:
|
|
351
|
+
for key in node.keys():
|
|
352
|
+
if key in allowed:
|
|
353
|
+
continue
|
|
354
|
+
errors.append(build_error(
|
|
355
|
+
ErrorCode.UNKNOWN_NODE_FIELD,
|
|
356
|
+
f"Node '{node_id}' ({node_type}) has field '{key}', which is not "
|
|
357
|
+
f"in this node's schema. Pydantic accepts it (extra='allow') but "
|
|
358
|
+
f"the executor never reads it, so the workflow looks configured "
|
|
359
|
+
f"but behaves differently than it appears.",
|
|
360
|
+
severity=sev_r5,
|
|
361
|
+
location=ErrorLocation(
|
|
362
|
+
node_id=node_id,
|
|
363
|
+
node_type=node_type if isinstance(node_type, str) else None,
|
|
364
|
+
field_path=key if isinstance(key, str) else None,
|
|
365
|
+
),
|
|
366
|
+
available_values=(
|
|
367
|
+
suggest_close_match(key, sorted(allowed))
|
|
368
|
+
if isinstance(key, str) else None
|
|
369
|
+
),
|
|
370
|
+
suggestion=(
|
|
371
|
+
f"'{key}' 는 {node_type} 노드에 없는 필드예요. 오탈자이거나 "
|
|
372
|
+
f"다른 노드의 필드일 수 있습니다. 이 노드의 config_schema 에 "
|
|
373
|
+
f"정의된 필드만 사용하세요 (예: ScheduleNode 는 cron, "
|
|
374
|
+
f"PositionSizingNode 는 method)."
|
|
375
|
+
),
|
|
376
|
+
details={"rule": RULE_UNKNOWN_NODE_FIELD, "field": key},
|
|
377
|
+
))
|
|
378
|
+
|
|
285
379
|
# ── R2: AIAgent structured output with no schema (and no preset) ──
|
|
286
380
|
if sev_r2 is not None and _is_aiagent_node(node_type):
|
|
287
381
|
if node.get("output_format") == "structured":
|
|
@@ -396,6 +490,7 @@ __all__ = [
|
|
|
396
490
|
"RULE_STRUCTURED_OUTPUT_NO_SCHEMA",
|
|
397
491
|
"RULE_HARDCODED_ORDER_QTY",
|
|
398
492
|
"RULE_ORDER_IGNORED_FIELD",
|
|
493
|
+
"RULE_UNKNOWN_NODE_FIELD",
|
|
399
494
|
"ALL_RULES",
|
|
400
495
|
"DEFAULT_SEMANTIC_SEVERITIES",
|
|
401
496
|
"STRICT_SEMANTIC_SEVERITIES",
|
|
@@ -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.30.0"
|
|
9
9
|
license = "AGPL-3.0-or-later"
|
|
10
10
|
description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
|
|
11
11
|
readme = "README.md"
|
|
@@ -29,7 +29,7 @@ lxml = "^6.0.2"
|
|
|
29
29
|
pytickersymbols = {version = ">=1.17.5", python = ">=3.12,<4.0"}
|
|
30
30
|
aiosqlite = "^0.20.0"
|
|
31
31
|
litellm = ">=1.40.0"
|
|
32
|
-
programgarden-core = "^1.
|
|
32
|
+
programgarden-core = "^1.22.0"
|
|
33
33
|
programgarden-finance = "^1.6.14"
|
|
34
34
|
programgarden-community = "^1.13.11"
|
|
35
35
|
|
|
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-1.29.4 → programgarden-1.30.0}/programgarden/database/workflow_position_tracker.py
RENAMED
|
File without changes
|
{programgarden-1.29.4 → programgarden-1.30.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
|
|
File without changes
|
|
File without changes
|