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
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
//! Models for the `orders` resource group.
//!
//! Derived from the Nordnet `Order`, `OrderReply`, `ActivationCondition`,
//! `Validity`, `Amount` (modelled as [`OrderAmount`]) and `TradableId`
//! schemas plus the per-op parameter tables.
//!
//!
//! ## Doc notes
//!
//! - **Wire format for `place_order` / `modify_order`: `application/x-www-form-urlencoded`.**
//!   The parameter tables describe every body parameter as Swagger 2.0
//!   `FormData`. The resource layer sends these via
//!   `nordnet_api::Client::post_form` / `nordnet_api::Client::put_form`. The
//!   request structs intentionally do NOT carry the
//!   `rust_decimal::serde::arbitrary_precision_option` adapter on
//!   `Decimal` fields (it serializes via a `serde_json` magic struct that
//!   `serde_urlencoded` rejects with `unsupported value`). The default
//!   `Decimal` `Display`-based serialization produces a decimal string in
//!   both formats, which is what the live API expects on the wire.
//! - This module's [`OrderType`] enum is the closed set of values
//!   accepted by `place_order` on the request side. The structurally
//!   different `(name, type)` pair in the `tradables` group lives there
//!   as [`crate::models::tradables::AllowedOrderType`].
//! - `ActivationCondition` exists in two distinct shapes in the docs:
//!   the **request** form is a single enum string sent as the
//!   `activation_condition` form field on `place_order`; the **response**
//!   form is a struct nested inside `Order`. We model both:
//!   [`OrderActivationCondition`] (enum, request) and
//!   [`ActivationCondition`] (struct, response).
//! - `Order.modified` is documented as `integer(int64)` UNIX-millisecond
//!   epoch. Kept as `i64`. Same precedent as `tradables::PublicTrade`.
//! - Several numeric fields on `Order` are typed as `number(double)` in
//!   the docs (`open_volume`, `traded_volume`, `volume`). They are
//!   modelled as [`rust_decimal::Decimal`] with the
//!   `arbitrary_precision` adapter — never `f64`. Because of this `Order`
//!   cannot derive [`Eq`].
//! - The `Validity` definition models `valid_until` as
//!   `integer(int64)` (UNIX ms). Kept as `i64`.

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

// ---------------------------------------------------------------------------
// Money shape
// ---------------------------------------------------------------------------

/// Re-export of the shared `{currency, value}` amount type, kept under the
/// in-group spelling `OrderAmount`.
pub use crate::models::shared::AmountWithCurrency as OrderAmount;

// ---------------------------------------------------------------------------
// Response types
// ---------------------------------------------------------------------------

/// Composite key identifying a tradable (market + symbol). Wire form is
/// the nested object `{ "identifier": "...", "market_id": ... }` per the
/// documented `TradableId` schema.
///
/// Note: this is the *response-body* nested representation. The
/// `tradables::TradableKey` value (which renders as `market_id:identifier`
/// for path slots) is unrelated and lives in its own module.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct OrderTradable {
    /// The Nordnet tradable identifier.
    pub identifier: TradableId,
    /// The Nordnet market identifier.
    pub market_id: MarketId,
}

/// Activation-condition object nested inside [`Order`] (response shape).
///
/// `type` is the documented enum; renamed from the wire `type` (Rust
/// keyword) via `#[serde(rename)]`.
///
/// `trailing_value` and `trigger_value` are `number(double)` per the
/// schema; modelled as `Decimal`, so this struct cannot derive [`Eq`].
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ActivationCondition {
    /// The fix point that the trigger_value and target_value percent is
    /// calculated from. Only used when type is `STOP_ACTPRICE_PERC`.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "rust_decimal::serde::arbitrary_precision_option"
    )]
    pub trailing_value: Option<Decimal>,
    /// The comparison that should be used on `trigger_value`. Valid values
    /// are `<=` (less than or equal to) or `>=` (greater than or equal to).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trigger_condition: Option<String>,
    /// The trigger value. If type is `STOP_ACTPRICE_PERC` the value is
    /// given in percentage points. If type is `STOP_ACTPRICE` the value is
    /// a fixed price.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "rust_decimal::serde::arbitrary_precision_option"
    )]
    pub trigger_value: Option<Decimal>,
    /// The stop-loss activation condition. Wire field name is `type`.
    #[serde(rename = "type")]
    pub r#type: ActivationConditionType,
}

