nordnet-model 0.1.0

Pure data types and crypto for the Nordnet External API v2 (no I/O).
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
//! Models for the `accounts` resource group.
//!
//! Derived from the Nordnet `Account`, `AccountInfo`,
//! `AccountTransactionsToday`, `Amount`, `Ledger`, `LedgerInformation`,
//! `Position`, `Reserved`, `Trade`, `TradableId` (schema object) and
//! `Instrument` (referenced from `Position.instrument`) schemas.
//!
//!
//! ## Doc notes
//!
//! - The documented `Amount` schema is `{currency: string, value:
//!   number(double)}`. The local `Amount` alias is typedef'd to
//!   [`crate::models::shared::AmountWithCurrency`] and uses the
//!   [`crate::models::shared::Currency`] newtype for `currency`. Wire
//!   format unchanged (Currency is `serde(transparent)` over `String`).
//! - [`PositionInstrument`] is a local definition for the `Instrument`
//!   type used by `Position.instrument`. The full `instruments::Instrument`
//!   lives in another group's models module. Only the fields we have
//!   schema evidence for are present here. The two shapes have different
//!   field sets (the `Position.instrument` shape lacks `tradables`,
//!   `underlyings`, `key_information_documents`, `mifid2_category`, etc.).
//! - [`TradableRef`] is the documented `TradableId` *schema object*
//!   (`{identifier, market_id}`) used as the `tradable` field of
//!   [`Trade`]. The bare-string newtype `crate::ids::TradableId` is a
//!   different concept (a single `identifier` value); we keep both
//!   distinct here.
//! - `Position.qty` and `Trade.volume` are `number(float)` /
//!   `number(double)` per the schema. They are typed as
//!   [`rust_decimal::Decimal`] (with the `arbitrary_precision` adapter)
//!   — never `f64`. Because of this `Position`, `Trade`,
//!   [`crate::models::shared::AmountWithCurrency`], `AccountInfo`,
//!   `Ledger`, `LedgerInformation`, `Reserved` and
//!   `AccountTransactionsToday` cannot derive [`Eq`].
//! - `Account.atyid` is documented as `integer(int32)`. Kept as `i32`.
//! - `Trade.tradetime` is `integer(int64)` UNIX milliseconds. Kept as
//!   plain `i64`.
//! - `AccountInfo.registration_date` is `string(date)` (`YYYY-MM-DD`),
//!   serialized via [`crate::models::shared::date_iso8601::option`].
//! - `PositionInstrument.expiration_date` (same `string(date)` shape) is
//!   typed the same way.
//! - The `Account.type` and `AccountInfo.account_currency` fields are bare
//!   strings per the schema; we deliberately do NOT use
//!   [`crate::models::shared::Currency`] for `account_currency` because
//!   the schema documents it as a plain `string` (no separate `currency`
//!   wire shape), and the field is documented to mirror the bare ledger
//!   currency strings used elsewhere on `Ledger`.

use crate::ids::{AccountId, MarketId, OrderId, TradableId};
use crate::models::shared::{date_iso8601, opt_arb_prec};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

/// In-group alias for the documented `Amount` schema (`{currency, value}`),
/// which is shared with other groups via
/// [`crate::models::shared::AmountWithCurrency`].
pub use crate::models::shared::AmountWithCurrency as Amount;

/// One Nordnet account the authenticated user has access to.
///
/// Schema: `_definitions/Account.md`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct Account {
    /// The account identifier. Optional per schema (not applicable for
    /// partners).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub accid: Option<AccountId>,
    /// The Nordnet account number. Always refers to a specific account.
    pub accno: i64,
    /// The account alias. Set by the customer.
    pub alias: String,
    /// The account type identifier. `integer(int32)` per schema.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub atyid: Option<i32>,
    /// The reason why the account is blocked. Translated to the language
    /// specified in the request.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub blocked_reason: Option<String>,
    /// `true` if this is the default account.
    pub default: bool,
    /// `true` if the account is blocked. No queries can be made against a
    /// blocked account.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub is_blocked: Option<bool>,
    /// The account type. Translated.
    #[serde(rename = "type")]
    pub r#type: String,
}

/// Summary of trading-power reservations attached to an [`AccountInfo`].
///
/// Schema: `_definitions/Reserved.md`. All fields are required.
///
/// Cannot derive [`Eq`] because the nested [`Amount::value`] is a `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Reserved {
    /// Reserved trading power for corporate actions.
    pub corporate_actions: Amount,
    /// Reserved trading power for mutual fund orders.
    pub fund_orders: Amount,
    /// Reserved trading power for exchange traded monthly savings.
    pub monthly_savings_exchange_traded: Amount,
    /// Sum of other trading power reservations.
    pub other: Amount,
    /// Total reserved trading power.
    pub total: Amount,
}

