from __future__ import annotations import math from typing import Any KRX_TICK_TABLE: tuple[tuple[float, int], ...] = ( (2_000, 1), (5_000, 5), (20_000, 10), (50_000, 50), (200_000, 100), (500_000, 500), (math.inf, 1000), ) def krx_tick_unit(price: float) -> int: for threshold, tick in KRX_TICK_TABLE: if price < threshold: return tick return 1000 def normalize_tick(price: float) -> int: tick = krx_tick_unit(price) return int(math.floor(price / tick) * tick) def compute_stop_price_core( entry_price: float | None, atr20: float | None, current_price: float | None = None, atr_multiplier: float | None = None, ) -> dict[str, Any]: if entry_price is None: return { "stop_price": None, "stop_price_status": "NO_STOP_PRICE", "data_missing": ["entry_price"], } if atr20 is None and atr_multiplier is None: return { "stop_price": entry_price * 0.92, "stop_price_status": "DATA_MISSING — 하네스 업데이트 필요", "data_missing": ["atr20"], } if atr_multiplier is None and current_price in (None, 0): return { "stop_price": entry_price * 0.92, "stop_price_status": "DATA_MISSING — 하네스 업데이트 필요", "data_missing": [name for name, value in (("atr20", atr20), ("current_price", current_price)) if value in (None, 0)], } if atr_multiplier is None: atr20_pct = (atr20 / current_price) * 100 atr_multiplier = 2.0 if atr20_pct >= 8 else 1.5 else: atr20_pct = (atr20 / current_price) * 100 if current_price not in (None, 0) else None return { "stop_price": max(entry_price * 0.92, entry_price - atr20 * atr_multiplier), "stop_price_status": "PASS", "atr20_pct": atr20_pct, "atr_multiplier": atr_multiplier, } def compute_stop_action_ladder(context: dict[str, Any]) -> dict[str, Any]: timing_action = str(context.get("timingAction") or context.get("timing_action") or "").upper() rw_partial = int(context.get("rw_partial") or 0) rw_partial_excluding_rw2b = int(context.get("rw_partial_excluding_rw2b") or 0) regime_prelim = str(context.get("REGIME_PRELIM") or context.get("regime_prelim") or "").upper() timing_exit_score = float(context.get("timingExitScore") or context.get("timing_exit_score") or 0.0) profit_pct = float(context.get("profitPct") or context.get("profit_pct") or 0.0) days_to_time_stop = int(context.get("daysToTimeStop") or context.get("days_to_time_stop") or 9999) trailing_stop_breach = bool(context.get("trailingStopBreach") or context.get("trailing_stop_breach") or False) rw2b_fast_track = bool(context.get("RW2b_5d_rapid_weakness") or context.get("rw2b_5d_rapid_weakness") or False) if timing_action == "STOP_OR_TIME_EXIT_READY" or rw_partial >= 4: return { "action": "EXIT_100", "reason": "STOP_OR_TIME_EXIT_READY" if timing_action == "STOP_OR_TIME_EXIT_READY" else "RW_EXIT_STRONG", "quantity_pct": 100, "priority": 1, } if regime_prelim in {"RISK_OFF", "RISK_OFF_CANDIDATE"}: return { "action": "REGIME_TRIM_50", "reason": "REGIME_RISK_OFF" if regime_prelim == "RISK_OFF" else "REGIME_RISK_OFF_CANDIDATE", "quantity_pct": 50, "priority": 2, } if rw2b_fast_track and rw_partial_excluding_rw2b >= 1: return { "action": "TRIM_50", "reason": "RW2B_FAST_TRACK", "quantity_pct": 50, "priority": 2.5, } if rw_partial >= 3 or timing_exit_score >= 75: return { "action": "TRIM_70", "reason": "RW_EXIT" if rw_partial >= 3 else "TIMING_EXIT_SCORE", "quantity_pct": 70, "priority": 3, } if trailing_stop_breach or rw_partial >= 2 or (rw_partial >= 1 and timing_exit_score >= 50): return { "action": "TRIM_50", "reason": "TRAILING_STOP_BREACH" if trailing_stop_breach else "RW_OR_TIMING_EXIT", "quantity_pct": 50, "priority": 4, } if profit_pct >= 10: return { "action": "TAKE_PROFIT_TIER1", "reason": "PROFIT_PCT_THRESHOLD", "quantity_pct": 25, "priority": 5, } if days_to_time_stop <= 0: return { "action": "TIME_EXIT_100", "reason": "TIME_STOP_EXPIRED", "quantity_pct": 100, "priority": 6, } return { "action": "REVIEW_HUMAN", "reason": "NO_FORCED_EXIT", "quantity_pct": 0, "priority": 7, } def compute_dynamic_heat_thresholds(regime: str | None) -> dict[str, float]: r = str(regime or "").upper() if "EVENT_SHOCK" in r: return {"hardBlock": 5.0, "halve": 3.5} if "RISK_OFF" in r: return {"hardBlock": 7.0, "halve": 5.0} if "SECULAR_LEADER" in r and "RISK_ON" in r: return {"hardBlock": 13.0, "halve": 9.0} if "RISK_ON" in r: return {"hardBlock": 12.0, "halve": 8.5} return {"hardBlock": 10.0, "halve": 7.0} def compute_cash_shortfall_harness( as_result: dict[str, Any], total_asset: float, cash_floor_info: dict[str, Any], mrs_score: float, ) -> dict[str, Any]: asset = total_asset if math.isfinite(total_asset) and total_asset > 0 else 0.0 d2_krw = float(as_result.get("settlementCashD2Krw") or 0.0) min_pct = float(cash_floor_info.get("minPct") or 0.0) target_cash_pct = max(5 + (mrs_score / 10) * 15, min_pct) return { "cash_current_pct_d2": round((d2_krw / asset * 100), 2) if asset > 0 else 0, "cash_target_pct": target_cash_pct, "cash_shortfall_min_krw": max(0, round(asset * min_pct / 100 - d2_krw)), "cash_shortfall_target_krw": max(0, round(asset * target_cash_pct / 100 - d2_krw)), } def compute_timing_decision(ctx: dict[str, Any]) -> dict[str, Any]: reasons: list[str] = [] entry_score = 0 exit_score = 0 entry_gate = str(ctx.get("entryModeGate") or "") entry_mode = str(ctx.get("entryMode") or "") leader_gate = str(ctx.get("leaderGate") or "") ac_gate = str(ctx.get("acGate") or "") exit_signal = str(ctx.get("exitSignalDetail") or "") flow_credit = ctx.get("flowCredit") leader_total = ctx.get("leaderTotal") rw_partial = ctx.get("rwPartial") rsi14 = ctx.get("rsi14") disparity = ctx.get("disparity") ma20_slope = ctx.get("ma20Slope") spread_pct = ctx.get("spreadPct") avg_trade_value_5d = ctx.get("avgTradeValue5D") profit_pct = ctx.get("profitPct") days_to_time_stop = ctx.get("daysToTimeStop") if entry_gate == "PASS": entry_score += 25 reasons.append(f"entry_{entry_mode}") elif entry_gate == "BLOCK": entry_score -= 25 reasons.append("entry_block") if isinstance(leader_total, (int, float)) and math.isfinite(float(leader_total)): if leader_total >= 4: entry_score += 20 reasons.append("leader_scan>=4") elif leader_total >= 3: entry_score += 10 reasons.append("leader_watch") if leader_gate in {"PASS", "EXPLORE_CANDIDATE"}: entry_score += 10 if isinstance(flow_credit, (int, float)) and math.isfinite(float(flow_credit)): if flow_credit >= 0.7: entry_score += 20 reasons.append("flow_strong") elif flow_credit >= 0.4: entry_score += 10 reasons.append("flow_partial") if ac_gate == "CLEAR": entry_score += 15 reasons.append("anti_climax_clear") elif ac_gate == "CAUTION": entry_score += 5 reasons.append("anti_climax_caution") elif ac_gate == "BLOCK": entry_score -= 35 exit_score += 15 reasons.append("anti_climax_block") if isinstance(ma20_slope, (int, float)) and math.isfinite(float(ma20_slope)): if ma20_slope > 0: entry_score += 8 else: entry_score -= 8 exit_score += 8 reasons.append("ma20_down") if isinstance(disparity, (int, float)) and math.isfinite(float(disparity)): if -5 <= disparity <= 4: entry_score += 10 elif 4 < disparity <= 8: entry_score += 5 elif disparity > 12: entry_score -= 25 exit_score += 20 reasons.append("overextended") elif disparity < -10: entry_score -= 10 exit_score += 10 reasons.append("trend_damage") if isinstance(rsi14, (int, float)) and math.isfinite(float(rsi14)): if 40 <= rsi14 <= 65: entry_score += 10 elif 65 < rsi14 <= 72: entry_score += 4 elif rsi14 > 75: entry_score -= 25 exit_score += 20 reasons.append("rsi_overbought") elif rsi14 < 35: entry_score -= 5 exit_score += 8 reasons.append("weak_rsi") if ( isinstance(avg_trade_value_5d, (int, float)) and math.isfinite(float(avg_trade_value_5d)) and avg_trade_value_5d >= 50 and (not isinstance(spread_pct, (int, float)) or not math.isfinite(float(spread_pct)) or spread_pct <= 0.8) ): entry_score += 10 else: entry_score -= 15 reasons.append("liquidity_or_spread_fail") if isinstance(rw_partial, (int, float)) and math.isfinite(float(rw_partial)): exit_score += min(100, max(0, int(rw_partial) * 25)) if exit_signal: exit_score += len([part for part in exit_signal.split("|") if part]) * 10 if isinstance(days_to_time_stop, (int, float)) and 0 <= float(days_to_time_stop) <= 7: exit_score += 20 reasons.append("time_stop_near") if isinstance(profit_pct, (int, float)) and profit_pct >= 10: exit_score += 15 reasons.append("profit_protect_zone") entry_score = max(0, min(100, round(entry_score))) exit_score = max(0, min(100, round(exit_score))) action = "HOLD_NO_TIMING_EDGE" atr20 = ctx.get("atr20") price_status = str(ctx.get("priceStatus") or "") if price_status != "PRICE_OK" or not isinstance(atr20, (int, float)) or not math.isfinite(float(atr20)): action = "OBSERVE_DATA_MISSING" elif exit_score >= 75 or (isinstance(rw_partial, (int, float)) and rw_partial >= 4): action = "STOP_OR_TIME_EXIT_READY" elif exit_score >= 50 or (isinstance(rw_partial, (int, float)) and rw_partial >= 3): action = "EXIT_REVIEW" elif entry_gate == "BLOCK" or ac_gate == "BLOCK" or entry_mode == "OVERBOUGHT": action = "NO_BUY_OVERHEATED" elif entry_score >= 75 and entry_gate == "PASS" and isinstance(leader_total, (int, float)) and leader_total >= 4: action = "BUY_BREAKOUT_PILOT_ONLY" if entry_mode == "BREAKOUT" else "BUY_STAGE1_READY" elif entry_score >= 60 and entry_gate == "PASS": action = "BUY_BREAKOUT_PILOT_ONLY" if entry_mode == "BREAKOUT" else "BUY_PULLBACK_WAIT" elif ( (isinstance(leader_total, (int, float)) and leader_total >= 3) or (isinstance(flow_credit, (int, float)) and flow_credit >= 0.4) ): action = "WATCH_TIMING_SETUP" return { "entry_score": entry_score, "exit_score": exit_score, "action": action, "reason": "|".join(reasons[:6]), } def compute_sell_decision(ctx: dict[str, Any]) -> dict[str, Any]: close = ctx.get("close") stop_price = ctx.get("stopPrice") trailing_stop = ctx.get("trailingStop") tp1_price = ctx.get("tp1Price") tp2_price = ctx.get("tp2Price") profit_pct = ctx.get("profitPct") rw_partial = ctx.get("rwPartial") timing_exit_score = ctx.get("timingExitScore") days_to_time_stop = ctx.get("daysToTimeStop") timing_action = str(ctx.get("timingAction") or "") regime = str(ctx.get("regime") or "") atr20 = ctx.get("atr20") close_f = float(close) if isinstance(close, (int, float)) else float("nan") stop_f = float(stop_price) if isinstance(stop_price, (int, float)) else float("nan") trailing_f = float(trailing_stop) if isinstance(trailing_stop, (int, float)) else float("nan") tp1_f = float(tp1_price) if isinstance(tp1_price, (int, float)) else float("nan") tp2_f = float(tp2_price) if isinstance(tp2_price, (int, float)) else float("nan") profit_f = float(profit_pct) if isinstance(profit_pct, (int, float)) else float("nan") rw_f = float(rw_partial) if isinstance(rw_partial, (int, float)) else float("nan") timing_exit_f = float(timing_exit_score) if isinstance(timing_exit_score, (int, float)) else float("nan") days_f = float(days_to_time_stop) if isinstance(days_to_time_stop, (int, float)) else float("nan") atr_f = float(atr20) if isinstance(atr20, (int, float)) else float("nan") action = "HOLD" ratio = 0 reason = "" price: Any = "" price_source = "" price_basis = "" execution_window = "" order_type = "" stop_candidate = trailing_f if math.isfinite(trailing_f) and trailing_f > 0 else stop_f if not (math.isfinite(stop_candidate) and stop_candidate > 0) and math.isfinite(close_f) and close_f > 0: stop_candidate = close_f * 0.995 protective_limit = round(min(close_f * 0.995, stop_candidate if math.isfinite(stop_candidate) else close_f * 0.995)) if math.isfinite(close_f) and close_f > 0 else "" atr_buffer = atr_f * 0.3 if math.isfinite(atr_f) and atr_f > 0 else (close_f * 0.005 if math.isfinite(close_f) else 0) close_protect_limit = round(close_f - atr_buffer) if math.isfinite(close_f) and close_f > 0 else "" if timing_action == "STOP_OR_TIME_EXIT_READY" or (math.isfinite(rw_f) and rw_f >= 4): action = "EXIT_100" ratio = 100 reason = "RW_EXIT_STRONG" if math.isfinite(rw_f) and rw_f >= 4 else "STOP_OR_TIME_EXIT_READY" price = protective_limit price_source = "TRAILING_STOP" if math.isfinite(trailing_f) else "STOP_OR_CLOSE" price_basis = "TRAILING_STOP_TRIGGER" if math.isfinite(trailing_f) else "STOP_OR_CLOSE_PROTECT" execution_window = "INTRADAY_ON_TRIGGER" order_type = "PROTECTIVE_LIMIT_SELL" elif math.isfinite(rw_f) and rw_f >= 3 or (math.isfinite(timing_exit_f) and timing_exit_f >= 75): action = "TRIM_70" ratio = 70 reason = "RW_EXIT" if math.isfinite(rw_f) and rw_f >= 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" elif math.isfinite(trailing_f) and trailing_f > 0 and math.isfinite(close_f) and close_f <= trailing_f: action = "TRAILING_STOP_BREACH" ratio = 70 reason = "TRAILING_STOP_PRICE_BREACH" price = round(trailing_f) price_source = "TRAILING_STOP_PRICE" price_basis = "TRAILING_STOP_TRIGGER" execution_window = "INTRADAY_ON_TRIGGER" order_type = "PROTECTIVE_LIMIT_SELL" elif (math.isfinite(rw_f) and rw_f >= 2) or (math.isfinite(rw_f) and rw_f >= 1 and math.isfinite(timing_exit_f) and timing_exit_f >= 50): action = "TRIM_50" ratio = 50 reason = "RW_REVIEW" if math.isfinite(rw_f) and rw_f >= 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" elif math.isfinite(rw_f) and rw_f >= 1 and math.isfinite(timing_exit_f) and timing_exit_f >= 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" elif math.isfinite(rw_f) and rw_f >= 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" elif math.isfinite(profit_f) and profit_f >= 50: action = "PROFIT_TRIM_50" ratio = 50 reason = "PROFIT_PROTECT_50" price = round(tp2_f) if math.isfinite(tp2_f) and tp2_f > 0 else close_protect_limit price_source = "TP2_PRICE" if math.isfinite(tp2_f) else "CLOSE_PROFIT_PROTECT" price_basis = "TAKE_PROFIT_TIER2_PRICE" if math.isfinite(tp2_f) else "PRIOR_CLOSE_X_0.998" execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW" order_type = "LIMIT_SELL" elif math.isfinite(profit_f) and profit_f >= 30: action = "PROFIT_TRIM_35" ratio = 35 reason = "PROFIT_PROTECT_30" price = round(tp2_f) if math.isfinite(tp2_f) and tp2_f > 0 else close_protect_limit price_source = "TP2_PRICE" if math.isfinite(tp2_f) else "CLOSE_PROFIT_PROTECT" price_basis = "TAKE_PROFIT_TIER2_PRICE" if math.isfinite(tp2_f) else "PRIOR_CLOSE_X_0.998" execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW" order_type = "LIMIT_SELL" elif math.isfinite(profit_f) and profit_f >= 20: action = "PROFIT_TRIM_25" ratio = 25 reason = "PROFIT_PROTECT_20" price = round(tp1_f) if math.isfinite(tp1_f) and tp1_f > 0 else close_protect_limit price_source = "TP1_PRICE" if math.isfinite(tp1_f) else "CLOSE_PROFIT_PROTECT" price_basis = "TAKE_PROFIT_TIER1_PRICE" if math.isfinite(tp1_f) else "PRIOR_CLOSE_X_0.998" execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW" order_type = "LIMIT_SELL" elif math.isfinite(profit_f) and profit_f >= 10: action = "TAKE_PROFIT_TIER1" ratio = 25 reason = "TP1_PROFIT_10PCT" price = round(tp1_f) if math.isfinite(tp1_f) and tp1_f > 0 else close_protect_limit price_source = "TP1_PRICE" if math.isfinite(tp1_f) else "CLOSE_PROFIT_PROTECT" price_basis = "TAKE_PROFIT_TIER1_PRICE" if math.isfinite(tp1_f) else "PRIOR_CLOSE_X_0.998" execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW" order_type = "LIMIT_SELL" elif math.isfinite(days_f) and days_f <= 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 math.isfinite(days_f) and days_f <= 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 math.isfinite(days_f) and days_f <= 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" validation = "NO_SELL_ACTION" if action != "HOLD": validation = "SIGNAL_CONFIRMED" if isinstance(price, (int, float)) and float(price) > 0 else "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_ratio": 0, "cash_preserve_reason": [], } def compute_final_decision(ctx: dict[str, Any]) -> dict[str, Any]: sell_action = str(ctx.get("sellAction") or "HOLD") sell_validation = str(ctx.get("sellValidation") or "") allowed_action = str(ctx.get("allowedAction") or "") timing_action = str(ctx.get("timingAction") or "") timing_entry = ctx.get("timingScoreEntry") timing_exit = ctx.get("timingScoreExit") ss001_total = ctx.get("ss001Total") flow_credit = ctx.get("flowCredit") leader_total = ctx.get("leaderTotal") rw_partial = ctx.get("rwPartial") profit_pct = ctx.get("profitPct") days_to_time_stop = ctx.get("daysToTimeStop") weight_pct = ctx.get("weightPct") ac_gate = str(ctx.get("acGate") or "") liquidity_status = str(ctx.get("liquidityStatus") or "") spread_status = str(ctx.get("spreadStatus") or "") dart_risk = bool(ctx.get("dartRisk")) missing_fields = str(ctx.get("missingFields") or "") final_action = "HOLD" action_priority = 99 decision_source = "RULE_ENGINE" if sell_action != "HOLD" and sell_validation == "SIGNAL_CONFIRMED": final_action = "SELL_READY" action_priority = 10 elif allowed_action == "EXIT_SIGNAL" or timing_action == "STOP_OR_TIME_EXIT_READY": final_action = "EXIT_SIGNAL" action_priority = 28 elif allowed_action == "REVIEW_EXIT" or timing_action == "EXIT_REVIEW": final_action = "EXIT_REVIEW" action_priority = 32 elif timing_action == "NO_BUY_OVERHEATED" and not dart_risk: final_action = "NO_BUY_OVERHEATED" action_priority = 50 elif allowed_action == "BUY_STAGE1_READY" or timing_action == "BUY_STAGE1_READY": final_action = "BUY_STAGE1_READY" action_priority = 60 elif allowed_action == "BUY_BREAKOUT_PILOT_ONLY" or timing_action == "BUY_BREAKOUT_PILOT_ONLY": final_action = "BUY_BREAKOUT_PILOT_ONLY" action_priority = 70 elif allowed_action == "BUY_PULLBACK_WAIT" or timing_action == "BUY_PULLBACK_WAIT": final_action = "BUY_PULLBACK_WAIT" action_priority = 80 elif allowed_action == "WATCH_CANDIDATE": final_action = "WATCH_TIMING_SETUP" action_priority = 90 if missing_fields: decision_source = "RULE_ENGINE_WITH_MISSING_DATA" def _finite(value: Any) -> bool: return isinstance(value, (int, float)) and math.isfinite(float(value)) time_stop_urgency = max(0, 20 - min(20, float(days_to_time_stop) * 3)) if _finite(days_to_time_stop) and days_to_time_stop >= 0 else 0 overweight_penalty = 15 if _finite(weight_pct) and weight_pct > 7 else 0 overheat_penalty = 30 if ac_gate == "BLOCK" else 10 if ac_gate == "CAUTION" else 0 liquidity_penalty = 15 if liquidity_status in {"LOW", "DATA_MISSING"} or spread_status in {"BLOCK", "WIDE", "QUOTE_NO_MATCH"} else 0 if action_priority <= 40: priority_score = (float(timing_exit) if _finite(timing_exit) else 0) * 0.35 + (float(rw_partial) if _finite(rw_partial) else 0) * 15 + max(0, float(profit_pct) if _finite(profit_pct) else 0) * 0.30 + time_stop_urgency + overweight_penalty elif 50 <= action_priority <= 80: priority_score = (float(timing_entry) if _finite(timing_entry) else 0) * 0.35 + (float(ss001_total) if _finite(ss001_total) else 0) * 0.30 + (float(flow_credit) if _finite(flow_credit) else 0) * 20 + (float(leader_total) if _finite(leader_total) else 0) * 5 - overheat_penalty - liquidity_penalty else: priority_score = (float(timing_entry) if _finite(timing_entry) else 0) * 0.20 + (float(timing_exit) if _finite(timing_exit) else 0) * 0.20 + (float(flow_credit) if _finite(flow_credit) else 0) * 10 return { "final_action": final_action, "action_priority": action_priority, "priority_score": float(max(0, priority_score)), "decision_source": decision_source, }