use chrono::Datelike;
use chrono::offset::Offset;
use chrono::{DateTime, NaiveDate, TimeZone, Timelike, Utc};
use paft::market::responses::history::{Candle, HistoryMeta};
use std::convert::TryFrom;
use chrono::offset::LocalResult;
const DAY: i64 = 86_400;
const fn week_start_day(day: i64) -> i64 {
day - ((day + 3).rem_euclid(7))
}
const fn week_start_ts(ts: DateTime<Utc>) -> Option<DateTime<Utc>> {
let day = ts.timestamp().div_euclid(DAY);
let ws = week_start_day(day);
DateTime::from_timestamp(ws * DAY, 0)
}
use crate::BorsaError;
fn resample_by<F>(mut candles: Vec<Candle>, bucket_of: F) -> Result<Vec<Candle>, BorsaError>
where
F: Fn(DateTime<Utc>) -> Option<DateTime<Utc>>,
{
if candles.is_empty() {
return Ok(candles);
}
candles.sort_by_key(|c| c.ts);
let mut out: Vec<Candle> = Vec::new();
let mut series_currency: Option<paft::money::Currency> = None;
let mut iter = candles.into_iter();
let Some(first) = iter.find(|c| bucket_of(c.ts).is_some()) else {
return Ok(Vec::new());
};
let mut cur_bucket = bucket_of(first.ts).unwrap();
let mut open = first.open;
let mut high = first.high;
let mut low = first.low;
let mut close = first.close;
let mut vol_sum = first.volume.map(u128::from);
for c in iter {
let Some(bucket) = bucket_of(c.ts) else {
continue;
};
if bucket == cur_bucket {
if !(c.open.currency() == high.currency()
&& c.open.currency() == low.currency()
&& c.open.currency() == close.currency())
{
return Err(BorsaError::Data(format!(
"Mixed currencies in resample bucket at {}: open={:?} high={:?} low={:?} close={:?}",
cur_bucket,
c.open.currency(),
high.currency(),
low.currency(),
close.currency()
)));
}
if c.high.amount() > high.amount() {
high = c.high;
}
if c.low.amount() < low.amount() {
low = c.low;
}
close = c.close;
if let Some(v) = c.volume {
vol_sum = Some(vol_sum.unwrap_or(0) + u128::from(v));
}
} else {
finalize_bucket(
&mut out,
&mut series_currency,
cur_bucket,
BucketAgg {
open,
high,
low,
close,
vol_sum,
},
)?;
cur_bucket = bucket;
open = c.open;
high = c.high;
low = c.low;
close = c.close;
vol_sum = c.volume.map(u128::from);
}
}
finalize_bucket(
&mut out,
&mut series_currency,
cur_bucket,
BucketAgg {
open,
high,
low,
close,
vol_sum,
},
)?;
Ok(out)
}
struct BucketAgg {
open: paft::money::Money,
high: paft::money::Money,
low: paft::money::Money,
close: paft::money::Money,
vol_sum: Option<u128>,
}
fn finalize_bucket(
out: &mut Vec<Candle>,
series_currency: &mut Option<paft::money::Currency>,
cur_bucket: DateTime<Utc>,
agg: BucketAgg,
) -> Result<(), BorsaError> {
if !(agg.open.currency() == agg.high.currency()
&& agg.open.currency() == agg.low.currency()
&& agg.open.currency() == agg.close.currency())
{
return Err(BorsaError::Data(format!(
"Mixed currencies in resample bucket (finalize) at {}: open={:?} high={:?} low={:?} close={:?}",
cur_bucket,
agg.open.currency(),
agg.high.currency(),
agg.low.currency(),
agg.close.currency()
)));
}
if let Some(cur) = series_currency {
if cur != agg.open.currency() {
return Err(BorsaError::Data(format!(
"Mixed currencies across resampled series at {}: expected {:?}, got {:?}",
cur_bucket,
cur,
agg.open.currency()
)));
}
} else if out.is_empty() {
*series_currency = Some(agg.open.currency().clone());
}
out.push(Candle {
ts: cur_bucket,
open: agg.open,
high: agg.high,
low: agg.low,
close: agg.close,
close_unadj: None,
volume: agg
.vol_sum
.and_then(|v| u64::try_from(v.min(u128::from(u64::MAX))).ok()),
});
Ok(())
}
const fn bucket_day_with_offset(ts: DateTime<Utc>, offset_seconds: i64) -> Option<DateTime<Utc>> {
let shifted = ts.timestamp() + offset_seconds;
let day = shifted.div_euclid(DAY);
let local_day_start = day * DAY - offset_seconds;
DateTime::from_timestamp(local_day_start, 0)
}
const fn bucket_week_monday_with_offset(
ts: DateTime<Utc>,
offset_seconds: i64,
) -> Option<DateTime<Utc>> {
let shifted_day = (ts.timestamp() + offset_seconds).div_euclid(DAY);
let ws = week_start_day(shifted_day);
let local_week_start = ws * DAY - offset_seconds;
DateTime::from_timestamp(local_week_start, 0)
}
const fn bucket_minutes_with_offset(
ts: DateTime<Utc>,
minutes: i64,
offset_seconds: i64,
) -> Option<DateTime<Utc>> {
let step = minutes * 60;
let shifted = ts.timestamp() + offset_seconds;
let bucket = shifted - shifted.rem_euclid(step);
let back = bucket - offset_seconds;
DateTime::from_timestamp(back, 0)
}
fn local_midnight_utc_for_date(
ts: DateTime<Utc>,
date: NaiveDate,
tz: chrono_tz::Tz,
) -> Option<DateTime<Utc>> {
let naive_midnight = date.and_hms_opt(0, 0, 0)?;
match tz.from_local_datetime(&naive_midnight) {
LocalResult::Single(dt) => Some(dt.with_timezone(&Utc)),
LocalResult::Ambiguous(dt1, _) => Some(dt1.with_timezone(&Utc)),
LocalResult::None => {
let day = ts.timestamp().div_euclid(DAY);
DateTime::from_timestamp(day * DAY, 0)
}
}
}
fn bucket_day_with_tz(ts: DateTime<Utc>, tz: chrono_tz::Tz) -> Option<DateTime<Utc>> {
let local = ts.with_timezone(&tz);
let date = local.date_naive();
local_midnight_utc_for_date(ts, date, tz)
}
fn bucket_week_monday_with_tz(ts: DateTime<Utc>, tz: chrono_tz::Tz) -> Option<DateTime<Utc>> {
let local = ts.with_timezone(&tz);
let date = local.date_naive();
let days_from_monday = i64::from(local.weekday().num_days_from_monday());
let week_start_date = date
.checked_sub_signed(chrono::Duration::days(days_from_monday))
.unwrap_or(date);
local_midnight_utc_for_date(ts, week_start_date, tz)
}
fn bucket_minutes_with_tz(
ts: DateTime<Utc>,
minutes: i64,
tz: chrono_tz::Tz,
) -> Option<DateTime<Utc>> {
let step = minutes * 60;
let local = ts.with_timezone(&tz);
let date = local.date_naive();
let seconds_since_midnight = i64::from(local.num_seconds_from_midnight());
let bucket_sec = seconds_since_midnight - seconds_since_midnight.rem_euclid(step);
let naive_midnight = date.and_hms_opt(0, 0, 0)?;
let local_bucket_naive = naive_midnight + chrono::Duration::seconds(bucket_sec);
match tz.from_local_datetime(&local_bucket_naive) {
LocalResult::Single(dt) => Some(dt.with_timezone(&Utc)),
LocalResult::Ambiguous(dt1, dt2) => {
let local_offset = local.offset().fix().local_minus_utc();
let dt1_offset = dt1.offset().fix().local_minus_utc();
if dt1_offset == local_offset {
Some(dt1.with_timezone(&Utc))
} else {
Some(dt2.with_timezone(&Utc))
}
}
LocalResult::None => {
let bucket = ts.timestamp() - ts.timestamp().rem_euclid(step);
DateTime::from_timestamp(bucket, 0)
}
}
}
fn choose_bucket_day(ts: DateTime<Utc>, meta: Option<&HistoryMeta>) -> Option<DateTime<Utc>> {
if let Some(m) = meta {
if let Some(tz) = m.timezone {
return bucket_day_with_tz(ts, tz);
}
if let Some(off) = m.utc_offset_seconds {
return bucket_day_with_offset(ts, off);
}
}
let day = ts.timestamp().div_euclid(DAY);
DateTime::from_timestamp(day * DAY, 0)
}
fn choose_bucket_week(ts: DateTime<Utc>, meta: Option<&HistoryMeta>) -> Option<DateTime<Utc>> {
if let Some(m) = meta {
if let Some(tz) = m.timezone {
return bucket_week_monday_with_tz(ts, tz);
}
if let Some(off) = m.utc_offset_seconds {
return bucket_week_monday_with_offset(ts, off);
}
}
week_start_ts(ts)
}
fn choose_bucket_minutes(
ts: DateTime<Utc>,
minutes: i64,
meta: Option<&HistoryMeta>,
) -> Option<DateTime<Utc>> {
if let Some(m) = meta {
if let Some(tz) = m.timezone {
return bucket_minutes_with_tz(ts, minutes, tz);
}
if let Some(off) = m.utc_offset_seconds {
return bucket_minutes_with_offset(ts, minutes, off);
}
}
let step = minutes * 60;
let bucket = ts.timestamp() - (ts.timestamp().rem_euclid(step));
DateTime::from_timestamp(bucket, 0)
}
pub fn resample_to_daily(candles: Vec<Candle>) -> Result<Vec<Candle>, BorsaError> {
resample_by(candles, |ts| {
let day = ts.timestamp().div_euclid(DAY);
DateTime::from_timestamp(day * DAY, 0)
})
}
pub fn resample_to_daily_with_meta(
candles: Vec<Candle>,
meta: Option<&HistoryMeta>,
) -> Result<Vec<Candle>, BorsaError> {
resample_by(candles, move |ts| choose_bucket_day(ts, meta))
}
pub fn resample_to_weekly(candles: Vec<Candle>) -> Result<Vec<Candle>, BorsaError> {
resample_by(candles, week_start_ts)
}
pub fn resample_to_weekly_with_meta(
candles: Vec<Candle>,
meta: Option<&HistoryMeta>,
) -> Result<Vec<Candle>, BorsaError> {
resample_by(candles, move |ts| choose_bucket_week(ts, meta))
}
pub fn resample_to_minutes(candles: Vec<Candle>, minutes: i64) -> Result<Vec<Candle>, BorsaError> {
if candles.is_empty() || minutes <= 0 {
return Ok(candles);
}
let step = minutes * 60;
resample_by(candles, move |ts| {
let bucket = ts.timestamp() - (ts.timestamp().rem_euclid(step));
DateTime::from_timestamp(bucket, 0)
})
}
pub fn resample_to_minutes_with_meta(
candles: Vec<Candle>,
minutes: i64,
meta: Option<&HistoryMeta>,
) -> Result<Vec<Candle>, BorsaError> {
if candles.is_empty() || minutes <= 0 {
return Ok(candles);
}
resample_by(candles, move |ts| choose_bucket_minutes(ts, minutes, meta))
}