/// Account information details for a single account.
///
/// Schema: `_definitions/AccountInfo.md`.
///
/// Cannot derive [`Eq`] because nested [`Amount`] values use `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct AccountInfo {
    /// The account identifier. Optional per schema.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub accid: Option<AccountId>,
    /// The Nordnet account number.
    pub accno: i64,
    /// The account credit.
    pub account_credit: Amount,
    /// The account currency. Bare `string` per schema (see module doc note).
    pub account_currency: String,
    /// The combined sum of all ledgers.
    pub account_sum: Amount,
    /// The bonus cash if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bonus_cash: Option<Amount>,
    /// The combined value of all pending buy orders.
    pub buy_orders_value: Amount,
    /// The collateral claim for options.
    pub collateral: Amount,
    /// The accrued interest for credit account if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub credit_account_interest: Option<Amount>,
    /// The sum for credit account if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub credit_account_sum: Option<Amount>,
    /// The sum of `own_capital` and `credit_account_sum`.
    pub equity: Amount,
    /// The locked amount for forwards.
    pub forward_sum: Amount,
    /// The total market value.
    pub full_marketvalue: Amount,
    /// The sum of intraday realized profits/losses for futures in account
    /// currency. Reset at night. Differs from
    /// `unrealized_future_profit_loss` which looks at existing positions.
    pub future_sum: Amount,
    /// The interest on the account.
    pub interest: Amount,
    /// The intraday credit if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub intraday_credit: Option<Amount>,
    /// The maximum loan limit, regardless of pawn value.
    pub loan_limit: Amount,
    /// The sum of `account_sum`, `full_marketvalue`, `interest`,
    /// `forward_sum`, `future_sum` and `unrealized_future_profit_loss`.
    pub own_capital: Amount,
    /// Own capital calculated in the morning. Does not change during the
    /// day.
    pub own_capital_morning: Amount,
    /// The pawn value of all positions combined.
    pub pawn_value: Amount,
    /// The registration date of the account formatted as `YYYY-MM-DD`;
    /// typed as [`time::Date`] via the `date_iso8601::option` adapter.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "date_iso8601::option"
    )]
    pub registration_date: Option<time::Date>,
    /// Summary of reserved trading power.
    pub reserved: Reserved,
    /// The short position margin if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub short_positions_margin: Option<Amount>,
    /// The amount available for trading.
    pub trading_power: Amount,
    /// The sum of profit and loss for all currently existing futures
    /// positions. Not the same as `future_sum`.
    pub unrealized_future_profit_loss: Amount,
}

/// One currency ledger of an account.
///
/// Schema: `_definitions/Ledger.md`. All fields are required.
///
/// Cannot derive [`Eq`] because nested [`Amount`] values use `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Ledger {
    /// The interest credit in the ledger currency.
    pub acc_int_cred: Amount,
    /// The interest debit in the ledger currency.
    pub acc_int_deb: Amount,
    /// The sum in the ledger currency.
    pub account_sum: Amount,
    /// The sum in the account currency.
    pub account_sum_acc: Amount,
    /// The currency of the ledger. Bare `string` per schema.
    pub currency: String,
    /// The price to convert to base currency.
    pub exchange_rate: Amount,
}

/// All ledgers for an account, plus account-currency totals.
///
/// Schema: `_definitions/LedgerInformation.md`. All fields are required.
///
/// Cannot derive [`Eq`] because nested [`Amount`] values use `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct LedgerInformation {
    /// The list of all ledgers.
    pub ledgers: Vec<Ledger>,
    /// The total of all the ledgers in the account currency.
    pub total: Amount,
    /// The total interest credit in the account currency.
    pub total_acc_int_cred: Amount,
    /// The total interest debit in the account currency.
    pub total_acc_int_deb: Amount,
}

/// Today's withdrawal/deposit transaction amounts for an account.
///
/// Schema: `_definitions/AccountTransactionsToday.md`. All fields required.
///
/// Cannot derive [`Eq`] because the nested [`Amount::value`] is a `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct AccountTransactionsToday {
    /// Transaction amounts today.
    pub transactions: Amount,
}

