programgarden 1.24.0__tar.gz → 1.24.2__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.24.0 → programgarden-1.24.2}/PKG-INFO +5 -4
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/executor.py +447 -29
- {programgarden-1.24.0 → programgarden-1.24.2}/pyproject.toml +5 -4
- {programgarden-1.24.0 → programgarden-1.24.2}/README.md +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/__init__.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/binding_validator.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/client.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/context.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/database/__init__.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/deep_fixtures.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/node_runner.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/resolver.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/resource/context.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/semantic_rules.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.24.0 → programgarden-1.24.2}/programgarden/validation_recommender.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 1.24.
|
|
3
|
+
Version: 1.24.2
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
|
+
License-Expression: AGPL-3.0-or-later
|
|
5
6
|
Author: 프로그램동산
|
|
6
7
|
Author-email: coding@programgarden.com
|
|
7
8
|
Requires-Python: >=3.12
|
|
@@ -14,9 +15,9 @@ Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
|
|
|
14
15
|
Requires-Dist: croniter (>=6.0.0,<7.0.0)
|
|
15
16
|
Requires-Dist: litellm (>=1.40.0)
|
|
16
17
|
Requires-Dist: lxml (>=6.0.2,<7.0.0)
|
|
17
|
-
Requires-Dist: programgarden-community (>=1.13.
|
|
18
|
-
Requires-Dist: programgarden-core (>=1.15.
|
|
19
|
-
Requires-Dist: programgarden-finance (>=1.6.
|
|
18
|
+
Requires-Dist: programgarden-community (>=1.13.9,<2.0.0)
|
|
19
|
+
Requires-Dist: programgarden-core (>=1.15.1,<2.0.0)
|
|
20
|
+
Requires-Dist: programgarden-finance (>=1.6.11,<2.0.0)
|
|
20
21
|
Requires-Dist: psutil (>=6.0.0,<7.0.0)
|
|
21
22
|
Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
|
|
22
23
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
@@ -20,7 +20,10 @@ from programgarden.resolver import WorkflowResolver, ResolvedWorkflow, Validatio
|
|
|
20
20
|
from programgarden_core import (
|
|
21
21
|
BalanceUnavailableError,
|
|
22
22
|
ConditionEvaluationError,
|
|
23
|
+
EmptyOrderReason,
|
|
24
|
+
OrderRejectInfo,
|
|
23
25
|
ValidationLimits,
|
|
26
|
+
map_reject_code,
|
|
24
27
|
)
|
|
25
28
|
from programgarden.context import ExecutionContext, WorkflowEvent
|
|
26
29
|
from programgarden.reconnect_handler import ReconnectHandler
|
|
@@ -1264,8 +1267,12 @@ class WatchlistNodeExecutor(NodeExecutorBase):
|
|
|
1264
1267
|
context: ExecutionContext,
|
|
1265
1268
|
**kwargs,
|
|
1266
1269
|
) -> Dict[str, Any]:
|
|
1270
|
+
# config 내 {{ }} 표현식 평가 (list-of-dict 중첩 포함 — MarketDataNode
|
|
1271
|
+
# 와 동일 사유. evaluate_fields 는 리스트 항목 dict 를 재귀하지 않음).
|
|
1272
|
+
config = evaluate_all_bindings(config, context, node_id)
|
|
1273
|
+
|
|
1267
1274
|
symbols_raw = config.get("symbols", [])
|
|
1268
|
-
|
|
1275
|
+
|
|
1269
1276
|
# symbols 처리: [{exchange, symbol}, ...] 형태로 정규화
|
|
1270
1277
|
processed_symbols = []
|
|
1271
1278
|
for entry in symbols_raw:
|
|
@@ -1421,6 +1428,10 @@ class ExclusionListNodeExecutor(NodeExecutorBase):
|
|
|
1421
1428
|
context: ExecutionContext,
|
|
1422
1429
|
**kwargs,
|
|
1423
1430
|
) -> Dict[str, Any]:
|
|
1431
|
+
# config 내 {{ }} 표현식 평가 (list-of-dict 중첩 포함 — MarketDataNode
|
|
1432
|
+
# 와 동일 사유. evaluate_fields 는 리스트 항목 dict 를 재귀하지 않음).
|
|
1433
|
+
config = evaluate_all_bindings(config, context, node_id)
|
|
1434
|
+
|
|
1424
1435
|
default_reason = config.get("default_reason", "")
|
|
1425
1436
|
|
|
1426
1437
|
# 1. 수동 입력 종목 (각 항목에 reason 포함 가능)
|
|
@@ -8976,6 +8987,13 @@ class MarketDataNodeExecutor(NodeExecutorBase):
|
|
|
8976
8987
|
) -> Dict[str, Any]:
|
|
8977
8988
|
"""현재가 조회"""
|
|
8978
8989
|
|
|
8990
|
+
# config 내 {{ }} 표현식 평가 (list-of-dict 중첩 포함).
|
|
8991
|
+
# 메인 루프의 _resolve_config_expressions(evaluate_fields)는 리스트
|
|
8992
|
+
# 항목이 dict 이면 재귀하지 않아 [{"symbol": "{{ ... }}"}] 형태의
|
|
8993
|
+
# 중첩 표현식이 literal 로 남는다. 전용 executor 는 evaluate_all_bindings
|
|
8994
|
+
# (dict/list 완전 재귀)를 직접 호출해야 한다. (형제 executor 패턴과 동일)
|
|
8995
|
+
config = evaluate_all_bindings(config, context, node_id)
|
|
8996
|
+
|
|
8979
8997
|
# 입력 symbols 가져오기 (포트 또는 config에서 명시적 입력 필수)
|
|
8980
8998
|
input_symbols = context.get_output(f"_input_{node_id}", "symbols")
|
|
8981
8999
|
config_symbols = config.get("symbols")
|
|
@@ -9319,6 +9337,10 @@ class FundamentalNodeExecutor(NodeExecutorBase):
|
|
|
9319
9337
|
) -> Dict[str, Any]:
|
|
9320
9338
|
"""종목정보(펀더멘털) 조회"""
|
|
9321
9339
|
|
|
9340
|
+
# config 내 {{ }} 표현식 평가 (list-of-dict 중첩 포함 — MarketDataNode
|
|
9341
|
+
# 와 동일 사유. evaluate_fields 는 리스트 항목 dict 를 재귀하지 않음).
|
|
9342
|
+
config = evaluate_all_bindings(config, context, node_id)
|
|
9343
|
+
|
|
9322
9344
|
# symbols 획득: input port → config.symbols → config.symbol (단일→배열 변환)
|
|
9323
9345
|
input_symbols = context.get_output(f"_input_{node_id}", "symbols")
|
|
9324
9346
|
config_symbols = config.get("symbols")
|
|
@@ -11369,7 +11391,13 @@ class PositionSizingNodeExecutor(NodeExecutorBase):
|
|
|
11369
11391
|
|
|
11370
11392
|
if not symbols:
|
|
11371
11393
|
context.log("warning", "No symbols provided for position sizing", node_id)
|
|
11372
|
-
|
|
11394
|
+
# 상류가 빈 리스트에 error 신호를 동봉했으면 fetch_failed, 아니면 종목 미지정.
|
|
11395
|
+
err = None
|
|
11396
|
+
if isinstance(symbols_input, dict) and symbols_input.get("error"):
|
|
11397
|
+
err = str(symbols_input.get("error"))
|
|
11398
|
+
if err:
|
|
11399
|
+
return self._empty_result(EmptyOrderReason.FETCH_FAILED, err)
|
|
11400
|
+
return self._empty_result(EmptyOrderReason.NO_SYMBOL, "")
|
|
11373
11401
|
|
|
11374
11402
|
# Refuse to silently zero-out when the upstream AccountNode flagged
|
|
11375
11403
|
# a partial fetch. fixed_quantity ignores balance entirely, so it
|
|
@@ -11403,7 +11431,20 @@ class PositionSizingNodeExecutor(NodeExecutorBase):
|
|
|
11403
11431
|
# fixed_quantity 메서드는 balance 없이도 동작 (테스트용)
|
|
11404
11432
|
if available_balance <= 0 and method != "fixed_quantity":
|
|
11405
11433
|
context.log("warning", f"No available balance: {available_balance}", node_id)
|
|
11406
|
-
|
|
11434
|
+
# A zero balance caused by a partial fetch failure is a pipeline
|
|
11435
|
+
# error (fetch_failed), not a normal "no signal" no-op. The hard
|
|
11436
|
+
# _partial_failure raise above already covers non-dry-run; in
|
|
11437
|
+
# dry_run (exempt) still surface the failure reason in the result.
|
|
11438
|
+
if isinstance(balance_data, dict) and balance_data.get("_partial_failure") is True:
|
|
11439
|
+
detail = str(
|
|
11440
|
+
balance_data.get("_failure_reason")
|
|
11441
|
+
or "balance fetch partially failed"
|
|
11442
|
+
)
|
|
11443
|
+
return self._empty_result(EmptyOrderReason.FETCH_FAILED, detail)
|
|
11444
|
+
return self._empty_result(
|
|
11445
|
+
EmptyOrderReason.NO_SIGNAL,
|
|
11446
|
+
"No available balance for position sizing",
|
|
11447
|
+
)
|
|
11407
11448
|
|
|
11408
11449
|
context.log(
|
|
11409
11450
|
"info",
|
|
@@ -11435,12 +11476,18 @@ class PositionSizingNodeExecutor(NodeExecutorBase):
|
|
|
11435
11476
|
price_data, context, node_id
|
|
11436
11477
|
)
|
|
11437
11478
|
else:
|
|
11479
|
+
# Unknown sizing method is a configuration error, not a missing
|
|
11480
|
+
# signal — classify as NO_SYMBOL (config) so the consumer sees a
|
|
11481
|
+
# deterministic config-fix cue rather than a bare no_signal.
|
|
11438
11482
|
context.log("error", f"Unknown sizing method: {method}", node_id)
|
|
11439
|
-
return self._empty_result(
|
|
11440
|
-
|
|
11483
|
+
return self._empty_result(
|
|
11484
|
+
EmptyOrderReason.NO_SYMBOL,
|
|
11485
|
+
f"Unknown sizing method '{method}'",
|
|
11486
|
+
)
|
|
11487
|
+
|
|
11441
11488
|
# 결과에 원본 symbols 추가
|
|
11442
11489
|
result["symbols"] = symbols_input
|
|
11443
|
-
|
|
11490
|
+
|
|
11444
11491
|
return result
|
|
11445
11492
|
|
|
11446
11493
|
def _normalize_symbols(self, symbols_input: Any) -> List[Dict[str, Any]]:
|
|
@@ -11981,14 +12028,43 @@ class PositionSizingNodeExecutor(NodeExecutorBase):
|
|
|
11981
12028
|
|
|
11982
12029
|
return None
|
|
11983
12030
|
|
|
11984
|
-
|
|
11985
|
-
|
|
12031
|
+
# Human/AI-facing English explanations keyed by EmptyOrderReason.
|
|
12032
|
+
_EMPTY_SIZING_MESSAGES: Dict[str, str] = {
|
|
12033
|
+
EmptyOrderReason.NO_SIGNAL.value: (
|
|
12034
|
+
"No trading signal today (upstream produced an empty result normally)."
|
|
12035
|
+
),
|
|
12036
|
+
EmptyOrderReason.FETCH_FAILED.value: (
|
|
12037
|
+
"Upstream data fetch failed; no order placed (pipeline error)."
|
|
12038
|
+
),
|
|
12039
|
+
EmptyOrderReason.NO_SYMBOL.value: (
|
|
12040
|
+
"No symbol provided to the order node (configuration missing)."
|
|
12041
|
+
),
|
|
12042
|
+
}
|
|
12043
|
+
|
|
12044
|
+
def _empty_result(
|
|
12045
|
+
self,
|
|
12046
|
+
reason: EmptyOrderReason = EmptyOrderReason.NO_SIGNAL,
|
|
12047
|
+
detail: str = "",
|
|
12048
|
+
) -> Dict[str, Any]:
|
|
12049
|
+
"""빈 포지션 사이징 결과 반환.
|
|
12050
|
+
|
|
12051
|
+
주문 노드 ``_empty_result`` 와 동일하게 ``reason`` (no_signal / fetch_failed /
|
|
12052
|
+
no_symbol) 으로 "신호 없음" 과 "파이프라인 고장" 을 구분한다. 기존 키
|
|
12053
|
+
(orders/total_amount/symbols/method/config) 는 하위호환을 위해 그대로 두고
|
|
12054
|
+
``reason`` / ``message`` / ``detail`` 만 추가한다.
|
|
12055
|
+
"""
|
|
12056
|
+
message = self._EMPTY_SIZING_MESSAGES.get(
|
|
12057
|
+
reason.value, self._EMPTY_SIZING_MESSAGES[EmptyOrderReason.NO_SIGNAL.value]
|
|
12058
|
+
)
|
|
11986
12059
|
return {
|
|
11987
12060
|
"orders": [],
|
|
11988
12061
|
"total_amount": 0.0,
|
|
11989
12062
|
"symbols": [],
|
|
11990
12063
|
"method": None,
|
|
11991
12064
|
"config": {},
|
|
12065
|
+
"reason": reason.value,
|
|
12066
|
+
"message": message,
|
|
12067
|
+
"detail": detail,
|
|
11992
12068
|
}
|
|
11993
12069
|
|
|
11994
12070
|
|
|
@@ -12448,6 +12524,13 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
12448
12524
|
paper_trading = broker_connection.get("paper_trading", False) if product != "korea_stock" else False
|
|
12449
12525
|
|
|
12450
12526
|
# === 2. 바인딩 표현식 평가 ===
|
|
12527
|
+
# Capture the RAW (pre-evaluation) order binding expression BEFORE
|
|
12528
|
+
# evaluate_all_bindings rebinds config. A missing upstream port (e.g.
|
|
12529
|
+
# '{{ nodes.sizing.order }}' where sizing only emits 'orders') resolves
|
|
12530
|
+
# to None and the expression text is lost after evaluation — so the
|
|
12531
|
+
# upstream node reference needed for empty-order diagnosis must be read
|
|
12532
|
+
# from the raw config here.
|
|
12533
|
+
raw_order_expr = config.get("order")
|
|
12451
12534
|
config = evaluate_all_bindings(config, context, node_id)
|
|
12452
12535
|
|
|
12453
12536
|
# === 3. 주문 정보 추출 (order 단일 객체) ===
|
|
@@ -12460,7 +12543,10 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
12460
12543
|
|
|
12461
12544
|
if not normalized_order:
|
|
12462
12545
|
context.log("warning", f"{node_type}: 주문할 종목이 없습니다", node_id)
|
|
12463
|
-
|
|
12546
|
+
reason, detail = self._diagnose_empty_reason(
|
|
12547
|
+
order, config, raw_order_expr, context
|
|
12548
|
+
)
|
|
12549
|
+
return self._empty_result(reason, detail)
|
|
12464
12550
|
|
|
12465
12551
|
context.log(
|
|
12466
12552
|
"info",
|
|
@@ -12621,6 +12707,155 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
12621
12707
|
"price": float(price) if price else 0.0,
|
|
12622
12708
|
}
|
|
12623
12709
|
|
|
12710
|
+
def _diagnose_empty_reason(
|
|
12711
|
+
self,
|
|
12712
|
+
order: Any,
|
|
12713
|
+
config: Dict[str, Any],
|
|
12714
|
+
raw_order_expr: Any = None,
|
|
12715
|
+
context: Optional[ExecutionContext] = None,
|
|
12716
|
+
) -> tuple:
|
|
12717
|
+
"""빈 주문 결과의 reason 을 상류 신호로 판정.
|
|
12718
|
+
|
|
12719
|
+
no_signal(정상 빈 결과) vs fetch_failed(상류 파이프라인 고장) vs
|
|
12720
|
+
no_symbol(설정 누락) 을 결정적으로 구분한다. 메모리 정책(no silent
|
|
12721
|
+
failure)상 fetch_failed 미탐을 최소화하기 위해, 상류 노드의 FULL 구조화
|
|
12722
|
+
출력(``error`` / ``reason='fetch_failed'`` / ``_partial_failure``) +
|
|
12723
|
+
balance ``_partial_failure`` + 미해석 바인딩 리터럴 같은 확실한 실패
|
|
12724
|
+
신호를 우선 사용한다. 불확실하면 보수적으로 FETCH_FAILED 쪽으로 기운다.
|
|
12725
|
+
|
|
12726
|
+
``raw_order_expr`` 는 evaluate_all_bindings 가 config 를 덮어쓰기 전에
|
|
12727
|
+
캡처한 원본 ``order`` 바인딩 표현식이다. ``'{{ nodes.sizing.order }}'``
|
|
12728
|
+
처럼 상류의 누락 포트를 가리켜 None 으로 해석된 경우, 그 표현식에서 상류
|
|
12729
|
+
노드 id 를 뽑아 ``context.get_all_outputs`` 로 FULL 출력을 조회한다.
|
|
12730
|
+
|
|
12731
|
+
Args:
|
|
12732
|
+
order: evaluate 된 order 값(보통 None / dict / list).
|
|
12733
|
+
config: evaluate 된 노드 config.
|
|
12734
|
+
raw_order_expr: evaluate 이전 원본 order 표현식(없으면 None).
|
|
12735
|
+
context: 상류 출력 조회용 ExecutionContext(없으면 생략).
|
|
12736
|
+
|
|
12737
|
+
Returns:
|
|
12738
|
+
(EmptyOrderReason, detail) 튜플.
|
|
12739
|
+
"""
|
|
12740
|
+
# 1) balance dict 의 부분 실패 → 상류 조회 실패.
|
|
12741
|
+
balance = config.get("balance")
|
|
12742
|
+
if isinstance(balance, dict) and balance.get("_partial_failure") is True:
|
|
12743
|
+
detail = str(balance.get("_failure_reason") or "balance fetch partially failed")
|
|
12744
|
+
return EmptyOrderReason.FETCH_FAILED, detail
|
|
12745
|
+
|
|
12746
|
+
# 2) order 바인딩이 미해석 리터럴('{{ ... }}') 로 그대로 넘어온 경우 —
|
|
12747
|
+
# 표현식이 평가되지 못했다(주로 상류 노드 미실행/오류). 보수적으로
|
|
12748
|
+
# FETCH_FAILED 로 본다(silent no-op 차단).
|
|
12749
|
+
if isinstance(order, str) and "{{" in order and "}}" in order:
|
|
12750
|
+
return (
|
|
12751
|
+
EmptyOrderReason.FETCH_FAILED,
|
|
12752
|
+
f"Order binding was not resolved (unresolved expression: {order})",
|
|
12753
|
+
)
|
|
12754
|
+
|
|
12755
|
+
# 3) 원본 order 표현식이 상류 노드를 가리키는데 None 으로 해석됐다면,
|
|
12756
|
+
# 그 상류 노드의 FULL 구조화 출력을 직접 조회해 실패 신호를 본다.
|
|
12757
|
+
# 핵심 케이스: '{{ nodes.sizing.order }}' 인데 sizing 이 'orders'(복수)
|
|
12758
|
+
# 만 내보내 'order' 포트가 없어 None 으로 swallow 된 경우 — 상류가
|
|
12759
|
+
# fetch_failed/_partial_failure 면 NO_SIGNAL 로 오분류하지 않는다.
|
|
12760
|
+
upstream = self._lookup_upstream_output(raw_order_expr, context)
|
|
12761
|
+
if upstream is not None:
|
|
12762
|
+
err = self._extract_upstream_error(upstream)
|
|
12763
|
+
if err is not None:
|
|
12764
|
+
return EmptyOrderReason.FETCH_FAILED, err
|
|
12765
|
+
|
|
12766
|
+
# 4) 상류가 낸 빈 리스트에 실패 신호 동봉 → 조회 실패(파이프라인 고장).
|
|
12767
|
+
# order/symbols 바인딩 결과가 dict 이면서 error/_partial_failure/
|
|
12768
|
+
# reason='fetch_failed' 를 들고 있는 경우를 본다.
|
|
12769
|
+
for candidate in (order, config.get("symbols"), config.get("order")):
|
|
12770
|
+
err = self._extract_upstream_error(candidate)
|
|
12771
|
+
if err is not None:
|
|
12772
|
+
return EmptyOrderReason.FETCH_FAILED, err
|
|
12773
|
+
|
|
12774
|
+
# 5) 주문 대상 자체가 비었으면(종목 미지정) → 설정 누락.
|
|
12775
|
+
# order 가 명시적으로 비어있고(빈 리스트/빈 dict/None) 상류 실패 신호도
|
|
12776
|
+
# 없는 경우는 "종목 미지정"으로 본다.
|
|
12777
|
+
if self._is_empty_order_payload(order):
|
|
12778
|
+
return EmptyOrderReason.NO_SYMBOL, ""
|
|
12779
|
+
|
|
12780
|
+
# 6) 그 외 → 정상 빈 결과(오늘 신호 없음).
|
|
12781
|
+
return EmptyOrderReason.NO_SIGNAL, ""
|
|
12782
|
+
|
|
12783
|
+
@staticmethod
|
|
12784
|
+
def _lookup_upstream_output(
|
|
12785
|
+
raw_order_expr: Any,
|
|
12786
|
+
context: Optional[ExecutionContext],
|
|
12787
|
+
) -> Optional[Dict[str, Any]]:
|
|
12788
|
+
"""원본 order 표현식이 '{{ nodes.<id>.<port> }}' 형태면 그 상류 노드의
|
|
12789
|
+
FULL 출력 dict 를 반환한다. 못 찾으면(미해석/auto-iterate item/노드 미실행)
|
|
12790
|
+
None.
|
|
12791
|
+
|
|
12792
|
+
Auto-iterate 의 ``{{ item.xxx }}`` 처럼 nodes.<id> 패턴이 없으면 None 으로
|
|
12793
|
+
graceful fallback 한다(기존 동작 유지).
|
|
12794
|
+
"""
|
|
12795
|
+
if not isinstance(raw_order_expr, str) or context is None:
|
|
12796
|
+
return None
|
|
12797
|
+
import re
|
|
12798
|
+
|
|
12799
|
+
match = re.search(r"nodes\.(\w+)", raw_order_expr)
|
|
12800
|
+
if not match:
|
|
12801
|
+
return None
|
|
12802
|
+
node_id = match.group(1)
|
|
12803
|
+
try:
|
|
12804
|
+
outputs = context.get_all_outputs(node_id)
|
|
12805
|
+
except Exception:
|
|
12806
|
+
return None
|
|
12807
|
+
# 미실행/미지 노드 → {} → 빈 상류는 FETCH_FAILED 오판하지 않도록 None.
|
|
12808
|
+
if not outputs:
|
|
12809
|
+
return None
|
|
12810
|
+
return outputs
|
|
12811
|
+
|
|
12812
|
+
@staticmethod
|
|
12813
|
+
def _extract_upstream_error(candidate: Any) -> Optional[str]:
|
|
12814
|
+
"""상류 출력(dict)에서 실패 신호를 추출. 없으면 None.
|
|
12815
|
+
|
|
12816
|
+
인식하는 실패 신호(확장됨): ``error`` 키, ``reason == 'fetch_failed'``,
|
|
12817
|
+
``_partial_failure is True``. 진단 텍스트는 detail/message/_failure_reason
|
|
12818
|
+
/error 순으로 골라 반환한다. 정상 결과 오탐을 막기 위해, ``symbols`` 가
|
|
12819
|
+
채워져 있는데 단지 ``error`` 만 있는 경우(부분 경고)는 실패로 보지 않는다.
|
|
12820
|
+
"""
|
|
12821
|
+
if not isinstance(candidate, dict):
|
|
12822
|
+
return None
|
|
12823
|
+
|
|
12824
|
+
# symbols 가 채워져 있으면 정상 결과로 본다(test_normal_nonempty_symbols).
|
|
12825
|
+
symbols = candidate.get("symbols")
|
|
12826
|
+
symbols_non_empty = bool(symbols) and symbols not in (None, [], ())
|
|
12827
|
+
|
|
12828
|
+
def _detail_text() -> str:
|
|
12829
|
+
for key in ("detail", "message", "_failure_reason", "error"):
|
|
12830
|
+
val = candidate.get(key)
|
|
12831
|
+
if val:
|
|
12832
|
+
return str(val)
|
|
12833
|
+
return "upstream fetch failed"
|
|
12834
|
+
|
|
12835
|
+
# (a) _partial_failure / reason=='fetch_failed' 는 명시적 실패 마커 —
|
|
12836
|
+
# symbols 유무와 무관하게 실패로 본다(silent no-op 차단 우선).
|
|
12837
|
+
if candidate.get("_partial_failure") is True:
|
|
12838
|
+
return _detail_text()
|
|
12839
|
+
if candidate.get("reason") == EmptyOrderReason.FETCH_FAILED.value:
|
|
12840
|
+
return _detail_text()
|
|
12841
|
+
|
|
12842
|
+
# (b) error 키 — symbols 가 비었을 때만 fetch_failed 로 본다(정상 오탐 방지).
|
|
12843
|
+
err = candidate.get("error")
|
|
12844
|
+
if err and not symbols_non_empty:
|
|
12845
|
+
# symbols 키가 아예 없거나 비어있는 경우만 실패로 본다.
|
|
12846
|
+
if symbols in (None, [], ()) or "symbols" not in candidate:
|
|
12847
|
+
return str(err)
|
|
12848
|
+
return None
|
|
12849
|
+
|
|
12850
|
+
@staticmethod
|
|
12851
|
+
def _is_empty_order_payload(order: Any) -> bool:
|
|
12852
|
+
"""주문 페이로드가 '종목 미지정' 으로 볼 만큼 비었는지 판정."""
|
|
12853
|
+
if order is None:
|
|
12854
|
+
return True
|
|
12855
|
+
if isinstance(order, (list, tuple, dict, str)) and len(order) == 0:
|
|
12856
|
+
return True
|
|
12857
|
+
return False
|
|
12858
|
+
|
|
12624
12859
|
async def _execute_overseas_stock(
|
|
12625
12860
|
self,
|
|
12626
12861
|
ls,
|
|
@@ -12698,8 +12933,19 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
12698
12933
|
context.log("debug", f"COSAT00301 response: rsp_cd={response.rsp_cd}, rsp_msg={response.rsp_msg}", node_id)
|
|
12699
12934
|
|
|
12700
12935
|
if response.error_msg:
|
|
12701
|
-
|
|
12702
|
-
|
|
12936
|
+
reject = map_reject_code(
|
|
12937
|
+
"overseas_stock", response.rsp_cd or "", response.error_msg
|
|
12938
|
+
)
|
|
12939
|
+
context.log(
|
|
12940
|
+
"warning",
|
|
12941
|
+
f"Order failed: {symbol} - {response.error_msg} ({reject.cause})",
|
|
12942
|
+
node_id,
|
|
12943
|
+
)
|
|
12944
|
+
await self._notify_order_reject(context, node_id, symbol, reject)
|
|
12945
|
+
return self._order_result(
|
|
12946
|
+
False, symbol, exchange, side, qty, price,
|
|
12947
|
+
response.error_msg, reject_info=reject,
|
|
12948
|
+
)
|
|
12703
12949
|
|
|
12704
12950
|
order_no = ""
|
|
12705
12951
|
if response.block2:
|
|
@@ -12709,7 +12955,20 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
12709
12955
|
if not order_no:
|
|
12710
12956
|
msg = response.rsp_msg or "주문번호 없음"
|
|
12711
12957
|
context.log("warning", f"Order submitted but no OrderNo returned: {symbol} - {msg}", node_id)
|
|
12712
|
-
|
|
12958
|
+
# rsp_cd 가 성공("00000")인데 OrderNo 가 안 온 케이스 — rsp_cd 매핑이
|
|
12959
|
+
# 아니라 전용 진단을 동봉한다(장 마감 / 브로커 지연 등).
|
|
12960
|
+
reject = OrderRejectInfo(
|
|
12961
|
+
rsp_cd=response.rsp_cd or "",
|
|
12962
|
+
cause="Order accepted but no order number returned (likely market closed or broker delay)",
|
|
12963
|
+
tip="Verify market hours and re-query open orders to confirm whether the order was actually placed.",
|
|
12964
|
+
raw_msg=msg,
|
|
12965
|
+
known=True,
|
|
12966
|
+
)
|
|
12967
|
+
await self._notify_order_reject(context, node_id, symbol, reject)
|
|
12968
|
+
return self._order_result(
|
|
12969
|
+
False, symbol, exchange, side, qty, price,
|
|
12970
|
+
f"Empty OrderNo: {msg}", reject_info=reject,
|
|
12971
|
+
)
|
|
12713
12972
|
|
|
12714
12973
|
context.log("info", f"Order submitted: {symbol} {side} {qty}@{price} → order_id={order_no}", node_id)
|
|
12715
12974
|
|
|
@@ -12729,7 +12988,11 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
12729
12988
|
|
|
12730
12989
|
except Exception as e:
|
|
12731
12990
|
context.log("warning", f"Order exception: {symbol} - {e}", node_id)
|
|
12732
|
-
|
|
12991
|
+
# 예외는 rsp_cd 가 없으므로 known=False 폴백으로 동봉(일관성).
|
|
12992
|
+
reject = map_reject_code("overseas_stock", "", str(e))
|
|
12993
|
+
return self._order_result(
|
|
12994
|
+
False, symbol, exchange, side, qty, price, str(e), reject_info=reject,
|
|
12995
|
+
)
|
|
12733
12996
|
|
|
12734
12997
|
return {
|
|
12735
12998
|
"order_result": {
|
|
@@ -12962,19 +13225,49 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
12962
13225
|
context.log("debug", f"CIDBT00100 response: rsp_cd={response.rsp_cd}, rsp_msg={response.rsp_msg}", node_id)
|
|
12963
13226
|
|
|
12964
13227
|
if response.error_msg:
|
|
12965
|
-
|
|
12966
|
-
|
|
13228
|
+
reject = map_reject_code(
|
|
13229
|
+
"overseas_futures", response.rsp_cd or "", response.error_msg
|
|
13230
|
+
)
|
|
13231
|
+
context.log(
|
|
13232
|
+
"warning",
|
|
13233
|
+
f"Order failed: {symbol} - {response.error_msg} ({reject.cause})",
|
|
13234
|
+
node_id,
|
|
13235
|
+
)
|
|
13236
|
+
await self._notify_order_reject(
|
|
13237
|
+
context, node_id, symbol, reject,
|
|
13238
|
+
node_type="OverseasFuturesNewOrderNode",
|
|
13239
|
+
)
|
|
13240
|
+
return self._order_result(
|
|
13241
|
+
False, symbol, exchange, side, qty, price,
|
|
13242
|
+
response.error_msg, reject_info=reject,
|
|
13243
|
+
)
|
|
12967
13244
|
|
|
12968
13245
|
order_no = ""
|
|
12969
13246
|
if response.block2:
|
|
12970
|
-
# 해외선물 주문번호 필드: OvrsFutsOrdNo
|
|
13247
|
+
# 해외선물 주문번호 필드: OvrsFutsOrdNo (str, default "")
|
|
12971
13248
|
order_no = str(response.block2.OvrsFutsOrdNo) if hasattr(response.block2, "OvrsFutsOrdNo") and response.block2.OvrsFutsOrdNo else ""
|
|
12972
13249
|
|
|
12973
13250
|
# 주문번호가 없으면 경고 + 기록 거부
|
|
12974
13251
|
if not order_no:
|
|
12975
13252
|
msg = response.rsp_msg or "주문번호 없음"
|
|
12976
13253
|
context.log("warning", f"Futures order submitted but no OrderNo returned: {symbol} - {msg}", node_id)
|
|
12977
|
-
|
|
13254
|
+
# rsp_cd 성공인데 OrderNo 가 안 온 케이스 — 전용 lifecycle 진단 동봉
|
|
13255
|
+
# (장 마감 / 브로커 지연 등). stock 경로와 동일 구조.
|
|
13256
|
+
reject = OrderRejectInfo(
|
|
13257
|
+
rsp_cd=response.rsp_cd or "",
|
|
13258
|
+
cause="Order accepted but no order number returned (likely market closed or broker delay)",
|
|
13259
|
+
tip="Verify market hours and re-query open orders to confirm whether the order was actually placed.",
|
|
13260
|
+
raw_msg=msg,
|
|
13261
|
+
known=True,
|
|
13262
|
+
)
|
|
13263
|
+
await self._notify_order_reject(
|
|
13264
|
+
context, node_id, symbol, reject,
|
|
13265
|
+
node_type="OverseasFuturesNewOrderNode",
|
|
13266
|
+
)
|
|
13267
|
+
return self._order_result(
|
|
13268
|
+
False, symbol, exchange, side, qty, price,
|
|
13269
|
+
f"Empty OrderNo: {msg}", reject_info=reject,
|
|
13270
|
+
)
|
|
12978
13271
|
|
|
12979
13272
|
context.log("info", f"Futures order submitted: {symbol} {side} {qty}@{price} → order_id={order_no}", node_id)
|
|
12980
13273
|
|
|
@@ -12994,7 +13287,11 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
12994
13287
|
|
|
12995
13288
|
except Exception as e:
|
|
12996
13289
|
context.log("warning", f"Futures order exception: {symbol} - {e}", node_id)
|
|
12997
|
-
|
|
13290
|
+
# 예외는 rsp_cd 가 없으므로 known=False 폴백으로 동봉(stock 경로와 일관).
|
|
13291
|
+
reject = map_reject_code("overseas_futures", "", str(e))
|
|
13292
|
+
return self._order_result(
|
|
13293
|
+
False, symbol, exchange, side, qty, price, str(e), reject_info=reject,
|
|
13294
|
+
)
|
|
12998
13295
|
|
|
12999
13296
|
def _check_exclusion_list(
|
|
13000
13297
|
self,
|
|
@@ -13067,15 +13364,47 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
13067
13364
|
response = await order_api.req_async()
|
|
13068
13365
|
|
|
13069
13366
|
if response.error_msg:
|
|
13070
|
-
|
|
13367
|
+
reject = map_reject_code(
|
|
13368
|
+
"korea_stock", response.rsp_cd or "", response.error_msg
|
|
13369
|
+
)
|
|
13370
|
+
context.log(
|
|
13371
|
+
"warning",
|
|
13372
|
+
f"Korea stock order failed: {response.error_msg} ({reject.cause})",
|
|
13373
|
+
node_id,
|
|
13374
|
+
)
|
|
13375
|
+
await self._notify_order_reject(
|
|
13376
|
+
context, node_id, symbol, reject,
|
|
13377
|
+
node_type="KoreaStockNewOrderNode",
|
|
13378
|
+
)
|
|
13071
13379
|
return self._order_result(
|
|
13072
13380
|
False, symbol, "KRX", side, qty, price,
|
|
13073
|
-
response.error_msg
|
|
13381
|
+
response.error_msg, reject_info=reject,
|
|
13074
13382
|
)
|
|
13075
13383
|
|
|
13076
|
-
|
|
13077
|
-
|
|
13078
|
-
|
|
13384
|
+
# CSPAT00601 OrdNo 는 int(default 0). str(0)=='0' 은 truthy 라서
|
|
13385
|
+
# OrdNo 가 0/garbage 면 성공으로 silently 기록되는 위험이 있다.
|
|
13386
|
+
# stock 경로와 동일하게 빈/0 OrderNo 를 명시 거부로 본다.
|
|
13387
|
+
ord_no_raw = response.block2.OrdNo if response.block2 else None
|
|
13388
|
+
order_no = str(ord_no_raw) if ord_no_raw else ""
|
|
13389
|
+
|
|
13390
|
+
if not order_no:
|
|
13391
|
+
msg = response.rsp_msg or "주문번호 없음"
|
|
13392
|
+
context.log("warning", f"Korea stock order submitted but no OrderNo returned: {symbol} - {msg}", node_id)
|
|
13393
|
+
reject = OrderRejectInfo(
|
|
13394
|
+
rsp_cd=response.rsp_cd or "",
|
|
13395
|
+
cause="Order accepted but no order number returned (likely market closed or broker delay)",
|
|
13396
|
+
tip="Verify market hours and re-query open orders to confirm whether the order was actually placed.",
|
|
13397
|
+
raw_msg=msg,
|
|
13398
|
+
known=True,
|
|
13399
|
+
)
|
|
13400
|
+
await self._notify_order_reject(
|
|
13401
|
+
context, node_id, symbol, reject,
|
|
13402
|
+
node_type="KoreaStockNewOrderNode",
|
|
13403
|
+
)
|
|
13404
|
+
return self._order_result(
|
|
13405
|
+
False, symbol, "KRX", side, qty, price,
|
|
13406
|
+
f"Empty OrderNo: {msg}", reject_info=reject,
|
|
13407
|
+
)
|
|
13079
13408
|
|
|
13080
13409
|
context.log(
|
|
13081
13410
|
"info",
|
|
@@ -13092,9 +13421,47 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
13092
13421
|
|
|
13093
13422
|
except Exception as e:
|
|
13094
13423
|
context.log("warning", f"Korea stock order exception: {e}", node_id)
|
|
13424
|
+
reject = map_reject_code("korea_stock", "", str(e))
|
|
13095
13425
|
return self._order_result(
|
|
13096
|
-
False, symbol, "KRX", side, qty, price, str(e)
|
|
13426
|
+
False, symbol, "KRX", side, qty, price, str(e), reject_info=reject,
|
|
13427
|
+
)
|
|
13428
|
+
|
|
13429
|
+
async def _notify_order_reject(
|
|
13430
|
+
self,
|
|
13431
|
+
context: ExecutionContext,
|
|
13432
|
+
node_id: str,
|
|
13433
|
+
symbol: str,
|
|
13434
|
+
reject: OrderRejectInfo,
|
|
13435
|
+
node_type: str = "OverseasStockNewOrderNode",
|
|
13436
|
+
) -> None:
|
|
13437
|
+
"""주문 거부 시 투자자 알림 1건 발행(on_notification).
|
|
13438
|
+
|
|
13439
|
+
on_log warning 과 별개로, AI/UI/텔레그램 소비자가 거부 사유(cause)와 대응
|
|
13440
|
+
팁(tip)을 구조화 payload 로 받도록 한다. ``node_type`` 은 시장별 주문 노드
|
|
13441
|
+
이름(해외주식/해외선물/국내주식)을 정확히 라벨링하기 위해 호출자가 전달한다.
|
|
13442
|
+
"""
|
|
13443
|
+
try:
|
|
13444
|
+
await context.send_notification(
|
|
13445
|
+
# ORDER_REJECTED is the semantically correct category for a
|
|
13446
|
+
# broker reject (RISK_ALERT means drawdown/risk-halt).
|
|
13447
|
+
category=NotificationCategory.ORDER_REJECTED,
|
|
13448
|
+
severity=NotificationSeverity.WARNING,
|
|
13449
|
+
title=f"Order rejected: {symbol}",
|
|
13450
|
+
message=f"{symbol} order rejected — {reject.cause}",
|
|
13451
|
+
node_id=node_id,
|
|
13452
|
+
node_type=node_type,
|
|
13453
|
+
data={
|
|
13454
|
+
"symbol": symbol,
|
|
13455
|
+
"rsp_cd": reject.rsp_cd,
|
|
13456
|
+
"cause": reject.cause,
|
|
13457
|
+
"tip": reject.tip,
|
|
13458
|
+
"raw_msg": reject.raw_msg,
|
|
13459
|
+
"known": reject.known,
|
|
13460
|
+
},
|
|
13097
13461
|
)
|
|
13462
|
+
except Exception:
|
|
13463
|
+
# 알림 전파 실패가 주문 결과 반환을 막아서는 안 된다.
|
|
13464
|
+
pass
|
|
13098
13465
|
|
|
13099
13466
|
def _order_result(
|
|
13100
13467
|
self,
|
|
@@ -13106,8 +13473,15 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
13106
13473
|
price: float,
|
|
13107
13474
|
error: Optional[str] = None,
|
|
13108
13475
|
order_id: str = "",
|
|
13476
|
+
reject_info: Optional[OrderRejectInfo] = None,
|
|
13109
13477
|
) -> Dict[str, Any]:
|
|
13110
|
-
"""단일 주문 결과
|
|
13478
|
+
"""단일 주문 결과 반환.
|
|
13479
|
+
|
|
13480
|
+
``error`` (기존 LS 원문 단일 문자열) 은 하위호환을 위해 그대로 유지하고,
|
|
13481
|
+
구조화된 거부 진단(``rsp_cd`` / ``cause`` / ``tip`` / ``raw_msg`` / ``known``)
|
|
13482
|
+
은 ``order_result.diagnostics`` 로 추가 동봉한다. AI 챗봇 소비자는
|
|
13483
|
+
``diagnostics`` 로 자가수정/안내를 결정적으로 분기할 수 있다.
|
|
13484
|
+
"""
|
|
13111
13485
|
return {
|
|
13112
13486
|
"order_result": {
|
|
13113
13487
|
"success": success,
|
|
@@ -13118,16 +13492,46 @@ class NewOrderNodeExecutor(NodeExecutorBase):
|
|
|
13118
13492
|
"price": price,
|
|
13119
13493
|
"status": "submitted" if success else "failed",
|
|
13120
13494
|
"error": error,
|
|
13495
|
+
"diagnostics": reject_info.model_dump() if reject_info else None,
|
|
13121
13496
|
},
|
|
13122
13497
|
"order_id": order_id,
|
|
13123
13498
|
}
|
|
13124
13499
|
|
|
13125
|
-
|
|
13126
|
-
|
|
13500
|
+
# Human/AI-facing English explanations keyed by EmptyOrderReason.
|
|
13501
|
+
_EMPTY_ORDER_MESSAGES: Dict[str, str] = {
|
|
13502
|
+
EmptyOrderReason.NO_SIGNAL.value: (
|
|
13503
|
+
"No trading signal today (upstream produced an empty result normally)."
|
|
13504
|
+
),
|
|
13505
|
+
EmptyOrderReason.FETCH_FAILED.value: (
|
|
13506
|
+
"Upstream data fetch failed; no order placed (pipeline error)."
|
|
13507
|
+
),
|
|
13508
|
+
EmptyOrderReason.NO_SYMBOL.value: (
|
|
13509
|
+
"No symbol provided to the order node (configuration missing)."
|
|
13510
|
+
),
|
|
13511
|
+
}
|
|
13512
|
+
|
|
13513
|
+
def _empty_result(
|
|
13514
|
+
self,
|
|
13515
|
+
reason: EmptyOrderReason = EmptyOrderReason.NO_SIGNAL,
|
|
13516
|
+
detail: str = "",
|
|
13517
|
+
) -> Dict[str, Any]:
|
|
13518
|
+
"""빈 주문 결과 반환.
|
|
13519
|
+
|
|
13520
|
+
``reason`` (no_signal / fetch_failed / no_symbol) 로 "오늘 신호 없음(정상)"
|
|
13521
|
+
과 "상류 파이프라인 고장(이상)" 을 결정적으로 구분한다. 기존 ``error`` 키는
|
|
13522
|
+
하위호환을 위해 보존하되, reason 기반 영어 설명(``message``)을 추가한다.
|
|
13523
|
+
``detail`` 은 상류 error 원문 등 진단 부가정보(없으면 빈 문자열).
|
|
13524
|
+
"""
|
|
13525
|
+
message = self._EMPTY_ORDER_MESSAGES.get(
|
|
13526
|
+
reason.value, self._EMPTY_ORDER_MESSAGES[EmptyOrderReason.NO_SIGNAL.value]
|
|
13527
|
+
)
|
|
13127
13528
|
return {
|
|
13128
13529
|
"order_result": {
|
|
13129
13530
|
"success": False,
|
|
13130
13531
|
"error": "No order to submit",
|
|
13532
|
+
"reason": reason.value,
|
|
13533
|
+
"message": message,
|
|
13534
|
+
"detail": detail,
|
|
13131
13535
|
},
|
|
13132
13536
|
"order_id": "",
|
|
13133
13537
|
}
|
|
@@ -17876,8 +18280,22 @@ class WorkflowJob:
|
|
|
17876
18280
|
# A-4: schedule_tick 은 정당한 신규 ordering 세대 → cycle +1.
|
|
17877
18281
|
# (entry 세대 0 과 분리되어 동일 주문이 매 tick 차단되지 않음)
|
|
17878
18282
|
self._order_cycle += 1
|
|
17879
|
-
|
|
17880
|
-
|
|
18283
|
+
# 사이클 격리: 단일 사이클의 노드 예외(잔고 부분실패의
|
|
18284
|
+
# ConditionEvaluationError, ScreenerNode silent-failure 가드의
|
|
18285
|
+
# RuntimeError 등)가 스케줄 잡 전체를 종료시키지 않도록 한다.
|
|
18286
|
+
# 실패 사이클은 silent 하지 않게 'cycle_failed' 로 통지하고
|
|
18287
|
+
# 다음 tick 에서 재시도한다 (24시간 무인 운영 보호).
|
|
18288
|
+
try:
|
|
18289
|
+
await self._execute_main_flow()
|
|
18290
|
+
except Exception as cycle_err:
|
|
18291
|
+
self.stats["errors_count"] = self.stats.get("errors_count", 0) + 1
|
|
18292
|
+
self.context.log(
|
|
18293
|
+
"error",
|
|
18294
|
+
f"Schedule cycle failed (job continues to next tick): {cycle_err}",
|
|
18295
|
+
)
|
|
18296
|
+
await self.context.notify_job_state("cycle_failed", self.stats)
|
|
18297
|
+
else:
|
|
18298
|
+
await self.context.notify_job_state("cycle_completed", self.stats)
|
|
17881
18299
|
|
|
17882
18300
|
logger.info("Event loop ended")
|
|
17883
18301
|
|
|
@@ -5,7 +5,8 @@ authors = [
|
|
|
5
5
|
homepage = "https://programgarden.com"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
name = "programgarden"
|
|
8
|
-
version = "1.24.
|
|
8
|
+
version = "1.24.2"
|
|
9
|
+
license = "AGPL-3.0-or-later"
|
|
9
10
|
description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
|
|
10
11
|
readme = "README.md"
|
|
11
12
|
|
|
@@ -28,9 +29,9 @@ lxml = "^6.0.2"
|
|
|
28
29
|
pytickersymbols = {version = ">=1.17.5", python = ">=3.12,<4.0"}
|
|
29
30
|
aiosqlite = "^0.20.0"
|
|
30
31
|
litellm = ">=1.40.0"
|
|
31
|
-
programgarden-core = "^1.15.
|
|
32
|
-
programgarden-finance = "^1.6.
|
|
33
|
-
programgarden-community = "^1.13.
|
|
32
|
+
programgarden-core = "^1.15.1"
|
|
33
|
+
programgarden-finance = "^1.6.11"
|
|
34
|
+
programgarden-community = "^1.13.9"
|
|
34
35
|
|
|
35
36
|
[tool.poetry.group.dev.dependencies]
|
|
36
37
|
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
|
|
File without changes
|
{programgarden-1.24.0 → programgarden-1.24.2}/programgarden/database/workflow_position_tracker.py
RENAMED
|
File without changes
|
{programgarden-1.24.0 → programgarden-1.24.2}/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
|
|
File without changes
|