use chrono::{DateTime, Duration, Local, SubsecRound, Timelike, Utc};
use crate::{
shared::OhlcResolution,
sync::{
LNM_SETTLEMENT_A_END, LNM_SETTLEMENT_B_END, LNM_SETTLEMENT_B_START, LNM_SETTLEMENT_C_START,
LNM_SETTLEMENT_INTERVAL_8H,
},
};
pub(crate) trait DateTimeExt {
fn ceil_sec(&self) -> DateTime<Utc>;
fn floor_minute(&self) -> DateTime<Utc>;
fn is_round_minute(&self) -> bool;
fn format_local_millis(&self) -> String;
fn format_local_short(&self) -> String;
fn floor_to_resolution(&self, resolution: OhlcResolution) -> DateTime<Utc>;
fn step_back_candles(&self, resolution: OhlcResolution, candles: u64) -> DateTime<Utc>;
fn is_valid_funding_settlement_time(&self) -> bool;
fn ceil_funding_settlement_time(&self) -> DateTime<Utc>;
fn floor_funding_settlement_time(&self) -> DateTime<Utc>;
fn floor_hour(&self) -> DateTime<Utc>;
fn floor_day(&self) -> DateTime<Utc>;
}
impl DateTimeExt for DateTime<Utc> {
fn ceil_sec(&self) -> DateTime<Utc> {
let trunc_time_sec = self.trunc_subsecs(0);
if trunc_time_sec == *self {
trunc_time_sec
} else {
trunc_time_sec + Duration::seconds(1)
}
}
fn floor_minute(&self) -> DateTime<Utc> {
self.trunc_subsecs(0)
.with_second(0)
.expect("second is always valid")
}
fn is_round_minute(&self) -> bool {
*self == self.floor_minute()
}
fn format_local_millis(&self) -> String {
let local_time = self.with_timezone(&Local);
local_time.format("%Y-%m-%d %H:%M:%S.%3f (%Z)").to_string()
}
fn format_local_short(&self) -> String {
let local_time = self.with_timezone(&Local);
local_time.format("%Y/%m/%d %H:%M:%S (%Z)").to_string()
}
fn floor_to_resolution(&self, resolution: OhlcResolution) -> DateTime<Utc> {
let secs_per_bucket = resolution.as_seconds() as i64;
let floored_timestamp = (self.timestamp() / secs_per_bucket) * secs_per_bucket;
DateTime::from_timestamp(floored_timestamp, 0).expect("floored timestamp is always valid")
}
fn step_back_candles(&self, resolution: OhlcResolution, candles: u64) -> DateTime<Utc> {
let floored = self.floor_to_resolution(resolution);
floored - Duration::minutes(resolution.as_minutes() as i64 * candles as i64)
}
fn is_valid_funding_settlement_time(&self) -> bool {
let hour = self.hour();
let clean = self.minute() == 0 && self.second() == 0 && self.nanosecond() == 0;
let interval_hours = LNM_SETTLEMENT_INTERVAL_8H.num_hours() as u32;
if *self < LNM_SETTLEMENT_B_START {
clean && hour == 8 } else if *self < LNM_SETTLEMENT_C_START {
clean && hour % interval_hours == 4 } else {
clean && hour.is_multiple_of(interval_hours) }
}
fn ceil_funding_settlement_time(&self) -> DateTime<Utc> {
if self.is_valid_funding_settlement_time() {
return *self;
}
if *self > LNM_SETTLEMENT_A_END && *self < LNM_SETTLEMENT_B_START {
return LNM_SETTLEMENT_B_START;
}
if *self > LNM_SETTLEMENT_B_END && *self < LNM_SETTLEMENT_C_START {
return LNM_SETTLEMENT_C_START;
}
if *self < LNM_SETTLEMENT_B_START {
let base = self
.date_naive()
.and_hms_opt(8, 0, 0)
.expect("valid time")
.and_utc();
let result = if *self <= base {
base
} else {
base + Duration::hours(24)
};
return if result > LNM_SETTLEMENT_A_END {
LNM_SETTLEMENT_B_START
} else {
result
};
}
let interval = LNM_SETTLEMENT_INTERVAL_8H.num_hours() as i32;
let phase_offset: i32 = if *self < LNM_SETTLEMENT_C_START { 4 } else { 0 };
let hour = self.hour() as i32;
let next_slot = ((hour - phase_offset).div_euclid(interval) + 1) * interval + phase_offset;
let base = self
.date_naive()
.and_hms_opt(0, 0, 0)
.expect("valid time")
.and_utc();
let result = if next_slot >= 24 {
base + Duration::hours(24 + phase_offset as i64)
} else {
base + Duration::hours(next_slot as i64)
};
if result > LNM_SETTLEMENT_B_END && result < LNM_SETTLEMENT_C_START {
LNM_SETTLEMENT_C_START
} else {
result
}
}
fn floor_funding_settlement_time(&self) -> DateTime<Utc> {
if self.is_valid_funding_settlement_time() {
return *self;
}
if *self > LNM_SETTLEMENT_A_END && *self < LNM_SETTLEMENT_B_START {
return LNM_SETTLEMENT_A_END;
}
if *self > LNM_SETTLEMENT_B_END && *self < LNM_SETTLEMENT_C_START {
return LNM_SETTLEMENT_B_END;
}
if *self < LNM_SETTLEMENT_B_START {
let base = self
.date_naive()
.and_hms_opt(8, 0, 0)
.expect("valid time")
.and_utc();
return if *self >= base {
base
} else {
base - Duration::hours(24)
};
}
let interval = LNM_SETTLEMENT_INTERVAL_8H.num_hours() as i32;
let phase_offset: i32 = if *self < LNM_SETTLEMENT_C_START { 4 } else { 0 };
let hour = self.hour() as i32;
let slot = (hour - phase_offset).div_euclid(interval) * interval + phase_offset;
if slot < 0 {
let prev_day = self.date_naive().pred_opt().expect("valid date");
prev_day
.and_hms_opt((24 + slot) as u32, 0, 0)
.expect("valid time")
.and_utc()
} else {
self.date_naive()
.and_hms_opt(slot as u32, 0, 0)
.expect("valid time")
.and_utc()
}
}
fn floor_hour(&self) -> DateTime<Utc> {
self.trunc_subsecs(0)
.with_second(0)
.expect("second is always valid")
.with_minute(0)
.expect("minute is always valid")
}
fn floor_day(&self) -> DateTime<Utc> {
self.date_naive()
.and_hms_opt(0, 0, 0)
.expect("valid time")
.and_utc()
}
}
#[cfg(test)]
mod tests;