tastytrade 0.5.0

Library for trading through tastytrade's API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
/******************************************************************************
   Author: Joaquín Béjar García
   Email: jb@taunais.com
   Date: 9/3/25
******************************************************************************/
use crate::PriceEffect;
use crate::accounts::AccountNumber;
use chrono::{DateTime, FixedOffset, NaiveDate};
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt;

/// Represents an account balance.
///
/// This struct holds various balance-related information for a trading account, including cash balance,
/// equity values for different asset classes (long and short positions), derivative values, futures values,
/// margin requirements, available funds, and various call values.  It's designed for deserialization
/// from a data source using `serde` with kebab-case renaming.  All numeric values are represented as
/// `Decimal` for precision.
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Balance {
    /// The account number associated with this balance information.
    pub account_number: AccountNumber,

    /// The cash balance available in the account.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub cash_balance: Decimal,

    /// The total value of long equity positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_equity_value: Decimal,

    /// The total value of short equity positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_equity_value: Decimal,

    /// The total value of long derivative positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_derivative_value: Decimal,

    /// The total value of short derivative positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_derivative_value: Decimal,

    /// The total value of long futures positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_futures_value: Decimal,

    /// The total value of short futures positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_futures_value: Decimal,

    /// The total value of long futures derivative positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_futures_derivative_value: Decimal,

    /// The total value of short futures derivative positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_futures_derivative_value: Decimal,

    /// The total value of long marginable positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_margineable_value: Decimal,

    /// The total value of short marginable positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_margineable_value: Decimal,

    /// The margin equity.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub margin_equity: Decimal,

    /// The equity buying power.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub equity_buying_power: Decimal,

    /// The derivative buying power.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub derivative_buying_power: Decimal,

    /// The day trading buying power.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub day_trading_buying_power: Decimal,

    /// The futures margin requirement.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub futures_margin_requirement: Decimal,

    /// The available trading funds.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub available_trading_funds: Decimal,

    /// The maintenance requirement.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub maintenance_requirement: Decimal,

    /// The maintenance call value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub maintenance_call_value: Decimal,

    /// The Reg T call value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub reg_t_call_value: Decimal,

    /// The day trading call value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub day_trading_call_value: Decimal,

    /// The day equity call value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub day_equity_call_value: Decimal,

    /// The net liquidating value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub net_liquidating_value: Decimal,

    /// The cash available to withdraw.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub cash_available_to_withdraw: Decimal,

    /// The day trade excess.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub day_trade_excess: Decimal,

    /// The pending cash.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub pending_cash: Decimal,

    /// The pending cash effect.
    pub pending_cash_effect: PriceEffect,

    /// The pending margin interest.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub pending_margin_interest: Decimal,

    /// Effective cryptocurrency buying power
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub effective_cryptocurrency_buying_power: Decimal,

    // Everything below is `Option`. The published schema marks no balance
    // field required, and the venue genuinely omits some depending on what the
    // account is permitted to trade — a cash account has no futures margin. A
    // required field the venue skips would fail the whole decode, which on the
    // streaming path means an account balance notification silently becoming
    // an unreadable frame.
    //
    // `default` is not redundant next to `with`: an explicit `with` cancels
    // serde's implicit "absent Option is None", so a field that has both and
    // no `default` fails on absence.
    /// Margin equity as Apex recorded it at the start of the day.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub apex_starting_day_margin_equity: Option<Decimal>,

    /// Margin required against bond positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub bond_margin_requirement: Option<Decimal>,

    /// A manual adjustment applied to buying power.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub buying_power_adjustment: Option<Decimal>,

    /// Cash that has settled.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub cash_settle_balance: Option<Decimal>,

    /// Balance available for closed-loop transfers.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub closed_loop_available_balance: Option<Decimal>,

    /// Margin required against cryptocurrency positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub cryptocurrency_margin_requirement: Option<Decimal>,

    /// Margin required against equity offerings.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub equity_offering_margin_requirement: Option<Decimal>,

    /// Margin required against fixed-income positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub fixed_income_security_margin_requirement: Option<Decimal>,

    /// Intraday margin required against futures.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub futures_intraday_margin_requirement: Option<Decimal>,

    /// Overnight margin required against futures.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub futures_overnight_margin_requirement: Option<Decimal>,

    /// Equities cash moving intraday.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub intraday_equities_cash_amount: Option<Decimal>,

    /// Futures cash moving intraday.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub intraday_futures_cash_amount: Option<Decimal>,

    /// Total value of long bond positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub long_bond_value: Option<Decimal>,

    /// Total value of long cryptocurrency positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub long_cryptocurrency_value: Option<Decimal>,

    /// Total value of long fixed-income positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub long_fixed_income_security_value: Option<Decimal>,

    /// Total value of long index derivative positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub long_index_derivative_value: Option<Decimal>,

    /// Equity above the maintenance requirement.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub maintenance_excess: Option<Decimal>,

    /// Margin balance that has settled.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub margin_settle_balance: Option<Decimal>,

    /// Previous day's cryptocurrency fiat movement.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub previous_day_cryptocurrency_fiat_amount: Option<Decimal>,

    /// Margin required under Regulation T.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub reg_t_margin_requirement: Option<Decimal>,

    /// Total value of short cryptocurrency positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub short_cryptocurrency_value: Option<Decimal>,

    /// Total value of short index derivative positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub short_index_derivative_value: Option<Decimal>,

    /// Equity option buying power from the special memorandum account.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub sma_equity_option_buying_power: Option<Decimal>,

    /// Apex's adjustment to the special memorandum account.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub special_memorandum_account_apex_adjustment: Option<Decimal>,

    /// The special memorandum account value.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub special_memorandum_account_value: Option<Decimal>,

    /// Liquidity pool rebate still pending.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub total_pending_liquidity_pool_rebate: Option<Decimal>,

    /// Total settled balance.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub total_settle_balance: Option<Decimal>,

    /// Cryptocurrency fiat movement that has not settled.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub unsettled_cryptocurrency_fiat_amount: Option<Decimal>,

    /// Derivative buying power already committed.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub used_derivative_buying_power: Option<Decimal>,

    /// Whether the buying-power adjustment is a debit or a credit.
    #[serde(default)]
    pub buying_power_adjustment_effect: Option<PriceEffect>,

    /// Whether the intraday equities cash is a debit or a credit.
    #[serde(default)]
    pub intraday_equities_cash_effect: Option<PriceEffect>,

    /// Whether the intraday futures cash is a debit or a credit.
    #[serde(default)]
    pub intraday_futures_cash_effect: Option<PriceEffect>,

    /// Whether the previous day's cryptocurrency fiat movement is a debit or a credit.
    #[serde(default)]
    pub previous_day_cryptocurrency_fiat_effect: Option<PriceEffect>,

    /// Whether the unsettled cryptocurrency fiat movement is a debit or a credit.
    #[serde(default)]
    pub unsettled_cryptocurrency_fiat_effect: Option<PriceEffect>,

    /// Calendar day the intraday equities cash becomes effective.
    #[serde(default, with = "crate::types::wire::date_option")]
    pub intraday_equities_cash_effective_date: Option<NaiveDate>,

    /// Calendar day the intraday futures cash becomes effective.
    #[serde(default, with = "crate::types::wire::date_option")]
    pub intraday_futures_cash_effective_date: Option<NaiveDate>,

    /// Calendar day the previous cryptocurrency fiat movement became effective.
    #[serde(default, with = "crate::types::wire::date_option")]
    pub previous_date_cryptocurrency_fiat_effective_date: Option<NaiveDate>,

    /// Calendar day this balance describes.
    #[serde(default, with = "crate::types::wire::date_option")]
    pub snapshot_date: Option<NaiveDate>,

    /// The currency these figures are denominated in, such as `USD`.
    #[serde(default)]
    pub currency: Option<String>,

    /// Which end of the trading day this balance describes.
    #[serde(default)]
    pub time_of_day: Option<SnapshotTimeOfDay>,
    /// The timestamp of the last balance update.
    #[serde(with = "crate::types::wire::datetime")]
    pub updated_at: DateTime<FixedOffset>,
}

