1use brokapi_core::{Candle, Timeframe, UnixNanos};
11
12const SEC: i64 = 1_000_000_000;
13const M10_NANOS: i64 = 600 * SEC;
14
15pub fn floor_m10(ts: UnixNanos) -> UnixNanos {
17 UnixNanos::from_nanos(ts.as_nanos().div_euclid(M10_NANOS) * M10_NANOS)
18}
19
20fn merge(parts: &[&Candle]) -> Candle {
23 let mut sorted: Vec<&Candle> = parts.to_vec();
24 sorted.sort_by_key(|c| c.open_ts);
25 let first = sorted[0];
26 let last = sorted[sorted.len() - 1];
27 let open_ts = floor_m10(first.open_ts);
28 Candle {
29 instrument: first.instrument.clone(),
30 timeframe: Timeframe::M10,
31 open: first.open,
32 high: sorted.iter().map(|c| c.high).max().unwrap_or(first.high),
33 low: sorted.iter().map(|c| c.low).min().unwrap_or(first.low),
34 close: last.close,
35 volume: sorted.iter().map(|c| c.volume).sum(),
36 open_ts,
37 close_ts: UnixNanos::from_nanos(open_ts.as_nanos() + M10_NANOS),
38 ts: last.ts,
39 }
40}
41
42pub fn merge_m5(m5: &[Candle]) -> Vec<Candle> {
44 let mut groups: std::collections::BTreeMap<UnixNanos, Vec<&Candle>> = Default::default();
45 for c in m5 {
46 groups.entry(floor_m10(c.open_ts)).or_default().push(c);
47 }
48 groups.values().map(|g| merge(g)).collect()
49}
50
51#[derive(Debug, Default)]
90pub struct M10Builder {
91 current: Option<UnixNanos>,
92 parts: Vec<Candle>,
94}
95
96impl M10Builder {
97 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn on_m5(&mut self, bar: &Candle) -> Option<Candle> {
108 let group = floor_m10(bar.open_ts);
109 match self.current {
110 Some(cur) if group < cur => return None,
111 Some(cur) if group == cur => {}
112 _ => {
113 self.current = Some(group);
114 self.parts.clear();
115 }
116 }
117 match self.parts.iter_mut().find(|p| p.open_ts == bar.open_ts) {
118 Some(p) => *p = bar.clone(),
119 None => self.parts.push(bar.clone()),
120 }
121 let refs: Vec<&Candle> = self.parts.iter().collect();
122 Some(merge(&refs))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use brokapi_core::{Decimal, dec};
130 use brokapi_core::{InstrumentId, InstrumentKey, NativeId, Timestamps};
131 use std::time::Instant;
132
133 fn id() -> InstrumentId {
134 InstrumentId::new(
135 InstrumentKey::new("SBER", "MISX"),
136 NativeId::SymbolMic {
137 symbol: "SBER".into(),
138 mic: "MISX".into(),
139 },
140 )
141 }
142
143 fn m5(minute: i64, o: &str, h: &str, l: &str, c: &str, v: &str) -> Candle {
145 let open_ts = UnixNanos::from_nanos(1_800_000 * SEC + minute * 60 * SEC);
146 Candle {
147 instrument: id(),
148 timeframe: Timeframe::M5,
149 open: o.parse().unwrap(),
150 high: h.parse().unwrap(),
151 low: l.parse().unwrap(),
152 close: c.parse().unwrap(),
153 volume: v.parse().unwrap(),
154 open_ts,
155 close_ts: UnixNanos::from_nanos(open_ts.as_nanos() + 300 * SEC),
156 ts: Timestamps::new(UnixNanos::ZERO, Instant::now()),
157 }
158 }
159
160 #[test]
161 fn floor_is_a_multiple_of_ten_minutes() {
162 let t = UnixNanos::from_nanos(1_800_000 * SEC + 7 * 60 * SEC + 13);
163 assert_eq!(floor_m10(t).as_nanos(), 1_800_000 * SEC);
164 assert_eq!(floor_m10(floor_m10(t)), floor_m10(t));
165 }
166
167 #[test]
168 fn a_pair_becomes_one_candle() {
169 let out = merge_m5(&[
170 m5(0, "10.0", "12.0", "9.0", "11.0", "100.0"),
171 m5(5, "11.0", "15.0", "10.5", "14.0", "50.0"),
172 ]);
173 assert_eq!(out.len(), 1);
174 let c = &out[0];
175 assert_eq!(c.timeframe, Timeframe::M10);
176 assert_eq!(
177 (
178 c.open.decimal(),
179 c.high.decimal(),
180 c.low.decimal(),
181 c.close.decimal()
182 ),
183 (dec!(10.0), dec!(15.0), dec!(9.0), dec!(14.0))
184 );
185 assert_eq!(c.volume.decimal(), dec!(150.0));
186 assert_eq!(c.open_ts.as_nanos(), 1_800_000 * SEC);
187 assert_eq!(c.close_ts.as_nanos(), 1_800_000 * SEC + 600 * SEC);
188 }
189
190 #[test]
191 fn pairs_are_cut_on_ten_minute_boundaries_not_by_position() {
192 let out = merge_m5(&[
194 m5(5, "11.0", "15.0", "10.5", "14.0", "50.0"),
195 m5(10, "20.0", "21.0", "19.0", "20.5", "7.0"),
196 m5(15, "20.5", "23.0", "20.0", "22.0", "3.0"),
197 ]);
198 assert_eq!(out.len(), 2);
199 assert_eq!(
201 (
202 out[0].open.decimal(),
203 out[0].close.decimal(),
204 out[0].volume.decimal()
205 ),
206 (dec!(11.0), dec!(14.0), dec!(50.0))
207 );
208 assert_eq!(out[0].open_ts.as_nanos(), 1_800_000 * SEC);
209 assert_eq!(
210 (
211 out[1].open.decimal(),
212 out[1].high.decimal(),
213 out[1].close.decimal()
214 ),
215 (dec!(20.0), dec!(23.0), dec!(22.0))
216 );
217 assert_eq!(out[1].open_ts.as_nanos(), 1_800_000 * SEC + 600 * SEC);
218 }
219
220 #[test]
221 fn incomplete_last_pair_is_emitted() {
222 let out = merge_m5(&[
223 m5(0, "1.0", "2.0", "0.5", "1.5", "10.0"),
224 m5(5, "1.5", "3.0", "1.0", "2.5", "10.0"),
225 m5(10, "2.5", "2.6", "2.4", "2.5", "4.0"),
226 ]);
227 assert_eq!(out.len(), 2);
228 assert_eq!(out[1].volume.decimal(), dec!(4.0));
229 assert_eq!(out[1].close.decimal(), dec!(2.5));
230 assert_eq!(out[1].open_ts.as_nanos(), 1_800_000 * SEC + 600 * SEC);
231 }
232
233 #[test]
234 fn input_order_does_not_matter_and_empty_is_empty() {
235 let a = merge_m5(&[
236 m5(5, "11.0", "15.0", "10.5", "14.0", "50.0"),
237 m5(0, "10.0", "12.0", "9.0", "11.0", "100.0"),
238 ]);
239 assert_eq!(
240 (a[0].open.decimal(), a[0].close.decimal()),
241 (dec!(10.0), dec!(14.0))
242 );
243 assert!(merge_m5(&[]).is_empty());
244 }
245
246 #[test]
247 fn builder_follows_the_forming_candle() {
248 let mut b = M10Builder::new();
249 let c = b
251 .on_m5(&m5(0, "10.0", "11.0", "9.5", "10.5", "5.0"))
252 .unwrap();
253 assert_eq!(
254 (c.open.decimal(), c.close.decimal(), c.volume.decimal()),
255 (dec!(10.0), dec!(10.5), dec!(5.0))
256 );
257 let c = b
258 .on_m5(&m5(0, "10.0", "12.0", "9.5", "11.0", "8.0"))
259 .unwrap();
260 assert_eq!(
261 (c.high.decimal(), c.close.decimal(), c.volume.decimal()),
262 (dec!(12.0), dec!(11.0), dec!(8.0)),
263 "an update replaces, it does not add"
264 );
265 let c = b
267 .on_m5(&m5(5, "11.0", "13.0", "10.0", "12.5", "4.0"))
268 .unwrap();
269 assert_eq!(
270 (
271 c.open.decimal(),
272 c.high.decimal(),
273 c.low.decimal(),
274 c.close.decimal()
275 ),
276 (dec!(10.0), dec!(13.0), dec!(9.5), dec!(12.5))
277 );
278 assert_eq!(c.volume.decimal(), dec!(12.0));
279 let c = b
281 .on_m5(&m5(10, "12.5", "12.6", "12.4", "12.5", "1.0"))
282 .unwrap();
283 assert_eq!(
284 (c.open.decimal(), c.volume.decimal()),
285 (dec!(12.5), dec!(1.0))
286 );
287 assert_eq!(c.open_ts.as_nanos(), 1_800_000 * SEC + 600 * SEC);
288 assert!(
290 b.on_m5(&m5(5, "11.0", "13.0", "10.0", "12.5", "4.0"))
291 .is_none()
292 );
293 }
294}