use core::fmt::{self, Write as _};
use std::collections::{BTreeMap, HashSet, btree_map::Entry};
use std::hash::{Hash, Hasher};
use crate::BorsaError;
use chrono::{DateTime, Utc};
use paft::market::action::Action;
use paft::market::responses::history::{Candle, HistoryMeta, HistoryResponse};
pub fn merge_history<I>(responses: I) -> Result<HistoryResponse, BorsaError>
where
I: IntoIterator<Item = HistoryResponse>,
{
let mut adjusted_all: Option<bool> = None;
let mut first_adjusted: bool = false;
let mut meta: Option<HistoryMeta> = None;
let mut candle_map: BTreeMap<DateTime<Utc>, Candle> = BTreeMap::new();
let mut actions: Vec<Action> = vec![];
let mut series_currency: Option<paft::money::Currency> = None;
for (idx, mut r) in responses.into_iter().enumerate() {
if idx == 0 {
first_adjusted = r.adjusted;
meta = r.meta.take().or(meta);
} else if meta.is_none() {
meta = r.meta.take();
}
let mut contributed = false;
for c in r.candles {
match candle_map.entry(c.ts) {
Entry::Vacant(v) => {
let open_cur_ref = c.open.currency();
if !(open_cur_ref == c.high.currency()
&& open_cur_ref == c.low.currency()
&& open_cur_ref == c.close.currency())
{
return Err(BorsaError::Data(
"Connector provided mixed-currency history".into(),
));
}
if let Some(cur) = &series_currency {
if cur != open_cur_ref {
return Err(BorsaError::Data(
"Connector provided mixed-currency history".into(),
));
}
} else {
series_currency = Some(open_cur_ref.clone());
}
v.insert(c);
contributed = true;
}
Entry::Occupied(_) => {}
}
}
if contributed {
adjusted_all = Some(adjusted_all.unwrap_or(true) & r.adjusted);
}
actions.extend(r.actions.into_iter());
}
let mut candles: Vec<Candle> = candle_map.into_values().collect();
for c in &mut candles {
c.close_unadj = None;
}
let actions = dedup_actions(actions);
Ok(HistoryResponse {
candles,
actions,
adjusted: first_adjusted && adjusted_all.unwrap_or(false),
meta,
})
}
pub fn merge_candles_by_priority<I>(series: I) -> Result<Vec<Candle>, BorsaError>
where
I: IntoIterator<Item = Vec<Candle>>,
{
let mut map: BTreeMap<DateTime<Utc>, Candle> = BTreeMap::new();
let mut series_currency: Option<paft::money::Currency> = None;
for s in series {
for mut c in s {
let open_cur_ref = c.open.currency();
if !(open_cur_ref == c.high.currency()
&& open_cur_ref == c.low.currency()
&& open_cur_ref == c.close.currency())
{
return Err(BorsaError::Data(format!(
"Mixed currencies within candle at {}: open={:?} high={:?} low={:?} close={:?}",
c.ts,
c.open.currency(),
c.high.currency(),
c.low.currency(),
c.close.currency()
)));
}
if let Some(cur) = &series_currency {
if cur != open_cur_ref {
return Err(BorsaError::Data(format!(
"Mixed currencies across merged series at {}: expected {:?}, got {:?}",
c.ts, cur, open_cur_ref
)));
}
} else if map.is_empty() {
series_currency = Some(open_cur_ref.clone());
}
c.close_unadj = None;
map.entry(c.ts).or_insert(c);
}
}
Ok(map.into_values().collect())
}
struct HashWriter<'a, H: Hasher>(&'a mut H);
impl<H: Hasher> fmt::Write for HashWriter<'_, H> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.0.write(s.as_bytes());
Ok(())
}
}
fn compare_actions(a: &Action, b: &Action) -> std::cmp::Ordering {
use std::cmp::Ordering;
let (ats, ak): (DateTime<Utc>, u8) = match *a {
Action::Dividend { ts, .. } => (ts, 0),
Action::Split { ts, .. } => (ts, 1),
Action::CapitalGain { ts, .. } => (ts, 2),
};
let (bts, bk): (DateTime<Utc>, u8) = match *b {
Action::Dividend { ts, .. } => (ts, 0),
Action::Split { ts, .. } => (ts, 1),
Action::CapitalGain { ts, .. } => (ts, 2),
};
match ats.cmp(&bts) {
Ordering::Equal => {}
non_eq => return non_eq,
}
match ak.cmp(&bk) {
Ordering::Equal => {}
non_eq => return non_eq,
}
match (a, b) {
(Action::Dividend { amount: a_amt, .. }, Action::Dividend { amount: b_amt, .. }) => {
let ord = a_amt.amount().cmp(&b_amt.amount());
if ord == Ordering::Equal {
format!("{:?}", a_amt.currency()).cmp(&format!("{:?}", b_amt.currency()))
} else {
ord
}
}
(Action::CapitalGain { gain: a_gain, .. }, Action::CapitalGain { gain: b_gain, .. }) => {
let ord = a_gain.amount().cmp(&b_gain.amount());
if ord == Ordering::Equal {
format!("{:?}", a_gain.currency()).cmp(&format!("{:?}", b_gain.currency()))
} else {
ord
}
}
(
Action::Split {
numerator: an,
denominator: ad,
..
},
Action::Split {
numerator: bn,
denominator: bd,
..
},
) => an.cmp(bn).then(ad.cmp(bd)),
_ => Ordering::Equal,
}
}
fn hash_action(hasher: &mut std::collections::hash_map::DefaultHasher, a: &Action) {
match a {
Action::Dividend { ts, amount } => {
0u8.hash(hasher);
ts.timestamp().hash(hasher);
amount.amount().hash(hasher);
let mut hw = HashWriter(hasher);
let _ = write!(&mut hw, "{:?}", amount.currency());
}
Action::Split {
ts,
numerator,
denominator,
} => {
1u8.hash(hasher);
ts.timestamp().hash(hasher);
numerator.hash(hasher);
denominator.hash(hasher);
}
Action::CapitalGain { ts, gain } => {
2u8.hash(hasher);
ts.timestamp().hash(hasher);
gain.amount().hash(hasher);
let mut hw = HashWriter(hasher);
let _ = write!(&mut hw, "{:?}", gain.currency());
}
}
}
#[must_use]
pub fn dedup_actions(mut actions: Vec<Action>) -> Vec<Action> {
actions.sort_unstable_by(compare_actions);
let mut seen: HashSet<u64> = HashSet::new();
let mut out: Vec<Action> = Vec::with_capacity(actions.len());
for a in actions {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
hash_action(&mut hasher, &a);
let h = hasher.finish();
if seen.insert(h) {
out.push(a);
}
}
out
}