/// Represents a snapshot of an account's balance at a specific point in time.
///
/// This struct is designed to be deserialized from a data source using `serde`,
/// with field names matching the `kebab-case` convention.  It provides a comprehensive
/// view of various balance components, including cash, equities, derivatives, futures,
/// and margin-related values.  All monetary values are represented using `Decimal`
/// for precision.
#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct BalanceSnapshot {
    /// The account number associated with this balance snapshot.
    pub account_number: AccountNumber,
    /// The cash balance in the account.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub cash_balance: Decimal,
    /// The value of long equity positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_equity_value: Decimal,
    /// The value of short equity positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_equity_value: Decimal,
    /// The value of long derivative positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_derivative_value: Decimal,
    /// The value of short derivative positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_derivative_value: Decimal,
    /// The value of long futures positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_futures_value: Decimal,
    /// The value of short futures positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_futures_value: Decimal,
    /// The value of long futures derivative positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_futures_derivative_value: Decimal,
    /// The value of short futures derivative positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_futures_derivative_value: Decimal,
    /// The value of long margineable positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub long_margineable_value: Decimal,
    /// The value of short margineable positions.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub short_margineable_value: Decimal,
    /// The margin equity.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub margin_equity: Decimal,
    /// The equity buying power.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub equity_buying_power: Decimal,
    /// The derivative buying power.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub derivative_buying_power: Decimal,
    /// The day trading buying power.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub day_trading_buying_power: Decimal,
    /// The futures margin requirement.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub futures_margin_requirement: Decimal,
    /// The available trading funds.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub available_trading_funds: Decimal,
    /// The maintenance requirement.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub maintenance_requirement: Decimal,
    /// The maintenance call value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub maintenance_call_value: Decimal,
    /// The Reg T call value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub reg_t_call_value: Decimal,
    /// The day trading call value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub day_trading_call_value: Decimal,
    /// The day equity call value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub day_equity_call_value: Decimal,
    /// The net liquidating value.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub net_liquidating_value: Decimal,
    /// The cash available to withdraw.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub cash_available_to_withdraw: Decimal,
    /// The day trade excess.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub day_trade_excess: Decimal,
    /// The pending cash.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub pending_cash: Decimal,
    /// The effect of pending cash on the account.
    pub pending_cash_effect: PriceEffect,
    /// The date of the snapshot.
    pub snapshot_date: chrono::NaiveDate,

    // Everything below was in the venue's schema and not in this struct, so a
    // caller reading a snapshot saw twenty-nine fewer numbers than arrived.
    // All `Option`: certification omits fields production sends, and a
    // balance that defaults to zero is a balance that lies.
    /// Margin required against bond positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub bond_margin_requirement: Option<Decimal>,
    /// Cash portion of the settlement balance.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub cash_settle_balance: Option<Decimal>,
    /// Balance available through closed-loop transfer.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub closed_loop_available_balance: Option<Decimal>,
    /// Margin required against cryptocurrency positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub cryptocurrency_margin_requirement: Option<Decimal>,
    /// Which currency this row is denominated in.
    #[serde(default)]
    pub currency: Option<String>,
    /// Margin required against equity offerings.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub equity_offering_margin_requirement: Option<Decimal>,
    /// Margin required against fixed-income positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub fixed_income_security_margin_requirement: Option<Decimal>,
    /// Intraday equities cash movement.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub intraday_equities_cash_amount: Option<Decimal>,
    /// Whether the intraday equities cash moved in or out.
    #[serde(default)]
    pub intraday_equities_cash_effect: Option<PriceEffect>,
    /// When the intraday equities cash settles.
    #[serde(default, with = "crate::types::wire::date_option")]
    pub intraday_equities_cash_effective_date: Option<chrono::NaiveDate>,
    /// Intraday futures cash movement.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub intraday_futures_cash_amount: Option<Decimal>,
    /// Whether the intraday futures cash moved in or out.
    #[serde(default)]
    pub intraday_futures_cash_effect: Option<PriceEffect>,
    /// When the intraday futures cash settles.
    #[serde(default, with = "crate::types::wire::date_option")]
    pub intraday_futures_cash_effective_date: Option<chrono::NaiveDate>,
    /// Value of long bond positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub long_bond_value: Option<Decimal>,
    /// Value of long cryptocurrency positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub long_cryptocurrency_value: Option<Decimal>,
    /// Value of long fixed-income positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub long_fixed_income_security_value: Option<Decimal>,
    /// Margin portion of the settlement balance.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub margin_settle_balance: Option<Decimal>,
    /// When the previous day's cryptocurrency fiat movement settles.
    #[serde(default, with = "crate::types::wire::date_option")]
    pub previous_date_cryptocurrency_fiat_effective_date: Option<chrono::NaiveDate>,
    /// Previous day's cryptocurrency fiat movement.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub previous_day_cryptocurrency_fiat_amount: Option<Decimal>,
    /// Whether that movement was in or out.
    #[serde(default)]
    pub previous_day_cryptocurrency_fiat_effect: Option<PriceEffect>,
    /// Value of short cryptocurrency positions.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub short_cryptocurrency_value: Option<Decimal>,
    /// Equity-option buying power from the special memorandum account.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub sma_equity_option_buying_power: Option<Decimal>,
    /// Clearing-firm adjustment to the special memorandum account.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub special_memorandum_account_apex_adjustment: Option<Decimal>,
    /// Special memorandum account value.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub special_memorandum_account_value: Option<Decimal>,
    /// Whether this snapshot is beginning or end of day.
    #[serde(default)]
    pub time_of_day: Option<SnapshotTimeOfDay>,
    /// Total settlement balance.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub total_settle_balance: Option<Decimal>,
    /// Unsettled cryptocurrency fiat amount.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub unsettled_cryptocurrency_fiat_amount: Option<Decimal>,
    /// Whether the unsettled cryptocurrency fiat is in or out.
    #[serde(default)]
    pub unsettled_cryptocurrency_fiat_effect: Option<PriceEffect>,
    /// Derivative buying power already consumed.
    #[serde(default, with = "crate::types::wire::decimal_option")]
    pub used_derivative_buying_power: Option<Decimal>,
}