/// Activation-condition `type` value as it appears in [`Order`] (response).
///
/// Distinct from [`OrderActivationCondition`] which is the
/// **request-side** value sent on `place_order`. The two enums share the
/// non-`NONE` variants but the response form additionally documents
/// `NONE` for orders that have no activation condition.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ActivationConditionType {
    /// No activation condition. Sent directly to the market.
    None,
    /// The order is inactive in the Nordnet system and is activated by the
    /// customer.
    Manual,
    /// Trailing stop-loss. The order is activated when the price changes
    /// by the given percentage.
    StopActpricePerc,
    /// The order is activated when the market price of the instrument
    /// reaches a trigger price.
    StopActprice,
}

/// `Validity` object nested inside [`Order`].
///
/// Schema: `_definitions/Validity.md`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct Validity {
    /// Validity type. Wire field name is `type`.
    #[serde(rename = "type")]
    pub r#type: ValidityType,
    /// The cancel date, only used when type is `UNTIL_DATE`. UNIX
    /// timestamp in milliseconds.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub valid_until: Option<i64>,
}

/// Validity `type` value. Documented set: `DAY, UNTIL_DATE, EXTENDED_HOURS,
/// IMMEDIATE`.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ValidityType {
    /// Day order.
    Day,
    /// Cancel date set explicitly via `valid_until`.
    UntilDate,
    /// Order valid during US extended-hours trading.
    ExtendedHours,
    /// Immediate-or-cancel.
    Immediate,
}

/// Action state of the last action performed on an order. Documented set
/// per `_definitions/Order.md`.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ActionState {
    /// Delete request failed and the order is still active on the market.
    DelFail,
    /// Delete request in progress and unconfirmed by the market.
    DelPend,
    /// Delete confirmed by the market.
    DelConf,
    /// Deleted by the market.
    DelPush,
    /// Insert failed.
    InsFail,
    /// Pending insert.
    InsPend,
    /// Confirmed insert.
    InsConf,
    /// Inserted into the Nordnet system and stopped (inactive / not
    /// triggered stop-loss).
    InsStop,
    /// Modification failed; previous order values still valid.
    ModFail,
    /// Modification in progress and waiting confirmation from the market.
    ModPend,
    /// Modified by the market.
    ModPush,
    /// Insert waiting for market opening.
    InsWait,
    /// Modification of order on the market, waiting for market opening.
    ModWait,
    /// Delete of order on the market, waiting for market opening.
    DelWait,
    /// Modification confirmed by the market.
    ModConf,
}

/// Order-state value. Documented set per `_definitions/Order.md`.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderState {
    /// Order is deleted.
    Deleted,
    /// The order is offline/local and eligible for activation.
    Local,
    /// The order is active on the market.
    OnMarket,
    /// The order can't be modified by the customer.
    Locked,
}

/// `side` value used on both [`Order`] (response) and
/// [`PlaceOrderRequest`] (request body). Documented set: `BUY, SELL`.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderSide {
    /// Buy.
    Buy,
    /// Sell.
    Sell,
}

/// `price_condition` value documented on [`Order`].
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PriceCondition {
    /// The order is limited by the given price.
    Limit,
    /// The order is entered at the current market price. Not supported by
    /// most markets.
    AtMarket,
}

/// `volume_condition` value documented on [`Order`].
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum VolumeCondition {
    /// All types of fills are accepted.
    Normal,
    /// Partial fills are not accepted.
    AllOrNothing,
}

