feat: add opportunity evaluation optimizer

This commit is contained in:
2026-04-22 00:29:02 +08:00
parent 436bef4814
commit 076a5f1b1c
11 changed files with 1224 additions and 37 deletions

View File

@@ -23,6 +23,45 @@ def _range_pct(values: list[float], denominator: float) -> float:
return (max(values) - min(values)) / denominator
_DEFAULT_OPPORTUNITY_MODEL_WEIGHTS = {
"trend": 0.1406,
"compression": 0.1688,
"breakout_proximity": 0.0875,
"higher_lows": 0.15,
"range_position": 0.45,
"fresh_breakout": 0.2,
"volume": 0.525,
"momentum": 0.1562,
"setup": 1.875,
"trigger": 1.875,
"liquidity": 0.3,
"volatility_penalty": 0.8,
"extension_penalty": 0.45,
}
def get_opportunity_model_weights(opportunity_config: dict[str, Any]) -> dict[str, float]:
configured = opportunity_config.get("model_weights", {})
return {
key: float(configured.get(key, default))
for key, default in _DEFAULT_OPPORTUNITY_MODEL_WEIGHTS.items()
}
def _weighted_quality(values: dict[str, float], weights: dict[str, float]) -> float:
weighted_sum = 0.0
total_weight = 0.0
for key, value in values.items():
weight = max(float(weights.get(key, 0.0)), 0.0)
if weight == 0:
continue
weighted_sum += weight * value
total_weight += weight
if total_weight == 0:
return 0.0
return _clamp(weighted_sum / total_weight, -1.0, 1.0)
def get_signal_weights(config: dict[str, Any]) -> dict[str, float]:
signal_config = config.get("signal", {})
return {
@@ -104,11 +143,17 @@ def score_opportunity_signal(
ticker: dict[str, Any],
opportunity_config: dict[str, Any],
) -> tuple[float, dict[str, float]]:
model_weights = get_opportunity_model_weights(opportunity_config)
if len(closes) < 6 or len(volumes) < 2:
return 0.0, {
"setup_score": 0.0,
"trigger_score": 0.0,
"liquidity_score": 0.0,
"edge_score": 0.0,
"setup_quality": 0.0,
"trigger_quality": 0.0,
"liquidity_quality": 0.0,
"risk_quality": 0.0,
"extension_penalty": 0.0,
"breakout_pct": 0.0,
"recent_runup": 0.0,
@@ -117,11 +162,20 @@ def score_opportunity_signal(
}
current = closes[-1]
sma_short = mean(closes[-5:])
sma_long = mean(closes[-20:]) if len(closes) >= 20 else mean(closes)
if current >= sma_short >= sma_long:
trend_quality = 1.0
elif current < sma_short < sma_long:
trend_quality = -1.0
else:
trend_quality = 0.0
prior_closes = closes[:-1]
prev_high = max(prior_closes[-20:]) if prior_closes else current
recent_low = min(closes[-20:])
range_width = prev_high - recent_low
range_position = _clamp((current - recent_low) / range_width, 0.0, 1.2) if range_width else 0.0
range_position_quality = 2.0 * _clamp(1.0 - abs(range_position - 0.62) / 0.62, 0.0, 1.0) - 1.0
breakout_pct = _safe_pct(current, prev_high)
recent_range = _range_pct(closes[-6:], current)
@@ -131,27 +185,45 @@ def score_opportunity_signal(
recent_low_window = min(closes[-5:])
prior_low_window = min(closes[-10:-5]) if len(closes) >= 10 else min(closes[:-5])
higher_lows = 1.0 if recent_low_window > prior_low_window else 0.0
higher_lows = 1.0 if recent_low_window > prior_low_window else -1.0
breakout_proximity = _clamp(1.0 - abs(breakout_pct) / 0.03, 0.0, 1.0)
setup_score = _clamp(0.45 * compression + 0.35 * breakout_proximity + 0.20 * higher_lows, 0.0, 1.0)
breakout_proximity_quality = 2.0 * breakout_proximity - 1.0
setup_quality = _weighted_quality(
{
"trend": trend_quality,
"compression": compression,
"breakout_proximity": breakout_proximity_quality,
"higher_lows": higher_lows,
"range_position": range_position_quality,
},
model_weights,
)
setup_score = _clamp((setup_quality + 1.0) / 2.0, 0.0, 1.0)
avg_volume = mean(volumes[:-1])
volume_confirmation = volumes[-1] / avg_volume if avg_volume else 1.0
volume_score = _clamp((volume_confirmation - 1.0) / 1.5, -0.5, 1.0)
volume_score = _clamp((volume_confirmation - 1.0) / 1.5, -1.0, 1.0)
momentum_3 = _safe_pct(closes[-1], closes[-4])
if momentum_3 <= 0:
controlled_momentum = _clamp(momentum_3 / 0.05, -0.5, 0.0)
controlled_momentum = _clamp(momentum_3 / 0.05, -1.0, 0.0)
elif momentum_3 <= 0.05:
controlled_momentum = momentum_3 / 0.05
elif momentum_3 <= 0.12:
controlled_momentum = 1.0 - ((momentum_3 - 0.05) / 0.07) * 0.5
else:
controlled_momentum = 0.2
controlled_momentum = -0.2
fresh_breakout = _clamp(1.0 - abs(breakout_pct) / 0.025, 0.0, 1.0)
trigger_score = _clamp(0.40 * fresh_breakout + 0.35 * volume_score + 0.25 * controlled_momentum, 0.0, 1.0)
fresh_breakout_quality = 2.0 * fresh_breakout - 1.0
trigger_quality = _weighted_quality(
{
"fresh_breakout": fresh_breakout_quality,
"volume": volume_score,
"momentum": controlled_momentum,
},
model_weights,
)
trigger_score = _clamp((trigger_quality + 1.0) / 2.0, 0.0, 1.0)
sma_short = mean(closes[-5:])
sma_long = mean(closes[-20:]) if len(closes) >= 20 else mean(closes)
extension_from_short = _safe_pct(current, sma_short)
recent_runup = _safe_pct(current, closes[-6])
extension_penalty = (
@@ -167,18 +239,46 @@ def score_opportunity_signal(
liquidity_score = _clamp(log10(max(quote_volume / min_quote_volume, 1.0)) / 2.0, 0.0, 1.0)
else:
liquidity_score = 1.0
score = (
setup_score
+ 1.2 * trigger_score
+ 0.4 * liquidity_score
- 0.8 * volatility
- 0.9 * extension_penalty
liquidity_quality = 2.0 * liquidity_score - 1.0
volatility_quality = 1.0 - 2.0 * _clamp(volatility / 0.12, 0.0, 1.0)
extension_quality = 1.0 - 2.0 * _clamp(extension_penalty / 2.0, 0.0, 1.0)
risk_quality = _weighted_quality(
{
"volatility_penalty": volatility_quality,
"extension_penalty": extension_quality,
},
model_weights,
)
edge_score = _weighted_quality(
{
"setup": setup_quality,
"trigger": trigger_quality,
"liquidity": liquidity_quality,
"trend": trend_quality,
"range_position": range_position_quality,
"volatility_penalty": volatility_quality,
"extension_penalty": extension_quality,
},
model_weights,
)
score = 1.0 + edge_score
metrics = {
"setup_score": round(setup_score, 4),
"trigger_score": round(trigger_score, 4),
"liquidity_score": round(liquidity_score, 4),
"edge_score": round(edge_score, 4),
"setup_quality": round(setup_quality, 4),
"trigger_quality": round(trigger_quality, 4),
"liquidity_quality": round(liquidity_quality, 4),
"risk_quality": round(risk_quality, 4),
"trend_quality": round(trend_quality, 4),
"range_position_quality": round(range_position_quality, 4),
"breakout_proximity_quality": round(breakout_proximity_quality, 4),
"volume_quality": round(volume_score, 4),
"momentum_quality": round(controlled_momentum, 4),
"extension_quality": round(extension_quality, 4),
"volatility_quality": round(volatility_quality, 4),
"extension_penalty": round(extension_penalty, 4),
"compression": round(compression, 4),
"range_position": round(range_position, 4),