Skip to main content

kestrel_chartkit/engine/
location_quality.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4use crate::engine::acceptance_detector::AcceptanceDetectorOutput;
5use crate::engine::market_context::VolumeNodeKind;
6use crate::model::MarketRegime;
7
8/// Location Quality score (0..100) evaluating how attractive the current trade location is.
9#[derive(Debug, Clone, Copy, PartialEq)]
10#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11pub struct LocationQualityScore {
12    pub score: f64, // 0.0 .. 100.0
13    pub distance_score: f64,
14    pub regime_alignment_score: f64,
15    pub acceptance_score: f64,
16}
17
18pub fn calculate_location_quality(
19    price: f64,
20    key_level: f64,
21    atr_raw: f64,
22    volume_node: VolumeNodeKind,
23    regime: MarketRegime,
24    acceptance: &AcceptanceDetectorOutput,
25    is_long: bool,
26) -> LocationQualityScore {
27    let atr = atr_raw.max(1e-8);
28    let dist_atr = (price - key_level).abs() / atr;
29
30    // 1. Distance component (ideal entry is close to structural level: < 1.0 ATR)
31    let distance_score = (1.0 - (dist_atr / 2.0).min(1.0)) * 40.0;
32
33    // 2. Regime alignment & volume node component
34    let node_score = match volume_node {
35        VolumeNodeKind::Lvn => 30.0, // High potential / free space
36        VolumeNodeKind::Neutral => 20.0,
37        VolumeNodeKind::Hvn => 10.0, // Inside high volume density balance
38    };
39
40    let regime_boost = match (regime, is_long) {
41        (MarketRegime::BullishExpansion, true) => 10.0,
42        (MarketRegime::BearishExpansion, false) => 10.0,
43        (MarketRegime::Consolidation, _) => 5.0,
44        _ => 0.0,
45    };
46    let regime_alignment_score = node_score + regime_boost;
47
48    // 3. Acceptance / Rejection score
49    let acceptance_score = match acceptance.rejection_kind {
50        crate::engine::acceptance_detector::RejectionKind::RejectionLow if is_long => 20.0,
51        crate::engine::acceptance_detector::RejectionKind::RejectionHigh if !is_long => 20.0,
52        _ => match acceptance.acceptance_level {
53            crate::engine::market_context::AcceptanceLevel::High if is_long => 15.0,
54            crate::engine::market_context::AcceptanceLevel::Low if !is_long => 15.0,
55            _ => 5.0,
56        },
57    };
58
59    let total_score =
60        (distance_score + regime_alignment_score + acceptance_score).clamp(0.0, 100.0);
61
62    LocationQualityScore {
63        score: total_score,
64        distance_score,
65        regime_alignment_score,
66        acceptance_score,
67    }
68}