WBS-7: Phase 7 보완·고도화 — 9개 항목 완료, F05/F10 포팅 준비
완료 항목 (9개): ✅ WBS-7.1: 캘리브레이션 도구 (data_gated, 실거래 축적 대기) ✅ WBS-7.2: T+5 단일 진실원천 통일 (spec/27_bch_calibration_runbook.yaml) ✅ WBS-7.3 부분: 9/15 GAS→Python 마이그레이션 - F02-F06 (priceBasis 로직) - F07 (점수 임계값) - F09, F11, F14, F15 (각종 게이트 및 판정 로직) ✅ WBS-7.4: Deprecated 별칭 정리 (2026-06-21 완료) ✅ WBS-7.5: 하드코딩 폴백 정규화 (3개 항목 → threshold 등록) ✅ WBS-7.6: 슬리피지 5bps 정규화 (EXECUTION_SLIPPAGE_BPS) ✅ WBS-7.7: E2E 통합테스트 (3개 항목 모두 PASS) ✅ WBS-7.9: Naver Cloudflare 403 모니터링 (구조화된 에러 처리) ✅ WBS-7.10: 공매도 수동 CSV 운영절차 명문화 미완료 항목 (2개, 다음 세션): 🔄 F05: calcExitSellAction_() 포팅 - formulas/execution_decision_v1.py 생성 (430줄) - 로직 완성 (calc_exit_sell_action, calc_cash_preservation_plan) - 상태: CONFIRMED_PORTABLE, 테스트 디버깅 필요 - 추정 시간: 2-3시간 (parity 테스트 완성, F10과 함께) 🔄 F10: runRouteFlow_() 포팅 - 242줄 함수, 6개 게이트 로직 (stop_breach, relative_stop, intraday_lock, heat, mean_reversion, ...) - 상태: CONFIRMED_PORTABLE (GAS API 미사용, 순수 함수) - 추정 시간: 2-3시간 (F05와 함께) 전체 테스트: 102/102 단위 테스트 PASS 다음 세션 계획: 1. F05/F10 parity 테스트 구축 및 PASS (각 ~50줄 테스트) 2. ledger 업데이트 (F05/F10 → DONE) 3. WBS-7.3 최종 종결 (15/15 완료 또는 최종 상태 확정) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
Exit/sell action decision logic for portfolio execution.
|
||||
|
||||
F05/F10 porting: Determines the sell action, ratio, price target, and execution details
|
||||
based on market signals (RW, timing, profit levels, time stops, stop losses).
|
||||
|
||||
Ported from: src/gas_adapter_parts/gdf_01_price_metrics.gs:calcExitSellAction_
|
||||
src/gas_adapter_parts/gdf_01_price_metrics.gs:calcCashPreservationPlan_
|
||||
Parity reference: tests/parity/test_execution_decision_parity_v1.py
|
||||
"""
|
||||
|
||||
import math
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def is_finite(value: Any) -> bool:
|
||||
"""Check if value is a finite number (matches JavaScript Number.isFinite())."""
|
||||
return isinstance(value, (int, float)) and math.isfinite(value)
|
||||
|
||||
|
||||
def calc_cash_preservation_plan(ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Calculate cash preservation adjustment to sell action.
|
||||
|
||||
Factors: core/leader status, rebound holdback score, cash floor, regime, liquidity,
|
||||
account type (tax), RW signals.
|
||||
|
||||
Args:
|
||||
ctx: Dict with keys:
|
||||
- cashFloorStatus: "TRIM_REQUIRED", "HARD_BLOCK", etc.
|
||||
- regime: Market regime (e.g., "RISK_OFF")
|
||||
- sellAction: Sell action (e.g., "TRIM_50")
|
||||
- isCoreLeader: bool
|
||||
- isEtf: bool
|
||||
- liquidityStatus: "LOW", "OK", etc.
|
||||
- spreadStatus: "WIDE", "OK", "BLOCK", etc.
|
||||
- accountType: "일반계좌", "연금계좌", etc.
|
||||
- profitPct: Profit percentage
|
||||
- rwPartial: Relative weakness signal count (0-5)
|
||||
- reboundHoldbackScore: Rebound preservation score
|
||||
|
||||
Returns:
|
||||
Dict: {
|
||||
"style": "CORE_LAST" | "STEP_25" | "STEP_33" | "STEP_50",
|
||||
"recommended_ratio": 0-50 (sell ratio override),
|
||||
"protection_bonus": integer (risk bonus points),
|
||||
"reasons": "reason1 | reason2 | ..."
|
||||
}
|
||||
"""
|
||||
cash_floor_status = str(ctx.get("cashFloorStatus", ""))
|
||||
regime = str(ctx.get("regime", ""))
|
||||
sell_action = str(ctx.get("sellAction", ctx.get("action", "")))
|
||||
is_sell_like = re.search(r"(SELL|TRIM|EXIT)", sell_action) is not None
|
||||
is_core_leader = bool(ctx.get("isCoreLeader"))
|
||||
is_etf = bool(ctx.get("isEtf"))
|
||||
liquidity_status = str(ctx.get("liquidityStatus", ""))
|
||||
spread_status = str(ctx.get("spreadStatus", ""))
|
||||
account_type = str(ctx.get("accountType", ""))
|
||||
profit_pct = float(ctx.get("profitPct", float("nan")))
|
||||
rw_partial = int(ctx.get("rwPartial", 0))
|
||||
rebound_holdback = float(ctx.get("reboundHoldbackScore", float("nan")))
|
||||
holdback_score = rebound_holdback if is_finite(rebound_holdback) else 0
|
||||
|
||||
recommended_ratio = 50 if is_sell_like else 0
|
||||
style = "STEP_50"
|
||||
protection_bonus = 0
|
||||
reasons = []
|
||||
|
||||
if is_core_leader and holdback_score >= 12:
|
||||
style = "CORE_LAST"
|
||||
recommended_ratio = 25 if cash_floor_status == "TRIM_REQUIRED" else 0
|
||||
protection_bonus += 12
|
||||
reasons.append("core_last")
|
||||
elif holdback_score >= 18:
|
||||
style = "STEP_25"
|
||||
recommended_ratio = 25
|
||||
protection_bonus += 10
|
||||
reasons.append("strong_rebound")
|
||||
elif holdback_score >= 10:
|
||||
style = "STEP_33"
|
||||
recommended_ratio = 33
|
||||
protection_bonus += 6
|
||||
reasons.append("rebound_preserve")
|
||||
|
||||
if is_etf and holdback_score < 10:
|
||||
protection_bonus -= 2
|
||||
reasons.append("etf_cash_raise")
|
||||
|
||||
if cash_floor_status == "TRIM_REQUIRED" or re.search(r"RISK_OFF", regime):
|
||||
protection_bonus += 2
|
||||
reasons.append("cash_preserve")
|
||||
|
||||
if liquidity_status == "LOW" or spread_status in ("WIDE", "BLOCK"):
|
||||
protection_bonus += 4
|
||||
reasons.append("impact_avoid")
|
||||
|
||||
if account_type == "일반계좌" and is_finite(profit_pct) and profit_pct > 0:
|
||||
protection_bonus += 3 if profit_pct >= 20 else 2
|
||||
reasons.append("tax_drag")
|
||||
elif account_type == "일반계좌" and is_finite(profit_pct) and profit_pct < 0:
|
||||
protection_bonus -= 2
|
||||
reasons.append("tax_loss_harvest")
|
||||
|
||||
if rw_partial >= 3 and not is_core_leader:
|
||||
recommended_ratio = max(recommended_ratio, 50)
|
||||
protection_bonus -= 4
|
||||
reasons.append("rw_force")
|
||||
|
||||
if cash_floor_status == "HARD_BLOCK":
|
||||
recommended_ratio = max(recommended_ratio, 50)
|
||||
reasons.append("cash_hard_block")
|
||||
|
||||
if not is_sell_like:
|
||||
recommended_ratio = 0
|
||||
recommended_ratio = max(0, min(50, recommended_ratio))
|
||||
|
||||
return {
|
||||
"style": style,
|
||||
"recommended_ratio": recommended_ratio,
|
||||
"protection_bonus": max(0, round(protection_bonus)),
|
||||
"reasons": " | ".join(reasons),
|
||||
}
|
||||
|
||||
|
||||
def calc_exit_sell_action(ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Determine exit/sell action based on priority matrix of signals.
|
||||
|
||||
Priority hierarchy (spec/exit/stop_loss.yaml):
|
||||
1. Hard stop / strong RW (EXIT_100, rwPartial >= 4)
|
||||
2. REGIME_TRIM_50 (RISK_OFF — portfolio-level, skipped here)
|
||||
3. RW strong + timing (TRIM_70)
|
||||
4. Trailing stop breach
|
||||
5. RW medium / timing-based trims (TRIM_50, TRIM_33, TRIM_25)
|
||||
6. Profit-taking ladder (TP1/TP2 tiers)
|
||||
7. Time stop (TIME_EXIT_100, TIME_TRIM_*)
|
||||
|
||||
Args:
|
||||
ctx: Dict with keys from data_feed row + macro context:
|
||||
- close, stopPrice, trailingStop, tp1Price, tp2Price, profitPct
|
||||
- rwPartial, timingExitScore, daysToTimeStop, timingAction
|
||||
- exitSignalDetail, acGate, regime, atr20
|
||||
- cashFloorStatus, isCoreLeader, isEtf, liquidityStatus, spreadStatus
|
||||
- accountType, reboundHoldbackScore
|
||||
|
||||
Returns:
|
||||
Dict: {
|
||||
"action": "HOLD" | "EXIT_100" | "TRIM_70" | ... | "TIME_TRIM_25",
|
||||
"ratio_pct": 0-100,
|
||||
"limit_price": price (KRW integer) or "",
|
||||
"price_source": "TP2_PRICE" | "TRAILING_STOP" | ... | "ATR_PROTECT_LIMIT",
|
||||
"price_basis": "TAKE_PROFIT_TIER2_PRICE" | ... | "ATR_PROTECT_LIMIT",
|
||||
"execution_window": "INTRADAY_ON_TRIGGER" | "INTRADAY_LIMIT_OR_CLOSE_REVIEW" | ...,
|
||||
"order_type": "LIMIT_SELL" | "PROTECTIVE_LIMIT_SELL",
|
||||
"reason": "RW_EXIT_STRONG" | ... | "TIME_STOP_APPROACHING",
|
||||
"validation": "SIGNAL_CONFIRMED" | "NO_SELL_PRICE" | "NO_SELL_ACTION",
|
||||
"cash_preserve_style": "STEP_50" | ...,
|
||||
"cash_preserve_ratio": 0-50,
|
||||
"cash_preserve_reason": "..."
|
||||
}
|
||||
"""
|
||||
def safe_float(v, default=float("nan")):
|
||||
"""Safely convert to float, handling None/invalid values."""
|
||||
if v is None or v == "":
|
||||
return default
|
||||
try:
|
||||
return float(v)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
close = safe_float(ctx.get("close"))
|
||||
stop_price = safe_float(ctx.get("stopPrice"))
|
||||
trailing_stop = safe_float(ctx.get("trailingStop"))
|
||||
tp1_price = safe_float(ctx.get("tp1Price"))
|
||||
tp2_price = safe_float(ctx.get("tp2Price"))
|
||||
profit_pct = safe_float(ctx.get("profitPct"))
|
||||
rw_partial = int(ctx.get("rwPartial", 0))
|
||||
timing_exit_score = safe_float(ctx.get("timingExitScore"))
|
||||
days_to_time_stop = int(ctx.get("daysToTimeStop", 999))
|
||||
timing_action = str(ctx.get("timingAction", ""))
|
||||
regime = str(ctx.get("regime", ""))
|
||||
atr20 = safe_float(ctx.get("atr20"))
|
||||
|
||||
action = "HOLD"
|
||||
ratio = 0
|
||||
reason = ""
|
||||
price = ""
|
||||
price_source = ""
|
||||
price_basis = ""
|
||||
execution_window = ""
|
||||
order_type = ""
|
||||
|
||||
# Calculate protective limits
|
||||
stop_candidate = (
|
||||
trailing_stop if is_finite(trailing_stop) and trailing_stop > 0
|
||||
else stop_price if is_finite(stop_price) and stop_price > 0
|
||||
else close * 0.995 if is_finite(close) and close > 0
|
||||
else None
|
||||
)
|
||||
protective_limit = (
|
||||
round(min(close * 0.995, stop_candidate if stop_candidate else close * 0.995))
|
||||
if is_finite(close) and close > 0
|
||||
else ""
|
||||
)
|
||||
atr_buffer = (
|
||||
atr20 * 0.3 if is_finite(atr20) and atr20 > 0
|
||||
else close * 0.005 if is_finite(close)
|
||||
else 0
|
||||
)
|
||||
close_protect_limit = (
|
||||
round(close - atr_buffer)
|
||||
if is_finite(close) and close > 0
|
||||
else ""
|
||||
)
|
||||
|
||||
# Priority 1: Hard stop / strong RW
|
||||
if timing_action == "STOP_OR_TIME_EXIT_READY" or rw_partial >= 4:
|
||||
action = "EXIT_100"
|
||||
ratio = 100
|
||||
reason = "RW_EXIT_STRONG" if rw_partial >= 4 else "STOP_OR_TIME_EXIT_READY"
|
||||
price = protective_limit
|
||||
price_source = "TRAILING_STOP" if is_finite(trailing_stop) else "STOP_OR_CLOSE"
|
||||
price_basis = "TRAILING_STOP_TRIGGER" if is_finite(trailing_stop) else "STOP_OR_CLOSE_PROTECT"
|
||||
execution_window = "INTRADAY_ON_TRIGGER"
|
||||
order_type = "PROTECTIVE_LIMIT_SELL"
|
||||
# Priority 3: RW strong + timing
|
||||
elif rw_partial >= 3 or timing_exit_score >= 75:
|
||||
action = "TRIM_70"
|
||||
ratio = 70
|
||||
reason = "RW_EXIT" if rw_partial >= 3 else "TIMING_EXIT_SCORE"
|
||||
price = protective_limit
|
||||
price_source = "RISK_REDUCTION"
|
||||
price_basis = "RISK_REDUCTION_CLOSE_PROTECT"
|
||||
execution_window = "INTRADAY_AFTER_09_30"
|
||||
order_type = "PROTECTIVE_LIMIT_SELL"
|
||||
# Priority 4: Trailing stop breach
|
||||
elif is_finite(trailing_stop) and trailing_stop > 0 and is_finite(close) and close <= trailing_stop:
|
||||
action = "TRAILING_STOP_BREACH"
|
||||
ratio = 70
|
||||
reason = "TRAILING_STOP_PRICE_BREACH"
|
||||
price = round(trailing_stop)
|
||||
price_source = "TRAILING_STOP_PRICE"
|
||||
price_basis = "TRAILING_STOP_TRIGGER"
|
||||
execution_window = "INTRADAY_ON_TRIGGER"
|
||||
order_type = "PROTECTIVE_LIMIT_SELL"
|
||||
# Priority 4 (cont): RW medium
|
||||
elif rw_partial >= 2 or (rw_partial >= 1 and timing_exit_score >= 50):
|
||||
action = "TRIM_50"
|
||||
ratio = 50
|
||||
reason = "RW_REVIEW" if rw_partial >= 2 else "TIMING_EXIT_REVIEW"
|
||||
price = close_protect_limit
|
||||
price_source = "RELATIVE_WEAKNESS_CLOSE"
|
||||
price_basis = "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_AFTER_09_30"
|
||||
order_type = "LIMIT_SELL"
|
||||
# Priority 4b: RW early warning
|
||||
elif rw_partial >= 1 and timing_exit_score >= 30:
|
||||
action = "TRIM_33"
|
||||
ratio = 33
|
||||
reason = "RW_EARLY_WARNING"
|
||||
price = close_protect_limit
|
||||
price_source = "EARLY_WARNING_CLOSE"
|
||||
price_basis = "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_AFTER_09_30"
|
||||
order_type = "LIMIT_SELL"
|
||||
# Priority 4c: RW signal only
|
||||
elif rw_partial >= 1:
|
||||
action = "TRIM_25"
|
||||
ratio = 25
|
||||
reason = "RW_SIGNAL_ONLY"
|
||||
price = close_protect_limit
|
||||
price_source = "SIGNAL_ONLY_CLOSE"
|
||||
price_basis = "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
|
||||
order_type = "LIMIT_SELL"
|
||||
# Priority 5: Profit-taking ladder
|
||||
elif is_finite(profit_pct) and profit_pct >= 50:
|
||||
action = "PROFIT_TRIM_50"
|
||||
ratio = 50
|
||||
reason = "PROFIT_PROTECT_50"
|
||||
price = round(tp2_price) if is_finite(tp2_price) and tp2_price > 0 else close_protect_limit
|
||||
price_source = "TP2_PRICE" if is_finite(tp2_price) else "CLOSE_PROFIT_PROTECT"
|
||||
price_basis = "TAKE_PROFIT_TIER2_PRICE" if is_finite(tp2_price) else "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif is_finite(profit_pct) and profit_pct >= 30:
|
||||
action = "PROFIT_TRIM_35"
|
||||
ratio = 35
|
||||
reason = "PROFIT_PROTECT_30"
|
||||
price = round(tp2_price) if is_finite(tp2_price) and tp2_price > 0 else close_protect_limit
|
||||
price_source = "TP2_PRICE" if is_finite(tp2_price) else "CLOSE_PROFIT_PROTECT"
|
||||
price_basis = "TAKE_PROFIT_TIER2_PRICE" if is_finite(tp2_price) else "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif is_finite(profit_pct) and profit_pct >= 20:
|
||||
action = "PROFIT_TRIM_25"
|
||||
ratio = 25
|
||||
reason = "PROFIT_PROTECT_20"
|
||||
price = round(tp1_price) if is_finite(tp1_price) and tp1_price > 0 else close_protect_limit
|
||||
price_source = "TP1_PRICE" if is_finite(tp1_price) else "CLOSE_PROFIT_PROTECT"
|
||||
price_basis = "TAKE_PROFIT_TIER1_PRICE" if is_finite(tp1_price) else "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif is_finite(profit_pct) and profit_pct >= 10:
|
||||
action = "TAKE_PROFIT_TIER1"
|
||||
ratio = 25
|
||||
reason = "TP1_PROFIT_10PCT"
|
||||
price = round(tp1_price) if is_finite(tp1_price) and tp1_price > 0 else close_protect_limit
|
||||
price_source = "TP1_PRICE" if is_finite(tp1_price) else "CLOSE_PROFIT_PROTECT"
|
||||
price_basis = "TAKE_PROFIT_TIER1_PRICE" if is_finite(tp1_price) else "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
|
||||
order_type = "LIMIT_SELL"
|
||||
# Priority 6: Time stop
|
||||
elif is_finite(days_to_time_stop) and days_to_time_stop <= 0:
|
||||
action = "TIME_EXIT_100"
|
||||
ratio = 100
|
||||
reason = "TIME_STOP_EXPIRED"
|
||||
price = protective_limit
|
||||
price_source = "TIME_STOP_CLOSE"
|
||||
price_basis = "TIME_STOP_CLOSE_PROTECT"
|
||||
execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
|
||||
order_type = "PROTECTIVE_LIMIT_SELL"
|
||||
elif is_finite(days_to_time_stop) and days_to_time_stop <= 7:
|
||||
action = "TIME_TRIM_50"
|
||||
ratio = 50
|
||||
reason = "TIME_STOP_NEAR"
|
||||
price = close_protect_limit
|
||||
price_source = "TIME_STOP_NEAR_CLOSE"
|
||||
price_basis = "ATR_PROTECT_LIMIT"
|
||||
execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif is_finite(days_to_time_stop) and days_to_time_stop <= 14:
|
||||
action = "TIME_TRIM_25"
|
||||
ratio = 25
|
||||
reason = "TIME_STOP_APPROACHING"
|
||||
price = close_protect_limit
|
||||
price_source = "TIME_STOP_APPROACHING_CLOSE"
|
||||
price_basis = "ATR_PROTECT_LIMIT"
|
||||
execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
|
||||
order_type = "LIMIT_SELL"
|
||||
|
||||
# Apply cash preservation plan adjustments
|
||||
cash_preserve_plan = calc_cash_preservation_plan({
|
||||
"cashFloorStatus": ctx.get("cashFloorStatus", ""),
|
||||
"regime": regime,
|
||||
"sellAction": action,
|
||||
"isCoreLeader": ctx.get("isCoreLeader"),
|
||||
"isEtf": ctx.get("isEtf"),
|
||||
"liquidityStatus": ctx.get("liquidityStatus", ""),
|
||||
"spreadStatus": ctx.get("spreadStatus", ""),
|
||||
"accountType": ctx.get("accountType", ""),
|
||||
"profitPct": profit_pct,
|
||||
"rwPartial": rw_partial,
|
||||
"reboundHoldbackScore": float(ctx.get("reboundHoldbackScore", float("nan"))),
|
||||
})
|
||||
|
||||
if action not in ("EXIT_100", "TRAILING_STOP_BREACH", "HOLD"):
|
||||
target_ratio = cash_preserve_plan.get("recommended_ratio", 0)
|
||||
if is_finite(target_ratio) and target_ratio > 0 and target_ratio < ratio:
|
||||
ratio = target_ratio
|
||||
if ratio <= 25:
|
||||
action = "TRIM_25"
|
||||
elif ratio <= 33:
|
||||
action = "TRIM_33"
|
||||
else:
|
||||
action = "TRIM_50"
|
||||
reason = (
|
||||
f"{reason}|CASH_PRESERVE:{cash_preserve_plan['style']}"
|
||||
if reason
|
||||
else f"CASH_PRESERVE:{cash_preserve_plan['style']}"
|
||||
)
|
||||
|
||||
# SL003 Priority Matrix: when multiple stop conditions trigger, use max price
|
||||
is_stop_type_action = re.match(
|
||||
r"^(EXIT_100|TRIM_70|TRAILING_STOP_BREACH|TRIM_50|TRIM_33|TRIM_25|TIME_EXIT_100|TIME_TRIM_50|TIME_TRIM_25)$",
|
||||
action
|
||||
) is not None
|
||||
|
||||
if is_stop_type_action and is_finite(close) and close > 0:
|
||||
slp_candidates = []
|
||||
|
||||
if timing_action == "STOP_OR_TIME_EXIT_READY" or rw_partial >= 4:
|
||||
if is_finite(protective_limit) and protective_limit > 0:
|
||||
slp_candidates.append({"src": "HARD_STOP", "p": protective_limit})
|
||||
|
||||
if rw_partial >= 3 or timing_exit_score >= 75:
|
||||
if is_finite(protective_limit) and protective_limit > 0:
|
||||
slp_candidates.append({"src": "RW_TRIM70", "p": protective_limit})
|
||||
|
||||
if is_finite(trailing_stop) and trailing_stop > 0 and is_finite(close) and close <= trailing_stop:
|
||||
slp_candidates.append({"src": "TRAILING", "p": round(trailing_stop)})
|
||||
|
||||
if rw_partial >= 2 or (rw_partial >= 1 and timing_exit_score >= 50):
|
||||
if is_finite(close_protect_limit) and close_protect_limit > 0:
|
||||
slp_candidates.append({"src": "RW_TRIM50", "p": close_protect_limit})
|
||||
|
||||
if is_finite(days_to_time_stop) and days_to_time_stop <= 7:
|
||||
if is_finite(close_protect_limit) and close_protect_limit > 0:
|
||||
slp_candidates.append({"src": "TIME_STOP", "p": close_protect_limit})
|
||||
|
||||
if len(slp_candidates) >= 2:
|
||||
max_slp = max(slp_candidates, key=lambda x: x["p"])
|
||||
cur_price = float(price) if price else 0
|
||||
if max_slp["p"] > cur_price:
|
||||
price = max_slp["p"]
|
||||
price_source = "PRIORITY_MATRIX_MAX"
|
||||
candidates_str = "|".join([f"{c['src']}:{c['p']}" for c in slp_candidates])
|
||||
price_basis = f"SL003_MAX({candidates_str})"
|
||||
|
||||
# Validation
|
||||
validation = "NO_SELL_ACTION"
|
||||
if action != "HOLD":
|
||||
try:
|
||||
price_val = float(price) if price else 0
|
||||
validation = "SIGNAL_CONFIRMED" if is_finite(price_val) and price_val > 0 else "NO_SELL_PRICE"
|
||||
except (ValueError, TypeError):
|
||||
validation = "NO_SELL_PRICE"
|
||||
|
||||
return {
|
||||
"action": action,
|
||||
"ratio_pct": ratio,
|
||||
"limit_price": price,
|
||||
"price_source": price_source,
|
||||
"price_basis": price_basis,
|
||||
"execution_window": execution_window,
|
||||
"order_type": order_type,
|
||||
"reason": reason,
|
||||
"validation": validation,
|
||||
"cash_preserve_style": cash_preserve_plan["style"],
|
||||
"cash_preserve_ratio": cash_preserve_plan["recommended_ratio"],
|
||||
"cash_preserve_reason": cash_preserve_plan["reasons"],
|
||||
}
|
||||
Reference in New Issue
Block a user