Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Rust:** add `SignalContext` — strategy signals and the catalyst facts behind them. `signals` (`GET /v1/signals`) queries signals with symbol / strategy / catalyst / time-range filters and paging; `signal` (`GET /v1/signals/{signal_id}`) returns one signal including the full strategy analysis in `json_data`; `security_facts` (`GET /v1/facts/security_facts`) lists a security's fact (catalyst) events. Bindings for the other languages are not wired up yet
- **All languages:** add `TradeContext.submit_multileg` (`POST /v1/trade/order/multileg`) — submit a multi-leg option combination order (vertical spreads, straddles, strangles, collars, etc.) whose legs are placed together as a single strategy order. Takes `side`, `order_type`, `submitted_quantity`, `strategy` (`MultiLegStrategy`), a list of legs (`symbol` + `ratio_quantity`), and optional `submitted_price` / `remark` / `client_request_id`; returns the existing `SubmitOrderResponse`
- **All languages:** order queries and the order push now expose multi-leg strategy information. `Order` (from `today_orders` / `history_orders`), `OrderDetail` (from `order_detail`), and the `PushOrderChanged` order-changed event gain an optional `multi_leg` field (`MultiLegInfo`) — present only for multi-leg option combination orders — carrying the `strategy`, `strategy_name`, `multileg_id`, `code`, and the combination `legs` (each with `symbol`, `side`, `position`, `ratio_quantity`, `strike_price`, `expire_date`, and `contract_direction`). Adds the `MultiLegStrategy`, `MultiLegPosition`, and `ContractDirection` enums
- **All languages:** add grid-trading support via a standalone `GridContext` — submit / replace / cancel / suspend / restart grid orders, list orders (paged and by IDs), fetch order detail and trigger history, submit the strategy risk-disclosure questionnaire, and query the security (symbol) info (`symbol_info` → `GridSymbolInfo`: name, last price, lot sizes, price-step rules, channel/authorization) needed to build a grid order. Available in the Rust, Python, Node.js, Java, and C/C++ bindings
Expand Down
2 changes: 2 additions & 0 deletions rust/src/blocking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod quote;
mod runtime;
mod screener;
mod sharelist;
mod signal;
mod trade;

pub use agent::AgentContextSync;
Expand All @@ -31,4 +32,5 @@ pub use portfolio::PortfolioContextSync;
pub use quote::QuoteContextSync;
pub use screener::ScreenerContextSync;
pub use sharelist::SharelistContextSync;
pub use signal::SignalContextSync;
pub use trade::TradeContextSync;
48 changes: 48 additions & 0 deletions rust/src/blocking/signal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::sync::Arc;

use tokio::sync::mpsc;

use crate::{
Config, Result,
blocking::runtime::BlockingRuntime,
signal::{SignalContext, types::*},
};

/// Blocking signal context
pub struct SignalContextSync {
rt: BlockingRuntime<SignalContext>,
}

impl SignalContextSync {
/// Create a [`SignalContextSync`]
pub fn new(config: Arc<Config>) -> Result<Self> {
let rt = BlockingRuntime::try_new(
move || {
let ctx = SignalContext::new(config);
let (tx, rx) = mpsc::unbounded_channel::<std::convert::Infallible>();
std::mem::forget(tx);
Ok::<_, crate::Error>((ctx, rx))
},
|_: std::convert::Infallible| {},
)?;
Ok(Self { rt })
}

/// Query signals
pub fn signals(&self, opts: SignalsOptions) -> Result<SignalsResponse> {
self.rt
.call(move |ctx| async move { ctx.signals(opts).await })
}

/// Get one signal by ID
pub fn signal(&self, signal_id: impl Into<String> + Send + 'static) -> Result<Signal> {
self.rt
.call(move |ctx| async move { ctx.signal(signal_id).await })
}

/// List the fact (catalyst) events for one security
pub fn security_facts(&self, opts: SecurityFactsOptions) -> Result<Vec<serde_json::Value>> {
self.rt
.call(move |ctx| async move { ctx.security_facts(opts).await })
}
}
2 changes: 2 additions & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub mod portfolio;
pub mod quote;
pub mod screener;
pub mod sharelist;
pub mod signal;
pub mod trade;

