use super::IndicatorHistoryRequirement;
use super::series::KlineSeries;
use crate::chart::Basis;
use exchange::adapter::Exchange;
use exchange::{TickerInfo, Timeframe, UnixMs};
const DEFAULT_FETCH_BARS: u64 = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FetchRange {
Kline(UnixMs, UnixMs),
ProfileKline {
timeframe: Timeframe,
from: UnixMs,
to: UnixMs,
},
OpenInterest {
from: UnixMs,
to: UnixMs,
},
Trades(UnixMs, UnixMs),
}
impl FetchRange {
pub const fn open_interest(from: UnixMs, to: UnixMs) -> Self {
Self::OpenInterest { from, to }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchBatch {
pub range: FetchRange,
pub tickers: Vec<TickerInfo>,
}
#[derive(Clone, Copy)]
pub struct FetchPlanInput<'a> {
pub basis: Basis,
pub horizontal_offset: f32,
pub base_ticker: TickerInfo,
pub series: &'a [KlineSeries],
pub open_interest_required: bool,
pub trade_data_required: bool,
pub trade_fetch_enabled: bool,
pub visible_points: u64,
pub history_requirement: IndicatorHistoryRequirement,
pub now: UnixMs,
}
impl FetchPlanInput<'_> {
pub fn desired_fetch_batches(self) -> Vec<FetchBatch> {
let Basis::Time(timeframe) = self.basis else {
return Vec::new();
};
let dt = timeframe.to_milliseconds().max(1);
let visible_points = self.visible_points.max(2);
let indicator_warmup_bars = self.history_requirement.warmup_bars();
let required_history_bars =
indicator_warmup_bars.max(self.history_requirement.history_bars());
let base_fetch_bars = DEFAULT_FETCH_BARS.max(visible_points + required_history_bars);
let comparison_fetch_bars = DEFAULT_FETCH_BARS.max(visible_points);
let base_span = base_fetch_bars.saturating_mul(dt);
let comparison_span = comparison_fetch_bars.saturating_mul(dt);
let initial_end = self.now;
let mut batches = Vec::new();
let mut empty_base = false;
let mut empty_comparisons = Vec::new();
for series in self.series {
if series.is_empty() {
if series.ticker_info == self.base_ticker {
empty_base = true;
} else {
empty_comparisons.push(series.ticker_info);
}
}
}
if empty_base {
let end = initial_end;
let start = end.saturating_sub(comparison_span);
batches.push(FetchBatch {
range: FetchRange::Kline(start, end),
tickers: vec![self.base_ticker],
});
}
if !empty_comparisons.is_empty() {
let end = initial_end;
batches.push(FetchBatch {
range: FetchRange::Kline(end.saturating_sub(comparison_span), end),
tickers: empty_comparisons,
});
}
if let Some((window_min, window_max)) = visible_window(
self.series,
self.horizontal_offset,
timeframe,
self.visible_points,
) {
let indicator_warmup_start = if indicator_warmup_bars > 0 {
window_min
.saturating_sub(indicator_warmup_bars.saturating_mul(dt))
.floor_to(timeframe)
} else {
window_min
};
let warmup_start = self
.history_requirement
.history_start()
.map(|start| start.min(indicator_warmup_start))
.unwrap_or(indicator_warmup_start);
let mut base_boundary_fetch_pending = false;
for series in self.series {
let (target_min, fetch_span, fetching_base_history) =
if series.ticker_info == self.base_ticker {
if window_min
< series
.first_point()
.map(|point| point.kline.time)
.unwrap_or(window_min)
{
(window_min, comparison_span, false)
} else {
(warmup_start, base_span, true)
}
} else {
(window_min, comparison_span, false)
};
if let Some(series_min) = series.first_point().map(|point| point.kline.time)
&& target_min < series_min
{
let end = series_min.floor_to(timeframe);
batches.push(FetchBatch {
range: FetchRange::Kline(end.saturating_sub(fetch_span), end),
tickers: vec![series.ticker_info],
});
base_boundary_fetch_pending |= fetching_base_history;
}
}
if !base_boundary_fetch_pending
&& let Some(base_series) = self
.series
.iter()
.find(|series| series.ticker_info == self.base_ticker)
&& let Some(series_earliest) =
base_series.first_point().map(|point| point.kline.time)
&& let Some(series_latest) =
base_series.latest_point().map(|point| point.kline.time)
{
let visible_span = window_max.as_u64().saturating_sub(window_min.as_u64());
let prefetch_earliest = window_min.saturating_sub(visible_span);
let check_earliest = warmup_start.min(prefetch_earliest).max(series_earliest);
let check_latest = window_max.saturating_add(dt).min(series_latest);
if let Some((missing_earliest, missing_latest)) =
base_series.missing_kline_range(check_earliest, check_latest)
{
batches.push(FetchBatch {
range: FetchRange::Kline(
missing_earliest.saturating_sub(dt),
missing_latest.saturating_add(dt),
),
tickers: vec![self.base_ticker],
});
}
}
if self.open_interest_required {
let Some(base_series) = self
.series
.iter()
.find(|series| series.ticker_info == self.base_ticker)
else {
return batches;
};
let Some(kline_latest) = base_series.latest_point().map(|point| point.kline.time)
else {
return batches;
};
let visible_window = visible_window(
self.series,
self.horizontal_offset,
timeframe,
self.visible_points,
);
let visible_earliest = visible_window.map(|(start, _)| start).unwrap_or(window_min);
let visible_span = visible_window
.map(|(start, end)| end.as_u64().saturating_sub(start.as_u64()))
.unwrap_or(base_span);
let prefetch_earliest = visible_earliest.saturating_sub(visible_span);
match base_series.open_interest_range() {
Some((oi_earliest, oi_latest)) => {
if visible_earliest < oi_earliest {
batches.push(FetchBatch {
range: FetchRange::open_interest(prefetch_earliest, oi_earliest),
tickers: vec![self.base_ticker],
});
}
if oi_latest < kline_latest {
let start = oi_latest.max(prefetch_earliest);
if start < kline_latest {
batches.push(FetchBatch {
range: FetchRange::open_interest(start, kline_latest),
tickers: vec![self.base_ticker],
});
}
}
}
None => {
if prefetch_earliest < kline_latest {
batches.push(FetchBatch {
range: FetchRange::open_interest(prefetch_earliest, kline_latest),
tickers: vec![self.base_ticker],
});
}
}
}
}
}
batches
}
pub fn desired_trade_fetch_range(self, target_ticker: TickerInfo) -> Option<FetchRange> {
if target_ticker != self.base_ticker {
return None;
}
let Basis::Time(timeframe) = self.basis else {
return None;
};
if !self.trade_data_required || !self.trade_fetch_enabled {
return None;
}
let target_series = self
.series
.iter()
.find(|series| series.ticker_info == target_ticker)?;
if target_series.is_empty() {
return None;
}
let dt = timeframe.to_milliseconds().max(1);
let visible_points = self.visible_points.max(2);
let (visible_earliest, visible_latest) = visible_window(
std::slice::from_ref(target_series),
self.horizontal_offset,
timeframe,
visible_points,
)
.map(|(earliest, latest)| {
let half_interval = dt / 2;
let half_visible_span = latest.saturating_diff(earliest) / 2;
(
earliest.saturating_sub(half_visible_span.saturating_add(half_interval)),
latest.saturating_add(half_interval),
)
})?;
if visible_earliest >= visible_latest {
return None;
}
if let Some((earliest, latest)) =
target_series.suggest_trade_fetch_range(visible_earliest, visible_latest)
{
return Some(FetchRange::Trades(earliest, latest));
}
None
}
}
pub fn visible_window(
series: &[KlineSeries],
horizontal_offset: f32,
timeframe: Timeframe,
visible_points: u64,
) -> Option<(UnixMs, UnixMs)> {
let max_seen = series
.iter()
.filter_map(|series| series.latest_point().map(|point| point.kline.time))
.max()?;
let dt = timeframe.to_milliseconds().max(1);
let span_units = visible_points.saturating_sub(1);
let right_offset = (f64::from(horizontal_offset) * dt as f64)
.round()
.clamp(i64::MIN as f64, i64::MAX as f64) as i64;
let right = super::ChartCoord::from_unix_ms(max_seen).saturating_add_i64(right_offset);
let span_offset = span_units.saturating_mul(dt);
let left = right.saturating_sub_i64(i64::try_from(span_offset).unwrap_or(i64::MAX));
Some((
left.to_unix_ms_non_negative(),
right.to_unix_ms_non_negative(),
))
}
pub fn supports_open_interest(ticker_info: TickerInfo, timeframe: Timeframe) -> bool {
supports_open_interest_exchange(ticker_info.exchange())
&& supports_open_interest_timeframe(timeframe)
}
pub fn supports_open_interest_exchange(exchange: Exchange) -> bool {
matches!(
exchange,
Exchange::BinanceLinear
| Exchange::BinanceInverse
| Exchange::BybitLinear
| Exchange::BybitInverse
| Exchange::OkexLinear
| Exchange::OkexInverse
)
}
pub fn supports_open_interest_timeframe(timeframe: Timeframe) -> bool {
timeframe >= Timeframe::M5 && timeframe <= Timeframe::H4 && timeframe != Timeframe::H2
}