1use std::str::FromStr;
4use std::time::{Instant, SystemTime, UNIX_EPOCH};
5
6use brokapi_core::{
7 ConfigError, Error, InstrumentId, InstrumentKey, NativeId, Timestamps, UnixNanos,
8};
9use brokapi_core::{Decimal as Dec, Money as BrokerMoney, Price, Qty};
10use brokapi_finam_protocol::pb::google::r#type::{Decimal, Money};
11
12pub fn symbol_of(id: &InstrumentId) -> Result<String, Error> {
14 match &id.native {
15 NativeId::SymbolMic { symbol, mic } => Ok(format!("{symbol}@{mic}")),
16 _ => Err(Error::Config(ConfigError::Message(
17 "instrument native id must be SymbolMic for Finam".into(),
18 ))),
19 }
20}
21
22pub fn instrument_from_symbol(symbol: &str) -> InstrumentId {
24 let (sym, mic) = symbol.rsplit_once('@').unwrap_or((symbol, ""));
25 InstrumentId::new(
26 InstrumentKey::new(sym, mic),
27 NativeId::SymbolMic {
28 symbol: sym.to_string(),
29 mic: mic.to_string(),
30 },
31 )
32}
33
34pub fn no_instrument() -> InstrumentId {
36 instrument_from_symbol("")
37}
38
39pub fn now_nanos() -> UnixNanos {
48 UnixNanos::from_nanos(
49 SystemTime::now()
50 .duration_since(UNIX_EPOCH)
51 .map(|d| d.as_nanos() as i64)
52 .unwrap_or(0),
53 )
54}
55
56pub fn ts_nanos(ts: &prost_types::Timestamp) -> UnixNanos {
58 UnixNanos::from_nanos(
59 ts.seconds
60 .saturating_mul(1_000_000_000)
61 .saturating_add(ts.nanos as i64),
62 )
63}
64
65pub fn timestamps(broker: Option<&prost_types::Timestamp>, rx_mono: Instant) -> Timestamps {
67 let t = Timestamps::new(now_nanos(), rx_mono);
68 match broker {
69 Some(b) if b.seconds != 0 || b.nanos != 0 => t.with_broker_ts(ts_nanos(b)),
70 _ => t,
71 }
72}
73
74pub fn to_timestamp(n: UnixNanos) -> prost_types::Timestamp {
88 let n = n.as_nanos();
89 prost_types::Timestamp {
90 seconds: n.div_euclid(1_000_000_000),
91 nanos: n.rem_euclid(1_000_000_000) as i32,
92 }
93}
94
95pub fn dec(d: &Option<Decimal>) -> Option<Dec> {
99 let s = d.as_ref()?.value.trim();
100 if s.is_empty() {
101 return None;
102 }
103 let s = s.strip_prefix('+').unwrap_or(s);
104 Dec::from_str(s).or_else(|_| Dec::from_scientific(s)).ok()
105}
106
107pub fn dec0(d: &Option<Decimal>) -> Dec {
109 dec(d).unwrap_or(Dec::ZERO)
110}
111
112pub fn price(d: &Option<Decimal>) -> Option<Price> {
114 dec(d).map(Price::new)
115}
116
117pub fn price0(d: &Option<Decimal>) -> Price {
119 Price::new(dec0(d))
120}
121
122pub fn qty(d: &Option<Decimal>) -> Option<Qty> {
124 dec(d).map(Qty::new)
125}
126
127pub fn qty0(d: &Option<Decimal>) -> Qty {
129 Qty::new(dec0(d))
130}
131
132pub fn to_decimal(v: Dec) -> Decimal {
135 Decimal {
136 value: v.to_string(),
137 }
138}
139
140pub fn money(m: &Money) -> (String, BrokerMoney) {
142 (
143 m.currency_code.clone(),
144 BrokerMoney::new((Dec::from(m.units) + Dec::new(i64::from(m.nanos), 9)).normalize()),
145 )
146}
147
148pub fn to_money(currency: &str, amount: BrokerMoney) -> Option<Money> {
154 let v = amount.decimal().normalize();
155 if v.scale() > 9 {
156 return None;
157 }
158 let units = i64::try_from(v.trunc()).ok()?;
159 let mut frac = v.fract();
160 frac.rescale(9);
161 let nanos = i32::try_from(frac.mantissa()).ok()?;
162 Some(Money {
163 currency_code: currency.to_string(),
164 units,
165 nanos,
166 })
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use proptest::prelude::*;
173
174 proptest! {
175 #[test]
177 fn decimal_text_round_trips(m in -1_000_000_000_000_000_000_i64..1_000_000_000_000_000_000, scale in 0u32..=12) {
178 let text = Dec::new(m, scale).to_string();
179 let back = to_decimal(dec(&Some(Decimal { value: text.clone() })).unwrap());
180 prop_assert_eq!(back.value, text);
181 }
182
183 #[test]
185 fn money_round_trips(units in -1_000_000_000_000_i64..1_000_000_000_000, nanos in 0i32..1_000_000_000, neg in any::<bool>()) {
186 let (units, nanos) = if neg { (-units.abs(), -nanos) } else { (units.abs(), nanos) };
187 let wire = Money { currency_code: "RUB".into(), units, nanos };
188 let (ccy, amount) = money(&wire);
189 prop_assert_eq!(to_money(&ccy, amount), Some(wire));
190 }
191 }
192
193 #[test]
194 fn money_with_a_tenth_of_a_nano_is_refused() {
195 assert_eq!(to_money("RUB", BrokerMoney::new(Dec::new(1, 10))), None);
196 }
197
198 #[test]
199 fn decimal_grammar() {
200 let d = |s: &str| Some(Decimal { value: s.into() });
201 let n = |s: &str| Some(s.parse::<Dec>().unwrap());
202 assert_eq!(dec(&d("123.45")), n("123.45"));
203 assert_eq!(dec(&d("+2.5")), n("2.5"));
204 assert_eq!(dec(&d("-0.001")), n("-0.001"));
205 assert_eq!(dec(&d(".5")), n("0.5"));
206 assert_eq!(dec(&d("2.5e3")), n("2500"));
207 assert_eq!(dec(&d("1E-2")), n("0.01"));
208 assert_eq!(dec(&d("")), None);
209 assert_eq!(dec(&d("abc")), None);
210 assert_eq!(dec(&d("NaN")), None);
211 assert_eq!(dec(&None), None);
212 }
213
214 #[test]
215 fn decimal_keeps_the_written_form() {
216 let d = |s: &str| Some(Decimal { value: s.into() });
217 assert_eq!(dec(&d("244.30")).unwrap().to_string(), "244.30");
218 assert_eq!(dec(&d("0.0001")).unwrap().to_string(), "0.0001");
219 assert_eq!(to_decimal(dec(&d("244.30")).unwrap()).value, "244.30");
220 }
221
222 #[test]
223 fn decimal_text_has_no_exponent() {
224 for s in [
225 "0",
226 "1",
227 "150.5",
228 "0.0000001",
229 "1000000000000000",
230 "123456.789",
231 "-4.2",
232 ] {
233 let v: Dec = s.parse().unwrap();
234 let out = to_decimal(v).value;
235 assert!(!out.contains('e') && !out.contains('E'), "{out}");
236 assert_eq!(out, s);
237 }
238 }
239
240 #[test]
241 fn money_units_and_nanos() {
242 let m = Money {
243 currency_code: "RUB".into(),
244 units: 1000,
245 nanos: 500_000_000,
246 };
247 assert_eq!(money(&m), ("RUB".into(), "1000.5".parse().unwrap()));
248 let neg = Money {
249 currency_code: "RUB".into(),
250 units: -3,
251 nanos: -10_000_000,
252 };
253 assert_eq!(money(&neg).1, "-3.01".parse().unwrap());
254 }
255
256 #[test]
257 fn symbol_roundtrip_without_network() {
258 let id = instrument_from_symbol("SBER@MISX");
259 assert_eq!(id.key.display(), "SBER@MISX");
260 assert_eq!(symbol_of(&id).unwrap(), "SBER@MISX");
261 }
262
263 #[test]
264 fn non_finam_native_id_is_a_config_error() {
265 let id = InstrumentId::new(
266 InstrumentKey::new("SBER", "TQBR"),
267 NativeId::Figi("X".into()),
268 );
269 assert!(matches!(symbol_of(&id), Err(Error::Config(_))));
270 }
271
272 #[test]
273 fn timestamp_conversions() {
274 let n = UnixNanos::from_nanos(1_700_000_000_123_456_789);
275 let t = to_timestamp(n);
276 assert_eq!(ts_nanos(&t), n);
277 }
278}