use std::fmt::Debug;
use chrono::{DateTime, Utc};
use polars::{
df,
frame::DataFrame,
prelude::{DataType, IntoLazy, PlSmallStr, TimeUnit, col},
};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr};
use crate::{
data::{
common::{ProfileAggregation, ProfileBinStats},
domain::{
CandleDirection, Count, CountryCode, DataBroker, EconomicCategory, EconomicDataSource,
EconomicEventImpact, EconomicValue, Exchange, ExecutionDepth, LiquiditySide,
MarketType, Period, Price, Quantity, Symbol, TradeId, Volume,
},
indicator::{EmaWindow, RsiWindow, SmaWindow},
},
error::{ChapatyError, ChapatyResult, DataError},
gym::trading::types::TradeType,
};
pub trait PriceReachable {
fn price_reached(&self, price: Price, direction: TradeType) -> bool;
}
pub trait ClosePriceProvider {
fn close_price(&self) -> Price;
fn close_timestamp(&self) -> DateTime<Utc>;
}
pub trait IndicatorValueProvider {
fn value(&self) -> Price;
fn timestamp(&self) -> DateTime<Utc>;
}
pub trait MarketEvent {
fn point_in_time(&self) -> DateTime<Utc>;
fn opened_at(&self) -> DateTime<Utc> {
self.point_in_time()
}
}
pub trait StreamId: Ord + Copy + Debug {
type Event: MarketEvent;
}
pub trait SymbolProvider {
fn symbol(&self) -> &Symbol;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct OhlcvId {
pub broker: DataBroker,
pub exchange: Exchange,
pub symbol: Symbol,
pub period: Period,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct Ohlcv {
pub open_timestamp: DateTime<Utc>,
pub close_timestamp: DateTime<Utc>,
pub open: Price,
pub high: Price,
pub low: Price,
pub close: Price,
pub volume: Volume,
pub quote_asset_volume: Option<Volume>,
pub number_of_trades: Option<Count>,
pub taker_buy_base_asset_volume: Option<Volume>,
pub taker_buy_quote_asset_volume: Option<Volume>,
}
impl PriceReachable for Ohlcv {
fn price_reached(&self, price: Price, _direction: TradeType) -> bool {
self.low.0 <= price.0 && price.0 <= self.high.0
}
}
impl ClosePriceProvider for Ohlcv {
fn close_price(&self) -> Price {
self.close
}
fn close_timestamp(&self) -> DateTime<Utc> {
self.close_timestamp
}
}
impl MarketEvent for Ohlcv {
fn point_in_time(&self) -> DateTime<Utc> {
self.close_timestamp
}
fn opened_at(&self) -> DateTime<Utc> {
self.open_timestamp
}
}
impl StreamId for OhlcvId {
type Event = Ohlcv;
}
impl SymbolProvider for OhlcvId {
fn symbol(&self) -> &Symbol {
&self.symbol
}
}
impl Ohlcv {
pub fn direction(&self) -> CandleDirection {
let open = self.open.0;
let close = self.close.0;
if close > open {
CandleDirection::Bullish
} else if close < open {
CandleDirection::Bearish
} else {
CandleDirection::Doji
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TradesId {
pub broker: DataBroker,
pub exchange: Exchange,
pub symbol: Symbol,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct TradeEvent {
pub timestamp: DateTime<Utc>,
pub price: Price,
pub quantity: Quantity,
pub trade_id: Option<TradeId>,
pub quote_asset_volume: Option<Volume>,
pub is_buyer_maker: Option<LiquiditySide>,
pub is_best_match: Option<ExecutionDepth>,
}
impl PriceReachable for TradeEvent {
fn price_reached(&self, target_price: Price, direction: TradeType) -> bool {
match direction {
TradeType::Long => self.price.0 <= target_price.0,
TradeType::Short => self.price.0 >= target_price.0,
}
}
}
impl ClosePriceProvider for TradeEvent {
fn close_price(&self) -> Price {
self.price
}
fn close_timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl MarketEvent for TradeEvent {
fn point_in_time(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl SymbolProvider for TradesId {
fn symbol(&self) -> &Symbol {
&self.symbol
}
}
impl StreamId for TradesId {
type Event = TradeEvent;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum ProfileCol {
WindowStart, WindowEnd,
PriceBinStart,
PriceBinEnd,
Volume,
TakerBuyBaseVol,
TakerSellBaseVol,
QuoteVol,
TakerBuyQuoteVol,
TakerSellQuoteVol,
NumTrades,
NumBuyTrades,
NumSellTrades,
TimeSlotCount,
}
impl From<&ProfileCol> for PlSmallStr {
fn from(value: &ProfileCol) -> Self {
value.as_str().into()
}
}
impl From<ProfileCol> for PlSmallStr {
fn from(value: ProfileCol) -> Self {
(&value).into()
}
}
impl ProfileCol {
pub fn name(&self) -> PlSmallStr {
(*self).into()
}
pub fn as_str(&self) -> &'static str {
self.into()
}
}
pub trait MarketProfile {
fn open_timestamp(&self) -> DateTime<Utc>;
fn close_timestamp(&self) -> DateTime<Utc>;
fn poc(&self) -> Price;
fn value_area_high(&self) -> Price;
fn value_area_low(&self) -> Price;
fn as_dataframe(&self) -> ChapatyResult<DataFrame>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TpoId {
pub broker: DataBroker,
pub exchange: Exchange,
pub symbol: Symbol,
pub aggregation: ProfileAggregation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tpo {
pub open_timestamp: DateTime<Utc>,
pub close_timestamp: DateTime<Utc>,
pub poc: Price,
pub value_area_high: Price,
pub value_area_low: Price,
pub bins: Box<[TpoBin]>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct TpoBin {
pub price_bin_start: Price,
pub price_bin_end: Price,
pub time_slot_count: Count,
}
impl SymbolProvider for TpoId {
fn symbol(&self) -> &Symbol {
&self.symbol
}
}
impl MarketEvent for Tpo {
fn point_in_time(&self) -> DateTime<Utc> {
self.close_timestamp
}
fn opened_at(&self) -> DateTime<Utc> {
self.open_timestamp
}
}
impl StreamId for TpoId {
type Event = Tpo;
}
impl MarketProfile for Tpo {
fn open_timestamp(&self) -> DateTime<Utc> {
self.open_timestamp
}
fn close_timestamp(&self) -> DateTime<Utc> {
self.close_timestamp
}
fn poc(&self) -> Price {
self.poc
}
fn value_area_high(&self) -> Price {
self.value_area_high
}
fn value_area_low(&self) -> Price {
self.value_area_low
}
fn as_dataframe(&self) -> ChapatyResult<DataFrame> {
let len = self.bins.len();
let window_starts = vec![self.open_timestamp.timestamp_micros(); len];
let window_ends = vec![self.close_timestamp.timestamp_micros(); len];
let mut price_starts = Vec::with_capacity(len);
let mut price_ends = Vec::with_capacity(len);
let mut counts = Vec::with_capacity(len);
for bin in self.bins.iter() {
price_starts.push(bin.price_bin_start.0);
price_ends.push(bin.price_bin_end.0);
counts.push(bin.time_slot_count.0); }
let df = df![
ProfileCol::WindowStart.to_string() => window_starts,
ProfileCol::WindowEnd.to_string() => window_ends,
ProfileCol::PriceBinStart.to_string() => price_starts,
ProfileCol::PriceBinEnd.to_string() => price_ends,
ProfileCol::TimeSlotCount.to_string() => counts,
]
.map_err(|e| ChapatyError::Data(DataError::DataFrame(e.to_string())))?;
let lf = df.lazy().with_columns([
col(ProfileCol::WindowStart).cast(DataType::Datetime(
TimeUnit::Microseconds,
Some(polars::prelude::TimeZone::UTC),
)),
col(ProfileCol::WindowEnd).cast(DataType::Datetime(
TimeUnit::Microseconds,
Some(polars::prelude::TimeZone::UTC),
)),
]);
lf.collect()
.map_err(|e| DataError::DataFrame(e.to_string()).into())
}
}
impl ProfileBinStats for TpoBin {
fn get_value(&self) -> f64 {
self.time_slot_count.0 as f64
}
fn get_price(&self) -> Price {
self.price_bin_start
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct VolumeProfileId {
pub broker: DataBroker,
pub exchange: Exchange,
pub symbol: Symbol,
pub aggregation: ProfileAggregation,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VolumeProfile {
pub open_timestamp: DateTime<Utc>,
pub close_timestamp: DateTime<Utc>,
pub poc: Price,
pub value_area_high: Price,
pub value_area_low: Price,
pub bins: Box<[VolumeProfileBin]>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct VolumeProfileBin {
pub price_bin_start: Price,
pub price_bin_end: Price,
pub volume: Volume,
pub taker_buy_base_asset_volume: Option<Volume>,
pub taker_sell_base_asset_volume: Option<Volume>,
pub quote_asset_volume: Option<Volume>,
pub taker_buy_quote_asset_volume: Option<Volume>,
pub taker_sell_quote_asset_volume: Option<Volume>,
pub number_of_trades: Option<Count>,
pub number_of_buy_trades: Option<Count>,
pub number_of_sell_trades: Option<Count>,
}
impl SymbolProvider for VolumeProfileId {
fn symbol(&self) -> &Symbol {
&self.symbol
}
}
impl MarketEvent for VolumeProfile {
fn point_in_time(&self) -> DateTime<Utc> {
self.close_timestamp
}
fn opened_at(&self) -> DateTime<Utc> {
self.open_timestamp
}
}
impl StreamId for VolumeProfileId {
type Event = VolumeProfile;
}
impl MarketProfile for VolumeProfile {
fn open_timestamp(&self) -> DateTime<Utc> {
self.open_timestamp
}
fn close_timestamp(&self) -> DateTime<Utc> {
self.close_timestamp
}
fn poc(&self) -> Price {
self.poc
}
fn value_area_high(&self) -> Price {
self.value_area_high
}
fn value_area_low(&self) -> Price {
self.value_area_low
}
fn as_dataframe(&self) -> ChapatyResult<DataFrame> {
let len = self.bins.len();
let window_starts = vec![self.open_timestamp.timestamp_micros(); len];
let window_ends = vec![self.close_timestamp.timestamp_micros(); len];
let mut p_starts = Vec::with_capacity(len);
let mut p_ends = Vec::with_capacity(len);
let mut vol = Vec::with_capacity(len);
let mut tb_base = Vec::with_capacity(len);
let mut ts_base = Vec::with_capacity(len);
let mut q_vol = Vec::with_capacity(len);
let mut tb_quote = Vec::with_capacity(len);
let mut ts_quote = Vec::with_capacity(len);
let mut n_trades = Vec::with_capacity(len);
let mut n_buy = Vec::with_capacity(len);
let mut n_sell = Vec::with_capacity(len);
for bin in self.bins.iter() {
p_starts.push(bin.price_bin_start.0);
p_ends.push(bin.price_bin_end.0);
vol.push(bin.volume.0);
tb_base.push(bin.taker_buy_base_asset_volume.map(|v| v.0));
ts_base.push(bin.taker_sell_base_asset_volume.map(|v| v.0));
q_vol.push(bin.quote_asset_volume.map(|v| v.0));
tb_quote.push(bin.taker_buy_quote_asset_volume.map(|v| v.0));
ts_quote.push(bin.taker_sell_quote_asset_volume.map(|v| v.0));
n_trades.push(bin.number_of_trades.map(|c| c.0));
n_buy.push(bin.number_of_buy_trades.map(|c| c.0));
n_sell.push(bin.number_of_sell_trades.map(|c| c.0));
}
let df = df![
ProfileCol::WindowStart.to_string() => window_starts,
ProfileCol::WindowEnd.to_string() => window_ends,
ProfileCol::PriceBinStart.to_string() => p_starts,
ProfileCol::PriceBinEnd.to_string() => p_ends,
ProfileCol::Volume.to_string() => vol,
ProfileCol::TakerBuyBaseVol.to_string() => tb_base,
ProfileCol::TakerSellBaseVol.to_string() => ts_base,
ProfileCol::QuoteVol.to_string() => q_vol,
ProfileCol::TakerBuyQuoteVol.to_string() => tb_quote,
ProfileCol::TakerSellQuoteVol.to_string() => ts_quote,
ProfileCol::NumTrades.to_string() => n_trades,
ProfileCol::NumBuyTrades.to_string() => n_buy,
ProfileCol::NumSellTrades.to_string() => n_sell,
]
.map_err(|e| ChapatyError::Data(DataError::DataFrame(e.to_string())))?;
let lf = df.lazy().with_columns([
col(ProfileCol::WindowStart).cast(DataType::Datetime(
TimeUnit::Microseconds,
Some(polars::prelude::TimeZone::UTC),
)),
col(ProfileCol::WindowEnd).cast(DataType::Datetime(
TimeUnit::Microseconds,
Some(polars::prelude::TimeZone::UTC),
)),
]);
lf.collect()
.map_err(|e| DataError::DataFrame(e.to_string()).into())
}
}
impl ProfileBinStats for VolumeProfileBin {
fn get_value(&self) -> f64 {
self.volume.0
}
fn get_price(&self) -> Price {
self.price_bin_start
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EmaId {
pub parent: OhlcvId,
pub length: EmaWindow,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Ema {
pub timestamp: DateTime<Utc>,
pub price: Price,
}
impl PriceReachable for Ema {
fn price_reached(&self, target_price: Price, direction: TradeType) -> bool {
match direction {
TradeType::Long => self.price.0 <= target_price.0,
TradeType::Short => self.price.0 >= target_price.0,
}
}
}
impl IndicatorValueProvider for Ema {
fn value(&self) -> Price {
self.price
}
fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl MarketEvent for Ema {
fn point_in_time(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl StreamId for EmaId {
type Event = Ema;
}
impl SymbolProvider for EmaId {
fn symbol(&self) -> &Symbol {
self.parent.symbol()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct RsiId {
pub parent: OhlcvId,
pub length: RsiWindow,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Rsi {
pub timestamp: DateTime<Utc>,
pub price: Price,
}
impl PriceReachable for Rsi {
fn price_reached(&self, target_price: Price, direction: TradeType) -> bool {
match direction {
TradeType::Long => self.price.0 <= target_price.0,
TradeType::Short => self.price.0 >= target_price.0,
}
}
}
impl IndicatorValueProvider for Rsi {
fn value(&self) -> Price {
self.price
}
fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl MarketEvent for Rsi {
fn point_in_time(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl StreamId for RsiId {
type Event = Rsi;
}
impl SymbolProvider for RsiId {
fn symbol(&self) -> &Symbol {
self.parent.symbol()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SmaId {
pub parent: OhlcvId,
pub length: SmaWindow,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Sma {
pub timestamp: DateTime<Utc>,
pub price: Price,
}
impl PriceReachable for Sma {
fn price_reached(&self, target_price: Price, direction: TradeType) -> bool {
match direction {
TradeType::Long => self.price.0 <= target_price.0,
TradeType::Short => self.price.0 >= target_price.0,
}
}
}
impl IndicatorValueProvider for Sma {
fn value(&self) -> Price {
self.price
}
fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl MarketEvent for Sma {
fn point_in_time(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl StreamId for SmaId {
type Event = Sma;
}
impl SymbolProvider for SmaId {
fn symbol(&self) -> &Symbol {
self.parent.symbol()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EconomicCalendarId {
pub broker: DataBroker,
pub data_source: Option<EconomicDataSource>,
pub country_code: Option<CountryCode>,
pub category: Option<EconomicCategory>,
pub importance: Option<EconomicEventImpact>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EconomicEvent {
pub timestamp: DateTime<Utc>,
pub data_source: String,
pub category: String,
pub news_name: String,
pub country_code: CountryCode,
pub currency_code: String,
pub economic_impact: EconomicEventImpact,
pub news_type: Option<String>,
pub news_type_confidence: Option<f64>,
pub news_type_source: Option<String>,
pub period: Option<String>,
pub actual: Option<EconomicValue>,
pub forecast: Option<EconomicValue>,
pub previous: Option<EconomicValue>,
}
impl MarketEvent for EconomicEvent {
fn point_in_time(&self) -> DateTime<Utc> {
self.timestamp
}
}
impl StreamId for EconomicCalendarId {
type Event = EconomicEvent;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct MarketId {
pub broker: DataBroker,
pub exchange: Exchange,
pub symbol: Symbol,
}
impl SymbolProvider for MarketId {
fn symbol(&self) -> &Symbol {
&self.symbol
}
}
impl MarketId {
pub fn market_type(&self) -> MarketType {
self.symbol.into()
}
}
impl From<&OhlcvId> for MarketId {
fn from(value: &OhlcvId) -> Self {
Self {
broker: value.broker,
exchange: value.exchange,
symbol: value.symbol,
}
}
}
impl From<OhlcvId> for MarketId {
fn from(value: OhlcvId) -> Self {
(&value).into()
}
}
impl From<&TradesId> for MarketId {
fn from(value: &TradesId) -> Self {
Self {
broker: value.broker,
exchange: value.exchange,
symbol: value.symbol,
}
}
}
impl From<TradesId> for MarketId {
fn from(value: TradesId) -> Self {
(&value).into()
}
}
#[cfg(test)]
mod test {
use super::*;
fn ts(s: &str) -> DateTime<Utc> {
DateTime::parse_from_rfc3339(s).unwrap().with_timezone(&Utc)
}
#[test]
fn tpo_as_df() {
let open_ts = ts("2023-01-01T00:00:00Z");
let close_ts = ts("2023-01-01T01:00:00Z");
let tpo = Tpo {
open_timestamp: open_ts,
close_timestamp: close_ts,
poc: Price(102.0),
value_area_high: Price(103.0),
value_area_low: Price(101.0),
bins: vec![
TpoBin {
price_bin_start: Price(100.0),
price_bin_end: Price(101.0),
time_slot_count: Count(5),
},
TpoBin {
price_bin_start: Price(101.0),
price_bin_end: Price(102.0),
time_slot_count: Count(15),
},
TpoBin {
price_bin_start: Price(102.0),
price_bin_end: Price(103.0),
time_slot_count: Count(10),
},
]
.into_boxed_slice(),
};
let df = tpo.as_dataframe().expect("to be df");
assert_eq!(df.shape(), (3, 5));
let price_start = df.column("price_bin_start").expect("to get column");
assert_eq!(price_start.dtype(), &DataType::Float64);
assert_eq!(price_start.f64().expect("to be f64").get(0), Some(100.0));
let counts = df.column("time_slot_count").expect("to get column");
assert_eq!(counts.i64().expect("to be i64").get(1), Some(15));
let starts = df.column("window_start").expect("to get column");
let _ts_val = starts
.datetime()
.expect("to be datetime")
.physical()
.get(0)
.unwrap();
assert_eq!(starts.len(), 3);
}
#[test]
fn vp_as_df() {
let open_ts = ts("2023-01-02T12:00:00Z");
let close_ts = ts("2023-01-02T16:00:00Z");
let vp = VolumeProfile {
open_timestamp: open_ts,
close_timestamp: close_ts,
poc: Price(50.0),
value_area_high: Price(55.0),
value_area_low: Price(45.0),
bins: vec![
VolumeProfileBin {
price_bin_start: Price(40.0),
price_bin_end: Price(45.0),
volume: Quantity(1000.0),
taker_buy_base_asset_volume: Some(Quantity(600.0)),
taker_sell_base_asset_volume: Some(Quantity(400.0)),
quote_asset_volume: Some(Quantity(50000.0)),
taker_buy_quote_asset_volume: None, taker_sell_quote_asset_volume: None, number_of_trades: Some(Count(50)),
number_of_buy_trades: Some(Count(30)),
number_of_sell_trades: Some(Count(20)),
},
VolumeProfileBin {
price_bin_start: Price(45.0),
price_bin_end: Price(50.0),
volume: Quantity(2500.0),
taker_buy_base_asset_volume: Some(Quantity(1200.0)),
taker_sell_base_asset_volume: Some(Quantity(1300.0)),
quote_asset_volume: Some(Quantity(125000.0)),
taker_buy_quote_asset_volume: Some(Quantity(60000.0)),
taker_sell_quote_asset_volume: Some(Quantity(65000.0)),
number_of_trades: Some(Count(120)),
number_of_buy_trades: Some(Count(60)),
number_of_sell_trades: Some(Count(60)),
},
]
.into_boxed_slice(),
};
let df = vp.as_dataframe().expect("to be df");
assert_eq!(df.shape(), (2, 13));
let vol = df.column("volume").expect("to get vol");
assert_eq!(vol.f64().expect("to be f64").get(1), Some(2500.0));
let tb_quote = df.column("taker_buy_quote_vol").expect("to get tb_quote");
assert!(tb_quote.f64().expect("to be f64").get(0).is_none()); assert_eq!(tb_quote.f64().expect("to be f64").get(1), Some(60000.0)); }
fn mock_trade(price: f64) -> TradeEvent {
TradeEvent {
timestamp: ts("2026-05-01T00:00:00Z"),
price: Price(price),
quantity: Quantity(1.0),
trade_id: None,
quote_asset_volume: None,
is_buyer_maker: None,
is_best_match: None,
}
}
fn mock_ohlcv(low: f64, high: f64) -> Ohlcv {
Ohlcv {
open_timestamp: ts("2026-05-01T00:00:00Z"),
close_timestamp: ts("2026-05-01T00:00:00Z"),
open: Price(0.0), high: Price(high),
low: Price(low),
close: Price(0.0), volume: Quantity(0.0),
quote_asset_volume: None,
number_of_trades: None,
taker_buy_base_asset_volume: None,
taker_buy_quote_asset_volume: None,
}
}
fn mock_sma(price: f64) -> Sma {
Sma {
timestamp: ts("2026-05-01T00:00:00Z"),
price: Price(price),
}
}
fn mock_ema(price: f64) -> Ema {
Ema {
timestamp: ts("2026-05-01T00:00:00Z"),
price: Price(price),
}
}
fn mock_rsi(value: f64) -> Rsi {
Rsi {
timestamp: ts("2026-05-01T00:00:00Z"),
price: Price(value),
}
}
#[test]
fn test_ohlcv_reachability() {
let target = Price(50000.0);
assert!(
mock_ohlcv(49000.0, 50000.0).price_reached(target, TradeType::Long),
"High wick exactly touches target"
);
assert!(
mock_ohlcv(50000.0, 51000.0).price_reached(target, TradeType::Short),
"Low wick exactly touches target"
);
assert!(mock_ohlcv(49000.0, 51000.0).price_reached(target, TradeType::Long));
assert!(mock_ohlcv(50000.0, 50000.0).price_reached(target, TradeType::Long));
assert!(
!mock_ohlcv(49000.0, 49999.999999).price_reached(target, TradeType::Long),
"Wick high barely misses"
);
assert!(
!mock_ohlcv(50000.000001, 51000.0).price_reached(target, TradeType::Short),
"Wick low barely misses"
);
}
#[test]
fn test_trade_long_reachability() {
let target = Price(50000.0);
assert!(!mock_trade(50000.000001).price_reached(target, TradeType::Long));
assert!(mock_trade(50000.0).price_reached(target, TradeType::Long));
assert!(mock_trade(49990.0).price_reached(target, TradeType::Long));
}
#[test]
fn test_trade_short_reachability() {
let target = Price(50000.0);
assert!(!mock_trade(49999.999999).price_reached(target, TradeType::Short));
assert!(mock_trade(50000.0).price_reached(target, TradeType::Short));
assert!(mock_trade(50010.0).price_reached(target, TradeType::Short));
}
#[test]
fn test_sma_long_reachability() {
let target = Price(50000.0);
assert!(!mock_sma(50000.1).price_reached(target, TradeType::Long));
assert!(mock_sma(50000.0).price_reached(target, TradeType::Long));
assert!(mock_sma(49000.0).price_reached(target, TradeType::Long));
}
#[test]
fn test_sma_short_reachability() {
let target = Price(50000.0);
assert!(!mock_sma(49999.9).price_reached(target, TradeType::Short));
assert!(mock_sma(50000.0).price_reached(target, TradeType::Short));
assert!(mock_sma(51000.0).price_reached(target, TradeType::Short));
}
#[test]
fn test_ema_long_reachability() {
let target = Price(100.5);
assert!(!mock_ema(100.50000001).price_reached(target, TradeType::Long));
assert!(mock_ema(100.5).price_reached(target, TradeType::Long));
assert!(mock_ema(100.49999999).price_reached(target, TradeType::Long));
}
#[test]
fn test_ema_short_reachability() {
let target = Price(100.5);
assert!(
!mock_ema(100.49999999).price_reached(target, TradeType::Short),
"EMA is just below target"
);
assert!(
mock_ema(100.5).price_reached(target, TradeType::Short),
"EMA exactly hits target"
);
assert!(
mock_ema(100.50000001).price_reached(target, TradeType::Short),
"EMA spikes just above target"
);
}
#[test]
fn test_rsi_oversold_long() {
let target = Price(30.0);
assert!(!mock_rsi(31.0).price_reached(target, TradeType::Long));
assert!(mock_rsi(30.0).price_reached(target, TradeType::Long));
assert!(mock_rsi(15.0).price_reached(target, TradeType::Long));
}
#[test]
fn test_rsi_overbought_short() {
let target = Price(70.0);
assert!(!mock_rsi(69.9).price_reached(target, TradeType::Short));
assert!(mock_rsi(70.0).price_reached(target, TradeType::Short));
assert!(mock_rsi(85.5).price_reached(target, TradeType::Short));
}
}