/// One order belonging to an account.
///
/// Schema: `_definitions/Order.md`. Cannot derive [`Eq`] because
/// `open_volume`, `traded_volume`, `volume`, `price.amount` and the nested
/// `activation_condition` numeric fields are `Decimal`.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Order {
    /// The account identifier. Optional per the 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 state of the last action performed on the order.
    pub action_state: ActionState,
    /// The activation condition for stop-loss orders. Optional per schema.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub activation_condition: Option<ActivationCondition>,
    /// Last modification time of the order. UNIX timestamp in
    /// milliseconds.
    pub modified: i64,
    /// The open volume of an iceberg order. Optional per schema.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "rust_decimal::serde::arbitrary_precision_option"
    )]
    pub open_volume: Option<Decimal>,
    /// The Nordnet order identifier.
    pub order_id: OrderId,
    /// The state of the order.
    pub order_state: OrderState,
    /// The type of the order. The doc lists this as `string` (not as a
    /// closed enum on the response side), so it is kept as `String` here
    /// to admit any value the server might return — defensive against a
    /// drift between the place-order request enum and what the server
    /// later reports back.
    pub order_type: String,
    /// The price of the order.
    pub price: OrderAmount,
    /// The price condition on the order.
    pub price_condition: PriceCondition,
    /// Customer reference for the order. Free-text. Optional per schema.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reference: Option<String>,
    /// `BUY` or `SELL`.
    pub side: OrderSide,
    /// The tradable identifier (composite market + symbol).
    pub tradable: OrderTradable,
    /// The total traded volume of the order.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub traded_volume: Decimal,
    /// The validity period for the order.
    pub validity: Validity,
    /// The original volume of the order.
    #[serde(with = "rust_decimal::serde::arbitrary_precision")]
    pub volume: Decimal,
    /// The volume condition on the order.
    pub volume_condition: VolumeCondition,
}

/// Reply payload returned by every write operation
/// (`place`, `modify`, `activate`, `cancel`).
///
/// Schema: `_definitions/OrderReply.md`. Only `order_id` and `result_code`
/// are required; `action_state`, `order_state`, and `message` are
/// optional.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct OrderReply {
    /// The action state. Can be missing if the order fails the
    /// prevalidation and never enters the order system.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub action_state: Option<ActionState>,
    /// Translated error message if `result_code` is not `OK`. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    /// The Nordnet order identifier.
    pub order_id: OrderId,
    /// The order state. Only returned for valid orders. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub order_state: Option<OrderState>,
    /// `OK` or an error code. Kept as `String` (the doc does not enumerate
    /// the full error-code set).
    pub result_code: String,
}

// ---------------------------------------------------------------------------
// Request types
// ---------------------------------------------------------------------------

/// Order-type code as accepted by `place_order`. Documented set:
/// `FAK, FOK, NORMAL, LIMIT, STOP_LIMIT, STOP_TRAILING, OCO`.
///
/// `NORMAL` is documented as the default and as deprecated — clients
/// should pick the explicit type matching their intent.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderType {
    /// Fill-and-kill.
    Fak,
    /// Fill-or-kill.
    Fok,
    /// Normal (deprecated default; system guesses based on parameters).
    Normal,
    /// Limit order.
    Limit,
    /// Stop-limit order.
    StopLimit,
    /// Stop-trailing order.
    StopTrailing,
    /// One-cancels-other.
    Oco,
}

/// Activation-condition value sent on `place_order` (request side).
///
/// Distinct from [`ActivationConditionType`] (response side), which
/// additionally has a `NONE` variant and is the `type` field nested in
/// the response [`ActivationCondition`] struct.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderActivationCondition {
    /// Trailing stop-loss. Activated when the price changes by the given
    /// percentage. Requires `target_value`, `trigger_value`,
    /// `trigger_condition`; `price` must be omitted.
    StopActpricePerc,
    /// Activated when the market price reaches a trigger price. Requires
    /// `trigger_value`, `trigger_condition`, `price`.
    StopActprice,
    /// Inactive in the Nordnet system until manually activated.
    Manual,
    /// One-cancels-other (one normal order + one stop-loss; either fill
    /// cancels the other).
    OcoStopActprice,
}

