Skip to main content

rustrade_execution/
shortability.rs

1//! Whether an instrument can be sold short, and on what terms: what a venue says about lending it.
2//!
3//! [`Shortability`] is the one description of an instrument's borrow terms, whether it comes from
4//! a live venue or from a backtest's own data. A simulated venue reads it from a
5//! [`ShortabilityProvider`], which also says when a short-sale restriction is in effect, and
6//! refuses a short the provider does not allow — see
7//! [`SimulatedVenue::with_shortability`](crate::exchange::mock::SimulatedVenue::with_shortability).
8//! [`ShortabilityTable`] is a provider built from per-instrument histories.
9
10use chrono::{DateTime, Utc};
11use fnv::FnvHashMap;
12use rust_decimal::Decimal;
13use rustrade_instrument::instrument::name::InstrumentNameExchange;
14use serde::{Deserialize, Serialize};
15use std::{collections::BTreeMap, fmt::Debug};
16
17/// What a venue says about lending an instrument for a short sale.
18///
19/// Every field is optional, as no venue reports all of them: `None` means not known, never
20/// "no". A consumer treats an unknown value as it sees fit; the simulated venue treats it as
21/// allowing the short.
22///
23/// # Advisory
24/// The values describe the venue's lending as it reported it, not a promise to lend. A venue can
25/// still refuse a short its flags allow — Alpaca has refused "cannot be sold short" for an asset
26/// it flagged both shortable and easy to borrow — so a short sale must still handle
27/// [`ApiError::BorrowRejected`](crate::error::ApiError::BorrowRejected).
28#[non_exhaustive]
29#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub struct Shortability {
31    /// Whether the venue allows the instrument to be sold short at all.
32    pub shortable: Option<bool>,
33    /// Whether the venue lists the instrument as easy to borrow, rather than hard to borrow.
34    pub easy_to_borrow: Option<bool>,
35    /// How much the venue has to lend, in the units an order on the instrument is sized in: its
36    /// total, not what is left after this account's own shorts.
37    pub available: Option<Decimal>,
38    /// The annualised fee for borrowing the instrument, as a fraction: `0.05` is 5% a year.
39    ///
40    /// Simple, not compounded, and on the venue's own year basis, which differs between venues:
41    /// Alpaca divides its borrow rate over a 360-day year, and Binance's hourly rate is annualised
42    /// over 365 days. A simulated venue's holding costs apply their own
43    /// [`DayCount`](crate::holding_cost::DayCount) to whatever rate they are given.
44    pub fee_rate: Option<Decimal>,
45    /// When the venue has announced it will stop lending the instrument or trading it on margin.
46    ///
47    /// A short still open then may be closed by the venue. A time already passed means the
48    /// instrument is no longer shortable: a
49    /// [`ShortabilityClient`](crate::client::ShortabilityClient) that reports one must also report
50    /// `shortable` as `Some(false)`, and the simulated venue refuses a short from this time on,
51    /// whatever `shortable` says.
52    pub delisting_at: Option<DateTime<Utc>>,
53}
54
55impl Shortability {
56    /// Nothing known.
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// With whether the instrument may be sold short at all.
62    #[must_use]
63    pub fn with_shortable(mut self, shortable: bool) -> Self {
64        self.shortable = Some(shortable);
65        self
66    }
67
68    /// With whether the instrument is easy to borrow.
69    #[must_use]
70    pub fn with_easy_to_borrow(mut self, easy_to_borrow: bool) -> Self {
71        self.easy_to_borrow = Some(easy_to_borrow);
72        self
73    }
74
75    /// With how much the venue has to lend in total.
76    #[must_use]
77    pub fn with_available(mut self, available: Decimal) -> Self {
78        self.available = Some(available);
79        self
80    }
81
82    /// With the annualised borrow fee, as a fraction.
83    #[must_use]
84    pub fn with_fee_rate(mut self, fee_rate: Decimal) -> Self {
85        self.fee_rate = Some(fee_rate);
86        self
87    }
88
89    /// With when the venue will stop lending the instrument or trading it on margin.
90    #[must_use]
91    pub fn with_delisting_at(mut self, delisting_at: DateTime<Utc>) -> Self {
92        self.delisting_at = Some(delisting_at);
93        self
94    }
95}
96
97/// Says what a simulated venue may lend for a short sale, at any instant of a run.
98///
99/// The venue asks when an order arrives that would open or increase a short. Both methods must be
100/// pure functions of their arguments, so a replay asks the same question and gets the same answer.
101///
102/// `Send + Sync` because a venue holds its provider behind an `Arc`, so that one provider, and the
103/// history it carries, can be shared by every run of a backtest sweep without being copied.
104pub trait ShortabilityProvider: Debug + Send + Sync {
105    /// What the venue says about lending `instrument` at `time`.
106    fn shortability(
107        &self,
108        instrument: &InstrumentNameExchange,
109        time: DateTime<Utc>,
110    ) -> Shortability;
111
112    /// Whether a short-sale restriction is in effect on `instrument` at `time`, such as the SEC's
113    /// Rule 201 circuit breaker after a 10% fall, under which a short sale may not be priced at or
114    /// below the best bid.
115    ///
116    /// Separate from [`shortability`](Self::shortability) because it is a rule of the market, not
117    /// a term of the loan. `false` unless implemented.
118    fn short_sale_restricted(
119        &self,
120        _instrument: &InstrumentNameExchange,
121        _time: DateTime<Utc>,
122    ) -> bool {
123        false
124    }
125}
126
127/// A [`ShortabilityProvider`] from per-instrument histories: each instrument's [`Shortability`]
128/// as a step function of time, and the windows in which a short-sale restriction is in effect.
129///
130/// Missing data allows the short: an instrument with no entry, or a time before its first one, is
131/// [`Shortability::new`], with nothing known, and an instrument is unrestricted outside its
132/// windows.
133#[derive(Debug, Clone, Default)]
134pub struct ShortabilityTable {
135    shortability: FnvHashMap<InstrumentNameExchange, BTreeMap<DateTime<Utc>, Shortability>>,
136    restrictions: FnvHashMap<InstrumentNameExchange, Vec<(DateTime<Utc>, DateTime<Utc>)>>,
137}
138
139impl ShortabilityTable {
140    /// An empty table, which allows every short.
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    /// With `instrument` described by `shortability` from `from` until its next entry. An entry
146    /// already at `from` is replaced. Entries may be added in any order.
147    #[must_use]
148    pub fn with_shortability(
149        mut self,
150        instrument: InstrumentNameExchange,
151        from: DateTime<Utc>,
152        shortability: Shortability,
153    ) -> Self {
154        self.shortability
155            .entry(instrument)
156            .or_default()
157            .insert(from, shortability);
158        self
159    }
160
161    /// With a short-sale restriction on `instrument` from `from`, inclusive, until `until`,
162    /// exclusive. Windows may overlap.
163    ///
164    /// # Panics
165    /// If `until` is not after `from`. Such a window would never be in effect, so it is refused
166    /// rather than ignored: it means the data it came from has its instants reversed.
167    #[must_use]
168    pub fn with_short_sale_restriction(
169        mut self,
170        instrument: InstrumentNameExchange,
171        from: DateTime<Utc>,
172        until: DateTime<Utc>,
173    ) -> Self {
174        assert!(
175            from < until,
176            "a short-sale restriction on {instrument} must end after it starts: {from} to {until}"
177        );
178        self.restrictions
179            .entry(instrument)
180            .or_default()
181            .push((from, until));
182        self
183    }
184}
185
186impl ShortabilityProvider for ShortabilityTable {
187    fn shortability(
188        &self,
189        instrument: &InstrumentNameExchange,
190        time: DateTime<Utc>,
191    ) -> Shortability {
192        self.shortability
193            .get(instrument)
194            .and_then(|series| series.range(..=time).next_back())
195            .map(|(_, shortability)| shortability.clone())
196            .unwrap_or_default()
197    }
198
199    fn short_sale_restricted(
200        &self,
201        instrument: &InstrumentNameExchange,
202        time: DateTime<Utc>,
203    ) -> bool {
204        self.restrictions.get(instrument).is_some_and(|windows| {
205            windows
206                .iter()
207                .any(|(from, until)| *from <= time && time < *until)
208        })
209    }
210}
211
212#[cfg(test)]
213#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
214mod tests {
215    use super::*;
216    use rust_decimal_macros::dec;
217
218    fn utc(text: &str) -> DateTime<Utc> {
219        DateTime::parse_from_rfc3339(text).unwrap().to_utc()
220    }
221
222    fn name() -> InstrumentNameExchange {
223        InstrumentNameExchange::new("AAPL")
224    }
225
226    #[test]
227    fn shortability_starts_unknown_and_each_setter_fills_one_field() {
228        assert_eq!(
229            Shortability::new(),
230            Shortability {
231                shortable: None,
232                easy_to_borrow: None,
233                available: None,
234                fee_rate: None,
235                delisting_at: None,
236            }
237        );
238
239        let full = Shortability::new()
240            .with_shortable(true)
241            .with_easy_to_borrow(false)
242            .with_available(dec!(1500))
243            .with_fee_rate(dec!(0.12))
244            .with_delisting_at(utc("2026-10-20T00:00:00Z"));
245        assert_eq!(full.shortable, Some(true));
246        assert_eq!(full.easy_to_borrow, Some(false));
247        assert_eq!(full.available, Some(dec!(1500)));
248        assert_eq!(full.fee_rate, Some(dec!(0.12)));
249        assert_eq!(full.delisting_at, Some(utc("2026-10-20T00:00:00Z")));
250    }
251
252    #[test]
253    fn shortability_round_trips_through_serde_and_reads_missing_fields_as_unknown() {
254        let full = Shortability::new()
255            .with_shortable(true)
256            .with_available(dec!(10))
257            .with_delisting_at(utc("2026-10-20T00:00:00Z"));
258        let json = serde_json::to_string(&full).unwrap();
259        assert_eq!(serde_json::from_str::<Shortability>(&json).unwrap(), full);
260
261        let sparse: Shortability = serde_json::from_str(r#"{"shortable":false}"#).unwrap();
262        assert_eq!(sparse, Shortability::new().with_shortable(false));
263    }
264
265    #[test]
266    fn a_table_steps_at_each_entry_and_knows_nothing_before_the_first() {
267        let hard = Shortability::new()
268            .with_shortable(true)
269            .with_easy_to_borrow(false);
270        let closed = Shortability::new().with_shortable(false);
271        // Added out of order: the table orders its entries by instant.
272        let table = ShortabilityTable::new()
273            .with_shortability(name(), utc("2026-01-03T00:00:00Z"), closed.clone())
274            .with_shortability(name(), utc("2026-01-01T00:00:00Z"), hard.clone());
275
276        let at = |text| table.shortability(&name(), utc(text));
277        assert_eq!(at("2025-12-31T23:59:59Z"), Shortability::new());
278        assert_eq!(at("2026-01-01T00:00:00Z"), hard);
279        assert_eq!(at("2026-01-02T23:59:59Z"), hard);
280        assert_eq!(at("2026-01-03T00:00:00Z"), closed);
281        assert_eq!(
282            table.shortability(
283                &InstrumentNameExchange::new("MSFT"),
284                utc("2026-01-03T00:00:00Z")
285            ),
286            Shortability::new()
287        );
288    }
289
290    #[test]
291    fn a_later_entry_at_the_same_instant_replaces_the_earlier() {
292        let instant = utc("2026-01-01T00:00:00Z");
293        let table = ShortabilityTable::new()
294            .with_shortability(name(), instant, Shortability::new().with_shortable(true))
295            .with_shortability(name(), instant, Shortability::new().with_shortable(false));
296
297        assert_eq!(table.shortability(&name(), instant).shortable, Some(false));
298    }
299
300    #[test]
301    fn a_restriction_is_in_effect_from_its_start_until_just_before_its_end() {
302        let table = ShortabilityTable::new()
303            .with_short_sale_restriction(
304                name(),
305                utc("2026-01-01T15:00:00Z"),
306                utc("2026-01-02T21:00:00Z"),
307            )
308            .with_short_sale_restriction(
309                name(),
310                utc("2026-01-05T15:00:00Z"),
311                utc("2026-01-06T21:00:00Z"),
312            );
313
314        let restricted = |text| table.short_sale_restricted(&name(), utc(text));
315        assert!(!restricted("2026-01-01T14:59:59Z"));
316        assert!(restricted("2026-01-01T15:00:00Z"));
317        assert!(restricted("2026-01-02T20:59:59Z"));
318        assert!(!restricted("2026-01-02T21:00:00Z"));
319        assert!(restricted("2026-01-06T00:00:00Z"));
320        assert!(!table.short_sale_restricted(
321            &InstrumentNameExchange::new("MSFT"),
322            utc("2026-01-01T16:00:00Z")
323        ));
324    }
325
326    #[test]
327    #[should_panic(expected = "must end after it starts")]
328    fn a_restriction_that_ends_before_it_starts_panics() {
329        let _ = ShortabilityTable::new().with_short_sale_restriction(
330            name(),
331            utc("2026-01-02T00:00:00Z"),
332            utc("2026-01-01T00:00:00Z"),
333        );
334    }
335
336    #[test]
337    fn a_provider_is_unrestricted_unless_it_says_otherwise() {
338        #[derive(Debug)]
339        struct Constant;
340        impl ShortabilityProvider for Constant {
341            fn shortability(&self, _: &InstrumentNameExchange, _: DateTime<Utc>) -> Shortability {
342                Shortability::new()
343            }
344        }
345
346        assert!(!Constant.short_sale_restricted(&name(), utc("2026-01-01T00:00:00Z")));
347    }
348}