/// Represents the time of day for a snapshot.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotTimeOfDay {
    /// End of Day.
    #[serde(rename = "EOD")]
    Eod,
    /// Beginning of Day.
    #[serde(rename = "BOD")]
    Bod,
}

impl SnapshotTimeOfDay {
    /// The text the venue uses for this value.
    ///
    /// `EOD` and `BOD`, not `Eod` and `Bod`. The distinction is not cosmetic:
    /// `time-of-day` is a **required** query parameter on the snapshot
    /// endpoint, and this is what goes into it.
    pub fn as_wire(&self) -> &'static str {
        match self {
            SnapshotTimeOfDay::Eod => "EOD",
            SnapshotTimeOfDay::Bod => "BOD",
        }
    }
}

impl fmt::Display for SnapshotTimeOfDay {
    /// The venue's spelling.
    ///
    /// This used to be the derived `Debug` — `Eod` — and the snapshot query
    /// was built from it, so the required `time-of-day` parameter went out as
    /// `Eod` on every request. The serde rename was right all along and only
    /// the `Display` was wrong, which is why nothing that round-tripped
    /// through JSON ever noticed.
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(self.as_wire())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::accounts::AccountNumber;
    use chrono::Datelike;
    use rust_decimal::Decimal;
    use std::str::FromStr;

