1use std::collections::BTreeMap;
4use std::time::Instant;
5
6use brokapi_core::{Balance, InstrumentId, Money, Portfolio, Position, Qty};
7use brokapi_tbank_protocol::v1 as pb;
8
9use crate::ids::{id_from_parts, money_of, price_of_money, qty_of_quotation, stamps};
10
11pub fn balances(
14 money: &[pb::MoneyValue],
15 blocked: &[pb::MoneyValue],
16 now: Instant,
17) -> Vec<Balance> {
18 let mut by_ccy: BTreeMap<String, (Money, Money)> = BTreeMap::new();
19 for m in money {
20 by_ccy
21 .entry(m.currency.to_uppercase())
22 .or_insert((Money::ZERO, Money::ZERO))
23 .0 += money_of(m);
24 }
25 for b in blocked {
26 by_ccy
27 .entry(b.currency.to_uppercase())
28 .or_insert((Money::ZERO, Money::ZERO))
29 .1 += money_of(b);
30 }
31 by_ccy
32 .into_iter()
33 .map(|(currency, (available, blocked))| Balance {
34 currency,
35 available,
36 blocked,
37 ts: stamps(now, None),
38 })
39 .collect()
40}
41
42pub fn balances_from_positions(p: &pb::PositionsResponse, now: Instant) -> Vec<Balance> {
44 balances(&p.money, &p.blocked, now)
45}
46
47pub fn balances_from_stream(money: &[pb::PositionsMoney], now: Instant) -> Vec<Balance> {
49 let avail: Vec<pb::MoneyValue> = money
50 .iter()
51 .filter_map(|m| m.available_value.clone())
52 .collect();
53 let blocked: Vec<pb::MoneyValue> = money
54 .iter()
55 .filter_map(|m| m.blocked_value.clone())
56 .collect();
57 balances(&avail, &blocked, now)
58}
59
60pub fn positions_from_portfolio(p: &pb::PortfolioResponse, now: Instant) -> Vec<Position> {
62 p.positions
63 .iter()
64 .filter(|x| x.instrument_type != "currency")
65 .map(|x| Position {
66 instrument: id_from_parts(&x.instrument_uid, &x.figi, &x.ticker, &x.class_code),
67 qty: x
68 .quantity
69 .as_ref()
70 .map(qty_of_quotation)
71 .unwrap_or(Qty::ZERO),
72 avg_price: x.average_position_price.as_ref().map(price_of_money),
73 current_price: x.current_price.as_ref().map(price_of_money),
74 ts: stamps(now, None),
75 })
76 .collect()
77}
78
79pub fn positions_from_securities(
81 securities: &[pb::PositionsSecurities],
82 instruments: Vec<InstrumentId>,
83 now: Instant,
84) -> Vec<Position> {
85 securities
86 .iter()
87 .zip(instruments)
88 .map(|(s, instrument)| Position {
89 instrument,
90 qty: Qty::from(s.balance + s.blocked),
91 avg_price: None,
92 current_price: None,
93 ts: stamps(now, None),
94 })
95 .collect()
96}
97
98pub fn portfolio(p: &pb::PortfolioResponse, cash: Vec<Balance>, now: Instant) -> Portfolio {
102 Portfolio {
103 balances: cash,
104 positions: positions_from_portfolio(p, now),
105 ts: stamps(now, None),
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use brokapi_core::{Decimal, dec};
113 use brokapi_tbank_protocol::money::{money, quotation};
114
115 #[test]
116 fn cash_by_currency_available_and_blocked() {
117 let b = balances(
118 &[money("rub", "1000.5"), money("usd", "10.0")],
119 &[money("rub", "200.0")],
120 Instant::now(),
121 );
122 assert_eq!(b.len(), 2);
123 let rub = b.iter().find(|x| x.currency == "RUB").unwrap();
124 assert_eq!(
125 (rub.available.decimal(), rub.blocked.decimal()),
126 (dec!(1000.5), dec!(200.0))
127 );
128 }
129
130 #[test]
131 fn currencies_are_not_positions() {
132 let p = pb::PortfolioResponse {
133 positions: vec![
134 pb::PortfolioPosition {
135 instrument_type: "share".into(),
136 ticker: "SBER".into(),
137 class_code: "TQBR".into(),
138 instrument_uid: "u".into(),
139 quantity: Some(quotation("30.0")),
140 average_position_price: Some(money("rub", "250.0")),
141 current_price: Some(money("rub", "260.0")),
142 ..Default::default()
143 },
144 pb::PortfolioPosition {
145 instrument_type: "currency".into(),
146 ..Default::default()
147 },
148 ],
149 ..Default::default()
150 };
151 let pos = positions_from_portfolio(&p, Instant::now());
152 assert_eq!(pos.len(), 1);
153 assert_eq!(pos[0].qty.decimal(), dec!(30.0));
154 assert_eq!(pos[0].avg_price.unwrap().decimal(), dec!(250.0));
155 assert_eq!(pos[0].instrument.key.symbol, "SBER");
156 }
157}