pub use agent::AgentContext;
Expand All @@ -62,6 +63,7 @@ pub use quote::{QuoteContext, USCryptoOverview};
pub use rust_decimal::Decimal;
pub use screener::ScreenerContext;
pub use sharelist::SharelistContext;
pub use signal::SignalContext;
pub use trade::{
GetUSHistoryOrders, GetUSRealizedPLOptions, QueryUSOrdersOptions, QueryUSOrdersResponse,
TradeContext, USAssetOverview, USCashEntry, USCryptoEntry, USOrderDetailResponse,
Expand Down
21 changes: 21 additions & 0 deletions rust/src/serde_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ pub(crate) mod timestamp {
}
}

pub(crate) mod timestamp_ms {
use super::*;

pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
let value = value.parse::<i64>().map_err(D::Error::custom)?;
OffsetDateTime::from_unix_timestamp_nanos(i128::from(value) * 1_000_000)
.map_err(D::Error::custom)
}

pub(crate) fn serialize<S>(datetime: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&(datetime.unix_timestamp_nanos() / 1_000_000))
}
}

pub(crate) mod timestamp_opt {
use super::*;

Expand Down
121 changes: 121 additions & 0 deletions rust/src/signal/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::sync::Arc;

use longbridge_httpcli::{HttpClient, Json, Method};
use serde::Deserialize;
use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};

use crate::{Config, Result, signal::types::*};

struct InnerSignalContext {
http_cli: HttpClient,
log_subscriber: Arc<dyn Subscriber + Send + Sync>,
}

impl Drop for InnerSignalContext {
fn drop(&mut self) {
dispatcher::with_default(&self.log_subscriber.clone().into(), || {
tracing::info!("signal context dropped");
});
}
}

/// Signal context — strategy signals and the catalyst facts behind them.
#[derive(Clone)]
pub struct SignalContext(Arc<InnerSignalContext>);

impl SignalContext {
/// Create a [`SignalContext`]
pub fn new(config: Arc<Config>) -> Self {
let log_subscriber = config.create_log_subscriber("signal");
dispatcher::with_default(&log_subscriber.clone().into(), || {
tracing::info!(language = ?config.language, "creating signal context");
});
let ctx = Self(Arc::new(InnerSignalContext {
http_cli: config.create_http_client(),
log_subscriber,
}));
dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
tracing::info!("signal context created");
});
ctx
}

/// Returns the log subscriber
#[inline]
pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
self.0.log_subscriber.clone()
}

/// Query signals, filtered by symbol, strategy, catalyst and time range.
///
/// Path: `GET /v1/signals`
pub async fn signals(&self, opts: SignalsOptions) -> Result<SignalsResponse> {
Ok(self
.0
.http_cli
.request(Method::GET, "/v1/signals")
.query_params(opts)
.response::<Json<SignalsResponse>>()
.send()
.with_subscriber(self.0.log_subscriber.clone())
.await?
.0)
}

/// Get one signal by ID, including the full analysis in
/// [`Signal::json_data`].
///
/// Path: `GET /v1/signals/{signal_id}`
pub async fn signal(&self, signal_id: impl Into<String>) -> Result<Signal> {
#[derive(Debug, Deserialize)]
struct Response {
signal: Signal,
}

let signal_id = signal_id.into();
Ok(self
.0
.http_cli
.request(Method::GET, format!("/v1/signals/{signal_id}"))
.response::<Json<Response>>()
.send()
.with_subscriber(self.0.log_subscriber.clone())
.await?
.0
.signal)
}

/// List the fact (catalyst) events for one security — anomaly detections,
/// factor readings, data sources and natural-language summaries.
///
/// Facts are what strategies react to: a signal names the fact that
/// triggered it in [`Signal::key_fact_id`].
///
/// Each fact is returned verbatim as a JSON object; the payload is
/// fact-type specific (news, fundamental, technical) and carries different
/// fields per type.
///
/// Path: `GET /v1/facts/security_facts`
pub async fn security_facts(
&self,
opts: SecurityFactsOptions,
) -> Result<Vec<serde_json::Value>> {
#[derive(Debug, Deserialize)]
struct Response {
#[serde(default)]
facts: Vec<serde_json::Value>,
}

Ok(self
.0
.http_cli
.request(Method::GET, "/v1/facts/security_facts")
.query_params(opts)
.response::<Json<Response>>()
.send()
.with_subscriber(self.0.log_subscriber.clone())
.await?
.0
.facts)
}
}
8 changes: 8 additions & 0 deletions rust/src/signal/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! Strategy signals and the catalyst facts behind them

mod context;
/// Signal and fact types
pub mod types;

pub use context::SignalContext;
pub use types::{Outlook, SecurityFactsOptions, Signal, SignalsOptions, SignalsResponse};
Loading
Loading