    /// The regression: this test used to assert `"Eod"`, pinning the defect
    /// rather than the contract. The snapshot query is built from `Display`,
    /// so a required parameter was going out with a value the venue does not
    /// use.
    #[test]
    fn test_snapshot_time_of_day_display() {
        assert_eq!(format!("{}", SnapshotTimeOfDay::Eod), "EOD");
        assert_eq!(format!("{}", SnapshotTimeOfDay::Bod), "BOD");
        // And it agrees with what serde writes, which is the whole point.
        assert_eq!(
            serde_json::to_string(&SnapshotTimeOfDay::Eod).expect("serialises"),
            "\"EOD\""
        );
    }

    #[test]
    fn test_snapshot_time_of_day_serialization() {
        let eod = SnapshotTimeOfDay::Eod;
        let serialized = serde_json::to_string(&eod).unwrap();
        assert_eq!(serialized, "\"EOD\"");

        let bod = SnapshotTimeOfDay::Bod;
        let serialized = serde_json::to_string(&bod).unwrap();
        assert_eq!(serialized, "\"BOD\"");
    }

    #[test]
    fn test_snapshot_time_of_day_deserialization() {
        let eod: SnapshotTimeOfDay = serde_json::from_str("\"EOD\"").unwrap();
        matches!(eod, SnapshotTimeOfDay::Eod);

        let bod: SnapshotTimeOfDay = serde_json::from_str("\"BOD\"").unwrap();
        matches!(bod, SnapshotTimeOfDay::Bod);
    }

