Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
152 changes: 152 additions & 0 deletions rust/src/alert/context.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Subscriber + Send + Sync>,
}

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<InnerAlertContext>);

impl AlertContext {
/// Create an `AlertContext`.
pub fn new(config: Arc<Config>) -> 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<dyn Subscriber + Send + Sync> {
self.0.log_subscriber.clone()
}

/// List all price alerts.
///
/// Path: GET /v1/notify/reminders
pub async fn list_alerts(&self) -> Result<serde_json::Value> {
#[derive(Serialize)]
struct Empty {}
Ok(self
.0
.http_cli
.request(Method::GET, "/v1/notify/reminders")
.query_params(Empty {})
.response::<Json<serde_json::Value>>()
.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<serde_json::Value> {
Ok(self
.0
.http_cli
.request(Method::POST, "/v1/notify/reminders")
.body(Json(opts))
.response::<Json<serde_json::Value>>()
.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<String>) -> 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<String>) -> Result<serde_json::Value> {
#[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::<Json<serde_json::Value>>()
.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<String>) -> Result<serde_json::Value> {
#[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::<Json<serde_json::Value>>()
.send()
.with_subscriber(self.0.log_subscriber.clone())
.await?
.0)
}
}
7 changes: 7 additions & 0 deletions rust/src/alert/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Price alert (reminder) module.

mod context;
mod types;

pub use context::AlertContext;
pub use types::{AddAlertOptions, AlertDirection};
23 changes: 23 additions & 0 deletions rust/src/alert/types.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}
27 changes: 26 additions & 1 deletion rust/src/asset/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
use crate::{
Config, Result,
asset::{
GetStatementListOptions, GetStatementListResponse, GetStatementOptions,
ExchangeRate, GetStatementListOptions, GetStatementListResponse, GetStatementOptions,
GetStatementResponse, core,
},
};
Expand Down Expand Up @@ -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<Vec<ExchangeRate>> {
#[derive(Serialize)]
struct Empty {}

#[derive(serde::Deserialize)]
struct Response {
list: Vec<ExchangeRate>,
}

Ok(self
.0
.http_cli
.request(Method::GET, "/v1/asset/exchange_rates")
.query_params(Empty {})
.response::<Json<Response>>()
.send()
.with_subscriber(self.0.log_subscriber.clone())
.await?
.0
.list)
}
}
35 changes: 35 additions & 0 deletions rust/src/asset/ext.rs
Original file line number Diff line number Diff line change
@@ -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<serde_json::Value> {
Ok(self
.0
.http_cli
.request(Method::GET, "/v1/asset/exchange_rates")
.response::<Json<serde_json::Value>>()
.send()
.with_subscriber(self.0.log_subscriber.clone())
.await?
.0)
}
}
11 changes: 11 additions & 0 deletions rust/src/asset/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Loading
Loading