/// Request body for `place_order` (`POST /accounts/{accid}/orders`).
///
/// Wire format: `application/x-www-form-urlencoded` (Swagger 2.0
/// `FormData`). Sent via `nordnet_api::Client::post_form`. The struct is flat
/// (no nested objects, sequences, or maps) so `serde_urlencoded` accepts
/// it. **Field declaration order determines wire field order** — keep
/// alphabetical (matches the doc parameter table) to keep the wire body
/// deterministic.
///
/// Required fields: `market_id`, `side`, `volume`. Everything else is
/// optional. Optional fields use `skip_serializing_if = "Option::is_none"`
/// so `None` is omitted from the wire body.
///
/// `price`, `target_value`, `trigger_value` are documented as
/// `number(double)`. Modelled as `Decimal` (never `f64`), so this type
/// cannot derive [`Eq`]. The `Decimal` fields
/// intentionally do NOT carry the `arbitrary_precision_option` adapter:
/// it relies on a `serde_json`-private magic struct that
/// `serde_urlencoded` rejects with `unsupported value`. Default
/// `Decimal` serde uses `Display`, producing the decimal-string form
/// (`101.5`) — correct for both wire formats.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct PlaceOrderRequest {
    /// Activation condition for stop-loss orders. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub activation_condition: Option<OrderActivationCondition>,
    /// The currency that the instrument is traded in. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub currency: Option<Currency>,
    /// If `true`, order is applicable for US pre-market trading. Default
    /// `false`. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extended_hours: Option<bool>,
    /// Nordnet tradable identifier. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identifier: Option<TradableId>,
    /// Nordnet market identifier. Required.
    pub market_id: MarketId,
    /// The visible part of an iceberg order. Only allowed for `LIMIT` /
    /// `NORMAL` order types. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub open_volume: Option<i64>,
    /// The order type. Defaults server-side to `NORMAL` if omitted.
    /// Optional on the wire; clients are encouraged to pick an explicit
    /// type.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub order_type: Option<OrderType>,
    /// The price limit of the order. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub price: Option<Decimal>,
    /// Free-text reference for the order. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reference: Option<String>,
    /// `BUY` or `SELL`. Required.
    pub side: OrderSide,
    /// Only used when activation type is `STOP_ACTPRICE_PERC` or
    /// `OCO_STOP_ACTPRICE`. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_value: Option<Decimal>,
    /// The comparison used on `trigger_value`. Valid values are `<=`
    /// or `>=`. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trigger_condition: Option<String>,
    /// Trigger value. For `STOP_ACTPRICE_PERC` it is in percentage points
    /// (minimum 1); for `STOP_ACTPRICE` it is a fixed price. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trigger_value: Option<Decimal>,
    /// Cancel date formatted as `YYYY-MM-DD`. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub valid_until: Option<String>,
    /// The volume of the order. Required.
    pub volume: i64,
}

/// Request body for `modify_order`
/// (`PUT /accounts/{accid}/orders/{order_id}`).
///
/// All fields optional; the doc notes `currency` is required when `price`
/// is changed but enforces no compile-time invariant.
///
/// Wire format: `application/x-www-form-urlencoded` (Swagger 2.0
/// `FormData`). Sent via `nordnet_api::Client::put_form`. See the
/// [`PlaceOrderRequest`] note for why `Decimal` fields omit the
/// `arbitrary_precision_option` adapter.
///
/// `price` is `number(double)` per the docs, modelled as `Decimal`; this
/// type cannot derive [`Eq`].
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ModifyOrderRequest {
    /// The currency of the instrument. Required when the price is
    /// changed. Optional otherwise.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub currency: Option<Currency>,
    /// The new open volume. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub open_volume: Option<i64>,
    /// The new price. If left out the price is left unchanged. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub price: Option<Decimal>,
    /// The new volume. Optional.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub volume: Option<i64>,
}