    #[test]
    fn test_balance_serialization() {
        let balance = Balance {
            account_number: AccountNumber("TEST123".to_string()),
            cash_balance: Decimal::from_str("1000.50").unwrap(),
            long_equity_value: Decimal::from_str("5000.00").unwrap(),
            short_equity_value: Decimal::from_str("0.00").unwrap(),
            long_derivative_value: Decimal::from_str("500.00").unwrap(),
            short_derivative_value: Decimal::from_str("0.00").unwrap(),
            long_futures_value: Decimal::from_str("0.00").unwrap(),
            short_futures_value: Decimal::from_str("0.00").unwrap(),
            long_futures_derivative_value: Decimal::from_str("0.00").unwrap(),
            short_futures_derivative_value: Decimal::from_str("0.00").unwrap(),
            long_margineable_value: Decimal::from_str("5000.00").unwrap(),
            short_margineable_value: Decimal::from_str("0.00").unwrap(),
            margin_equity: Decimal::from_str("6500.50").unwrap(),
            equity_buying_power: Decimal::from_str("13000.00").unwrap(),
            derivative_buying_power: Decimal::from_str("6500.50").unwrap(),
            day_trading_buying_power: Decimal::from_str("26000.00").unwrap(),
            futures_margin_requirement: Decimal::from_str("0.00").unwrap(),
            available_trading_funds: Decimal::from_str("6500.50").unwrap(),
            maintenance_requirement: Decimal::from_str("0.00").unwrap(),
            maintenance_call_value: Decimal::from_str("0.00").unwrap(),
            reg_t_call_value: Decimal::from_str("0.00").unwrap(),
            day_trading_call_value: Decimal::from_str("0.00").unwrap(),
            day_equity_call_value: Decimal::from_str("0.00").unwrap(),
            net_liquidating_value: Decimal::from_str("6500.50").unwrap(),
            cash_available_to_withdraw: Decimal::from_str("1000.50").unwrap(),
            day_trade_excess: Decimal::from_str("26000.00").unwrap(),
            pending_cash: Decimal::from_str("0.00").unwrap(),
            pending_cash_effect: PriceEffect::None,
            pending_margin_interest: Decimal::from_str("0.00").unwrap(),
            effective_cryptocurrency_buying_power: Decimal::from_str("0.00").unwrap(),
            updated_at: DateTime::parse_from_rfc3339("2024-01-01T12:00:00Z").unwrap(),
            apex_starting_day_margin_equity: None,
            bond_margin_requirement: None,
            buying_power_adjustment: None,
            cash_settle_balance: None,
            closed_loop_available_balance: None,
            cryptocurrency_margin_requirement: None,
            equity_offering_margin_requirement: None,
            fixed_income_security_margin_requirement: None,
            futures_intraday_margin_requirement: None,
            futures_overnight_margin_requirement: None,
            intraday_equities_cash_amount: None,
            intraday_futures_cash_amount: None,
            long_bond_value: None,
            long_cryptocurrency_value: None,
            long_fixed_income_security_value: None,
            long_index_derivative_value: None,
            maintenance_excess: None,
            margin_settle_balance: None,
            previous_day_cryptocurrency_fiat_amount: None,
            reg_t_margin_requirement: None,
            short_cryptocurrency_value: None,
            short_index_derivative_value: None,
            sma_equity_option_buying_power: None,
            special_memorandum_account_apex_adjustment: None,
            special_memorandum_account_value: None,
            total_pending_liquidity_pool_rebate: None,
            total_settle_balance: None,
            unsettled_cryptocurrency_fiat_amount: None,
            used_derivative_buying_power: None,
            buying_power_adjustment_effect: None,
            intraday_equities_cash_effect: None,
            intraday_futures_cash_effect: None,
            previous_day_cryptocurrency_fiat_effect: None,
            unsettled_cryptocurrency_fiat_effect: None,
            intraday_equities_cash_effective_date: None,
            intraday_futures_cash_effective_date: None,
            previous_date_cryptocurrency_fiat_effective_date: None,
            snapshot_date: None,
            currency: None,
            time_of_day: None,
        };

        let serialized = serde_json::to_string(&balance).unwrap();
        assert!(serialized.contains("TEST123"));
        assert!(serialized.contains("1000.50"));
        assert!(serialized.contains("5000.00"));
        assert!(serialized.contains("None"));
    }

