Skip to main content

brokapi_tbank/core/
md.rs

1//! Market-data core: one bidirectional `MarketDataStream`.
2//!
3//! The runtime owns the stream; this core decides what to send and turns what arrives into events.
4//!
5//! Logical subscriptions (what the client asked for, each with its `SubscriptionId`) are mapped to
6//! wire subscriptions (what T-Bank is told). Several logical subscriptions can share one wire
7//! subscription: an order book and quotes of one instrument are one order book subscription of the
8//! larger depth, two subscriptions to the same book are one on the wire, and unsubscribing one does not
9//! end the other. Quotes have no stream of their own at T-Bank: they are the best level of the book.
10
11use std::collections::{HashMap, VecDeque};
12use std::time::{Duration, Instant};
13
14use brokapi_core::{Event, InstrumentId, ResyncReason, SubscriptionId, Timeframe};
15use brokapi_sansio::{
16    BrokerCore, DesiredSubscription, EventSink, Outbound, SubscriptionKind, SubscriptionSet,
17};
18use brokapi_tbank_protocol::v1 as pb;
19use brokapi_tbank_protocol::v1::market_data_request::Payload as Req;
20use brokapi_tbank_protocol::v1::market_data_response::Payload as Resp;
21
22use crate::ids::{instrument_ref, matches};
23use crate::map::market;
24
25/// Pause before a subscription refused with a transient status (internal error, too many requests)
26/// is sent again.
27pub const RETRY_AFTER: Duration = Duration::from_secs(5);
28
29/// One inbound item for [`MdCore`]: what the runtime saw on the market data stream.
30// Boxing the large variant would change the public shape of the enum; these values are short-lived.
31#[allow(clippy::large_enum_variant)]
32#[derive(Debug, Clone)]
33pub enum MdFrame {
34    /// A (new) stream is open: every subscription is sent again.
35    Connected,
36    /// A message the server sent on the stream: data, a subscription answer or a ping.
37    Response(pb::MarketDataResponse),
38}
39
40/// A logical subscription changed state: confirmed or refused.
41///
42/// The connector takes these from [`MdCore::take_updates`] and wakes the caller that waits in
43/// `subscribe_*`.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SubUpdate {
46    /// The subscription that changed.
47    pub sub: SubscriptionId,
48    /// `Ok` when the broker confirmed it. `Err` holds the broker's subscription status name
49    /// (for example `SUBSCRIPTION_STATUS_INSTRUMENT_NOT_FOUND`).
50    pub result: Result<(), String>,
51}
52
53/// Where a logical subscription is in its life, see [`MdCore::state`].
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum LState {
56    /// Asked for, not confirmed yet.
57    Pending,
58    /// Confirmed by the broker. Data flows.
59    Active,
60    /// Refused by the broker. A transient refusal is retried after [`RETRY_AFTER`].
61    Failed,
62}
63
64#[derive(Debug, Clone, Hash, PartialEq, Eq)]
65enum WireKey {
66    Book(String),
67    Trades(String),
68    Candles(String, i32),
69}
70
71#[derive(Debug, Clone)]
72enum WireState {
73    Pending,
74    Active,
75    Failed { retry_at: Option<Instant> },
76}
77
78#[derive(Debug, Clone)]
79struct Wire {
80    instrument: InstrumentId,
81    /// Book depth on the wire (0 for trades / candles).
82    depth: u32,
83    state: WireState,
84}
85
86#[derive(Debug, Clone)]
87enum Kind {
88    Book { depth: u32 },
89    Quotes,
90    Trades,
91    Candles(Timeframe),
92}
93
94#[derive(Debug, Clone)]
95struct Logical {
96    instrument: InstrumentId,
97    kind: Kind,
98    wire: WireKey,
99    state: LState,
100}
101
102/// Sans-io core of the market data stream.
103///
104/// Logical subscriptions (what the client asked for) are mapped to wire subscriptions (what T-Bank
105/// is told). The core queues the requests ([`poll_request`](Self::poll_request)), turns responses
106/// into events, and reports confirmations ([`take_updates`](Self::take_updates)). It implements
107/// [`BrokerCore`] with [`MdFrame`] as the frame type. See the [module docs](crate::core) for an
108/// example.
109#[derive(Debug)]
110pub struct MdCore {
111    desired: SubscriptionSet,
112    logical: HashMap<SubscriptionId, Logical>,
113    wires: HashMap<WireKey, Wire>,
114    requests: VecDeque<pb::MarketDataRequest>,
115    updates: Vec<SubUpdate>,
116    /// Wire subscriptions we asked to drop and have not seen confirmed yet.
117    unsubscribing: Vec<(WireKey, InstrumentId)>,
118    unsub_ok: usize,
119    unsub_failed: usize,
120    connections: u32,
121    ping_ms: i32,
122}
123
124impl Default for MdCore {
125    fn default() -> Self {
126        Self::new(Duration::from_secs(30))
127    }
128}
129
130fn wire_key(instrument: &InstrumentId, kind: &Kind) -> WireKey {
131    let r = instrument_ref(instrument);
132    match kind {
133        Kind::Book { .. } | Kind::Quotes => WireKey::Book(r),
134        Kind::Trades => WireKey::Trades(r),
135        Kind::Candles(tf) => WireKey::Candles(r, market::subscription_interval(*tf) as i32),
136    }
137}
138
139impl MdCore {
140    /// `ping_interval` is requested from the server (5000..180000 ms are valid).
141    pub fn new(ping_interval: Duration) -> Self {
142        Self {
143            desired: SubscriptionSet::new(),
144            logical: HashMap::new(),
145            wires: HashMap::new(),
146            requests: VecDeque::new(),
147            updates: Vec::new(),
148            unsubscribing: Vec::new(),
149            unsub_ok: 0,
150            unsub_failed: 0,
151            connections: 0,
152            ping_ms: ping_interval.as_millis().clamp(5_000, 180_000) as i32,
153        }
154    }
155
156    /// Next request to write to the stream.
157    pub fn poll_request(&mut self) -> Option<pb::MarketDataRequest> {
158        self.requests.pop_front()
159    }
160
161    /// Takes the confirmations and refusals that happened since the last call.
162    pub fn take_updates(&mut self) -> Vec<SubUpdate> {
163        std::mem::take(&mut self.updates)
164    }
165
166    /// The set of subscriptions the consumer wants now, as last given to `apply`.
167    pub fn desired(&self) -> &SubscriptionSet {
168        &self.desired
169    }
170
171    /// The state of a logical subscription, or `None` if the core does not know the id.
172    pub fn state(&self, sub: SubscriptionId) -> Option<LState> {
173        self.logical.get(&sub).map(|l| l.state)
174    }
175
176    /// Wire subscriptions currently held at the broker (or being set up).
177    pub fn wire_count(&self) -> usize {
178        self.wires.len()
179    }
180
181    /// `(confirmed, refused, still waiting)` unsubscribe confirmations (graceful shutdown).
182    pub fn unsubscribe_progress(&self) -> (usize, usize, usize) {
183        (self.unsub_ok, self.unsub_failed, self.unsubscribing.len())
184    }
185
186    /// Counts what is still unconfirmed as failed (shutdown deadline passed).
187    pub fn give_up_unsubscribing(&mut self) {
188        self.unsub_failed += self.unsubscribing.len();
189        self.unsubscribing.clear();
190    }
191
192    fn instruments_request(
193        key: &WireKey,
194        w: &Wire,
195        action: pb::SubscriptionAction,
196    ) -> pb::MarketDataRequest {
197        let payload = match key {
198            WireKey::Book(r) => Req::SubscribeOrderBookRequest(pb::SubscribeOrderBookRequest {
199                subscription_action: action as i32,
200                instruments: vec![pb::OrderBookInstrument {
201                    depth: w.depth as i32,
202                    instrument_id: r.clone(),
203                    ..Default::default()
204                }],
205            }),
206            WireKey::Trades(r) => Req::SubscribeTradesRequest(pb::SubscribeTradesRequest {
207                subscription_action: action as i32,
208                instruments: vec![pb::TradeInstrument {
209                    instrument_id: r.clone(),
210                    ..Default::default()
211                }],
212                ..Default::default()
213            }),
214            WireKey::Candles(r, interval) => {
215                Req::SubscribeCandlesRequest(pb::SubscribeCandlesRequest {
216                    subscription_action: action as i32,
217                    instruments: vec![pb::CandleInstrument {
218                        interval: *interval,
219                        instrument_id: r.clone(),
220                        ..Default::default()
221                    }],
222                    waiting_close: false,
223                    candle_source_type: None,
224                })
225            }
226        };
227        pb::MarketDataRequest {
228            payload: Some(payload),
229        }
230    }
231
232    fn queue(&mut self, key: &WireKey, action: pb::SubscriptionAction) {
233        if let Some(w) = self.wires.get(key) {
234            let req = Self::instruments_request(key, w, action);
235            self.requests.push_back(req);
236        }
237    }
238
239    fn mark_active(&mut self, key: &WireKey) {
240        if let Some(w) = self.wires.get_mut(key) {
241            w.state = WireState::Active;
242        }
243        for (id, l) in self.logical.iter_mut() {
244            if &l.wire == key && l.state != LState::Active {
245                l.state = LState::Active;
246                self.updates.push(SubUpdate {
247                    sub: *id,
248                    result: Ok(()),
249                });
250            }
251        }
252    }
253
254    fn mark_failed(&mut self, key: &WireKey, status: pb::SubscriptionStatus, now: Instant) {
255        let transient = matches!(
256            status,
257            pb::SubscriptionStatus::InternalError | pb::SubscriptionStatus::TooManyRequests
258        );
259        if let Some(w) = self.wires.get_mut(key) {
260            w.state = WireState::Failed {
261                retry_at: transient.then_some(now + RETRY_AFTER),
262            };
263        }
264        for (id, l) in self.logical.iter_mut() {
265            if &l.wire == key && l.state == LState::Pending {
266                l.state = LState::Failed;
267                self.updates.push(SubUpdate {
268                    sub: *id,
269                    result: Err(status.as_str_name().to_string()),
270                });
271            }
272        }
273    }
274
275    /// Recomputes the wire subscriptions from the logical ones and queues the difference.
276    fn sync_wires(&mut self) {
277        let mut needed: HashMap<WireKey, (InstrumentId, u32)> = HashMap::new();
278        for l in self.logical.values() {
279            let depth = match l.kind {
280                Kind::Book { depth } => market::wire_depth(depth),
281                Kind::Quotes => 1,
282                _ => 0,
283            };
284            let e = needed
285                .entry(l.wire.clone())
286                .or_insert_with(|| (l.instrument.clone(), 0));
287            e.1 = e.1.max(depth);
288        }
289
290        let stale: Vec<WireKey> = self
291            .wires
292            .keys()
293            .filter(|k| !needed.contains_key(*k))
294            .cloned()
295            .collect();
296        for key in stale {
297            self.queue(&key, pb::SubscriptionAction::Unsubscribe);
298            if let Some(w) = self.wires.remove(&key) {
299                self.unsubscribing.push((key, w.instrument));
300            }
301        }
302
303        for (key, (instrument, depth)) in needed {
304            match self.wires.get(&key).map(|w| w.depth) {
305                None => {
306                    self.wires.insert(
307                        key.clone(),
308                        Wire {
309                            instrument,
310                            depth,
311                            state: WireState::Pending,
312                        },
313                    );
314                    self.queue(&key, pb::SubscriptionAction::Subscribe);
315                }
316                Some(old) if old != depth => {
317                    // Depth changed: drop the old one first, then take the new one.
318                    self.queue(&key, pb::SubscriptionAction::Unsubscribe);
319                    if let Some(w) = self.wires.get_mut(&key) {
320                        w.depth = depth;
321                        w.state = WireState::Pending;
322                    }
323                    self.queue(&key, pb::SubscriptionAction::Subscribe);
324                }
325                Some(_) => {}
326            }
327        }
328
329        // New logical subscriptions on a wire that is already up are confirmed at once.
330        let ready: Vec<(SubscriptionId, bool)> = self
331            .logical
332            .iter()
333            .filter(|(_, l)| l.state == LState::Pending)
334            .filter_map(|(id, l)| match self.wires.get(&l.wire).map(|w| &w.state) {
335                Some(WireState::Active) => Some((*id, true)),
336                _ => None,
337            })
338            .collect();
339        for (id, _) in ready {
340            if let Some(l) = self.logical.get_mut(&id) {
341                l.state = LState::Active;
342            }
343            self.updates.push(SubUpdate {
344                sub: id,
345                result: Ok(()),
346            });
347        }
348    }
349
350    fn on_connected(&mut self, out: &mut EventSink) {
351        self.connections += 1;
352        self.requests.clear();
353        self.unsubscribing.clear();
354        self.requests.push_back(pb::MarketDataRequest {
355            payload: Some(Req::PingSettings(pb::PingDelaySettings {
356                ping_delay_ms: Some(self.ping_ms),
357            })),
358        });
359        let keys: Vec<WireKey> = self.wires.keys().cloned().collect();
360        for key in keys {
361            if let Some(w) = self.wires.get_mut(&key) {
362                w.state = WireState::Pending;
363            }
364            self.queue(&key, pb::SubscriptionAction::Subscribe);
365        }
366        let reconnect = self.connections > 1;
367        let mut ids: Vec<SubscriptionId> = self.logical.keys().copied().collect();
368        ids.sort_by_key(|i| i.value());
369        for id in ids {
370            let l = self.logical.get_mut(&id).expect("listed");
371            l.state = LState::Pending;
372            if reconnect && matches!(l.kind, Kind::Book { .. } | Kind::Quotes) {
373                out.push_payload(
374                    Some(id),
375                    Event::Resync {
376                        sub: id,
377                        instrument: l.instrument.clone(),
378                        reason: ResyncReason::SubscriptionReset,
379                    },
380                );
381            }
382        }
383    }
384
385    fn find_wire(
386        &self,
387        kind: fn(&WireKey) -> bool,
388        uid: &str,
389        figi: &str,
390        ticker: &str,
391        class_code: &str,
392    ) -> Option<WireKey> {
393        self.wires
394            .iter()
395            .find(|(k, w)| kind(k) && matches(&w.instrument, uid, figi, ticker, class_code))
396            .map(|(k, _)| k.clone())
397    }
398
399    fn find_unsub(
400        &mut self,
401        kind: fn(&WireKey) -> bool,
402        uid: &str,
403        figi: &str,
404        ticker: &str,
405        class_code: &str,
406    ) -> bool {
407        let pos = self
408            .unsubscribing
409            .iter()
410            .position(|(k, i)| kind(k) && matches(i, uid, figi, ticker, class_code));
411        match pos {
412            Some(p) => {
413                self.unsubscribing.remove(p);
414                true
415            }
416            None => false,
417        }
418    }
419
420    #[allow(clippy::too_many_arguments)]
421    fn subscription_result(
422        &mut self,
423        kind: fn(&WireKey) -> bool,
424        action: i32,
425        status: i32,
426        ids: (&str, &str, &str, &str),
427        interval: Option<i32>,
428        now: Instant,
429    ) {
430        let (uid, figi, ticker, class_code) = ids;
431        let status =
432            pb::SubscriptionStatus::try_from(status).unwrap_or(pb::SubscriptionStatus::Unspecified);
433        if action == pb::SubscriptionAction::Unsubscribe as i32 {
434            if self.find_unsub(kind, uid, figi, ticker, class_code) {
435                if status == pb::SubscriptionStatus::Success {
436                    self.unsub_ok += 1;
437                } else {
438                    self.unsub_failed += 1;
439                }
440            }
441            return;
442        }
443        let key = self
444            .wires
445            .iter()
446            .find(|(k, w)| {
447                kind(k)
448                    && matches(&w.instrument, uid, figi, ticker, class_code)
449                    && match (k, interval) {
450                        (WireKey::Candles(_, i), Some(x)) => *i == x,
451                        _ => true,
452                    }
453            })
454            .map(|(k, _)| k.clone());
455        let Some(key) = key else { return };
456        if status == pb::SubscriptionStatus::Success {
457            self.mark_active(&key);
458        } else {
459            self.mark_failed(&key, status, now);
460        }
461    }
462
463    fn on_response(&mut self, resp: pb::MarketDataResponse, now: Instant, out: &mut EventSink) {
464        let Some(payload) = resp.payload else { return };
465        match payload {
466            Resp::SubscribeOrderBookResponse(r) => {
467                for s in &r.order_book_subscriptions {
468                    self.subscription_result(
469                        |k| matches!(k, WireKey::Book(_)),
470                        s.subscription_action,
471                        s.subscription_status,
472                        (&s.instrument_uid, &s.figi, &s.ticker, &s.class_code),
473                        None,
474                        now,
475                    );
476                }
477            }
478            Resp::SubscribeTradesResponse(r) => {
479                for s in &r.trade_subscriptions {
480                    self.subscription_result(
481                        |k| matches!(k, WireKey::Trades(_)),
482                        s.subscription_action,
483                        s.subscription_status,
484                        (&s.instrument_uid, &s.figi, &s.ticker, &s.class_code),
485                        None,
486                        now,
487                    );
488                }
489            }
490            Resp::SubscribeCandlesResponse(r) => {
491                for s in &r.candles_subscriptions {
492                    self.subscription_result(
493                        |k| matches!(k, WireKey::Candles(..)),
494                        s.subscription_action,
495                        s.subscription_status,
496                        (&s.instrument_uid, &s.figi, &s.ticker, &s.class_code),
497                        Some(s.interval),
498                        now,
499                    );
500                }
501            }
502            Resp::Orderbook(b) => self.on_book(&b, now, out),
503            Resp::Trade(t) => self.on_trade(&t, now, out),
504            Resp::Candle(c) => self.on_candle(&c, now, out),
505            // Ping, trading status, last price, open interest, info/last-price subscription results.
506            _ => {}
507        }
508    }
509
510    /// Data can arrive before the subscription result: a pending subscription that gets data is active.
511    fn activate_on_data(&mut self, key: &WireKey) {
512        if matches!(
513            self.wires.get(key).map(|w| &w.state),
514            Some(WireState::Pending)
515        ) {
516            self.mark_active(key);
517        }
518    }
519
520    fn on_book(&mut self, b: &pb::OrderBook, now: Instant, out: &mut EventSink) {
521        let Some(key) = self.find_wire(
522            |k| matches!(k, WireKey::Book(_)),
523            &b.instrument_uid,
524            &b.figi,
525            &b.ticker,
526            &b.class_code,
527        ) else {
528            return;
529        };
530        self.activate_on_data(&key);
531        let mut ids: Vec<SubscriptionId> = self
532            .logical
533            .iter()
534            .filter(|(_, l)| l.wire == key && matches!(l.kind, Kind::Book { .. } | Kind::Quotes))
535            .map(|(id, _)| *id)
536            .collect();
537        ids.sort_by_key(|i| i.value());
538        for id in ids {
539            let l = &self.logical[&id];
540            if !b.is_consistent {
541                // Not all orders made it into the book: do not publish it, tell the consumer.
542                out.push_payload(
543                    Some(id),
544                    Event::Resync {
545                        sub: id,
546                        instrument: l.instrument.clone(),
547                        reason: ResyncReason::SequenceGap,
548                    },
549                );
550                continue;
551            }
552            let payload = match l.kind {
553                Kind::Book { depth } => Event::OrderBook(market::book_snapshot(
554                    &l.instrument,
555                    &b.bids,
556                    &b.asks,
557                    depth as usize,
558                    b.time.as_ref(),
559                    now,
560                )),
561                Kind::Quotes => Event::Quote(market::quote(
562                    &l.instrument,
563                    &b.bids,
564                    &b.asks,
565                    b.time.as_ref(),
566                    now,
567                )),
568                _ => continue,
569            };
570            out.push_payload(Some(id), payload);
571        }
572    }
573
574    fn on_trade(&mut self, t: &pb::Trade, now: Instant, out: &mut EventSink) {
575        let Some(key) = self.find_wire(
576            |k| matches!(k, WireKey::Trades(_)),
577            &t.instrument_uid,
578            &t.figi,
579            &t.ticker,
580            &t.class_code,
581        ) else {
582            return;
583        };
584        self.activate_on_data(&key);
585        let mut ids: Vec<SubscriptionId> = self
586            .logical
587            .iter()
588            .filter(|(_, l)| l.wire == key && matches!(l.kind, Kind::Trades))
589            .map(|(id, _)| *id)
590            .collect();
591        ids.sort_by_key(|i| i.value());
592        for id in ids {
593            if let Some(m) = market::trade(&self.logical[&id].instrument, t, now) {
594                out.push_payload(Some(id), Event::MarketTrade(m));
595            }
596        }
597    }
598
599    fn on_candle(&mut self, c: &pb::Candle, now: Instant, out: &mut EventSink) {
600        let Some(key) = self.find_wire(
601            |k| matches!(k, WireKey::Candles(..)),
602            &c.instrument_uid,
603            &c.figi,
604            &c.ticker,
605            &c.class_code,
606        ) else {
607            return;
608        };
609        // Several intervals of one instrument are separate wire subscriptions: pick by interval.
610        let Some(WireKey::Candles(r, _)) = Some(key) else {
611            return;
612        };
613        let key = WireKey::Candles(r, c.interval);
614        if !self.wires.contains_key(&key) {
615            return;
616        }
617        self.activate_on_data(&key);
618        let Some(tf) = market::timeframe_from_interval(c.interval()) else {
619            return;
620        };
621        let mut ids: Vec<SubscriptionId> = self
622            .logical
623            .iter()
624            .filter(|(_, l)| l.wire == key && matches!(l.kind, Kind::Candles(_)))
625            .map(|(id, _)| *id)
626            .collect();
627        ids.sort_by_key(|i| i.value());
628        for id in ids {
629            out.push_payload(
630                Some(id),
631                Event::Candle(market::stream_candle(
632                    &self.logical[&id].instrument,
633                    tf,
634                    c,
635                    now,
636                )),
637            );
638        }
639    }
640}
641
642impl BrokerCore for MdCore {
643    type Frame = MdFrame;
644
645    fn on_frame(&mut self, frame: MdFrame, now: Instant, out: &mut EventSink) {
646        match frame {
647            MdFrame::Connected => self.on_connected(out),
648            MdFrame::Response(r) => self.on_response(r, now, out),
649        }
650    }
651
652    /// gRPC requests are typed: use [`MdCore::poll_request`]. `Outbound` only models text frames.
653    fn poll_transmit(&mut self) -> Option<Outbound> {
654        None
655    }
656
657    fn poll_timeout(&self) -> Option<Instant> {
658        self.wires
659            .values()
660            .filter_map(|w| match w.state {
661                WireState::Failed { retry_at } => retry_at,
662                _ => None,
663            })
664            .min()
665    }
666
667    /// Sends again the subscriptions that were refused with a transient status.
668    fn on_timeout(&mut self, now: Instant, _out: &mut EventSink) {
669        let due: Vec<WireKey> = self
670            .wires
671            .iter()
672            .filter(|(_, w)| matches!(w.state, WireState::Failed { retry_at: Some(t) } if t <= now))
673            .map(|(k, _)| k.clone())
674            .collect();
675        for key in due {
676            if let Some(w) = self.wires.get_mut(&key) {
677                w.state = WireState::Pending;
678            }
679            for l in self.logical.values_mut() {
680                if l.wire == key && l.state == LState::Failed {
681                    l.state = LState::Pending;
682                }
683            }
684            self.queue(&key, pb::SubscriptionAction::Subscribe);
685        }
686    }
687
688    fn apply(&mut self, desired: &SubscriptionSet) {
689        let keep: Vec<SubscriptionId> = desired.iter().map(|d| d.id).collect();
690        self.logical.retain(|id, _| keep.contains(id));
691        for DesiredSubscription { id, kind } in desired.iter() {
692            if self.logical.contains_key(id) {
693                continue;
694            }
695            let (instrument, kind) = match kind {
696                SubscriptionKind::OrderBook { instrument, depth } => (
697                    instrument.clone(),
698                    Kind::Book {
699                        depth: (*depth).max(1),
700                    },
701                ),
702                SubscriptionKind::Quotes { instrument } => (instrument.clone(), Kind::Quotes),
703                SubscriptionKind::LastTrades { instrument } => (instrument.clone(), Kind::Trades),
704                SubscriptionKind::LastCandle {
705                    instrument,
706                    timeframe,
707                } => (instrument.clone(), Kind::Candles(*timeframe)),
708                // User-data kinds do not belong to this core.
709                SubscriptionKind::OrderState
710                | SubscriptionKind::Portfolio
711                | SubscriptionKind::Executions => continue,
712            };
713            let wire = wire_key(&instrument, &kind);
714            self.logical.insert(
715                *id,
716                Logical {
717                    instrument,
718                    kind,
719                    wire,
720                    state: LState::Pending,
721                },
722            );
723        }
724        self.desired = desired.clone();
725        self.sync_wires();
726    }
727}