diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b4a3455..7cbe65ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added +- **Rust, Go:** ~56 new API methods across `QuoteContext`, `AssetContext`, `TradeContext`, and four new context types. + +### QuoteContext — Fundamental Data (counter_id endpoints) + - `financial_report(symbol, opts)` — GET /v1/quote/financial-reports + - `institution_ratings(symbol)` — GET /v1/quote/institution-ratings + - `institution_rating_latest(symbol)` — GET /v1/quote/institution-rating-latest + - `institution_rating_detail(symbol, opts)` — GET /v1/quote/institution-ratings/detail + - `dividends(symbol, opts)` — GET /v1/quote/dividends + - `dividend_detail(symbol, dividend_id)` — GET /v1/quote/dividends/details + - `forecast_eps(symbol)` — GET /v1/quote/forecast-eps + - `financial_consensus(symbol)` — GET /v1/quote/financial-consensus-detail + - `valuation(symbol, opts)` — GET /v1/quote/valuation + - `valuation_history(symbol, opts)` — GET /v1/quote/valuation/detail + - `industry_valuation(symbol)` — GET /v1/quote/industry-valuation-comparison + - `industry_valuation_distribution(symbol)` — GET /v1/quote/industry-valuation-distribution + - `company_overview(symbol)` — GET /v1/quote/comp-overview + - `company_executives(symbol)` — GET /v1/quote/company-professionals + - `shareholders(symbol)` — GET /v1/quote/shareholders + - `fund_holders(symbol)` — GET /v1/quote/fund-holders + - `corporate_actions(symbol, opts)` — GET /v1/quote/company-act + - `investor_relations(symbol)` — GET /v1/quote/invest-relations + - `operating_data(symbol, opts)` — GET /v1/quote/operatings + +### QuoteContext — Market Data (symbol passthrough) + - `market_status(market)` — GET /v1/quote/market-status + - `broker_holding(symbol, opts)` — GET /v1/quote/broker-holding + - `broker_holding_detail(symbol)` — GET /v1/quote/broker-holding/detail + - `broker_holding_daily(symbol, broker_id)` — GET /v1/quote/broker-holding/daily + - `ah_premium_klines(symbol, opts)` — GET /v1/quote/ahpremium/klines + - `ah_premium_timeshares(symbol)` — GET /v1/quote/ahpremium/timeshares + - `trade_statistics(symbol)` — GET /v1/quote/trades-statistics + - `market_anomaly(market)` — GET /v1/quote/changes + - `index_constituents(symbol)` — GET /v1/quote/index-constituents + - `finance_calendar(market, opts)` — GET /v1/quote/finance_calendar + +### AssetContext + - `exchange_rates()` — GET /v1/asset/exchange_rates + +### TradeContext + - `profit_analysis_summary(opts)` — GET /v1/portfolio/profit-analysis-summary + - `profit_analysis_sublist(opts)` — GET /v1/portfolio/profit-analysis-sublist + - `profit_analysis_detail(opts)` — GET /v1/portfolio/profit-analysis/detail + +### New module: `alert` (`AlertContext`) + - `list_alerts()` — GET /v1/notify/reminders + - `add_alert(opts)` — POST /v1/notify/reminders + - `delete_alerts(ids)` — DELETE /v1/notify/reminders + - `enable_alert(id)` — PUT /v1/notify/reminders + - `disable_alert(id)` — PUT /v1/notify/reminders + +### New module: `dca` (`DcaContext`) + - `list_dca_plans(status?)` — GET /v1/dailycoins/query + - `create_dca_plan(opts)` — POST /v1/dailycoins/create + - `update_dca_plan(opts)` — POST /v1/dailycoins/update + - `pause_dca_plan(plan_id)` — POST /v1/dailycoins/toggle + - `resume_dca_plan(plan_id)` — POST /v1/dailycoins/toggle + - `stop_dca_plan(plan_id)` — POST /v1/dailycoins/toggle + - `dca_history(opts)` — GET /v1/dailycoins/query-records + - `dca_statistics(symbol?)` — GET /v1/dailycoins/statistic + - `check_dca_support(opts)` — POST /v1/dailycoins/batch-check-support + +### New module: `sharelist` (`SharelistContext`) + - `list_sharelists(count?)` — GET /v1/sharelists + - `sharelist_detail(id)` — GET /v1/sharelists/{id} + - `create_sharelist(opts)` — POST /v1/sharelists + - `delete_sharelist(id)` — DELETE /v1/sharelists/{id} + - `add_sharelist_items(id, symbols)` — POST /v1/sharelists/{id}/items + - `remove_sharelist_items(id, symbols)` — DELETE /v1/sharelists/{id}/items + - `sort_sharelist_items(id, symbols)` — POST /v1/sharelists/{id}/items/sort + - `popular_sharelists(count?)` — GET /v1/sharelists/popular + +### New module: `quant` (`QuantContext`) + - `run_quant_script(opts)` — POST /v1/quant/run_script + +- **Rust:** `quote::utils::symbol_to_counter_id` helper converts symbols to counter-id format (`ST/MARKET/CODE`). +- **Rust:** New option types in `quote::extra_types` and `trade::extra_types` for the above methods. All new endpoints return `serde_json::Value`. +- **Go:** New option types co-located in each context file. All new endpoints return `json.RawMessage`. + - **Rust:** `Config::header(key, value)` builder method to inject custom headers into every HTTP request and WebSocket upgrade request. - **Rust, Python:** `ContentContext` adds three new methods: - `topic_detail(topic_id)` — get detail of a single topic. diff --git a/rust/src/alert/context.rs b/rust/src/alert/context.rs new file mode 100644 index 000000000..5fb854e82 --- /dev/null +++ b/rust/src/alert/context.rs @@ -0,0 +1,152 @@ +//! Alert context – CRUD for price reminders. + +use std::sync::Arc; + +use longbridge_httpcli::{HttpClient, Json, Method}; +use serde::Serialize; +use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; + +use crate::{Config, Result, alert::types::AddAlertOptions}; + +struct InnerAlertContext { + http_cli: HttpClient, + log_subscriber: Arc, +} + +impl Drop for InnerAlertContext { + fn drop(&mut self) { + dispatcher::with_default(&self.log_subscriber.clone().into(), || { + tracing::info!("alert context dropped"); + }); + } +} + +/// Alert context for managing price reminders. +#[derive(Clone)] +pub struct AlertContext(Arc); + +impl AlertContext { + /// Create an `AlertContext`. + pub fn new(config: Arc) -> Self { + let log_subscriber = config.create_log_subscriber("alert"); + dispatcher::with_default(&log_subscriber.clone().into(), || { + tracing::info!("creating alert context"); + }); + let ctx = Self(Arc::new(InnerAlertContext { + http_cli: config.create_http_client(), + log_subscriber, + })); + dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || { + tracing::info!("alert context created"); + }); + ctx + } + + /// Returns the log subscriber. + #[inline] + pub fn log_subscriber(&self) -> Arc { + self.0.log_subscriber.clone() + } + + /// List all price alerts. + /// + /// Path: GET /v1/notify/reminders + pub async fn list_alerts(&self) -> Result { + #[derive(Serialize)] + struct Empty {} + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/notify/reminders") + .query_params(Empty {}) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Add a new price alert. + /// + /// Path: POST /v1/notify/reminders + pub async fn add_alert(&self, opts: AddAlertOptions) -> Result { + Ok(self + .0 + .http_cli + .request(Method::POST, "/v1/notify/reminders") + .body(Json(opts)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Delete price alerts by IDs. + /// + /// Path: DELETE /v1/notify/reminders + pub async fn delete_alerts(&self, ids: Vec) -> Result<()> { + #[derive(Serialize)] + struct Request { + ids: String, + } + self.0 + .http_cli + .request(Method::DELETE, "/v1/notify/reminders") + .query_params(Request { + ids: ids.join(","), + }) + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await?; + Ok(()) + } + + /// Enable a price alert. + /// + /// Path: PUT /v1/notify/reminders + pub async fn enable_alert(&self, id: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + id: String, + enabled: bool, + } + Ok(self + .0 + .http_cli + .request(Method::PUT, "/v1/notify/reminders") + .body(Json(Request { + id: id.into(), + enabled: true, + })) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Disable a price alert. + /// + /// Path: PUT /v1/notify/reminders + pub async fn disable_alert(&self, id: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + id: String, + enabled: bool, + } + Ok(self + .0 + .http_cli + .request(Method::PUT, "/v1/notify/reminders") + .body(Json(Request { + id: id.into(), + enabled: false, + })) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } +} diff --git a/rust/src/alert/mod.rs b/rust/src/alert/mod.rs new file mode 100644 index 000000000..5a1cba791 --- /dev/null +++ b/rust/src/alert/mod.rs @@ -0,0 +1,7 @@ +//! Price alert (reminder) module. + +mod context; +mod types; + +pub use context::AlertContext; +pub use types::{AddAlertOptions, AlertDirection}; diff --git a/rust/src/alert/types.rs b/rust/src/alert/types.rs new file mode 100644 index 000000000..bc57e7f3a --- /dev/null +++ b/rust/src/alert/types.rs @@ -0,0 +1,23 @@ +//! Alert (price reminder) types. + +#![allow(missing_docs)] + +use serde::{Deserialize, Serialize}; + +/// Direction for a price alert. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AlertDirection { + Up, + Down, +} + +/// Options for creating a new price alert. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddAlertOptions { + pub symbol: String, + pub price: String, + pub direction: AlertDirection, + #[serde(skip_serializing_if = "Option::is_none")] + pub remark: Option, +} diff --git a/rust/src/asset/context.rs b/rust/src/asset/context.rs index 422bde9a1..3628ebf27 100644 --- a/rust/src/asset/context.rs +++ b/rust/src/asset/context.rs @@ -7,7 +7,7 @@ use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; use crate::{ Config, Result, asset::{ - GetStatementListOptions, GetStatementListResponse, GetStatementOptions, + ExchangeRate, GetStatementListOptions, GetStatementListResponse, GetStatementOptions, GetStatementResponse, core, }, }; @@ -93,4 +93,29 @@ impl AssetContext { self.get(core::GET_STATEMENT_DATA_DOWNLOAD_URL_PATH, options) .await } + + /// Get all exchange rates. + /// + /// Path: GET /v1/asset/exchange_rates + pub async fn exchange_rates(&self) -> Result> { + #[derive(Serialize)] + struct Empty {} + + #[derive(serde::Deserialize)] + struct Response { + list: Vec, + } + + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/asset/exchange_rates") + .query_params(Empty {}) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0 + .list) + } } diff --git a/rust/src/asset/ext.rs b/rust/src/asset/ext.rs new file mode 100644 index 000000000..7c35c6ea2 --- /dev/null +++ b/rust/src/asset/ext.rs @@ -0,0 +1,35 @@ +//! Extension methods for [`AssetContext`]. + +use longbridge_httpcli::{Json, Method}; +use serde::{Deserialize, Serialize}; +use tracing::instrument::WithSubscriber; + +use crate::{Result, asset::AssetContext}; + +/// A single exchange-rate entry returned by `/v1/asset/exchange_rates`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExchangeRate { + /// Source currency code. + pub from_currency: String, + /// Target currency code. + pub to_currency: String, + /// Exchange rate value. + pub rate: String, +} + +impl AssetContext { + /// Get all exchange rates. + /// + /// Path: GET /v1/asset/exchange_rates + pub async fn exchange_rates(&self) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/asset/exchange_rates") + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } +} diff --git a/rust/src/asset/types.rs b/rust/src/asset/types.rs index a3f9f374a..b74ab5895 100644 --- a/rust/src/asset/types.rs +++ b/rust/src/asset/types.rs @@ -20,3 +20,14 @@ pub struct StatementItem { pub struct GetStatementResponse { pub url: String, } + +/// An exchange rate entry. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExchangeRate { + /// Source currency code (e.g. "USD"). + pub from_currency: String, + /// Target currency code (e.g. "HKD"). + pub to_currency: String, + /// Exchange rate value as a string decimal. + pub rate: String, +} diff --git a/rust/src/dca/context.rs b/rust/src/dca/context.rs new file mode 100644 index 000000000..b8926626d --- /dev/null +++ b/rust/src/dca/context.rs @@ -0,0 +1,201 @@ +//! DCA context – Dollar-Cost Averaging plan management. + +use std::sync::Arc; + +use longbridge_httpcli::{HttpClient, Json, Method}; +use serde::Serialize; +use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; + +use crate::{ + Config, Result, + dca::types::{ + CheckDcaSupportOptions, CreateDcaPlanOptions, DcaHistoryOptions, UpdateDcaPlanOptions, + }, +}; + +struct InnerDcaContext { + http_cli: HttpClient, + log_subscriber: Arc, +} + +impl Drop for InnerDcaContext { + fn drop(&mut self) { + dispatcher::with_default(&self.log_subscriber.clone().into(), || { + tracing::info!("dca context dropped"); + }); + } +} + +/// DCA context for managing Dollar-Cost Averaging plans. +#[derive(Clone)] +pub struct DcaContext(Arc); + +impl DcaContext { + /// Create a `DcaContext`. + pub fn new(config: Arc) -> Self { + let log_subscriber = config.create_log_subscriber("dca"); + dispatcher::with_default(&log_subscriber.clone().into(), || { + tracing::info!("creating dca context"); + }); + let ctx = Self(Arc::new(InnerDcaContext { + http_cli: config.create_http_client(), + log_subscriber, + })); + dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || { + tracing::info!("dca context created"); + }); + ctx + } + + /// Returns the log subscriber. + #[inline] + pub fn log_subscriber(&self) -> Arc { + self.0.log_subscriber.clone() + } + + async fn get( + &self, + path: &'static str, + query: Q, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, path) + .query_params(query) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + async fn post( + &self, + path: &'static str, + body: B, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::POST, path) + .body(Json(body)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// List DCA plans. + /// + /// Path: GET /v1/dailycoins/query + pub async fn list_dca_plans(&self, status: Option) -> Result { + #[derive(Serialize)] + struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + status: Option, + } + self.get("/v1/dailycoins/query", Request { status }).await + } + + /// Create a DCA plan. + /// + /// Path: POST /v1/dailycoins/create + pub async fn create_dca_plan(&self, opts: CreateDcaPlanOptions) -> Result { + self.post("/v1/dailycoins/create", opts).await + } + + /// Update a DCA plan. + /// + /// Path: POST /v1/dailycoins/update + pub async fn update_dca_plan(&self, opts: UpdateDcaPlanOptions) -> Result { + self.post("/v1/dailycoins/update", opts).await + } + + /// Pause a DCA plan. + /// + /// Path: POST /v1/dailycoins/toggle + pub async fn pause_dca_plan(&self, plan_id: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + plan_id: String, + action: &'static str, + } + self.post( + "/v1/dailycoins/toggle", + Request { + plan_id: plan_id.into(), + action: "pause", + }, + ) + .await + } + + /// Resume a DCA plan. + /// + /// Path: POST /v1/dailycoins/toggle + pub async fn resume_dca_plan(&self, plan_id: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + plan_id: String, + action: &'static str, + } + self.post( + "/v1/dailycoins/toggle", + Request { + plan_id: plan_id.into(), + action: "resume", + }, + ) + .await + } + + /// Stop a DCA plan. + /// + /// Path: POST /v1/dailycoins/toggle + pub async fn stop_dca_plan(&self, plan_id: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + plan_id: String, + action: &'static str, + } + self.post( + "/v1/dailycoins/toggle", + Request { + plan_id: plan_id.into(), + action: "stop", + }, + ) + .await + } + + /// Get DCA plan execution history. + /// + /// Path: GET /v1/dailycoins/query-records + pub async fn dca_history(&self, opts: DcaHistoryOptions) -> Result { + self.get("/v1/dailycoins/query-records", opts).await + } + + /// Get DCA statistics, optionally filtered by symbol. + /// + /// Path: GET /v1/dailycoins/statistic + pub async fn dca_statistics(&self, symbol: Option) -> Result { + #[derive(Serialize)] + struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + symbol: Option, + } + self.get("/v1/dailycoins/statistic", Request { symbol }).await + } + + /// Check DCA support for a list of symbols. + /// + /// Path: POST /v1/dailycoins/batch-check-support + pub async fn check_dca_support( + &self, + opts: CheckDcaSupportOptions, + ) -> Result { + self.post("/v1/dailycoins/batch-check-support", opts).await + } +} diff --git a/rust/src/dca/mod.rs b/rust/src/dca/mod.rs new file mode 100644 index 000000000..f3ee96301 --- /dev/null +++ b/rust/src/dca/mod.rs @@ -0,0 +1,9 @@ +//! Dollar-Cost Averaging (DCA) plan module. + +mod context; +mod types; + +pub use context::DcaContext; +pub use types::{ + CheckDcaSupportOptions, CreateDcaPlanOptions, DcaHistoryOptions, UpdateDcaPlanOptions, +}; diff --git a/rust/src/dca/types.rs b/rust/src/dca/types.rs new file mode 100644 index 000000000..27a7e7dde --- /dev/null +++ b/rust/src/dca/types.rs @@ -0,0 +1,40 @@ +//! DCA (Dollar-Cost Averaging) types. + +#![allow(missing_docs)] + +use serde::{Deserialize, Serialize}; + +/// Options for creating a DCA plan. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateDcaPlanOptions { + pub symbol: String, + pub amount: String, + pub frequency: String, + #[serde(flatten)] + pub extra: serde_json::Value, +} + +/// Options for updating a DCA plan. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateDcaPlanOptions { + pub plan_id: String, + #[serde(flatten)] + pub extra: serde_json::Value, +} + +/// Options for checking DCA support. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CheckDcaSupportOptions { + pub symbols: Vec, +} + +/// Optional query parameters for listing DCA history. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct DcaHistoryOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub plan_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_date: Option, +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 435089072..7830b042f 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -20,18 +20,26 @@ mod types; pub mod blocking; pub use longbridge_oauth as oauth; +pub mod alert; pub mod asset; pub mod content; +pub mod dca; +pub mod quant; pub mod quote; +pub mod sharelist; pub mod trade; +pub use alert::AlertContext; pub use asset::AssetContext; pub use config::{Config, Language, PushCandlestickMode}; pub use content::ContentContext; +pub use dca::DcaContext; pub use error::{Error, Result, SimpleError, SimpleErrorKind}; pub use longbridge_httpcli as httpclient; pub use longbridge_wscli as wsclient; +pub use quant::QuantContext; pub use quote::QuoteContext; pub use rust_decimal::Decimal; +pub use sharelist::SharelistContext; pub use trade::TradeContext; pub use types::Market; diff --git a/rust/src/quant/context.rs b/rust/src/quant/context.rs new file mode 100644 index 000000000..51fcde09e --- /dev/null +++ b/rust/src/quant/context.rs @@ -0,0 +1,68 @@ +//! Quant context – run quantitative scripts. + +use std::sync::Arc; + +use longbridge_httpcli::{HttpClient, Json, Method}; +use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; + +use crate::{Config, Result, quant::types::RunQuantScriptOptions}; + +struct InnerQuantContext { + http_cli: HttpClient, + log_subscriber: Arc, +} + +impl Drop for InnerQuantContext { + fn drop(&mut self) { + dispatcher::with_default(&self.log_subscriber.clone().into(), || { + tracing::info!("quant context dropped"); + }); + } +} + +/// Quant context for executing quantitative scripts. +#[derive(Clone)] +pub struct QuantContext(Arc); + +impl QuantContext { + /// Create a `QuantContext`. + pub fn new(config: Arc) -> Self { + let log_subscriber = config.create_log_subscriber("quant"); + dispatcher::with_default(&log_subscriber.clone().into(), || { + tracing::info!("creating quant context"); + }); + let ctx = Self(Arc::new(InnerQuantContext { + http_cli: config.create_http_client(), + log_subscriber, + })); + dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || { + tracing::info!("quant context created"); + }); + ctx + } + + /// Returns the log subscriber. + #[inline] + pub fn log_subscriber(&self) -> Arc { + self.0.log_subscriber.clone() + } + + /// Run a quantitative script. + /// + /// Path: POST /v1/quant/run_script + pub async fn run_quant_script( + &self, + opts: RunQuantScriptOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::POST, "/v1/quant/run_script") + .body(Json(opts)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } +} diff --git a/rust/src/quant/mod.rs b/rust/src/quant/mod.rs new file mode 100644 index 000000000..6ea6f42c0 --- /dev/null +++ b/rust/src/quant/mod.rs @@ -0,0 +1,7 @@ +//! Quantitative script execution module. + +mod context; +mod types; + +pub use context::QuantContext; +pub use types::RunQuantScriptOptions; diff --git a/rust/src/quant/types.rs b/rust/src/quant/types.rs new file mode 100644 index 000000000..b09f3c0d9 --- /dev/null +++ b/rust/src/quant/types.rs @@ -0,0 +1,16 @@ +//! Quant script types. + +#![allow(missing_docs)] + +use serde::{Deserialize, Serialize}; + +/// Options for running a quant script. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RunQuantScriptOptions { + pub symbol: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub period: Option, + pub script: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, +} diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 190d51bf1..381088e07 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -1953,6 +1953,80 @@ impl QuoteContext { .map_err(|_| WsClientError::ClientClosed)?; Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?) } + + // ----------------------------------------------------------------------- + // Internal HTTP helpers used by ext.rs + // ----------------------------------------------------------------------- + + /// Perform a GET request returning a raw JSON value. + pub(crate) async fn http_get_json( + &self, + path: &'static str, + query: Q, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, path) + .query_params(query) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Perform a POST request with a JSON body, returning a raw JSON value. + pub(crate) async fn http_post_json( + &self, + path: &'static str, + body: B, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::POST, path) + .body(Json(body)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Perform a DELETE request with query params. + pub(crate) async fn http_delete_json( + &self, + path: &'static str, + query: Q, + ) -> Result<()> { + self.0 + .http_cli + .request(Method::DELETE, path) + .query_params(query) + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await?; + Ok(()) + } + + /// Perform a PUT request with a JSON body, returning a raw JSON value. + pub(crate) async fn http_put_json( + &self, + path: &'static str, + body: B, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::PUT, path) + .body(Json(body)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } } fn normalize_symbol(symbol: &str) -> &str { diff --git a/rust/src/quote/ext.rs b/rust/src/quote/ext.rs new file mode 100644 index 000000000..d441440e9 --- /dev/null +++ b/rust/src/quote/ext.rs @@ -0,0 +1,645 @@ +//! Extensions to [`QuoteContext`] – new fundamental-data and market-data methods. + +use serde::Serialize; + +use crate::{ + Result, + quote::{ + QuoteContext, + extra_types::{ + AhPremiumKlinesOptions, BrokerHoldingOptions, CorporateActionsOptions, + DividendsOptions, FinanceCalendarOptions, FinancialReportOptions, + InstitutionRatingDetailOptions, OperatingDataOptions, ValuationHistoryOptions, + ValuationOptions, + }, + utils::symbol_to_counter_id, + }, +}; + +impl QuoteContext { + // ----------------------------------------------------------------------- + // Domain A: Fundamental Data (counter_id conversion) + // ----------------------------------------------------------------------- + + /// Get financial reports for a symbol. + /// + /// Path: GET /v1/quote/financial-reports + pub async fn financial_report( + &self, + symbol: impl Into, + opts: FinancialReportOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + kind: Option, + #[serde(skip_serializing_if = "Option::is_none")] + report: Option, + } + self.http_get_json( + "/v1/quote/financial-reports", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + kind: opts.kind, + report: opts.report_type, + }, + ) + .await + } + + /// Get institution ratings for a symbol. + /// + /// Path: GET /v1/quote/institution-ratings + pub async fn institution_ratings( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/institution-ratings", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get the latest institution rating for a symbol. + /// + /// Path: GET /v1/quote/institution-rating-latest + pub async fn institution_rating_latest( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/institution-rating-latest", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get institution rating detail for a symbol. + /// + /// Path: GET /v1/quote/institution-ratings/detail + pub async fn institution_rating_detail( + &self, + symbol: impl Into, + opts: InstitutionRatingDetailOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page_size: Option, + } + self.http_get_json( + "/v1/quote/institution-ratings/detail", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + page: opts.page, + page_size: opts.page_size, + }, + ) + .await + } + + /// Get dividends for a symbol. + /// + /// Path: GET /v1/quote/dividends + pub async fn dividends( + &self, + symbol: impl Into, + opts: DividendsOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + end_date: Option, + } + self.http_get_json( + "/v1/quote/dividends", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + start_date: opts.start_date, + end_date: opts.end_date, + }, + ) + .await + } + + /// Get detail for a specific dividend of a symbol. + /// + /// Path: GET /v1/quote/dividends/details + pub async fn dividend_detail( + &self, + symbol: impl Into, + dividend_id: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + dividend_id: String, + } + self.http_get_json( + "/v1/quote/dividends/details", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + dividend_id: dividend_id.into(), + }, + ) + .await + } + + /// Get EPS forecasts for a symbol. + /// + /// Path: GET /v1/quote/forecast-eps + pub async fn forecast_eps(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/forecast-eps", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get financial consensus detail for a symbol. + /// + /// Path: GET /v1/quote/financial-consensus-detail + pub async fn financial_consensus( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/financial-consensus-detail", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get valuation data for a symbol. + /// + /// Path: GET /v1/quote/valuation + pub async fn valuation( + &self, + symbol: impl Into, + opts: ValuationOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + period: Option, + } + self.http_get_json( + "/v1/quote/valuation", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + period: opts.period, + }, + ) + .await + } + + /// Get valuation history for a symbol. + /// + /// Path: GET /v1/quote/valuation/detail + pub async fn valuation_history( + &self, + symbol: impl Into, + opts: ValuationHistoryOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + period: Option, + #[serde(skip_serializing_if = "Option::is_none")] + count: Option, + } + self.http_get_json( + "/v1/quote/valuation/detail", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + period: opts.period, + count: opts.count, + }, + ) + .await + } + + /// Get industry valuation comparison for a symbol. + /// + /// Path: GET /v1/quote/industry-valuation-comparison + pub async fn industry_valuation( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/industry-valuation-comparison", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get industry valuation distribution for a symbol. + /// + /// Path: GET /v1/quote/industry-valuation-distribution + pub async fn industry_valuation_distribution( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/industry-valuation-distribution", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get company overview for a symbol. + /// + /// Path: GET /v1/quote/comp-overview + pub async fn company_overview(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/comp-overview", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get company executives for a symbol. + /// + /// Path: GET /v1/quote/company-professionals + pub async fn company_executives(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/company-professionals", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get shareholders data for a symbol. + /// + /// Path: GET /v1/quote/shareholders + pub async fn shareholders(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/shareholders", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get fund holders for a symbol. + /// + /// Path: GET /v1/quote/fund-holders + pub async fn fund_holders(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/fund-holders", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get corporate actions for a symbol. + /// + /// Path: GET /v1/quote/company-act + pub async fn corporate_actions( + &self, + symbol: impl Into, + opts: CorporateActionsOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + action_type: Option, + } + self.http_get_json( + "/v1/quote/company-act", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + action_type: opts.action_type, + }, + ) + .await + } + + /// Get investor relations for a symbol. + /// + /// Path: GET /v1/quote/invest-relations + pub async fn investor_relations(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + } + self.http_get_json( + "/v1/quote/invest-relations", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get operating data for a symbol. + /// + /// Path: GET /v1/quote/operatings + pub async fn operating_data( + &self, + symbol: impl Into, + opts: OperatingDataOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + period: Option, + } + self.http_get_json( + "/v1/quote/operatings", + Request { + counter_id: symbol_to_counter_id(&symbol.into()), + period: opts.period, + }, + ) + .await + } + + // ----------------------------------------------------------------------- + // Domain B: Market Data (no counter_id conversion) + // ----------------------------------------------------------------------- + + /// Get market status. + /// + /// Path: GET /v1/quote/market-status + pub async fn market_status(&self, market: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + market: String, + } + self.http_get_json( + "/v1/quote/market-status", + Request { + market: market.into(), + }, + ) + .await + } + + /// Get broker holding data for a symbol. + /// + /// Path: GET /v1/quote/broker-holding + pub async fn broker_holding( + &self, + symbol: impl Into, + opts: BrokerHoldingOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + symbol: String, + #[serde(skip_serializing_if = "Option::is_none")] + period: Option, + } + self.http_get_json( + "/v1/quote/broker-holding", + Request { + symbol: symbol.into(), + period: opts.period, + }, + ) + .await + } + + /// Get broker holding detail for a symbol. + /// + /// Path: GET /v1/quote/broker-holding/detail + pub async fn broker_holding_detail( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + symbol: String, + } + self.http_get_json( + "/v1/quote/broker-holding/detail", + Request { + symbol: symbol.into(), + }, + ) + .await + } + + /// Get daily broker holding for a symbol and broker. + /// + /// Path: GET /v1/quote/broker-holding/daily + pub async fn broker_holding_daily( + &self, + symbol: impl Into, + broker_id: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + symbol: String, + broker_id: String, + } + self.http_get_json( + "/v1/quote/broker-holding/daily", + Request { + symbol: symbol.into(), + broker_id: broker_id.into(), + }, + ) + .await + } + + /// Get AH premium klines for a symbol. + /// + /// Path: GET /v1/quote/ahpremium/klines + pub async fn ah_premium_klines( + &self, + symbol: impl Into, + opts: AhPremiumKlinesOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + symbol: String, + #[serde(skip_serializing_if = "Option::is_none")] + period: Option, + #[serde(skip_serializing_if = "Option::is_none")] + count: Option, + } + self.http_get_json( + "/v1/quote/ahpremium/klines", + Request { + symbol: symbol.into(), + period: opts.period, + count: opts.count, + }, + ) + .await + } + + /// Get AH premium timeshares for a symbol. + /// + /// Path: GET /v1/quote/ahpremium/timeshares + pub async fn ah_premium_timeshares( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Request { + symbol: String, + } + self.http_get_json( + "/v1/quote/ahpremium/timeshares", + Request { + symbol: symbol.into(), + }, + ) + .await + } + + /// Get trade statistics for a symbol. + /// + /// Path: GET /v1/quote/trades-statistics + pub async fn trade_statistics(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + symbol: String, + } + self.http_get_json( + "/v1/quote/trades-statistics", + Request { + symbol: symbol.into(), + }, + ) + .await + } + + /// Get market anomaly data. + /// + /// Path: GET /v1/quote/changes + pub async fn market_anomaly(&self, market: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + market: String, + } + self.http_get_json( + "/v1/quote/changes", + Request { + market: market.into(), + }, + ) + .await + } + + /// Get index constituents for an index symbol. + /// + /// Path: GET /v1/quote/index-constituents + pub async fn index_constituents(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Request { + symbol: String, + } + self.http_get_json( + "/v1/quote/index-constituents", + Request { + symbol: symbol.into(), + }, + ) + .await + } + + // ----------------------------------------------------------------------- + // Domain C: Calendar + // ----------------------------------------------------------------------- + + /// Get finance calendar for a market. + /// + /// Path: GET /v1/quote/finance_calendar + pub async fn finance_calendar( + &self, + market: impl Into, + opts: FinanceCalendarOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + market: String, + #[serde(skip_serializing_if = "Option::is_none")] + start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + end_date: Option, + } + self.http_get_json( + "/v1/quote/finance_calendar", + Request { + market: market.into(), + start_date: opts.start_date, + end_date: opts.end_date, + }, + ) + .await + } +} diff --git a/rust/src/quote/extra_types.rs b/rust/src/quote/extra_types.rs new file mode 100644 index 000000000..bbff65742 --- /dev/null +++ b/rust/src/quote/extra_types.rs @@ -0,0 +1,99 @@ +#![allow(missing_docs)] + +use serde::{Deserialize, Serialize}; + +// --------------------------------------------------------------------------- +// Domain A – Fundamental Data helpers +// --------------------------------------------------------------------------- + +/// Optional parameters for `financial_report`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct FinancialReportOptions { + /// Report kind (e.g. "annual", "interim"). + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + /// Report type (e.g. "income", "balance", "cashflow"). + #[serde(skip_serializing_if = "Option::is_none")] + pub report_type: Option, +} + +/// Optional parameters for `institution_rating_detail`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct InstitutionRatingDetailOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub page_size: Option, +} + +/// Optional parameters for `dividends`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct DividendsOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_date: Option, +} + +/// Optional parameters for `valuation`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct ValuationOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub period: Option, +} + +/// Optional parameters for `valuation_history`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct ValuationHistoryOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub period: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub count: Option, +} + +/// Optional parameters for `corporate_actions`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct CorporateActionsOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub action_type: Option, +} + +/// Optional parameters for `operating_data`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct OperatingDataOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub period: Option, +} + +// --------------------------------------------------------------------------- +// Domain B – Market Data helpers +// --------------------------------------------------------------------------- + +/// Optional parameters for `broker_holding`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct BrokerHoldingOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub period: Option, +} + +/// Optional parameters for `ah_premium_klines`. +#[derive(Debug, Clone, Default, Serialize)] +pub struct AhPremiumKlinesOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub period: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub count: Option, +} + +// --------------------------------------------------------------------------- +// Domain C – Calendar helper +// --------------------------------------------------------------------------- + +/// Optional parameters for `finance_calendar`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct FinanceCalendarOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_date: Option, +} diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index a7deae152..5e10fe9f5 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -4,6 +4,8 @@ mod cache; mod cmd_code; mod context; mod core; +mod ext; +mod extra_types; mod push_types; mod store; mod sub_flags; @@ -11,6 +13,11 @@ mod types; mod utils; pub use context::QuoteContext; +pub use extra_types::{ + AhPremiumKlinesOptions, BrokerHoldingOptions, CorporateActionsOptions, DividendsOptions, + FinanceCalendarOptions, FinancialReportOptions, InstitutionRatingDetailOptions, + OperatingDataOptions, ValuationHistoryOptions, ValuationOptions, +}; pub use longbridge_proto::quote::{AdjustType, Period, TradeStatus}; pub use push_types::{ PushBrokers, PushCandlestick, PushDepth, PushEvent, PushEventDetail, PushQuote, PushTrades, diff --git a/rust/src/quote/utils.rs b/rust/src/quote/utils.rs index 9123605d2..32e74a88e 100644 --- a/rust/src/quote/utils.rs +++ b/rust/src/quote/utils.rs @@ -12,3 +12,12 @@ pub(crate) fn format_date(date: Date) -> String { date.format(time::macros::format_description!("[year][month][day]")) .unwrap() } + +/// Convert a symbol like "700.HK" into the counter-id format "ST/HK/700". +pub(crate) fn symbol_to_counter_id(symbol: &str) -> String { + if let Some((code, market)) = symbol.rsplit_once('.') { + format!("ST/{}/{}", market.to_uppercase(), code) + } else { + symbol.to_string() + } +} diff --git a/rust/src/sharelist/context.rs b/rust/src/sharelist/context.rs new file mode 100644 index 000000000..64bd0f85b --- /dev/null +++ b/rust/src/sharelist/context.rs @@ -0,0 +1,195 @@ +//! Sharelist context – manage share-lists (watchlists shared publicly). + +use std::sync::Arc; + +use longbridge_httpcli::{HttpClient, Json, Method}; +use serde::Serialize; +use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; + +use crate::{ + Config, Result, + sharelist::types::{CreateSharelistOptions, SharelistItemsOptions}, +}; + +struct InnerSharelistContext { + http_cli: HttpClient, + log_subscriber: Arc, +} + +impl Drop for InnerSharelistContext { + fn drop(&mut self) { + dispatcher::with_default(&self.log_subscriber.clone().into(), || { + tracing::info!("sharelist context dropped"); + }); + } +} + +/// Sharelist context for managing public share-lists. +#[derive(Clone)] +pub struct SharelistContext(Arc); + +impl SharelistContext { + /// Create a `SharelistContext`. + pub fn new(config: Arc) -> Self { + let log_subscriber = config.create_log_subscriber("sharelist"); + dispatcher::with_default(&log_subscriber.clone().into(), || { + tracing::info!("creating sharelist context"); + }); + let ctx = Self(Arc::new(InnerSharelistContext { + http_cli: config.create_http_client(), + log_subscriber, + })); + dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || { + tracing::info!("sharelist context created"); + }); + ctx + } + + /// Returns the log subscriber. + #[inline] + pub fn log_subscriber(&self) -> Arc { + self.0.log_subscriber.clone() + } + + async fn get( + &self, + path: impl Into, + query: Q, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, path) + .query_params(query) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + async fn post( + &self, + path: impl Into, + body: B, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::POST, path) + .body(Json(body)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + async fn delete( + &self, + path: impl Into, + query: Q, + ) -> Result<()> { + self.0 + .http_cli + .request(Method::DELETE, path) + .query_params(query) + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await?; + Ok(()) + } + + /// List share-lists. + /// + /// Path: GET /v1/sharelists + pub async fn list_sharelists(&self, count: Option) -> Result { + #[derive(Serialize)] + struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + count: Option, + } + self.get("/v1/sharelists", Request { count }).await + } + + /// Get detail for a specific share-list. + /// + /// Path: GET /v1/sharelists/{id} + pub async fn sharelist_detail(&self, id: impl Into) -> Result { + #[derive(Serialize)] + struct Empty {} + self.get(format!("/v1/sharelists/{}", id.into()), Empty {}) + .await + } + + /// Create a new share-list. + /// + /// Path: POST /v1/sharelists + pub async fn create_sharelist( + &self, + opts: CreateSharelistOptions, + ) -> Result { + self.post("/v1/sharelists", opts).await + } + + /// Delete a share-list. + /// + /// Path: DELETE /v1/sharelists/{id} + pub async fn delete_sharelist(&self, id: impl Into) -> Result<()> { + #[derive(Serialize)] + struct Empty {} + self.delete(format!("/v1/sharelists/{}", id.into()), Empty {}) + .await + } + + /// Add items to a share-list. + /// + /// Path: POST /v1/sharelists/{id}/items + pub async fn add_sharelist_items( + &self, + id: impl Into, + opts: SharelistItemsOptions, + ) -> Result { + self.post(format!("/v1/sharelists/{}/items", id.into()), opts) + .await + } + + /// Remove items from a share-list. + /// + /// Path: DELETE /v1/sharelists/{id}/items + pub async fn remove_sharelist_items( + &self, + id: impl Into, + opts: SharelistItemsOptions, + ) -> Result<()> { + self.delete(format!("/v1/sharelists/{}/items", id.into()), opts) + .await + } + + /// Sort items in a share-list. + /// + /// Path: POST /v1/sharelists/{id}/items/sort + pub async fn sort_sharelist_items( + &self, + id: impl Into, + opts: SharelistItemsOptions, + ) -> Result { + self.post( + format!("/v1/sharelists/{}/items/sort", id.into()), + opts, + ) + .await + } + + /// Get popular share-lists. + /// + /// Path: GET /v1/sharelists/popular + pub async fn popular_sharelists(&self, count: Option) -> Result { + #[derive(Serialize)] + struct Request { + #[serde(skip_serializing_if = "Option::is_none")] + count: Option, + } + self.get("/v1/sharelists/popular", Request { count }).await + } +} diff --git a/rust/src/sharelist/mod.rs b/rust/src/sharelist/mod.rs new file mode 100644 index 000000000..878e7b3fb --- /dev/null +++ b/rust/src/sharelist/mod.rs @@ -0,0 +1,7 @@ +//! Share-list module. + +mod context; +mod types; + +pub use context::SharelistContext; +pub use types::{CreateSharelistOptions, SharelistItemsOptions}; diff --git a/rust/src/sharelist/types.rs b/rust/src/sharelist/types.rs new file mode 100644 index 000000000..629c2a10b --- /dev/null +++ b/rust/src/sharelist/types.rs @@ -0,0 +1,19 @@ +//! Sharelist types. + +#![allow(missing_docs)] + +use serde::{Deserialize, Serialize}; + +/// Options for creating a sharelist. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateSharelistOptions { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// Options for adding/removing/sorting items in a sharelist. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SharelistItemsOptions { + pub symbols: Vec, +} diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index 4e4c1c0db..4271f1515 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -16,6 +16,10 @@ use crate::{ GetTodayExecutionsOptions, GetTodayOrdersOptions, MarginRatio, Order, OrderDetail, PushEvent, ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, TopicType, core::{Command, Core}, + extra_types::{ + ProfitAnalysisDetailOptions, ProfitAnalysisSummaryOptions, + ProfitAnalysisSublistOptions, + }, }, }; @@ -834,4 +838,61 @@ impl TradeContext { .await? .0) } + + /// Get portfolio profit analysis summary. + /// + /// Path: GET /v1/portfolio/profit-analysis-summary + pub async fn profit_analysis_summary( + &self, + opts: ProfitAnalysisSummaryOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/portfolio/profit-analysis-summary") + .query_params(opts) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Get portfolio profit analysis sub-list. + /// + /// Path: GET /v1/portfolio/profit-analysis-sublist + pub async fn profit_analysis_sublist( + &self, + opts: ProfitAnalysisSublistOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/portfolio/profit-analysis-sublist") + .query_params(opts) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Get portfolio profit analysis detail for a symbol. + /// + /// Path: GET /v1/portfolio/profit-analysis/detail + pub async fn profit_analysis_detail( + &self, + opts: ProfitAnalysisDetailOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/portfolio/profit-analysis/detail") + .query_params(opts) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } } diff --git a/rust/src/trade/ext.rs b/rust/src/trade/ext.rs new file mode 100644 index 000000000..25ff46fbb --- /dev/null +++ b/rust/src/trade/ext.rs @@ -0,0 +1,74 @@ +//! Extension methods for [`TradeContext`] – profit analysis. + +use longbridge_httpcli::{Json, Method}; +use tracing::instrument::WithSubscriber; + +use crate::{ + Result, + trade::{ + TradeContext, + extra_types::{ + ProfitAnalysisDetailOptions, ProfitAnalysisSummaryOptions, + ProfitAnalysisSublistOptions, + }, + }, +}; + +impl TradeContext { + /// Get profit analysis summary. + /// + /// Path: GET /v1/portfolio/profit-analysis-summary + pub async fn profit_analysis_summary( + &self, + opts: ProfitAnalysisSummaryOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/portfolio/profit-analysis-summary") + .query_params(opts) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Get profit analysis sub-list. + /// + /// Path: GET /v1/portfolio/profit-analysis-sublist + pub async fn profit_analysis_sublist( + &self, + opts: ProfitAnalysisSublistOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/portfolio/profit-analysis-sublist") + .query_params(opts) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Get profit analysis detail for a symbol. + /// + /// Path: GET /v1/portfolio/profit-analysis/detail + pub async fn profit_analysis_detail( + &self, + opts: ProfitAnalysisDetailOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/portfolio/profit-analysis/detail") + .query_params(opts) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } +} diff --git a/rust/src/trade/extra_types.rs b/rust/src/trade/extra_types.rs new file mode 100644 index 000000000..34f20f308 --- /dev/null +++ b/rust/src/trade/extra_types.rs @@ -0,0 +1,41 @@ +#![allow(missing_docs)] + +use serde::{Deserialize, Serialize}; + +/// Optional parameters for `profit_analysis_summary`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ProfitAnalysisSummaryOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub currency: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_date: Option, +} + +/// Optional parameters for `profit_analysis_sublist`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ProfitAnalysisSublistOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub currency: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub page_size: Option, +} + +/// Parameters for `profit_analysis_detail`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ProfitAnalysisDetailOptions { + pub symbol: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub currency: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub end_date: Option, +} diff --git a/rust/src/trade/mod.rs b/rust/src/trade/mod.rs index 21df810dc..1d8966c9d 100644 --- a/rust/src/trade/mod.rs +++ b/rust/src/trade/mod.rs @@ -3,11 +3,15 @@ mod cmd_code; mod context; mod core; +mod extra_types; mod push_types; mod requests; mod types; pub use context::{EstimateMaxPurchaseQuantityResponse, SubmitOrderResponse, TradeContext}; +pub use extra_types::{ + ProfitAnalysisDetailOptions, ProfitAnalysisSummaryOptions, ProfitAnalysisSublistOptions, +}; pub use push_types::{PushEvent, PushOrderChanged, TopicType}; pub use requests::{ EstimateMaxPurchaseQuantityOptions, GetCashFlowOptions, GetFundPositionsOptions,