    #[test]
    fn test_balance_snapshot_creation() {
        let snapshot = BalanceSnapshot {
            account_number: AccountNumber("SNAP123".to_string()),
            cash_balance: Decimal::from_str("2000.00").unwrap(),
            long_equity_value: Decimal::from_str("8000.00").unwrap(),
            short_equity_value: Decimal::from_str("0.00").unwrap(),
            long_derivative_value: Decimal::from_str("1000.00").unwrap(),
            short_derivative_value: Decimal::from_str("0.00").unwrap(),
            long_futures_value: Decimal::from_str("0.00").unwrap(),
            short_futures_value: Decimal::from_str("0.00").unwrap(),
            long_futures_derivative_value: Decimal::from_str("0.00").unwrap(),
            short_futures_derivative_value: Decimal::from_str("0.00").unwrap(),
            long_margineable_value: Decimal::from_str("8000.00").unwrap(),
            short_margineable_value: Decimal::from_str("0.00").unwrap(),
            margin_equity: Decimal::from_str("11000.00").unwrap(),
            equity_buying_power: Decimal::from_str("22000.00").unwrap(),
            derivative_buying_power: Decimal::from_str("11000.00").unwrap(),
            day_trading_buying_power: Decimal::from_str("44000.00").unwrap(),
            futures_margin_requirement: Decimal::from_str("0.00").unwrap(),
            available_trading_funds: Decimal::from_str("11000.00").unwrap(),
            maintenance_requirement: Decimal::from_str("0.00").unwrap(),
            maintenance_call_value: Decimal::from_str("0.00").unwrap(),
            reg_t_call_value: Decimal::from_str("0.00").unwrap(),
            day_trading_call_value: Decimal::from_str("0.00").unwrap(),
            day_equity_call_value: Decimal::from_str("0.00").unwrap(),
            net_liquidating_value: Decimal::from_str("11000.00").unwrap(),
            cash_available_to_withdraw: Decimal::from_str("2000.00").unwrap(),
            day_trade_excess: Decimal::from_str("44000.00").unwrap(),
            pending_cash: Decimal::from_str("0.00").unwrap(),
            pending_cash_effect: PriceEffect::Credit,
            snapshot_date: chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
            // The fields this struct gained from the venue's schema. `None`
            // rather than zero: a balance nobody reported is not a balance of
            // nothing.
            bond_margin_requirement: None,
            cash_settle_balance: None,
            closed_loop_available_balance: None,
            cryptocurrency_margin_requirement: None,
            currency: None,
            equity_offering_margin_requirement: None,
            fixed_income_security_margin_requirement: None,
            intraday_equities_cash_amount: None,
            intraday_equities_cash_effect: None,
            intraday_equities_cash_effective_date: None,
            intraday_futures_cash_amount: None,
            intraday_futures_cash_effect: None,
            intraday_futures_cash_effective_date: None,
            long_bond_value: None,
            long_cryptocurrency_value: None,
            long_fixed_income_security_value: None,
            margin_settle_balance: None,
            previous_date_cryptocurrency_fiat_effective_date: None,
            previous_day_cryptocurrency_fiat_amount: None,
            previous_day_cryptocurrency_fiat_effect: None,
            short_cryptocurrency_value: None,
            sma_equity_option_buying_power: None,
            special_memorandum_account_apex_adjustment: None,
            special_memorandum_account_value: None,
            time_of_day: None,
            total_settle_balance: None,
            unsettled_cryptocurrency_fiat_amount: None,
            unsettled_cryptocurrency_fiat_effect: None,
            used_derivative_buying_power: None,
        };

        assert_eq!(snapshot.account_number.0, "SNAP123");
        assert_eq!(snapshot.cash_balance, Decimal::from_str("2000.00").unwrap());
        assert_eq!(snapshot.snapshot_date.year(), 2024);
        matches!(snapshot.pending_cash_effect, PriceEffect::Credit);
    }

