|
|
|
@@ -0,0 +1,157 @@
|
|
|
|
|
# Exit Decisions Parity Module v1.0
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
import math
|
|
|
|
|
|
|
|
|
|
def normalize_tick(price: float) -> float:
|
|
|
|
|
if price < 2000:
|
|
|
|
|
return math.floor(price)
|
|
|
|
|
elif price < 5000:
|
|
|
|
|
return math.floor(price / 5) * 5
|
|
|
|
|
elif price < 20000:
|
|
|
|
|
return math.floor(price / 10) * 10
|
|
|
|
|
elif price < 50000:
|
|
|
|
|
return math.floor(price / 50) * 50
|
|
|
|
|
elif price < 200000:
|
|
|
|
|
return math.floor(price / 100) * 100
|
|
|
|
|
elif price < 500000:
|
|
|
|
|
return math.floor(price / 500) * 500
|
|
|
|
|
else:
|
|
|
|
|
return math.floor(price / 1000) * 1000
|
|
|
|
|
|
|
|
|
|
def compute_sell_decision(item: dict) -> dict:
|
|
|
|
|
close = item.get("close", 0)
|
|
|
|
|
profit_pct = item.get("profitPct", 0.0) or 0.0
|
|
|
|
|
tp1_price = item.get("tp1Price")
|
|
|
|
|
tp2_price = item.get("tp2Price")
|
|
|
|
|
rw_partial = item.get("rwPartial")
|
|
|
|
|
days_to_time_stop = item.get("daysToTimeStop")
|
|
|
|
|
|
|
|
|
|
# Time Exit Logic
|
|
|
|
|
if days_to_time_stop is not None:
|
|
|
|
|
if days_to_time_stop == 0:
|
|
|
|
|
return {"action": "TIME_EXIT_100", "ratio_pct": 100, "price_basis": "TIME_STOP_CLOSE_PROTECT", "order_type": "LIMIT_SELL", "limit_price": close, "reason": "TIME_STOP_EXPIRED"}
|
|
|
|
|
elif days_to_time_stop in (6, 7):
|
|
|
|
|
return {"action": "TIME_TRIM_50", "ratio_pct": 50, "price_basis": "TIME_STOP_CLOSE_PROTECT", "order_type": "LIMIT_SELL", "limit_price": close, "reason": "TIME_STOP_APPROACHING"}
|
|
|
|
|
elif days_to_time_stop == 14:
|
|
|
|
|
return {"action": "TIME_TRIM_25", "ratio_pct": 25, "price_basis": "TIME_STOP_CLOSE_PROTECT", "order_type": "LIMIT_SELL", "limit_price": close, "reason": "TIME_STOP_2WK_GATE"}
|
|
|
|
|
elif days_to_time_stop >= 15:
|
|
|
|
|
return {"action": "HOLD", "ratio_pct": 0, "price_basis": "MARKET_CLOSE", "order_type": "NONE", "limit_price": close, "reason": "TIME_STOP_NOT_ACTIVE"}
|
|
|
|
|
|
|
|
|
|
# Relative Weakness Logic
|
|
|
|
|
if rw_partial == 1:
|
|
|
|
|
return {"action": "TRIM_25", "ratio_pct": 25, "price_basis": "PRIOR_CLOSE_X_0.998", "order_type": "LIMIT_SELL", "limit_price": close * 0.998, "reason": "RW_PARTIAL_1"}
|
|
|
|
|
elif rw_partial == 2:
|
|
|
|
|
return {"action": "TRIM_50", "ratio_pct": 50, "price_basis": "PRIOR_CLOSE_X_0.998", "order_type": "LIMIT_SELL", "limit_price": close * 0.998, "reason": "RW_PARTIAL_2"}
|
|
|
|
|
|
|
|
|
|
# TP2 Logic
|
|
|
|
|
if profit_pct >= 50.0:
|
|
|
|
|
if tp2_price is not None and tp2_price > 0:
|
|
|
|
|
return {"action": "TAKE_PROFIT_TIER2", "ratio_pct": 50, "price_basis": "TAKE_PROFIT_TIER2_PRICE", "order_type": "LIMIT_SELL", "limit_price": tp2_price, "reason": "TP2_PROFIT_50PCT"}
|
|
|
|
|
else:
|
|
|
|
|
return {
|
|
|
|
|
"action": "PROFIT_TRIM_50",
|
|
|
|
|
"ratio_pct": 50,
|
|
|
|
|
"price_basis": "PRIOR_CLOSE_X_0.998",
|
|
|
|
|
"price_source": "CLOSE_PROFIT_PROTECT",
|
|
|
|
|
"order_type": "LIMIT_SELL",
|
|
|
|
|
"limit_price": close * 0.998,
|
|
|
|
|
"reason": "TP2_PROFIT_50PCT_NO_TARGET"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# TP1 Fallback / Profit Trim
|
|
|
|
|
if profit_pct >= 20.0 and tp1_price is None:
|
|
|
|
|
return {
|
|
|
|
|
"action": "PROFIT_TRIM_25",
|
|
|
|
|
"ratio_pct": 25,
|
|
|
|
|
"price_basis": "PRIOR_CLOSE_X_0.998",
|
|
|
|
|
"validation": "SIGNAL_CONFIRMED",
|
|
|
|
|
"order_type": "LIMIT_SELL",
|
|
|
|
|
"limit_price": close * 0.998,
|
|
|
|
|
"reason": "TP1_PROFIT_20PCT_NO_TARGET"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# TP1 Logic
|
|
|
|
|
if tp1_price is not None and tp1_price > 0:
|
|
|
|
|
if close >= tp1_price:
|
|
|
|
|
return {"action": "TAKE_PROFIT_TIER1", "ratio_pct": 25, "price_basis": "TAKE_PROFIT_TIER1_PRICE", "order_type": "LIMIT_SELL", "limit_price": tp1_price, "reason": "TP1_PRICE_TARGET_HIT"}
|
|
|
|
|
|
|
|
|
|
if profit_pct >= 10.0:
|
|
|
|
|
if tp1_price is not None and tp1_price > 0:
|
|
|
|
|
return {"action": "TAKE_PROFIT_TIER1", "ratio_pct": 25, "price_basis": "TAKE_PROFIT_TIER1_PRICE", "order_type": "LIMIT_SELL", "limit_price": tp1_price, "reason": "TP1_PROFIT_10PCT"}
|
|
|
|
|
else:
|
|
|
|
|
return {"action": "TAKE_PROFIT_TIER1", "ratio_pct": 25, "price_basis": "PRIOR_CLOSE_X_0.998", "order_type": "LIMIT_SELL", "limit_price": close * 0.998, "reason": "TP1_PROFIT_10PCT_NO_TARGET"}
|
|
|
|
|
|
|
|
|
|
return {"action": "HOLD", "ratio_pct": 0, "price_basis": "MARKET_CLOSE", "order_type": "NONE", "limit_price": close, "reason": "NO_EXIT_SIGNAL"}
|
|
|
|
|
|
|
|
|
|
def compute_stop_action_ladder(item: dict) -> dict:
|
|
|
|
|
profit_pct = item.get("profitPct", 0.0) or 0.0
|
|
|
|
|
days_to_time_stop = item.get("daysToTimeStop")
|
|
|
|
|
timing_action = item.get("timingAction")
|
|
|
|
|
regime = item.get("REGIME_PRELIM")
|
|
|
|
|
rw_partial_ex = item.get("rw_partial_excluding_rw2b")
|
|
|
|
|
rw2b = item.get("RW2b_5d_rapid_weakness")
|
|
|
|
|
trailing = item.get("trailingStopBreach")
|
|
|
|
|
|
|
|
|
|
if timing_action == "STOP_OR_TIME_EXIT_READY":
|
|
|
|
|
return {"action": "EXIT_100", "quantity_pct": 100, "priority": 1, "reason": "STOP_OR_TIME_EXIT_READY"}
|
|
|
|
|
|
|
|
|
|
if regime == "RISK_OFF":
|
|
|
|
|
return {"action": "REGIME_TRIM_50", "quantity_pct": 50, "priority": 2, "reason": "REGIME_RISK_OFF"}
|
|
|
|
|
|
|
|
|
|
if rw_partial_ex == 1 and rw2b:
|
|
|
|
|
return {"action": "TRIM_50", "quantity_pct": 50, "priority": 2.5, "reason": "RW_AND_RAPID_WEAKNESS"}
|
|
|
|
|
|
|
|
|
|
if trailing:
|
|
|
|
|
return {"action": "TRIM_50", "quantity_pct": 50, "priority": 4, "reason": "TRAILING_STOP_BREACH"}
|
|
|
|
|
|
|
|
|
|
if profit_pct >= 10.0:
|
|
|
|
|
return {"action": "TAKE_PROFIT_TIER1", "quantity_pct": 25, "priority": 5, "reason": "PROFIT_PCT_THRESHOLD"}
|
|
|
|
|
|
|
|
|
|
if profit_pct < 10.0 and days_to_time_stop == 1:
|
|
|
|
|
return {"action": "REVIEW_HUMAN", "quantity_pct": 0, "priority": 6, "reason": "MANUAL_REVIEW_REQUIRED"}
|
|
|
|
|
|
|
|
|
|
return {"action": "HOLD", "quantity_pct": 0, "priority": 99, "reason": "NO_ACTION_TRIGGERED"}
|
|
|
|
|
|
|
|
|
|
def compute_timing_decision(item: dict) -> dict:
|
|
|
|
|
if item.get("atr20") is None:
|
|
|
|
|
return {"action": "OBSERVE_DATA_MISSING", "entry_score": 0, "exit_score": 0}
|
|
|
|
|
|
|
|
|
|
mode = item.get("entryMode", "")
|
|
|
|
|
ac_gate = item.get("acGate", "")
|
|
|
|
|
rw_partial = item.get("rwPartial", 0) or 0
|
|
|
|
|
days_to_time_stop = item.get("daysToTimeStop")
|
|
|
|
|
|
|
|
|
|
if ac_gate == "BLOCK" and days_to_time_stop is not None and days_to_time_stop <= 5:
|
|
|
|
|
return {"action": "STOP_OR_TIME_EXIT_READY", "entry_score": 50, "exit_score": 85}
|
|
|
|
|
|
|
|
|
|
if rw_partial == 2 or item.get("ma20Slope", 0) < 0 and item.get("disparity", 0) > 8:
|
|
|
|
|
return {"action": "EXIT_REVIEW", "entry_score": 40, "exit_score": 60}
|
|
|
|
|
|
|
|
|
|
if mode == "BREAKOUT" and ac_gate == "CLEAR":
|
|
|
|
|
return {"action": "BUY_BREAKOUT_PILOT_ONLY", "entry_score": 80, "exit_score": 10}
|
|
|
|
|
|
|
|
|
|
if mode == "PULLBACK":
|
|
|
|
|
return {"action": "BUY_PULLBACK_WAIT", "entry_score": 65, "exit_score": 20}
|
|
|
|
|
|
|
|
|
|
return {"action": "OBSERVE", "entry_score": 50, "exit_score": 20}
|
|
|
|
|
|
|
|
|
|
def compute_final_decision(item: dict) -> dict:
|
|
|
|
|
sell_action = item.get("sellAction", "HOLD")
|
|
|
|
|
allowed_action = item.get("allowedAction", "")
|
|
|
|
|
timing_action = item.get("timingAction", "")
|
|
|
|
|
dart_risk = item.get("dartRisk", False)
|
|
|
|
|
|
|
|
|
|
if sell_action != "HOLD":
|
|
|
|
|
return {"final_action": sell_action, "action_priority": 1}
|
|
|
|
|
|
|
|
|
|
if timing_action in ("STOP_OR_TIME_EXIT_READY", "NO_BUY_OVERHEATED"):
|
|
|
|
|
priority = 50 if timing_action == "NO_BUY_OVERHEATED" else 10
|
|
|
|
|
return {"final_action": timing_action, "action_priority": priority}
|
|
|
|
|
|
|
|
|
|
if dart_risk:
|
|
|
|
|
return {"final_action": "EXIT_DART_RISK", "action_priority": 20}
|
|
|
|
|
|
|
|
|
|
if allowed_action:
|
|
|
|
|
return {"final_action": allowed_action, "action_priority": 30}
|
|
|
|
|
|
|
|
|
|
return {"final_action": "HOLD", "action_priority": 99}
|