Skip to main content

longbridge/portfolio/
types.rs

1use rust_decimal::Decimal;
2use serde::{Deserialize, Serialize};
3use strum_macros::{Display, EnumString};
4
5// ── exchange_rate ─────────────────────────────────────────────────
6
7/// Response for [`crate::PortfolioContext::exchange_rate`]
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ExchangeRates {
10    /// List of exchange rates
11    pub exchanges: Vec<ExchangeRate>,
12}
13
14/// One currency exchange rate
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ExchangeRate {
17    /// Average rate (base_currency / other_currency)
18    pub average_rate: f64,
19    /// Base currency, e.g. `"USD"`
20    pub base_currency: String,
21    /// Bid rate
22    pub bid_rate: f64,
23    /// Offer rate
24    pub offer_rate: f64,
25    /// Other currency, e.g. `"HKD"`
26    pub other_currency: String,
27}
28
29// ── profit_analysis ───────────────────────────────────────────────
30
31/// Summary response for [`crate::PortfolioContext::profit_analysis`]
32///
33/// This is a combined response from two API endpoints:
34/// `/v1/portfolio/profit-analysis-summary` and
35/// `/v1/portfolio/profit-analysis-sublist`.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ProfitAnalysis {
38    /// Summary overview
39    pub summary: ProfitAnalysisSummary,
40    /// Per-security breakdown
41    pub sublist: ProfitAnalysisSublist,
42}
43
44/// Account-level P&L summary
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ProfitAnalysisSummary {
47    /// Account currency
48    pub currency: String,
49    /// Current total asset value
50    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
51    pub current_total_asset: Option<Decimal>,
52    /// Query start date string
53    pub start_date: String,
54    /// Query end date string
55    pub end_date: String,
56    /// Start time (unix timestamp string)
57    pub start_time: String,
58    /// End time (unix timestamp string)
59    pub end_time: String,
60    /// Ending asset value
61    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
62    pub ending_asset_value: Option<Decimal>,
63    /// Initial asset value
64    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
65    pub initial_asset_value: Option<Decimal>,
66    /// Total invested amount
67    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
68    pub invest_amount: Option<Decimal>,
69    /// Whether any trades occurred
70    pub is_traded: bool,
71    /// Total profit/loss
72    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
73    pub sum_profit: Option<Decimal>,
74    /// Total profit/loss rate
75    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
76    pub sum_profit_rate: Option<Decimal>,
77    /// Per-asset-type breakdown
78    pub profits: ProfitSummaryBreakdown,
79}
80
81/// P&L breakdown by asset type
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ProfitSummaryBreakdown {
84    /// Stock P&L
85    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
86    pub stock: Option<Decimal>,
87    /// Fund P&L
88    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
89    pub fund: Option<Decimal>,
90    /// Crypto P&L
91    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
92    pub crypto: Option<Decimal>,
93    /// Money market fund P&L
94    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
95    pub mmf: Option<Decimal>,
96    /// Other P&L
97    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
98    pub other: Option<Decimal>,
99    /// Cumulative transaction amount
100    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
101    pub cumulative_transaction_amount: Option<Decimal>,
102    /// Total number of orders
103    pub trade_order_num: String,
104    /// Total number of traded securities
105    pub trade_stock_num: String,
106    /// IPO P&L
107    #[serde(default, with = "crate::serde_utils::decimal_opt_str_is_none")]
108    pub ipo: Option<Decimal>,
109    /// IPO hits
110    pub ipo_hit: i32,
111    /// IPO subscriptions
112    pub ipo_subscription: i32,
113    /// Per-category summary info
114    pub summary_info: Vec<ProfitSummaryInfo>,
115}
116
117/// P&L summary for one asset category
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ProfitSummaryInfo {
120    /// Asset type
121    pub asset_type: AssetType,
122    /// Security with the maximum profit
123    pub profit_max: String,
124    /// Name of the max-profit security
125    pub profit_max_name: String,
126    /// Security with the maximum loss
127    pub loss_max: String,
128    /// Name of the max-loss security
129    pub loss_max_name: String,
130}
131
132/// Per-security P&L breakdown
133#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134pub struct ProfitAnalysisSublist {
135    /// Start time (unix timestamp string)
136    pub start: String,
137    /// End time (unix timestamp string)
138    pub end: String,
139    /// Start date string
140    pub start_date: String,
141    /// End date string
142    pub end_date: String,
143    /// Last updated time (unix timestamp string)
144    pub updated_at: String,
145    /// Last updated date string
146    pub updated_date: String,
147    /// Per-security items
148    pub items: Vec<ProfitAnalysisItem>,
149}
150
151/// P&L for one security
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ProfitAnalysisItem {
154    /// Security name
155    pub name: String,
156    /// Market
157    pub market: String,
158    /// Whether still holding
159    pub is_holding: bool,
160    /// Profit/loss amount
161    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
162    pub profit: Option<Decimal>,
163    /// Profit/loss rate
164    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
165    pub profit_rate: Option<Decimal>,
166    /// Number of completed trades
167    pub clearance_times: i64,
168    /// Asset type
169    #[serde(rename = "type")]
170    pub item_type: AssetType,
171    /// Currency
172    pub currency: String,
173    /// Security symbol
174    pub symbol: String,
175    /// Holding period display string
176    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
177    pub holding_period: String,
178    /// Ticker code
179    pub security_code: String,
180    /// ISIN (for funds)
181    pub isin: String,
182    /// Underlying stock P&L
183    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
184    pub underlying_profit: Option<Decimal>,
185    /// Derivatives P&L
186    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
187    pub derivatives_profit: Option<Decimal>,
188    /// P&L in order currency
189    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
190    pub order_profit: Option<Decimal>,
191}
192
193// ── profit_analysis_detail ────────────────────────────────────────
194
195/// Response for [`crate::PortfolioContext::profit_analysis_detail`]
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ProfitAnalysisDetail {
198    /// Total profit/loss
199    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
200    pub profit: Option<Decimal>,
201    /// Underlying stock P&L details
202    pub underlying_details: ProfitDetails,
203    /// Derivative P&L details
204    pub derivative_pnl_details: ProfitDetails,
205    /// Security name
206    pub name: String,
207    /// Last updated time (unix timestamp string)
208    pub updated_at: String,
209    /// Last updated date string
210    pub updated_date: String,
211    /// Currency
212    pub currency: String,
213    /// Default detail tab: 0 = underlying, 1 = derivative
214    pub default_tag: i32,
215    /// Query start time (unix timestamp string)
216    pub start: String,
217    /// Query end time (unix timestamp string)
218    pub end: String,
219    /// Query start date string
220    pub start_date: String,
221    /// Query end date string
222    pub end_date: String,
223}
224
225/// Detailed P&L breakdown for one asset class
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ProfitDetails {
228    /// Current holding market value
229    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
230    pub holding_value: Option<Decimal>,
231    /// Total profit/loss
232    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
233    pub profit: Option<Decimal>,
234    /// Cumulative credited amount
235    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
236    pub cumulative_credited_amount: Option<Decimal>,
237    /// Credit detail entries
238    pub credited_details: Vec<ProfitDetailEntry>,
239    /// Cumulative debited amount
240    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
241    pub cumulative_debited_amount: Option<Decimal>,
242    /// Debit detail entries
243    pub debited_details: Vec<ProfitDetailEntry>,
244    /// Cumulative fee amount
245    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
246    pub cumulative_fee_amount: Option<Decimal>,
247    /// Fee detail entries
248    pub fee_details: Vec<ProfitDetailEntry>,
249    /// Short position holding value
250    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
251    pub short_holding_value: Option<Decimal>,
252    /// Long position holding value
253    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
254    pub long_holding_value: Option<Decimal>,
255    /// Opening position market value at period start
256    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
257    pub holding_value_at_beginning: Option<Decimal>,
258    /// Closing position market value at period end
259    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
260    pub holding_value_at_ending: Option<Decimal>,
261}
262
263/// One P&L detail line item (credit, debit, or fee)
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ProfitDetailEntry {
266    /// Description
267    pub describe: String,
268    /// Amount
269    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
270    pub amount: Option<Decimal>,
271}
272
273// ── profit_analysis_by_market ─────────────────────────────────────
274
275/// Response for [`crate::PortfolioContext::profit_analysis_by_market`]
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct ProfitAnalysisByMarket {
278    /// Total P&L across all returned items
279    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
280    pub profit: Option<Decimal>,
281    /// Whether more pages are available
282    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
283    pub has_more: bool,
284    /// Per-security P&L items for the requested market/page
285    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
286    pub stock_items: Vec<ProfitAnalysisByMarketItem>,
287}
288
289/// One security entry in a by-market P&L response
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct ProfitAnalysisByMarketItem {
292    /// Security symbol (ticker code)
293    pub code: String,
294    /// Security name
295    pub name: String,
296    /// Market, e.g. `"HK"`, `"US"`
297    pub market: String,
298    /// Profit/loss amount
299    #[serde(with = "crate::serde_utils::decimal_opt_str_is_none")]
300    pub profit: Option<Decimal>,
301}
302
303// ── profit_analysis_flows ─────────────────────────────────────────
304
305/// Response for [`crate::PortfolioContext::profit_analysis_flows`]
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ProfitAnalysisFlows {
308    /// Paginated list of flow items
309    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
310    pub flows_list: Vec<FlowItem>,
311    /// Whether there are more pages
312    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
313    pub has_more: bool,
314}
315
316/// One profit-analysis flow record
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct FlowItem {
319    /// Execution date string, e.g. `"2024-01-15"`
320    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
321    pub executed_date: String,
322    /// Execution timestamp as a Unix-seconds string (absent when not yet
323    /// executed)
324    #[serde(default, with = "crate::serde_utils::value_as_opt_string")]
325    pub executed_timestamp: Option<String>,
326    /// Security code / ticker
327    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
328    pub code: String,
329    /// Direction of the flow
330    pub direction: FlowDirection,
331    /// Executed quantity
332    #[serde(default, with = "crate::serde_utils::decimal_opt_str_is_none")]
333    pub executed_quantity: Option<Decimal>,
334    /// Executed price
335    #[serde(default, with = "crate::serde_utils::decimal_opt_str_is_none")]
336    pub executed_price: Option<Decimal>,
337    /// Executed cost
338    #[serde(default, with = "crate::serde_utils::decimal_opt_str_is_none")]
339    pub executed_cost: Option<Decimal>,
340    /// Human-readable description
341    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
342    pub describe: String,
343}
344
345/// Flow direction
346#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
347pub enum FlowDirection {
348    /// Unknown
349    Unknown,
350    /// Buy
351    #[strum(serialize = "buy")]
352    Buy,
353    /// Sell
354    #[strum(serialize = "sell")]
355    Sell,
356}
357
358impl_default_for_enum_string!(FlowDirection);
359impl_serde_for_enum_string!(FlowDirection);
360
361/// Asset type
362#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
363pub enum AssetType {
364    /// Unknown
365    Unknown,
366    /// Stock
367    #[strum(serialize = "stock")]
368    Stock,
369    /// Fund
370    #[strum(serialize = "fund")]
371    Fund,
372    /// Crypto
373    #[strum(serialize = "crypto")]
374    Crypto,
375}
376
377impl_default_for_enum_string!(AssetType);
378impl_serde_for_enum_string!(AssetType);