/// Minimal `Instrument` shape used by [`Position::instrument`].
///
/// Schema: `_definitions/Instrument.md`. Only fields with schema evidence
/// (required + commonly-populated optional) are exposed here. The full
/// `Instrument` type lives in `models/instruments.rs` for that group's
/// own ops; we keep a local copy here per the module-ownership rule.
///
/// Cannot derive [`Eq`] because several `number(double)` fields are
/// typed as `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct PositionInstrument {
    /// Asset class key word.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub asset_class: Option<String>,
    /// URL to brochure if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub brochure_url: Option<String>,
    /// The currency of the instrument. Bare `string` per schema.
    pub currency: String,
    /// The dividend policy.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dividend_policy: Option<String>,
    /// Expiration date if applicable. `YYYY-MM-DD` per schema; typed as
    /// [`time::Date`] via the `date_iso8601::option` adapter.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "date_iso8601::option"
    )]
    pub expiration_date: Option<time::Date>,
    /// The instrument group (wider description than instrument type).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub instrument_group_type: Option<String>,
    /// Unique identifier of the instrument. May be 0 if the instrument
    /// is not tradable. Plain `i64` rather than `crate::ids::InstrumentId`
    /// because the documented `Position` schema does not specify the
    /// strong-typed shape here, and 0 is a sentinel value.
    pub instrument_id: i64,
    /// The instrument type.
    pub instrument_type: String,
    /// The instrument ISIN code.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub isin_code: Option<String>,
    /// Marking market view for leverage instruments. `U` for up, `D` for
    /// down.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub market_view: Option<String>,
    /// The MiFID II category of the instrument.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mifid2_category: Option<i32>,
    /// The instrument multiplier. `Decimal` (never `f64`).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "opt_arb_prec"
    )]
    pub multiplier: Option<Decimal>,
    /// The instrument name.
    pub name: String,
    /// Number of securities, not available for all instruments. `Decimal`
    /// (never `f64`).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "opt_arb_prec"
    )]
    pub number_of_securities: Option<Decimal>,
    /// The pawn percentage if applicable. `Decimal` (never `f64`).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "opt_arb_prec"
    )]
    pub pawn_percentage: Option<Decimal>,
    /// Price type when trading. Examples: `monetary_amount`, `percentage`,
    /// `yield`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub price_type: Option<String>,
    /// The sector ID of the instrument.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sector: Option<String>,
    /// The sector group of the instrument.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sector_group: Option<String>,
    /// Strike price if applicable. `Decimal` (never `f64`).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "opt_arb_prec"
    )]
    pub strike_price: Option<Decimal>,
    /// The instrument symbol, e.g. `ERIC B`.
    pub symbol: String,
}

/// One position in an account.
///
/// Schema: `_definitions/Position.md`.
///
/// Cannot derive [`Eq`] because `qty` is a `Decimal` and the nested
/// [`Amount`] values are `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Position {
    /// The account identifier. Optional per schema.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub accid: Option<AccountId>,
    /// The Nordnet account number.
    pub accno: i64,
    /// The acquisition price in the tradable currency.
    pub acq_price: Amount,
    /// The acquisition price in the account currency.
    pub acq_price_acc: Amount,
    /// The position instrument.
    pub instrument: PositionInstrument,
    /// The collateral percentage required to cover this position if short
    /// (`qty` < 0). `integer(int32)` per schema.
    pub margin_percent: i32,
    /// The market value in the tradable currency.
    pub market_value: Amount,
    /// The market value in the account currency.
    pub market_value_acc: Amount,
    /// The price of the position instrument in the morning.
    pub morning_price: Amount,
    /// The percentage the user is allowed loan on this position.
    /// `integer(int32)` per schema.
    pub pawn_percent: i32,
    /// The quantity of the position. `number(float)` per schema; typed
    /// as [`Decimal`] (never `f32`/`f64`).
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub qty: Decimal,
}

/// Tradable reference (`{identifier, market_id}`) per the documented
/// `TradableId` *schema object*.
///
/// Distinct from [`crate::ids::TradableId`], which is the bare-string
/// identifier newtype. We compose the bare-string newtype here so the
/// documented field shape round-trips correctly while retaining the
/// strongly-typed identifier.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub struct TradableRef {
    /// The Nordnet tradable identifier.
    pub identifier: TradableId,
    /// The Nordnet market identifier.
    pub market_id: MarketId,
}

/// One executed trade against an account.
///
/// Schema: `_definitions/Trade.md`.
///
/// Cannot derive [`Eq`] because `volume` is a `Decimal` and nested
/// [`Amount`] values are `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Trade {
    /// The account identifier. Optional per schema.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub accid: Option<AccountId>,
    /// The Nordnet account number.
    pub accno: i64,
    /// The counterparty if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub counterparty: Option<String>,
    /// Nordnet order identifier.
    pub order_id: OrderId,
    /// The price of the trade.
    pub price: Amount,
    /// `BUY` or `SELL`. Bare `string` per schema (Nordnet does not enumerate
    /// the type in the docs as a typed enum); kept as `String` so unknown
    /// future variants do not break parsing.
    pub side: String,
    /// The tradable identifier (`{identifier, market_id}` per the schema).
    pub tradable: TradableRef,
    /// Trade identifier from the market if available.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trade_id: Option<String>,
    /// The time of the trade. UNIX timestamp in milliseconds (kept as
    /// plain `i64` — see module doc note).
    pub tradetime: i64,
    /// The volume of the trade. `number(double)` per schema; typed as
    /// [`Decimal`] (never `f64`).
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub volume: Decimal,
}