    #[test]
    fn test_balance_debug_format() {
        let balance = Balance {
            account_number: AccountNumber("DEBUG123".to_string()),
            cash_balance: Decimal::from_str("100.00").unwrap(),
            long_equity_value: Decimal::from_str("500.00").unwrap(),
            short_equity_value: Decimal::from_str("0.00").unwrap(),
            long_derivative_value: Decimal::from_str("0.00").unwrap(),
            short_derivative_value: Decimal::from_str("0.00").unwrap(),
            long_futures_value: Decimal::from_str("0.00").unwrap(),
            short_futures_value: Decimal::from_str("0.00").unwrap(),
            long_futures_derivative_value: Decimal::from_str("0.00").unwrap(),
            short_futures_derivative_value: Decimal::from_str("0.00").unwrap(),
            long_margineable_value: Decimal::from_str("500.00").unwrap(),
            short_margineable_value: Decimal::from_str("0.00").unwrap(),
            margin_equity: Decimal::from_str("600.00").unwrap(),
            equity_buying_power: Decimal::from_str("1200.00").unwrap(),
            derivative_buying_power: Decimal::from_str("600.00").unwrap(),
            day_trading_buying_power: Decimal::from_str("2400.00").unwrap(),
            futures_margin_requirement: Decimal::from_str("0.00").unwrap(),
            available_trading_funds: Decimal::from_str("600.00").unwrap(),
            maintenance_requirement: Decimal::from_str("0.00").unwrap(),
            maintenance_call_value: Decimal::from_str("0.00").unwrap(),
            reg_t_call_value: Decimal::from_str("0.00").unwrap(),
            day_trading_call_value: Decimal::from_str("0.00").unwrap(),
            day_equity_call_value: Decimal::from_str("0.00").unwrap(),
            net_liquidating_value: Decimal::from_str("600.00").unwrap(),
            cash_available_to_withdraw: Decimal::from_str("100.00").unwrap(),
            day_trade_excess: Decimal::from_str("2400.00").unwrap(),
            pending_cash: Decimal::from_str("0.00").unwrap(),
            pending_cash_effect: PriceEffect::Debit,
            pending_margin_interest: Decimal::from_str("0.00").unwrap(),
            effective_cryptocurrency_buying_power: Decimal::from_str("0.00").unwrap(),
            updated_at: DateTime::parse_from_rfc3339("2024-01-01T12:00:00Z").unwrap(),
            apex_starting_day_margin_equity: None,
            bond_margin_requirement: None,
            buying_power_adjustment: None,
            cash_settle_balance: None,
            closed_loop_available_balance: None,
            cryptocurrency_margin_requirement: None,
            equity_offering_margin_requirement: None,
            fixed_income_security_margin_requirement: None,
            futures_intraday_margin_requirement: None,
            futures_overnight_margin_requirement: None,
            intraday_equities_cash_amount: None,
            intraday_futures_cash_amount: None,
            long_bond_value: None,
            long_cryptocurrency_value: None,
            long_fixed_income_security_value: None,
            long_index_derivative_value: None,
            maintenance_excess: None,
            margin_settle_balance: None,
            previous_day_cryptocurrency_fiat_amount: None,
            reg_t_margin_requirement: None,
            short_cryptocurrency_value: None,
            short_index_derivative_value: None,
            sma_equity_option_buying_power: None,
            special_memorandum_account_apex_adjustment: None,
            special_memorandum_account_value: None,
            total_pending_liquidity_pool_rebate: None,
            total_settle_balance: None,
            unsettled_cryptocurrency_fiat_amount: None,
            used_derivative_buying_power: None,
            buying_power_adjustment_effect: None,
            intraday_equities_cash_effect: None,
            intraday_futures_cash_effect: None,
            previous_day_cryptocurrency_fiat_effect: None,
            unsettled_cryptocurrency_fiat_effect: None,
            intraday_equities_cash_effective_date: None,
            intraday_futures_cash_effective_date: None,
            previous_date_cryptocurrency_fiat_effective_date: None,
            snapshot_date: None,
            currency: None,
            time_of_day: None,
        };

        let debug_str = format!("{:?}", balance);
        assert!(debug_str.contains("DEBUG123"));
    }
}