use std::collections::HashMap;
use crate::backtesting::condition::{Condition, HtfIndicatorSpec};
use crate::backtesting::engine::compute_for_candles;
use crate::backtesting::resample::resample;
use crate::backtesting::strategy::StrategyContext;
use crate::constants::{Interval, Region};
use crate::indicators::Indicator;
#[derive(Clone)]
pub struct HtfCondition<C: Condition> {
interval: Interval,
inner: C,
utc_offset_secs: i64,
}
impl<C: Condition> Condition for HtfCondition<C> {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
let required = self.inner.required_indicators();
if !required.is_empty() {
let interval_str = self.interval.as_str();
let mut mini_indicators: HashMap<String, Vec<Option<f64>>> =
HashMap::with_capacity(required.len());
let mut all_found = true;
for (base_key, _) in &required {
let htf_key = format!("htf_{}_{}", interval_str, base_key);
if let Some(stretched) = ctx.indicators.get(&htf_key) {
let curr = stretched.get(ctx.index).copied().flatten();
let prev = ctx
.index
.checked_sub(1)
.and_then(|pi| stretched.get(pi).copied().flatten());
mini_indicators.insert(base_key.clone(), vec![prev, curr]);
} else {
all_found = false;
break;
}
}
if all_found {
let start = ctx.index.saturating_sub(1);
let htf_ctx = StrategyContext {
candles: &ctx.candles[start..=ctx.index],
index: ctx.index - start, position: ctx.position,
equity: ctx.equity,
indicators: &mini_indicators,
};
return self.inner.evaluate(&htf_ctx);
}
} else {
return self.inner.evaluate(ctx);
}
tracing::warn!(
interval = %self.interval,
"HtfCondition falling back to O(n²) dynamic resampling — \
implement Strategy::htf_requirements() or use StrategyBuilder \
to enable O(1) pre-computed HTF lookups"
);
self.evaluate_dynamic(ctx)
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn htf_requirements(&self) -> Vec<HtfIndicatorSpec> {
let interval_str = self.interval.as_str();
self.inner
.required_indicators()
.into_iter()
.map(|(base_key, indicator)| HtfIndicatorSpec {
interval: self.interval,
htf_key: format!("htf_{}_{}", interval_str, base_key),
base_key,
indicator,
utc_offset_secs: self.utc_offset_secs,
})
.collect()
}
fn description(&self) -> String {
format!("htf({}, {})", self.interval, self.inner.description())
}
}
impl<C: Condition> HtfCondition<C> {
fn evaluate_dynamic(&self, ctx: &StrategyContext) -> bool {
let htf_candles = resample(ctx.candles, self.interval, self.utc_offset_secs);
let current_ts = ctx.current_candle().timestamp;
let htf_idx = match htf_candles.iter().rposition(|c| c.timestamp < current_ts) {
Some(i) => i,
None => return false, };
let required = self.inner.required_indicators();
let htf_indicators = if required.is_empty() {
HashMap::new()
} else {
match compute_for_candles(&htf_candles, required) {
Ok(map) => map,
Err(e) => {
tracing::warn!("HTF indicator computation failed: {}", e);
return false;
}
}
};
let htf_ctx = StrategyContext {
candles: &htf_candles,
index: htf_idx,
position: ctx.position,
equity: ctx.equity,
indicators: &htf_indicators,
};
self.inner.evaluate(&htf_ctx)
}
}
pub fn htf<C: Condition>(interval: Interval, cond: C) -> HtfCondition<C> {
HtfCondition {
interval,
inner: cond,
utc_offset_secs: 0,
}
}
pub fn htf_region<C: Condition>(interval: Interval, region: Region, cond: C) -> HtfCondition<C> {
HtfCondition {
interval,
inner: cond,
utc_offset_secs: region.utc_offset_secs(),
}
}