diff --git a/.gitignore b/.gitignore index 70ec6bfc6..83f0b27f8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,8 @@ build # Eclipse IDE .project .classpath -.settings/ \ No newline at end of file +.settings/ + + +.DS_Store +*/.DS_Store \ No newline at end of file diff --git a/c/cbindgen.toml b/c/cbindgen.toml index c8d7e0de6..c7094fb3c 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -129,6 +129,10 @@ cpp_compat = true "CGranularity" = "lb_granularity_t" "CMarketTemperature" = "lb_market_temperature_t" "CHistoryMarketTemperatureResponse" = "lb_history_market_temperature_response_t" +"CFilingItem" = "lb_filing_item_t" +"CTopicItem" = "lb_topic_item_t" +"CNewsItem" = "lb_news_item_t" +"CContentContext" = "lb_content_context_t" "COAuth" = "lb_oauth_t" [export] @@ -169,5 +173,8 @@ include = [ "CQuotePackageDetail", "CMarketTemperature", "CHistoryMarketTemperatureResponse", + "CFilingItem", + "CTopicItem", + "CNewsItem", "COAuth", ] diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index d64664389..2c581d3ed 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -1283,6 +1283,11 @@ typedef enum lb_granularity_t { */ typedef struct lb_config_t lb_config_t; +/** + * Content context + */ +typedef struct lb_content_context_t lb_content_context_t; + typedef struct lb_decimal_t lb_decimal_t; typedef struct lb_error_t lb_error_t; @@ -1312,14 +1317,6 @@ typedef struct lb_quote_context_t lb_quote_context_t; */ typedef struct lb_trade_context_t lb_trade_context_t; -/** - * HTTP Header - */ -typedef struct lb_http_header_t { - const char *name; - const char *value; -} lb_http_header_t; - typedef struct lb_async_result_t { const void *ctx; const struct lb_error_t *error; @@ -1330,6 +1327,14 @@ typedef struct lb_async_result_t { typedef void (*lb_async_callback_t)(const struct lb_async_result_t*); +/** + * HTTP Header + */ +typedef struct lb_http_header_t { + const char *name; + const char *value; +} lb_http_header_t; + typedef void (*lb_free_userdata_func_t)(void*); /** @@ -3870,6 +3875,116 @@ typedef struct lb_history_market_temperature_response_t { uintptr_t num_records; } lb_history_market_temperature_response_t; +/** + * Filing item + */ +typedef struct lb_filing_item_t { + /** + * Filing ID + */ + const char *id; + /** + * Title + */ + const char *title; + /** + * Description + */ + const char *description; + /** + * File name + */ + const char *file_name; + /** + * File URLs + */ + const char *const *file_urls; + /** + * Number of file URLs + */ + uintptr_t num_file_urls; + /** + * Published time (Unix timestamp) + */ + int64_t published_at; +} lb_filing_item_t; + +/** + * Topic item + */ +typedef struct lb_topic_item_t { + /** + * Topic ID + */ + const char *id; + /** + * Title + */ + const char *title; + /** + * Description + */ + const char *description; + /** + * URL + */ + const char *url; + /** + * Published time (Unix timestamp) + */ + int64_t published_at; + /** + * Comments count + */ + int32_t comments_count; + /** + * Likes count + */ + int32_t likes_count; + /** + * Shares count + */ + int32_t shares_count; +} lb_topic_item_t; + +/** + * News item + */ +typedef struct lb_news_item_t { + /** + * News ID + */ + const char *id; + /** + * Title + */ + const char *title; + /** + * Description + */ + const char *description; + /** + * URL + */ + const char *url; + /** + * Published time (Unix timestamp) + */ + int64_t published_at; + /** + * Comments count + */ + int32_t comments_count; + /** + * Likes count + */ + int32_t likes_count; + /** + * Shares count + */ + int32_t shares_count; +} lb_news_item_t; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -3990,6 +4105,53 @@ void lb_config_set_log_path(struct lb_config_t *config, const char *log_path); */ void lb_config_free(struct lb_config_t *config); +/** + * Create a new `ContentContext` + * + * @param config Config object + * @param callback Async callback + * @param userdata User data passed to the callback + */ +void lb_content_context_new(const struct lb_config_t *config, + lb_async_callback_t callback, + void *userdata); + +/** + * Retain the content context (increment reference count) + */ +void lb_content_context_retain(const struct lb_content_context_t *ctx); + +/** + * Release the content context (decrement reference count) + */ +void lb_content_context_release(const struct lb_content_context_t *ctx); + +/** + * Get discussion topics list for a symbol + * + * @param ctx Content context + * @param symbol Security symbol (e.g. "700.HK") + * @param callback Async callback + * @param userdata User data passed to the callback + */ +void lb_content_context_topics(const struct lb_content_context_t *ctx, + const char *symbol, + lb_async_callback_t callback, + void *userdata); + +/** + * Get news list for a symbol + * + * @param ctx Content context + * @param symbol Security symbol (e.g. "700.HK") + * @param callback Async callback + * @param userdata User data passed to the callback + */ +void lb_content_context_news(const struct lb_content_context_t *ctx, + const char *symbol, + lb_async_callback_t callback, + void *userdata); + /** * Free the error object */ @@ -4507,6 +4669,14 @@ void lb_quote_context_realtime_candlesticks(const struct lb_quote_context_t *ctx lb_async_callback_t callback, void *userdata); +/** + * Get filings + */ +void lb_quote_context_filings(const struct lb_quote_context_t *ctx, + const char *symbol, + lb_async_callback_t callback, + void *userdata); + /** * Get security list */ diff --git a/c/src/content_context/context.rs b/c/src/content_context/context.rs new file mode 100644 index 000000000..4daa9e8d4 --- /dev/null +++ b/c/src/content_context/context.rs @@ -0,0 +1,102 @@ +use std::{ffi::c_void, os::raw::c_char, sync::Arc}; + +use longbridge::content::ContentContext; + +use crate::{ + async_call::{CAsyncCallback, CAsyncResult, execute_async}, + config::CConfig, + content_context::types::{CNewsItemOwned, CTopicItemOwned}, + types::{CVec, cstr_to_rust}, +}; + +/// Content context +pub struct CContentContext { + ctx: ContentContext, +} + +/// Create a new `ContentContext` +/// +/// @param config Config object +/// @param callback Async callback +/// @param userdata User data passed to the callback +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_content_context_new( + config: *const CConfig, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let config = Arc::new((*config).0.clone()); + let userdata_pointer = userdata as usize; + + execute_async( + callback, + std::ptr::null_mut::(), + userdata, + async move { + let ctx = ContentContext::try_new(config)?; + let arc_ctx = Arc::new(CContentContext { ctx }); + let ctx = Arc::into_raw(arc_ctx); + Ok(CAsyncResult { + ctx: ctx as *const c_void, + error: std::ptr::null(), + data: std::ptr::null_mut(), + length: 0, + userdata: userdata_pointer as *mut c_void, + }) + }, + ); +} + +/// Retain the content context (increment reference count) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_content_context_retain(ctx: *const CContentContext) { + Arc::increment_strong_count(ctx); +} + +/// Release the content context (decrement reference count) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_content_context_release(ctx: *const CContentContext) { + let _ = Arc::from_raw(ctx); +} + +/// Get discussion topics list for a symbol +/// +/// @param ctx Content context +/// @param symbol Security symbol (e.g. "700.HK") +/// @param callback Async callback +/// @param userdata User data passed to the callback +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_content_context_topics( + ctx: *const CContentContext, + symbol: *const c_char, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let symbol = cstr_to_rust(symbol); + execute_async(callback, ctx, userdata, async move { + let rows: CVec = ctx_inner.topics(symbol).await?.into(); + Ok(rows) + }); +} + +/// Get news list for a symbol +/// +/// @param ctx Content context +/// @param symbol Security symbol (e.g. "700.HK") +/// @param callback Async callback +/// @param userdata User data passed to the callback +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_content_context_news( + ctx: *const CContentContext, + symbol: *const c_char, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let symbol = cstr_to_rust(symbol); + execute_async(callback, ctx, userdata, async move { + let rows: CVec = ctx_inner.news(symbol).await?.into(); + Ok(rows) + }); +} diff --git a/c/src/content_context/mod.rs b/c/src/content_context/mod.rs new file mode 100644 index 000000000..513e07073 --- /dev/null +++ b/c/src/content_context/mod.rs @@ -0,0 +1,2 @@ +mod context; +mod types; diff --git a/c/src/content_context/types.rs b/c/src/content_context/types.rs new file mode 100644 index 000000000..f864d6e89 --- /dev/null +++ b/c/src/content_context/types.rs @@ -0,0 +1,175 @@ +use std::os::raw::c_char; + +use longbridge::content::{NewsItem, TopicItem}; + +use crate::types::{CString, ToFFI}; + +/// Topic item +#[repr(C)] +pub struct CTopicItem { + /// Topic ID + pub id: *const c_char, + /// Title + pub title: *const c_char, + /// Description + pub description: *const c_char, + /// URL + pub url: *const c_char, + /// Published time (Unix timestamp) + pub published_at: i64, + /// Comments count + pub comments_count: i32, + /// Likes count + pub likes_count: i32, + /// Shares count + pub shares_count: i32, +} + +#[derive(Debug)] +pub(crate) struct CTopicItemOwned { + id: CString, + title: CString, + description: CString, + url: CString, + published_at: i64, + comments_count: i32, + likes_count: i32, + shares_count: i32, +} + +impl From for CTopicItemOwned { + fn from(item: TopicItem) -> Self { + let TopicItem { + id, + title, + description, + url, + published_at, + comments_count, + likes_count, + shares_count, + } = item; + CTopicItemOwned { + id: id.into(), + title: title.into(), + description: description.into(), + url: url.into(), + published_at: published_at.unix_timestamp(), + comments_count, + likes_count, + shares_count, + } + } +} + +impl ToFFI for CTopicItemOwned { + type FFIType = CTopicItem; + + fn to_ffi_type(&self) -> Self::FFIType { + let CTopicItemOwned { + id, + title, + description, + url, + published_at, + comments_count, + likes_count, + shares_count, + } = self; + CTopicItem { + id: id.to_ffi_type(), + title: title.to_ffi_type(), + description: description.to_ffi_type(), + url: url.to_ffi_type(), + published_at: *published_at, + comments_count: *comments_count, + likes_count: *likes_count, + shares_count: *shares_count, + } + } +} + +/// News item +#[repr(C)] +pub struct CNewsItem { + /// News ID + pub id: *const c_char, + /// Title + pub title: *const c_char, + /// Description + pub description: *const c_char, + /// URL + pub url: *const c_char, + /// Published time (Unix timestamp) + pub published_at: i64, + /// Comments count + pub comments_count: i32, + /// Likes count + pub likes_count: i32, + /// Shares count + pub shares_count: i32, +} + +#[derive(Debug)] +pub(crate) struct CNewsItemOwned { + id: CString, + title: CString, + description: CString, + url: CString, + published_at: i64, + comments_count: i32, + likes_count: i32, + shares_count: i32, +} + +impl From for CNewsItemOwned { + fn from(item: NewsItem) -> Self { + let NewsItem { + id, + title, + description, + url, + published_at, + comments_count, + likes_count, + shares_count, + } = item; + CNewsItemOwned { + id: id.into(), + title: title.into(), + description: description.into(), + url: url.into(), + published_at: published_at.unix_timestamp(), + comments_count, + likes_count, + shares_count, + } + } +} + +impl ToFFI for CNewsItemOwned { + type FFIType = CNewsItem; + + fn to_ffi_type(&self) -> Self::FFIType { + let CNewsItemOwned { + id, + title, + description, + url, + published_at, + comments_count, + likes_count, + shares_count, + } = self; + CNewsItem { + id: id.to_ffi_type(), + title: title.to_ffi_type(), + description: description.to_ffi_type(), + url: url.to_ffi_type(), + published_at: *published_at, + comments_count: *comments_count, + likes_count: *likes_count, + shares_count: *shares_count, + } + } +} diff --git a/c/src/lib.rs b/c/src/lib.rs index fadb087bc..17faa3aa4 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -3,6 +3,7 @@ mod async_call; mod callback; mod config; +mod content_context; mod error; mod http_client; mod oauth; diff --git a/c/src/quote_context/context.rs b/c/src/quote_context/context.rs index 7cee329fd..a506ad3f8 100644 --- a/c/src/quote_context/context.rs +++ b/c/src/quote_context/context.rs @@ -26,8 +26,8 @@ use crate::{ }, types::{ CCandlestickOwned, CCapitalDistributionResponseOwned, CCapitalFlowLineOwned, - CCreateWatchlistGroup, CHistoryMarketTemperatureResponseOwned, CIntradayLineOwned, - CIssuerInfoOwned, CMarketTemperatureOwned, CMarketTradingDaysOwned, + CCreateWatchlistGroup, CFilingItemOwned, CHistoryMarketTemperatureResponseOwned, + CIntradayLineOwned, CIssuerInfoOwned, CMarketTemperatureOwned, CMarketTradingDaysOwned, CMarketTradingSessionOwned, COptionQuoteOwned, CParticipantInfoOwned, CPushBrokers, CPushBrokersOwned, CPushCandlestick, CPushCandlestickOwned, CPushDepth, CPushDepthOwned, CPushQuote, CPushQuoteOwned, CPushTrades, CPushTradesOwned, @@ -1180,6 +1180,22 @@ pub unsafe extern "C" fn lb_quote_context_realtime_candlesticks( }); } +/// Get filings +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_quote_context_filings( + ctx: *const CQuoteContext, + symbol: *const c_char, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let symbol = cstr_to_rust(symbol); + execute_async(callback, ctx, userdata, async move { + let rows: CVec = ctx_inner.filings(symbol).await?.into(); + Ok(rows) + }); +} + /// Get security list #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_quote_context_security_list( diff --git a/c/src/quote_context/types.rs b/c/src/quote_context/types.rs index 68d9c473b..ade90d445 100644 --- a/c/src/quote_context/types.rs +++ b/c/src/quote_context/types.rs @@ -2,7 +2,7 @@ use std::os::raw::c_char; use longbridge::quote::{ Brokers, Candlestick, CapitalDistribution, CapitalDistributionResponse, CapitalFlowLine, Depth, - HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, + FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionDirection, OptionQuote, OptionType, ParticipantInfo, Period, PrePostQuote, PushBrokers, PushCandlestick, PushDepth, PushQuote, PushTrades, QuotePackageDetail, RealtimeQuote, Security, SecurityBoard, SecurityBrokers, @@ -3021,3 +3021,77 @@ impl ToFFI for CHistoryMarketTemperatureResponseOwned { } } } + +/// Filing item +#[repr(C)] +pub struct CFilingItem { + /// Filing ID + pub id: *const c_char, + /// Title + pub title: *const c_char, + /// Description + pub description: *const c_char, + /// File name + pub file_name: *const c_char, + /// File URLs + pub file_urls: *const *const c_char, + /// Number of file URLs + pub num_file_urls: usize, + /// Published time (Unix timestamp) + pub published_at: i64, +} + +#[derive(Debug)] +pub(crate) struct CFilingItemOwned { + id: CString, + title: CString, + description: CString, + file_name: CString, + file_urls: CVec, + published_at: i64, +} + +impl From for CFilingItemOwned { + fn from(item: FilingItem) -> Self { + let FilingItem { + id, + title, + description, + file_name, + file_urls, + published_at, + } = item; + CFilingItemOwned { + id: id.into(), + title: title.into(), + description: description.into(), + file_name: file_name.into(), + file_urls: file_urls.into(), + published_at: published_at.unix_timestamp(), + } + } +} + +impl ToFFI for CFilingItemOwned { + type FFIType = CFilingItem; + + fn to_ffi_type(&self) -> Self::FFIType { + let CFilingItemOwned { + id, + title, + description, + file_name, + file_urls, + published_at, + } = self; + CFilingItem { + id: id.to_ffi_type(), + title: title.to_ffi_type(), + description: description.to_ffi_type(), + file_name: file_name.to_ffi_type(), + file_urls: file_urls.to_ffi_type(), + num_file_urls: file_urls.len(), + published_at: *published_at, + } + } +} diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 52ce02baa..f0ed2834d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -2,6 +2,7 @@ include_directories(../c/csrc/include include) set(SOURCES src/http_client.cpp src/config.cpp + src/content_context.cpp src/decimal.cpp src/status.cpp src/types.cpp diff --git a/cpp/include/content_context.hpp b/cpp/include/content_context.hpp new file mode 100644 index 000000000..10f9b23e8 --- /dev/null +++ b/cpp/include/content_context.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "async_result.hpp" +#include "callback.hpp" +#include "config.hpp" +#include "types.hpp" + +typedef struct lb_content_context_t lb_content_context_t; + +namespace longbridge { +namespace content { + +/// Content context +class ContentContext +{ +private: + const lb_content_context_t* ctx_; + +public: + ContentContext(); + ContentContext(const lb_content_context_t* ctx); + ContentContext(const ContentContext& ctx); + ContentContext(ContentContext&& ctx); + ~ContentContext(); + + ContentContext& operator=(const ContentContext& ctx); + + static void create(const Config& config, + AsyncCallback callback); + + /// Get discussion topics list for a symbol + void topics(const std::string& symbol, + AsyncCallback> callback) const; + + /// Get news list for a symbol + void news(const std::string& symbol, + AsyncCallback> callback) const; +}; + +} // namespace content +} // namespace longbridge diff --git a/cpp/include/longbridge.hpp b/cpp/include/longbridge.hpp index 4e00aea8d..9902091e0 100644 --- a/cpp/include/longbridge.hpp +++ b/cpp/include/longbridge.hpp @@ -5,5 +5,6 @@ #include "http_client.hpp" #include "oauth.hpp" #include "push.hpp" +#include "content_context.hpp" #include "quote_context.hpp" #include "trade_context.hpp" diff --git a/cpp/include/quote_context.hpp b/cpp/include/quote_context.hpp index 2d0b238e4..6bff78920 100644 --- a/cpp/include/quote_context.hpp +++ b/cpp/include/quote_context.hpp @@ -239,6 +239,11 @@ class QuoteContext void update_watchlist_group(const UpdateWatchlistGroup& req, AsyncCallback callback) const; + /// Get filings + void filings(const std::string& symbol, + AsyncCallback> callback) + const; + /// Get security list void security_list( Market market, diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index 74aff043b..032b13fa6 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -1246,6 +1246,23 @@ struct HistoryMarketTemperatureResponse std::vector records; }; +/// Filing item +struct FilingItem +{ + /// Filing ID + std::string id; + /// Title + std::string title; + /// Description + std::string description; + /// File name + std::string file_name; + /// File URLs + std::vector file_urls; + /// Published time (Unix timestamp) + int64_t published_at; +}; + } // namespace quote namespace trade { @@ -2066,4 +2083,50 @@ struct EstimateMaxPurchaseQuantityResponse } // namespace trade +namespace content { + +/// Topic item +struct TopicItem +{ + /// Topic ID + std::string id; + /// Title + std::string title; + /// Description + std::string description; + /// URL + std::string url; + /// Published time (Unix timestamp) + int64_t published_at; + /// Comments count + int32_t comments_count; + /// Likes count + int32_t likes_count; + /// Shares count + int32_t shares_count; +}; + +/// News item +struct NewsItem +{ + /// News ID + std::string id; + /// Title + std::string title; + /// Description + std::string description; + /// URL + std::string url; + /// Published time (Unix timestamp) + int64_t published_at; + /// Comments count + int32_t comments_count; + /// Likes count + int32_t likes_count; + /// Shares count + int32_t shares_count; +}; + +} // namespace content + } // namespace longbridge \ No newline at end of file diff --git a/cpp/src/content_context.cpp b/cpp/src/content_context.cpp new file mode 100644 index 000000000..d05b8da0a --- /dev/null +++ b/cpp/src/content_context.cpp @@ -0,0 +1,142 @@ +#include "content_context.hpp" +#include "convert.hpp" +#include +#include + +namespace longbridge { +namespace content { + +using longbridge::convert::convert; + +ContentContext::ContentContext() + : ctx_(nullptr) +{ +} + +ContentContext::ContentContext(const lb_content_context_t* ctx) +{ + ctx_ = ctx; + if (ctx_) { + lb_content_context_retain(ctx_); + } +} + +ContentContext::ContentContext(const ContentContext& ctx) +{ + ctx_ = ctx.ctx_; + if (ctx_) { + lb_content_context_retain(ctx_); + } +} + +ContentContext::ContentContext(ContentContext&& ctx) +{ + ctx_ = ctx.ctx_; + ctx.ctx_ = nullptr; +} + +ContentContext::~ContentContext() +{ + if (ctx_) { + lb_content_context_release(ctx_); + } +} + +ContentContext& +ContentContext::operator=(const ContentContext& ctx) +{ + ctx_ = ctx.ctx_; + if (ctx_) { + lb_content_context_retain(ctx_); + } + return *this; +} + +void +ContentContext::create(const Config& config, + AsyncCallback callback) +{ + lb_content_context_new( + config, + [](auto res) { + auto callback_ptr = + callback::get_async_callback(res->userdata); + auto* ctx_ptr = (lb_content_context_t*)res->ctx; + ContentContext ctx(ctx_ptr); + if (ctx_ptr) { + lb_content_context_release(ctx_ptr); + } + (*callback_ptr)( + AsyncResult(ctx, Status(res->error), nullptr)); + }, + new AsyncCallback(callback)); +} + +void +ContentContext::topics( + const std::string& symbol, + AsyncCallback> callback) const +{ + lb_content_context_topics( + ctx_, + symbol.c_str(), + [](auto res) { + auto callback_ptr = + callback::get_async_callback>( + res->userdata); + ContentContext ctx((const lb_content_context_t*)res->ctx); + Status status(res->error); + + if (status) { + auto rows = (const lb_topic_item_t*)res->data; + std::vector rows2; + std::transform(rows, + rows + res->length, + std::back_inserter(rows2), + [](auto row) { return convert(&row); }); + + (*callback_ptr)(AsyncResult>( + ctx, std::move(status), &rows2)); + } else { + (*callback_ptr)(AsyncResult>( + ctx, std::move(status), nullptr)); + } + }, + new AsyncCallback>(callback)); +} + +void +ContentContext::news( + const std::string& symbol, + AsyncCallback> callback) const +{ + lb_content_context_news( + ctx_, + symbol.c_str(), + [](auto res) { + auto callback_ptr = + callback::get_async_callback>( + res->userdata); + ContentContext ctx((const lb_content_context_t*)res->ctx); + Status status(res->error); + + if (status) { + auto rows = (const lb_news_item_t*)res->data; + std::vector rows2; + std::transform(rows, + rows + res->length, + std::back_inserter(rows2), + [](auto row) { return convert(&row); }); + + (*callback_ptr)(AsyncResult>( + ctx, std::move(status), &rows2)); + } else { + (*callback_ptr)(AsyncResult>( + ctx, std::move(status), nullptr)); + } + }, + new AsyncCallback>(callback)); +} + +} // namespace content +} // namespace longbridge diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index d92a02ede..f4e82ff8d 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -103,6 +103,9 @@ using longbridge::trade::SubmitOrderResponse; using longbridge::trade::TimeInForceType; using longbridge::trade::TopicType; using longbridge::trade::TriggerStatus; +using longbridge::quote::FilingItem; +using longbridge::content::NewsItem; +using longbridge::content::TopicItem; inline lb_language_t convert(Language language) @@ -2197,6 +2200,48 @@ convert(const lb_history_market_temperature_response_t* resp) records }; } +inline FilingItem +convert(const lb_filing_item_t* item) +{ + std::vector file_urls; + std::transform(item->file_urls, + item->file_urls + item->num_file_urls, + std::back_inserter(file_urls), + [](auto url) { return std::string(url); }); + return FilingItem{ item->id, + item->title, + item->description, + item->file_name, + file_urls, + item->published_at }; +} + +inline TopicItem +convert(const lb_topic_item_t* item) +{ + return TopicItem{ item->id, + item->title, + item->description, + item->url, + item->published_at, + item->comments_count, + item->likes_count, + item->shares_count }; +} + +inline NewsItem +convert(const lb_news_item_t* item) +{ + return NewsItem{ item->id, + item->title, + item->description, + item->url, + item->published_at, + item->comments_count, + item->likes_count, + item->shares_count }; +} + } // namespace convert } // namespace longbridge diff --git a/cpp/src/quote_context.cpp b/cpp/src/quote_context.cpp index 7d2e61a22..687295316 100644 --- a/cpp/src/quote_context.cpp +++ b/cpp/src/quote_context.cpp @@ -1279,6 +1279,38 @@ QuoteContext::update_watchlist_group( new AsyncCallback(callback)); } +void +QuoteContext::filings(const std::string& symbol, + AsyncCallback> callback) + const +{ + lb_quote_context_filings( + ctx_, + symbol.c_str(), + [](auto res) { + auto callback_ptr = + callback::get_async_callback>( + res->userdata); + QuoteContext ctx((const lb_quote_context_t*)res->ctx); + Status status(res->error); + + if (status) { + auto* rows = (const lb_filing_item_t*)res->data; + std::vector items; + std::transform(rows, + rows + res->length, + std::back_inserter(items), + [](auto row) { return convert(&row); }); + (*callback_ptr)(AsyncResult>( + ctx, std::move(status), &items)); + } else { + (*callback_ptr)(AsyncResult>( + ctx, std::move(status), nullptr)); + } + }, + new AsyncCallback>(callback)); +} + void QuoteContext::security_list( Market market, diff --git a/java/javasrc/src/main/java/com/longbridge/SdkNative.java b/java/javasrc/src/main/java/com/longbridge/SdkNative.java index 2aaf99ed3..65d084656 100644 --- a/java/javasrc/src/main/java/com/longbridge/SdkNative.java +++ b/java/javasrc/src/main/java/com/longbridge/SdkNative.java @@ -5,6 +5,7 @@ import java.util.function.Consumer; import org.scijava.nativelib.NativeLoader; +import com.longbridge.content.*; import com.longbridge.quote.*; import com.longbridge.trade.*; @@ -56,6 +57,14 @@ public static native void oauthBuild(String clientId, int callbackPort, public static native void freeOAuth(long oauth); + public static native void newContentContext(long config, AsyncCallback callback); + + public static native void freeContentContext(long context); + + public static native void contentContextTopics(long context, String symbol, AsyncCallback callback); + + public static native void contentContextNews(long context, String symbol, AsyncCallback callback); + public static native void newQuoteContext(long config, AsyncCallback callback); public static native void freeQuoteContext(long config); @@ -153,6 +162,8 @@ public static native void quoteContextDeleteWatchlistGroup(long context, DeleteW public static native void quoteContextUpdateWatchlistGroup(long context, UpdateWatchlistGroup req, AsyncCallback callback); + public static native void quoteContextFilings(long context, String symbol, AsyncCallback callback); + public static native void quoteContextSecurityList(long context, Market market, SecurityListCategory category, AsyncCallback callback); diff --git a/java/javasrc/src/main/java/com/longbridge/content/ContentContext.java b/java/javasrc/src/main/java/com/longbridge/content/ContentContext.java new file mode 100644 index 000000000..53210e764 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/content/ContentContext.java @@ -0,0 +1,59 @@ +package com.longbridge.content; + +import java.util.concurrent.CompletableFuture; + +import com.longbridge.*; + +/** + * Content context + */ +public class ContentContext implements AutoCloseable { + private long raw; + + /** + * Create a ContentContext object + * + * @param config Config object + * @return A Future representing the result of the operation + * @throws OpenApiException If an error occurs + */ + public static CompletableFuture create(Config config) + throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.newContentContext(config.getRaw(), callback); + }); + } + + @Override + public void close() throws Exception { + SdkNative.freeContentContext(raw); + } + + /** + * Get discussion topics list + * + * @param symbol Security symbol + * @return A Future representing the result of the operation + * @throws OpenApiException If an error occurs + */ + public CompletableFuture getTopics(String symbol) + throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.contentContextTopics(raw, symbol, callback); + }); + } + + /** + * Get news list + * + * @param symbol Security symbol + * @return A Future representing the result of the operation + * @throws OpenApiException If an error occurs + */ + public CompletableFuture getNews(String symbol) + throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.contentContextNews(raw, symbol, callback); + }); + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/content/NewsItem.java b/java/javasrc/src/main/java/com/longbridge/content/NewsItem.java new file mode 100644 index 000000000..9a4dcba30 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/content/NewsItem.java @@ -0,0 +1,96 @@ +package com.longbridge.content; + +import java.time.OffsetDateTime; + +/** + * News item + */ +public class NewsItem { + private String id; + private String title; + private String description; + private String url; + private OffsetDateTime publishedAt; + private int commentsCount; + private int likesCount; + private int sharesCount; + + /** + * Returns the news ID. + * + * @return the news ID + */ + public String getId() { + return id; + } + + /** + * Returns the title. + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * Returns the description. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns the URL. + * + * @return the URL + */ + public String getUrl() { + return url; + } + + /** + * Returns the published time. + * + * @return the published time + */ + public OffsetDateTime getPublishedAt() { + return publishedAt; + } + + /** + * Returns the comments count. + * + * @return the comments count + */ + public int getCommentsCount() { + return commentsCount; + } + + /** + * Returns the likes count. + * + * @return the likes count + */ + public int getLikesCount() { + return likesCount; + } + + /** + * Returns the shares count. + * + * @return the shares count + */ + public int getSharesCount() { + return sharesCount; + } + + @Override + public String toString() { + return "NewsItem [id=" + id + ", title=" + title + ", description=" + description + + ", url=" + url + ", publishedAt=" + publishedAt + ", commentsCount=" + commentsCount + + ", likesCount=" + likesCount + ", sharesCount=" + sharesCount + "]"; + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/content/TopicItem.java b/java/javasrc/src/main/java/com/longbridge/content/TopicItem.java new file mode 100644 index 000000000..3a1b1f44a --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/content/TopicItem.java @@ -0,0 +1,96 @@ +package com.longbridge.content; + +import java.time.OffsetDateTime; + +/** + * Topic item + */ +public class TopicItem { + private String id; + private String title; + private String description; + private String url; + private OffsetDateTime publishedAt; + private int commentsCount; + private int likesCount; + private int sharesCount; + + /** + * Returns the topic ID. + * + * @return the topic ID + */ + public String getId() { + return id; + } + + /** + * Returns the title. + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * Returns the description. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns the URL. + * + * @return the URL + */ + public String getUrl() { + return url; + } + + /** + * Returns the published time. + * + * @return the published time + */ + public OffsetDateTime getPublishedAt() { + return publishedAt; + } + + /** + * Returns the comments count. + * + * @return the comments count + */ + public int getCommentsCount() { + return commentsCount; + } + + /** + * Returns the likes count. + * + * @return the likes count + */ + public int getLikesCount() { + return likesCount; + } + + /** + * Returns the shares count. + * + * @return the shares count + */ + public int getSharesCount() { + return sharesCount; + } + + @Override + public String toString() { + return "TopicItem [id=" + id + ", title=" + title + ", description=" + description + + ", url=" + url + ", publishedAt=" + publishedAt + ", commentsCount=" + commentsCount + + ", likesCount=" + likesCount + ", sharesCount=" + sharesCount + "]"; + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/FilingItem.java b/java/javasrc/src/main/java/com/longbridge/quote/FilingItem.java new file mode 100644 index 000000000..6c4d751fd --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/FilingItem.java @@ -0,0 +1,74 @@ +package com.longbridge.quote; + +import java.time.OffsetDateTime; + +/** + * Filing item + */ +public class FilingItem { + private String id; + private String title; + private String description; + private String fileName; + private String[] fileUrls; + private OffsetDateTime publishedAt; + + /** + * Returns the filing ID. + * + * @return the filing ID + */ + public String getId() { + return id; + } + + /** + * Returns the title. + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * Returns the description. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Returns the file name. + * + * @return the file name + */ + public String getFileName() { + return fileName; + } + + /** + * Returns the file URLs. + * + * @return the file URLs + */ + public String[] getFileUrls() { + return fileUrls; + } + + /** + * Returns the published time. + * + * @return the published time + */ + public OffsetDateTime getPublishedAt() { + return publishedAt; + } + + @Override + public String toString() { + return "FilingItem [id=" + id + ", title=" + title + ", fileName=" + fileName + "]"; + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java b/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java index 2d3e9f72c..cdc318afc 100644 --- a/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java +++ b/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java @@ -1067,6 +1067,20 @@ public CompletableFuture updateWatchlistGroup(UpdateWatchlistGroup req) th }); } + /** + * Get filings list + * + * @param symbol Security symbol + * @return A Future representing the result of the operation + * @throws OpenApiException If an error occurs + */ + public CompletableFuture getFilings(String symbol) + throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.quoteContextFilings(this.raw, symbol, callback); + }); + } + /** * Security list * diff --git a/java/src/content_context.rs b/java/src/content_context.rs new file mode 100644 index 000000000..004b41ace --- /dev/null +++ b/java/src/content_context.rs @@ -0,0 +1,95 @@ +use std::sync::Arc; + +use jni::{ + JNIEnv, + errors::Result, + objects::{JClass, JObject, JValueOwned}, +}; +use longbridge::{Config, content::ContentContext}; + +use crate::{ + async_util, + error::jni_result, + init::CONTENT_CONTEXT_CLASS, + types::{FromJValue, IntoJValue, ObjectArray, set_field}, +}; + +struct ContextObj { + ctx: ContentContext, +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_newContentContext( + mut env: JNIEnv, + _class: JClass, + config: i64, + callback: JObject, +) { + struct ContextObjRef(i64); + + impl IntoJValue for ContextObjRef { + fn into_jvalue<'a>(self, env: &mut JNIEnv<'a>) -> Result> { + let ctx_obj = env.new_object(CONTENT_CONTEXT_CLASS.get().unwrap(), "()V", &[])?; + set_field(env, &ctx_obj, "raw", self.0)?; + Ok(JValueOwned::from(ctx_obj)) + } + } + + jni_result(&mut env, (), |env| { + let config = Arc::new((*(config as *const Config)).clone()); + + async_util::execute(env, callback, async move { + let ctx = ContentContext::try_new(config)?; + Ok(ContextObjRef( + Box::into_raw(Box::new(ContextObj { ctx })) as i64 + )) + })?; + + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_freeContentContext( + _env: JNIEnv, + _class: JClass, + ctx: i64, +) { + let _ = Box::from_raw(ctx as *mut ContextObj); +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_contentContextTopics( + mut env: JNIEnv, + _class: JClass, + context: i64, + symbol: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + async_util::execute(env, callback, async move { + Ok(ObjectArray(context.ctx.topics(symbol).await?)) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_contentContextNews( + mut env: JNIEnv, + _class: JClass, + context: i64, + symbol: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + async_util::execute(env, callback, async move { + Ok(ObjectArray(context.ctx.news(symbol).await?)) + })?; + Ok(()) + }) +} diff --git a/java/src/init.rs b/java/src/init.rs index ff7089d15..f1993e9e9 100644 --- a/java/src/init.rs +++ b/java/src/init.rs @@ -20,6 +20,7 @@ pub(crate) static TIME_LOCALDATETIME_CLASS: OnceLock = OnceLock::new( pub(crate) static TIME_ZONE_ID: OnceLock = OnceLock::new(); pub(crate) static QUOTE_CONTEXT_CLASS: OnceLock = OnceLock::new(); pub(crate) static TRADE_CONTEXT_CLASS: OnceLock = OnceLock::new(); +pub(crate) static CONTENT_CONTEXT_CLASS: OnceLock = OnceLock::new(); pub(crate) static DERIVATIVE_TYPE_CLASS: OnceLock = OnceLock::new(); pub(crate) static OPENAPI_EXCEPTION_CLASS: OnceLock = OnceLock::new(); @@ -72,7 +73,11 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( (DERIVATIVE_TYPE_CLASS, "com/longbridge/quote/DerivativeType"), (OPENAPI_EXCEPTION_CLASS, "com/longbridge/OpenApiException"), (QUOTE_CONTEXT_CLASS, "com/longbridge/quote/QuoteContext"), - (TRADE_CONTEXT_CLASS, "com/longbridge/trade/TradeContext") + (TRADE_CONTEXT_CLASS, "com/longbridge/trade/TradeContext"), + ( + CONTENT_CONTEXT_CLASS, + "com/longbridge/content/ContentContext" + ) ); init_timezone_id(&mut env); @@ -158,6 +163,7 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::quote::QuotePackageDetail, longbridge::quote::MarketTemperature, longbridge::quote::HistoryMarketTemperatureResponse, + longbridge::quote::FilingItem, longbridge::trade::PushOrderChanged, longbridge::trade::Execution, longbridge::trade::Order, @@ -178,6 +184,8 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::trade::OrderChargeItem, longbridge::trade::OrderChargeDetail, longbridge::trade::OrderDetail, - longbridge::trade::EstimateMaxPurchaseQuantityResponse + longbridge::trade::EstimateMaxPurchaseQuantityResponse, + longbridge::content::TopicItem, + longbridge::content::NewsItem ); } diff --git a/java/src/lib.rs b/java/src/lib.rs index 2d9a84325..dd6e3be33 100644 --- a/java/src/lib.rs +++ b/java/src/lib.rs @@ -3,6 +3,7 @@ mod async_util; mod config; +mod content_context; mod error; mod http_client; mod init; diff --git a/java/src/quote_context.rs b/java/src/quote_context.rs index b18250629..8e90c8cf7 100644 --- a/java/src/quote_context.rs +++ b/java/src/quote_context.rs @@ -1097,6 +1097,24 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextRealtime }) } +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextFilings( + mut env: JNIEnv, + _class: JClass, + context: i64, + symbol: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + async_util::execute(env, callback, async move { + Ok(ObjectArray(context.ctx.filings(symbol).await?)) + })?; + Ok(()) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextSecurityList( mut env: JNIEnv, diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index adfa4cad2..ce0bf48d8 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -965,3 +965,47 @@ impl_java_class!( records ] ); + +impl_java_class!( + "com/longbridge/quote/FilingItem", + longbridge::quote::FilingItem, + [ + id, + title, + description, + file_name, + #[java(objarray)] + file_urls, + published_at + ] +); + +impl_java_class!( + "com/longbridge/content/TopicItem", + longbridge::content::TopicItem, + [ + id, + title, + description, + url, + published_at, + comments_count, + likes_count, + shares_count + ] +); + +impl_java_class!( + "com/longbridge/content/NewsItem", + longbridge::content::NewsItem, + [ + id, + title, + description, + url, + published_at, + comments_count, + likes_count, + shares_count + ] +); diff --git a/mcp/src/main.rs b/mcp/src/main.rs index 83506b9e3..7918b439f 100644 --- a/mcp/src/main.rs +++ b/mcp/src/main.rs @@ -3,7 +3,7 @@ mod server; use std::{path::PathBuf, sync::Arc}; use clap::Parser; -use longbridge::{Config, QuoteContext, TradeContext}; +use longbridge::{Config, QuoteContext, TradeContext, content::ContentContext}; use poem::{EndpointExt, Route, Server, listener::TcpListener, middleware::Cors}; use poem_mcpserver::{McpServer, stdio::stdio, streamable_http}; use server::Longbridge; @@ -49,11 +49,12 @@ async fn main() -> Result<(), Box> { ); let (quote_context, _) = QuoteContext::try_new(config.clone()).await?; let (trade_context, _) = TradeContext::try_new(config.clone()).await?; + let content_context = ContentContext::try_new(config.clone())?; let readonly = cli.readonly; if !cli.http { tracing::info!("Starting MCP server with stdio transport"); - let server = create_mcp_server(quote_context, trade_context, readonly); + let server = create_mcp_server(quote_context, trade_context, content_context, readonly); stdio(server).await?; } else { tracing::info!( @@ -65,7 +66,12 @@ async fn main() -> Result<(), Box> { .at( "/", streamable_http::endpoint(move |_| { - create_mcp_server(quote_context.clone(), trade_context.clone(), readonly) + create_mcp_server( + quote_context.clone(), + trade_context.clone(), + content_context.clone(), + readonly, + ) }), ) .with(Cors::new()); @@ -78,9 +84,14 @@ async fn main() -> Result<(), Box> { fn create_mcp_server( quote_context: QuoteContext, trade_context: TradeContext, + content_context: ContentContext, readonly: bool, ) -> McpServer { - let mut server = McpServer::new().tools(Longbridge::new(quote_context, trade_context)); + let mut server = McpServer::new().tools(Longbridge::new( + quote_context, + trade_context, + content_context, + )); if readonly { server = server.disable_tools(["submit_order"]); } diff --git a/mcp/src/server.rs b/mcp/src/server.rs index 1ee8198c4..44d50d2a1 100644 --- a/mcp/src/server.rs +++ b/mcp/src/server.rs @@ -1,7 +1,8 @@ use longbridge::{ Decimal, Error, Market, QuoteContext, TradeContext, + content::{ContentContext, NewsItem, TopicItem}, quote::{ - AdjustType, Candlestick, CapitalDistributionResponse, CapitalFlowLine, + AdjustType, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, HistoryMarketTemperatureResponse, MarketTemperature, MarketTradingDays, OptionQuote, ParticipantInfo, Period, SecurityBrokers, SecurityDepth, SecurityQuote, SecurityStaticInfo, StrikePriceInfo, Trade, TradeSessions, @@ -25,14 +26,20 @@ const DATE_FORMAT: &[BorrowedFormatItem] = format_description!("[year]-[month]-[ pub(crate) struct Longbridge { quote_context: QuoteContext, trade_context: TradeContext, + content_context: ContentContext, } impl Longbridge { #[inline] - pub(crate) fn new(quote_context: QuoteContext, trade_context: TradeContext) -> Self { + pub(crate) fn new( + quote_context: QuoteContext, + trade_context: TradeContext, + content_context: ContentContext, + ) -> Self { Self { quote_context, trade_context, + content_context, } } } @@ -554,4 +561,37 @@ impl Longbridge { .map(Json) .collect::>()) } + + /// Get news list for a stock symbol. + async fn news(&self, symbol: String) -> Result>, Error> { + Ok(self + .content_context + .news(symbol) + .await? + .into_iter() + .map(Json) + .collect()) + } + + /// Get discussion topics list for a stock symbol. + async fn topics(&self, symbol: String) -> Result>, Error> { + Ok(self + .content_context + .topics(symbol) + .await? + .into_iter() + .map(Json) + .collect()) + } + + /// Get filings list for a stock symbol. + async fn filings(&self, symbol: String) -> Result>, Error> { + Ok(self + .quote_context + .filings(symbol) + .await? + .into_iter() + .map(Json) + .collect()) + } } diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 0b527d497..31f2874c5 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -217,6 +217,16 @@ export declare class Config { static fromOAuth(oauth: OAuth, extra?: ExtraConfigParams | undefined | null): Config } +/** Content context */ +export declare class ContentContext { + /** Create a new `ContentContext` */ + static new(config: Config): Promise + /** Get discussion topics list */ + topics(symbol: string): Promise> + /** Get news list */ + news(symbol: string): Promise> +} + export declare class Decimal { static E(): Decimal static E_INVERSE(): Decimal @@ -409,6 +419,24 @@ export declare class Execution { get price(): Decimal } +/** Filing item */ +export declare class FilingItem { + toString(): string + toJSON(): any + /** Filing ID */ + get id(): string + /** Title */ + get title(): string + /** Description */ + get description(): string + /** File name */ + get fileName(): string + /** File URLs */ + get fileUrls(): Array + /** Published time */ + get publishedAt(): Date +} + /** Frozen transaction fee */ export declare class FrozenTransactionFee { toString(): string @@ -608,6 +636,28 @@ export declare class NaiveDatetime { toJSON(): any } +/** News item */ +export declare class NewsItem { + toString(): string + toJSON(): any + /** News ID */ + get id(): string + /** Title */ + get title(): string + /** Description */ + get description(): string + /** URL */ + get url(): string + /** Published time */ + get publishedAt(): Date + /** Comments count */ + get commentsCount(): number + /** Likes count */ + get likesCount(): number + /** Shares count */ + get sharesCount(): number +} + /** * OAuth 2.0 client handle for Longbridge OpenAPI * @@ -1542,6 +1592,8 @@ export declare class QuoteContext { * ``` */ updateWatchlistGroup(req: UpdateWatchlistGroup): Promise + /** Get filings list */ + filings(symbol: string): Promise> /** * Get security list * @@ -1989,11 +2041,33 @@ export declare class Subscription { export declare class Time { constructor(hour: number, minute: number, second: number) get hour(): number - get monute(): number + get minute(): number get toString(): string toJSON(): any } +/** Topic item */ +export declare class TopicItem { + toString(): string + toJSON(): any + /** Topic ID */ + get id(): string + /** Title */ + get title(): string + /** Description */ + get description(): string + /** URL */ + get url(): string + /** Published time */ + get publishedAt(): Date + /** Comments count */ + get commentsCount(): number + /** Likes count */ + get likesCount(): number + /** Shares count */ + get sharesCount(): number +} + /** Trade */ export declare class Trade { toString(): string diff --git a/nodejs/index.js b/nodejs/index.js index 61e3f8af4..7f197c5e3 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -402,10 +402,12 @@ module.exports.CapitalFlowLine = nativeBinding.CapitalFlowLine module.exports.CashFlow = nativeBinding.CashFlow module.exports.CashInfo = nativeBinding.CashInfo module.exports.Config = nativeBinding.Config +module.exports.ContentContext = nativeBinding.ContentContext module.exports.Decimal = nativeBinding.Decimal module.exports.Depth = nativeBinding.Depth module.exports.EstimateMaxPurchaseQuantityResponse = nativeBinding.EstimateMaxPurchaseQuantityResponse module.exports.Execution = nativeBinding.Execution +module.exports.FilingItem = nativeBinding.FilingItem module.exports.FrozenTransactionFee = nativeBinding.FrozenTransactionFee module.exports.FundPosition = nativeBinding.FundPosition module.exports.FundPositionChannel = nativeBinding.FundPositionChannel @@ -420,6 +422,7 @@ module.exports.MarketTradingDays = nativeBinding.MarketTradingDays module.exports.MarketTradingSession = nativeBinding.MarketTradingSession module.exports.NaiveDate = nativeBinding.NaiveDate module.exports.NaiveDatetime = nativeBinding.NaiveDatetime +module.exports.NewsItem = nativeBinding.NewsItem module.exports.OAuth = nativeBinding.OAuth module.exports.OptionQuote = nativeBinding.OptionQuote module.exports.Order = nativeBinding.Order @@ -457,6 +460,7 @@ module.exports.StrikePriceInfo = nativeBinding.StrikePriceInfo module.exports.SubmitOrderResponse = nativeBinding.SubmitOrderResponse module.exports.Subscription = nativeBinding.Subscription module.exports.Time = nativeBinding.Time +module.exports.TopicItem = nativeBinding.TopicItem module.exports.Trade = nativeBinding.Trade module.exports.TradeContext = nativeBinding.TradeContext module.exports.TradingSessionInfo = nativeBinding.TradingSessionInfo diff --git a/nodejs/src/content/context.rs b/nodejs/src/content/context.rs new file mode 100644 index 000000000..609120d49 --- /dev/null +++ b/nodejs/src/content/context.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use napi::Result; + +use crate::{ + config::Config, + content::types::{NewsItem, TopicItem}, + error::ErrorNewType, +}; + +/// Content context +#[napi_derive::napi] +#[derive(Clone)] +pub struct ContentContext { + ctx: longbridge::content::ContentContext, +} + +#[napi_derive::napi] +impl ContentContext { + /// Create a new `ContentContext` + #[napi] + pub async fn new(config: &Config) -> napi::Result { + Ok(Self { + ctx: longbridge::content::ContentContext::try_new(Arc::new(config.0.clone())) + .map_err(ErrorNewType)?, + }) + } + + /// Get discussion topics list + #[napi] + pub async fn topics(&self, symbol: String) -> Result> { + self.ctx + .topics(symbol) + .await + .map_err(ErrorNewType)? + .into_iter() + .map(TryInto::try_into) + .collect() + } + + /// Get news list + #[napi] + pub async fn news(&self, symbol: String) -> Result> { + self.ctx + .news(symbol) + .await + .map_err(ErrorNewType)? + .into_iter() + .map(TryInto::try_into) + .collect() + } +} diff --git a/nodejs/src/content/mod.rs b/nodejs/src/content/mod.rs new file mode 100644 index 000000000..0561d4d5a --- /dev/null +++ b/nodejs/src/content/mod.rs @@ -0,0 +1,2 @@ +pub mod context; +pub mod types; diff --git a/nodejs/src/content/types.rs b/nodejs/src/content/types.rs new file mode 100644 index 000000000..5b9489fac --- /dev/null +++ b/nodejs/src/content/types.rs @@ -0,0 +1,50 @@ +use chrono::{DateTime, Utc}; +use longbridge_nodejs_macros::JsObject; + +/// Topic item +#[napi_derive::napi] +#[derive(Debug, JsObject, Clone)] +#[js(remote = "longbridge::content::TopicItem")] +pub struct TopicItem { + /// Topic ID + id: String, + /// Title + title: String, + /// Description + description: String, + /// URL + url: String, + /// Published time + #[js(datetime)] + published_at: DateTime, + /// Comments count + comments_count: i32, + /// Likes count + likes_count: i32, + /// Shares count + shares_count: i32, +} + +/// News item +#[napi_derive::napi] +#[derive(Debug, JsObject, Clone)] +#[js(remote = "longbridge::content::NewsItem")] +pub struct NewsItem { + /// News ID + id: String, + /// Title + title: String, + /// Description + description: String, + /// URL + url: String, + /// Published time + #[js(datetime)] + published_at: DateTime, + /// Comments count + comments_count: i32, + /// Likes count + likes_count: i32, + /// Shares count + shares_count: i32, +} diff --git a/nodejs/src/lib.rs b/nodejs/src/lib.rs index 6bdee3182..2c809a51f 100644 --- a/nodejs/src/lib.rs +++ b/nodejs/src/lib.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] mod config; +mod content; mod decimal; mod error; mod http_client; diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index 626250b1d..07c14fbc2 100644 --- a/nodejs/src/quote/context.rs +++ b/nodejs/src/quote/context.rs @@ -14,7 +14,7 @@ use crate::{ requests::{CreateWatchlistGroup, DeleteWatchlistGroup, UpdateWatchlistGroup}, types::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, + FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, QuotePackageDetail, RealtimeQuote, Security, SecurityBrokers, SecurityCalcIndex, @@ -1006,6 +1006,18 @@ impl QuoteContext { .map_err(ErrorNewType)?) } + /// Get filings list + #[napi] + pub async fn filings(&self, symbol: String) -> Result> { + self.ctx + .filings(symbol) + .await + .map_err(ErrorNewType)? + .into_iter() + .map(TryInto::try_into) + .collect() + } + /// Get security list /// /// #### Example diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index f5bd8a26e..11524eb1a 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1404,6 +1404,26 @@ pub struct MarketTemperature { timestamp: DateTime, } +/// Filing item +#[napi_derive::napi] +#[derive(Debug, JsObject, Clone)] +#[js(remote = "longbridge::quote::FilingItem")] +pub struct FilingItem { + /// Filing ID + id: String, + /// Title + title: String, + /// Description + description: String, + /// File name + file_name: String, + /// File URLs + file_urls: Vec, + /// Published time + #[js(datetime)] + published_at: DateTime, +} + /// Data granularity #[napi_derive::napi] #[derive(JsEnum, Debug, Hash, Eq, PartialEq, Copy, Clone)] diff --git a/python/docs/index.md b/python/docs/index.md index ca5cf6bda..f9fdfaeb9 100644 --- a/python/docs/index.md +++ b/python/docs/index.md @@ -23,7 +23,15 @@ - [AsyncTradeContext](reference_all.md#longbridge.openapi.AsyncTradeContext) Async trade API for use with asyncio; create via `AsyncTradeContext.create(config)` and await in asyncio. - + +- [ContentContext](reference_all.md#longbridge.openapi.ContentContext) + + The Content API part of the SDK, e.g.: get news, discussion topics for a security. + +- [AsyncContentContext](reference_all.md#longbridge.openapi.AsyncContentContext) + + Async content API for use with asyncio; create via `AsyncContentContext.create(config)` and await in asyncio. + ## Quickstart _Install Longbridge OpenAPI SDK_ @@ -137,6 +145,25 @@ resp = ctx.submit_order( print(resp) ``` +## Content API _(Get news and topics for a security)_ + +```python +from longbridge.openapi import Config, ContentContext + +config = Config.from_apikey_env() + +# Create a context for content APIs +ctx = ContentContext(config) + +# Get news for a security +news = ctx.news("700.HK") +print(news) + +# Get discussion topics for a security +topics = ctx.topics("700.HK") +print(topics) +``` + ## Asynchronous API The SDK provides async contexts and an async HTTP client for use with Python's `asyncio`. All I/O methods return awaitables; callbacks (e.g. for push events) are set the same way as in the sync API. diff --git a/python/src/content/context.rs b/python/src/content/context.rs new file mode 100644 index 000000000..416f65faf --- /dev/null +++ b/python/src/content/context.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; + +use longbridge::blocking::ContentContextSync; +use pyo3::prelude::*; + +use crate::{ + config::Config, + content::types::{NewsItem, TopicItem}, + error::ErrorNewType, +}; + +#[pyclass] +pub(crate) struct ContentContext { + ctx: ContentContextSync, +} + +#[pymethods] +impl ContentContext { + #[new] + fn new(config: &Config) -> PyResult { + Ok(Self { + ctx: ContentContextSync::try_new(Arc::new(config.0.clone())).map_err(ErrorNewType)?, + }) + } + + /// Get discussion topics list + pub fn topics(&self, symbol: String) -> PyResult> { + self.ctx + .topics(symbol) + .map_err(ErrorNewType)? + .into_iter() + .map(TryInto::try_into) + .collect() + } + + /// Get news list + pub fn news(&self, symbol: String) -> PyResult> { + self.ctx + .news(symbol) + .map_err(ErrorNewType)? + .into_iter() + .map(TryInto::try_into) + .collect() + } +} diff --git a/python/src/content/context_async.rs b/python/src/content/context_async.rs new file mode 100644 index 000000000..2e4d6784d --- /dev/null +++ b/python/src/content/context_async.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use longbridge::content::ContentContext; +use pyo3::{prelude::*, types::PyType}; + +use crate::{ + config::Config, + content::types::{NewsItem, TopicItem}, + error::ErrorNewType, +}; + +/// Async content context. +#[pyclass] +pub(crate) struct AsyncContentContext { + ctx: Arc, +} + +#[pymethods] +impl AsyncContentContext { + /// Create an async content context. + #[classmethod] + fn create(cls: &Bound, config: &Config) -> PyResult> { + let py = cls.py(); + let config = Arc::new(config.0.clone()); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(AsyncContentContext { + ctx: Arc::new(ContentContext::try_new(config).map_err(ErrorNewType)?), + }) + }) + .map(|b| b.unbind()) + } + + /// Get discussion topics list. Returns awaitable. + fn topics(&self, py: Python<'_>, symbol: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx.topics(symbol).await.map_err(ErrorNewType)?; + v.into_iter() + .map(|x| -> PyResult { x.try_into() }) + .collect::>>() + }) + .map(|b| b.unbind()) + } + + /// Get news list. Returns awaitable. + fn news(&self, py: Python<'_>, symbol: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx.news(symbol).await.map_err(ErrorNewType)?; + v.into_iter() + .map(|x| -> PyResult { x.try_into() }) + .collect::>>() + }) + .map(|b| b.unbind()) + } +} diff --git a/python/src/content/mod.rs b/python/src/content/mod.rs new file mode 100644 index 000000000..ce391b31f --- /dev/null +++ b/python/src/content/mod.rs @@ -0,0 +1,13 @@ +mod context; +mod context_async; +mod types; + +use pyo3::prelude::*; + +pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + Ok(()) +} diff --git a/python/src/content/types.rs b/python/src/content/types.rs new file mode 100644 index 000000000..cb186115e --- /dev/null +++ b/python/src/content/types.rs @@ -0,0 +1,50 @@ +use longbridge_python_macros::PyObject; +use pyo3::prelude::*; + +use crate::time::PyOffsetDateTimeWrapper; + +/// Topic item +#[pyclass(skip_from_py_object)] +#[derive(Debug, PyObject, Clone)] +#[py(remote = "longbridge::content::TopicItem")] +pub(crate) struct TopicItem { + /// Topic ID + id: String, + /// Title + title: String, + /// Description + description: String, + /// URL + url: String, + /// Published time + published_at: PyOffsetDateTimeWrapper, + /// Comments count + comments_count: i32, + /// Likes count + likes_count: i32, + /// Shares count + shares_count: i32, +} + +/// News item +#[pyclass(skip_from_py_object)] +#[derive(Debug, PyObject, Clone)] +#[py(remote = "longbridge::content::NewsItem")] +pub(crate) struct NewsItem { + /// News ID + id: String, + /// Title + title: String, + /// Description + description: String, + /// URL + url: String, + /// Published time + published_at: PyOffsetDateTimeWrapper, + /// Comments count + comments_count: i32, + /// Likes count + likes_count: i32, + /// Shares count + shares_count: i32, +} diff --git a/python/src/lib.rs b/python/src/lib.rs index 3cffa3199..dadfcb1a8 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,5 +1,6 @@ mod async_callback; mod config; +mod content; mod decimal; mod error; mod http_client; @@ -25,6 +26,7 @@ fn longbridge(py: Python<'_>, m: Bound) -> PyResult<()> { openapi.add_class::()?; quote::register_types(&openapi)?; trade::register_types(&openapi)?; + content::register_types(&openapi)?; m.add_submodule(&openapi)?; Ok(()) diff --git a/python/src/quote/context.rs b/python/src/quote/context.rs index a1b161b09..4ccaf623c 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -15,7 +15,7 @@ use crate::{ push::handle_push_event, types::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, + FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, QuotePackageDetail, RealtimeQuote, SecuritiesUpdateMode, Security, SecurityBrokers, @@ -530,6 +530,16 @@ impl QuoteContext { Ok(()) } + /// Get filings list + pub fn filings(&self, symbol: String) -> PyResult> { + self.ctx + .filings(symbol) + .map_err(ErrorNewType)? + .into_iter() + .map(TryInto::try_into) + .collect() + } + /// Get security list #[pyo3(signature = (market, category = None))] pub fn security_list( diff --git a/python/src/quote/context_async.rs b/python/src/quote/context_async.rs index de746a241..4b455f258 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -18,7 +18,7 @@ use crate::{ push::handle_push_event, types::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, + FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, QuotePackageDetail, RealtimeQuote, SecuritiesUpdateMode, Security, SecurityBrokers, @@ -699,6 +699,19 @@ impl AsyncQuoteContext { .map(|b| b.unbind()) } + /// Get filings list. Returns awaitable. + #[pyo3(signature = (symbol))] + fn filings(&self, py: Python<'_>, symbol: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx.filings(symbol).await.map_err(ErrorNewType)?; + v.into_iter() + .map(|x| -> PyResult { x.try_into() }) + .collect::>>() + }) + .map(|b| b.unbind()) + } + /// Get security list. Returns awaitable. #[pyo3(signature = (market, category = None))] fn security_list( diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index 8f8fc0e4f..25a888c3f 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1345,6 +1345,25 @@ pub(crate) enum TradeSessions { All, } +/// Filing item +#[pyclass(skip_from_py_object)] +#[derive(Debug, PyObject, Clone)] +#[py(remote = "longbridge::quote::FilingItem")] +pub(crate) struct FilingItem { + /// Filing ID + id: String, + /// Title + title: String, + /// Description + description: String, + /// File name + file_name: String, + /// File URLs + file_urls: Vec, + /// Published time + published_at: PyOffsetDateTimeWrapper, +} + /// Market temperature #[pyclass(skip_from_py_object)] #[derive(Debug, PyObject, Clone)] diff --git a/rust/crates/httpclient/src/client.rs b/rust/crates/httpclient/src/client.rs index b90d8cdd3..30f59f6ea 100644 --- a/rust/crates/httpclient/src/client.rs +++ b/rust/crates/httpclient/src/client.rs @@ -9,6 +9,7 @@ use serde::Deserialize; use crate::{HttpClientConfig, HttpClientError, HttpClientResult, Json, RequestBuilder}; /// Longbridge HTTP client +#[derive(Clone)] pub struct HttpClient { pub(crate) http_cli: Client, pub(crate) config: Arc, diff --git a/rust/src/blocking/content.rs b/rust/src/blocking/content.rs new file mode 100644 index 000000000..33477add2 --- /dev/null +++ b/rust/src/blocking/content.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use tokio::sync::mpsc; + +use crate::{ + Config, Result, + blocking::runtime::BlockingRuntime, + content::{ContentContext, NewsItem, TopicItem}, +}; + +/// Blocking content context +pub struct ContentContextSync { + rt: BlockingRuntime, +} + +impl ContentContextSync { + /// Create a `ContentContextSync` + pub fn try_new(config: Arc) -> Result { + let rt = BlockingRuntime::try_new( + move || async move { + let ctx = ContentContext::try_new(config)?; + let (tx, rx) = mpsc::unbounded_channel::(); + std::mem::forget(tx); // keep sender alive so event_rx never closes + Ok::<_, crate::Error>((ctx, rx)) + }, + |_: std::convert::Infallible| {}, + )?; + Ok(Self { rt }) + } + + /// Get discussion topics list + pub fn topics(&self, symbol: impl Into) -> Result> { + let symbol = symbol.into(); + self.rt + .call(move |ctx| async move { ctx.topics(symbol).await }) + } + + /// Get news list + pub fn news(&self, symbol: impl Into) -> Result> { + let symbol = symbol.into(); + self.rt + .call(move |ctx| async move { ctx.news(symbol).await }) + } +} diff --git a/rust/src/blocking/mod.rs b/rust/src/blocking/mod.rs index 42ba4ce4d..fc92ab07c 100644 --- a/rust/src/blocking/mod.rs +++ b/rust/src/blocking/mod.rs @@ -1,10 +1,12 @@ //! Longbridge OpenAPI SDK blocking API +mod content; mod error; mod quote; mod runtime; mod trade; +pub use content::ContentContextSync; pub use error::BlockingError; pub use quote::QuoteContextSync; pub use trade::TradeContextSync; diff --git a/rust/src/blocking/quote.rs b/rust/src/blocking/quote.rs index b0f7a1260..297ffc1a0 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -7,13 +7,14 @@ use crate::{ blocking::runtime::BlockingRuntime, quote::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, - IntradayLine, IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, - OptionQuote, ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, - RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, - SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, - SortOrderType, StrikePriceInfo, SubFlags, Subscription, Trade, TradeSessions, WarrantInfo, - WarrantQuote, WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, + FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, + HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, + MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, PushEvent, + QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, + RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, + SecurityListCategory, SecurityQuote, SecurityStaticInfo, SortOrderType, StrikePriceInfo, + SubFlags, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, + WarrantStatus, WarrantType, WatchlistGroup, }, }; @@ -901,6 +902,13 @@ impl QuoteContextSync { .call(move |ctx| async move { ctx.update_watchlist_group(req).await }) } + /// Get filings list + pub fn filings(&self, symbol: impl Into) -> Result> { + let symbol = symbol.into(); + self.rt + .call(move |ctx| async move { ctx.filings(symbol).await }) + } + /// Get security list pub fn security_list( &self, diff --git a/rust/src/content/context.rs b/rust/src/content/context.rs new file mode 100644 index 000000000..b00158d61 --- /dev/null +++ b/rust/src/content/context.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; + +use longbridge_httpcli::{HttpClient, Json, Method}; +use serde::Deserialize; + +use super::types::{NewsItem, TopicItem}; +use crate::{Config, Result}; + +struct InnerContentContext { + http_cli: HttpClient, +} + +/// Content context +#[derive(Clone)] +pub struct ContentContext(Arc); + +impl ContentContext { + /// Create a `ContentContext` + pub fn try_new(config: Arc) -> Result { + Ok(Self(Arc::new(InnerContentContext { + http_cli: config.create_http_client(), + }))) + } + + /// Get discussion topics list + pub async fn topics(&self, symbol: impl Into) -> Result> { + #[derive(Debug, Deserialize)] + struct Response { + items: Vec, + } + + let symbol = symbol.into(); + Ok(self + .0 + .http_cli + .request(Method::GET, format!("/v1/content/{symbol}/topics")) + .response::>() + .send() + .await? + .0 + .items) + } + + /// Get news list + pub async fn news(&self, symbol: impl Into) -> Result> { + #[derive(Debug, Deserialize)] + struct Response { + items: Vec, + } + + let symbol = symbol.into(); + Ok(self + .0 + .http_cli + .request(Method::GET, format!("/v1/content/{symbol}/news")) + .response::>() + .send() + .await? + .0 + .items) + } +} diff --git a/rust/src/content/mod.rs b/rust/src/content/mod.rs new file mode 100644 index 000000000..fdd32b94e --- /dev/null +++ b/rust/src/content/mod.rs @@ -0,0 +1,7 @@ +//! Content related types + +mod context; +mod types; + +pub use context::ContentContext; +pub use types::{NewsItem, TopicItem}; diff --git a/rust/src/content/types.rs b/rust/src/content/types.rs new file mode 100644 index 000000000..11e6320b5 --- /dev/null +++ b/rust/src/content/types.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use crate::serde_utils; + +/// Topic item +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopicItem { + /// Topic ID + pub id: String, + /// Title + #[serde(default)] + pub title: String, + /// Description + #[serde(default)] + pub description: String, + /// URL + pub url: String, + /// Published time + #[serde( + serialize_with = "time::serde::rfc3339::serialize", + deserialize_with = "serde_utils::timestamp::deserialize" + )] + pub published_at: OffsetDateTime, + /// Comments count + pub comments_count: i32, + /// Likes count + pub likes_count: i32, + /// Shares count + pub shares_count: i32, +} + +/// News item +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NewsItem { + /// News ID + pub id: String, + /// Title + #[serde(default)] + pub title: String, + /// Description + #[serde(default)] + pub description: String, + /// URL + pub url: String, + /// Published time + #[serde( + serialize_with = "time::serde::rfc3339::serialize", + deserialize_with = "serde_utils::timestamp::deserialize" + )] + pub published_at: OffsetDateTime, + /// Comments count + pub comments_count: i32, + /// Likes count + pub likes_count: i32, + /// Shares count + pub shares_count: i32, +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f00320624..fdee319e8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -18,10 +18,12 @@ mod types; pub mod blocking; pub use longbridge_oauth as oauth; +pub mod content; pub mod quote; pub mod trade; pub use config::{Config, Language, PushCandlestickMode}; +pub use content::ContentContext; pub use error::{Error, Result, SimpleError, SimpleErrorKind}; pub use longbridge_httpcli as httpclient; pub use longbridge_wscli as wsclient; diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index c8c1bee10..fd71a3b03 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -12,7 +12,7 @@ use crate::{ Config, Error, Language, Market, Result, quote::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, + FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, @@ -1562,6 +1562,33 @@ impl QuoteContext { .list) } + /// Get filings list + pub async fn filings(&self, symbol: impl Into) -> Result> { + #[derive(Debug, Serialize)] + struct Request { + symbol: String, + } + + #[derive(Debug, Deserialize)] + struct Response { + items: Vec, + } + + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/quote/filings") + .query_params(Request { + symbol: symbol.into(), + }) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0 + .items) + } + /// Get current market temperature /// /// Reference: diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index 16c4ad6e9..a7deae152 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -18,13 +18,13 @@ pub use push_types::{ pub use sub_flags::SubFlags; pub use types::{ Brokers, CalcIndex, Candlestick, CapitalDistribution, CapitalDistributionResponse, - CapitalFlowLine, Depth, DerivativeType, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, - Granularity, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionDirection, OptionQuote, OptionType, - ParticipantInfo, PrePostQuote, QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, - RequestUpdateWatchlistGroup, SecuritiesUpdateMode, Security, SecurityBoard, SecurityBrokers, - SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, - SortOrderType, StrikePriceInfo, Subscription, Trade, TradeDirection, TradeSession, - TradeSessions, TradingSessionInfo, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, - WarrantType, WatchlistGroup, WatchlistSecurity, + CapitalFlowLine, Depth, DerivativeType, FilingItem, FilterWarrantExpiryDate, + FilterWarrantInOutBoundsType, Granularity, HistoryMarketTemperatureResponse, IntradayLine, + IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionDirection, + OptionQuote, OptionType, ParticipantInfo, PrePostQuote, QuotePackageDetail, RealtimeQuote, + RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, SecuritiesUpdateMode, Security, + SecurityBoard, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, + SecurityQuote, SecurityStaticInfo, SortOrderType, StrikePriceInfo, Subscription, Trade, + TradeDirection, TradeSession, TradeSessions, TradingSessionInfo, WarrantInfo, WarrantQuote, + WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, WatchlistSecurity, }; diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index ae0a7e700..57ebf92df 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -1981,6 +1981,29 @@ pub struct HistoryMarketTemperatureResponse { pub records: Vec, } +/// Filing item +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FilingItem { + /// Filing ID + pub id: String, + /// Title + pub title: String, + /// Description + #[serde(default)] + pub description: String, + /// File name + pub file_name: String, + /// File URLs + pub file_urls: Vec, + /// Published time + #[serde( + rename = "publish_at", + serialize_with = "time::serde::rfc3339::serialize", + deserialize_with = "crate::serde_utils::timestamp::deserialize" + )] + pub published_at: OffsetDateTime, +} + impl_serde_for_enum_string!(Granularity); impl_default_for_enum_string!( OptionType,