use crate::model::Bar;
use chrono::{DateTime, Datelike, Duration, Utc, Weekday};
pub trait SessionProvider: Send + Sync {
fn session_break_between(
&self,
from: &DateTime<Utc>,
to: &DateTime<Utc>,
) -> Option<SessionBreak>;
fn session_breaks_in_range(
&self,
start: &DateTime<Utc>,
end: &DateTime<Utc>,
) -> Vec<SessionBreak> {
let mut breaks = Vec::new();
let mut current = *start;
while current < *end {
let next = current + Duration::minutes(1);
if let Some(session_break) = self.session_break_between(¤t, &next) {
breaks.push(session_break);
}
current = next;
}
breaks
}
fn name(&self) -> &str;
}
#[derive(Debug, Clone)]
pub struct SessionBreak {
pub ts: DateTime<Utc>,
pub break_type: SessionBreakType,
pub label: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionBreakType {
Daily,
Weekly,
Monthly,
Custom,
}
#[derive(Debug, Clone, Default)]
pub struct DailySessionProvider {
pub break_hour: u32,
pub break_minute: u32,
}
impl DailySessionProvider {
pub fn new(hour: u32, minute: u32) -> Self {
Self {
break_hour: hour.min(23),
break_minute: minute.min(59),
}
}
pub fn nyse() -> Self {
Self::new(21, 0) }
pub fn continuous() -> Self {
Self::default()
}
}
impl SessionProvider for DailySessionProvider {
fn session_break_between(
&self,
from: &DateTime<Utc>,
to: &DateTime<Utc>,
) -> Option<SessionBreak> {
let from_day = from.ordinal();
let to_day = to.ordinal();
if from_day != to_day {
Some(SessionBreak {
ts: *to,
break_type: SessionBreakType::Daily,
label: Some(format!("{}", to.format("%Y-%m-%d"))),
})
} else {
None
}
}
fn name(&self) -> &str {
"Daily Sessions"
}
}
#[derive(Debug, Clone)]
pub struct WeeklySessionProvider {
pub break_day: Weekday,
}
impl Default for WeeklySessionProvider {
fn default() -> Self {
Self {
break_day: Weekday::Fri, }
}
}
impl WeeklySessionProvider {
pub fn new(break_day: Weekday) -> Self {
Self { break_day }
}
}
impl SessionProvider for WeeklySessionProvider {
fn session_break_between(
&self,
from: &DateTime<Utc>,
to: &DateTime<Utc>,
) -> Option<SessionBreak> {
let from_week = from.iso_week().week();
let to_week = to.iso_week().week();
if from_week != to_week {
Some(SessionBreak {
ts: *to,
break_type: SessionBreakType::Weekly,
label: Some(format!("Week {to_week}")),
})
} else {
None
}
}
fn name(&self) -> &str {
"Weekly Sessions"
}
}
#[derive(Debug, Clone, Default)]
pub struct MonthlySessionProvider;
impl SessionProvider for MonthlySessionProvider {
fn session_break_between(
&self,
from: &DateTime<Utc>,
to: &DateTime<Utc>,
) -> Option<SessionBreak> {
if from.month() != to.month() || from.year() != to.year() {
Some(SessionBreak {
ts: *to,
break_type: SessionBreakType::Monthly,
label: Some(format!("{}", to.format("%B %Y"))),
})
} else {
None
}
}
fn name(&self) -> &str {
"Monthly Sessions"
}
}
pub struct CompositeSessionProvider {
providers: Vec<Box<dyn SessionProvider>>,
}
impl CompositeSessionProvider {
pub fn new() -> Self {
Self {
providers: Vec::new(),
}
}
pub fn add_provider(&mut self, provider: Box<dyn SessionProvider>) {
self.providers.push(provider);
}
pub fn standard() -> Self {
let mut composite = Self::new();
composite.add_provider(Box::new(DailySessionProvider::default()));
composite.add_provider(Box::new(WeeklySessionProvider::default()));
composite.add_provider(Box::new(MonthlySessionProvider));
composite
}
}
impl Default for CompositeSessionProvider {
fn default() -> Self {
Self::new()
}
}
impl SessionProvider for CompositeSessionProvider {
fn session_break_between(
&self,
from: &DateTime<Utc>,
to: &DateTime<Utc>,
) -> Option<SessionBreak> {
for provider in &self.providers {
if let Some(break_info) = provider.session_break_between(from, to) {
return Some(break_info);
}
}
None
}
fn name(&self) -> &str {
"Composite Sessions"
}
}
pub fn find_session_breaks(
bars: &[Bar],
provider: &dyn SessionProvider,
) -> Vec<(usize, SessionBreak)> {
let mut breaks = Vec::new();
for (i, window) in bars.windows(2).enumerate() {
if let Some(session_break) =
provider.session_break_between(&window[0].time, &window[1].time)
{
breaks.push((i + 1, session_break)); }
}
breaks
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn create_test_bars() -> Vec<Bar> {
vec![
Bar {
time: Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
open: 100.0,
high: 105.0,
low: 99.0,
close: 102.0,
volume: 1000.0,
},
Bar {
time: Utc.with_ymd_and_hms(2024, 1, 2, 12, 0, 0).unwrap(),
open: 102.0,
high: 107.0,
low: 101.0,
close: 105.0,
volume: 1200.0,
},
Bar {
time: Utc.with_ymd_and_hms(2024, 2, 1, 12, 0, 0).unwrap(),
open: 105.0,
high: 110.0,
low: 104.0,
close: 108.0,
volume: 1500.0,
},
]
}
#[test]
fn test_daily_session_break() {
let provider = DailySessionProvider::default();
let bars = create_test_bars();
let breaks = find_session_breaks(&bars, &provider);
assert_eq!(breaks.len(), 2); }
#[test]
fn test_monthly_session_break() {
let provider = MonthlySessionProvider;
let bars = create_test_bars();
let breaks = find_session_breaks(&bars, &provider);
assert_eq!(breaks.len(), 1);
assert_eq!(breaks[0].0, 2); assert_eq!(breaks[0].1.break_type, SessionBreakType::Monthly);
}
#[test]
fn test_composite_provider() {
let mut composite = CompositeSessionProvider::new();
composite.add_provider(Box::new(DailySessionProvider::default()));
composite.add_provider(Box::new(MonthlySessionProvider));
let bars = create_test_bars();
let breaks = find_session_breaks(&bars, &composite);
assert!(!breaks.is_empty());
}
}