From 05470a167152b9ebad59cd5ce5177cdab2fa02af Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 13 Jun 2024 10:48:18 +0800 Subject: [PATCH 01/13] Remove `GitHub:` prefix from feishu notify. --- .github/workflows/issue-notify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index 4b2f45cf1..412e699de 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -13,6 +13,6 @@ jobs: msg_type: text content: | text: | - GitHub: ${{ github.event.issue.title }} #${{ github.event.issue.number }} + ${{ github.event.issue.title }} #${{ github.event.issue.number }} ${{ github.event.issue.html_url }} From d90c86a03ecc6669faeef2ffec7d462422d88545 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 25 Jun 2024 17:17:10 +0800 Subject: [PATCH 02/13] Avoid feishu notify twice. --- .github/workflows/issue-notify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index 412e699de..846f79759 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -1,7 +1,7 @@ name: Issue Notify on: issues: - types: [opened, edited] + types: [opened] jobs: notify: From f353e09a153e16161f3693d2e69ef5f70a2da557 Mon Sep 17 00:00:00 2001 From: Sunli Date: Wed, 3 Jul 2024 15:08:37 +0800 Subject: [PATCH 03/13] Update serde_utils.rs --- rust/src/serde_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/serde_utils.rs b/rust/src/serde_utils.rs index 0d27f4fdf..4858326ba 100644 --- a/rust/src/serde_utils.rs +++ b/rust/src/serde_utils.rs @@ -74,7 +74,7 @@ pub(crate) mod date_opt { &value, time::macros::format_description!("[year]-[month]-[day]"), ) - .map_err(|_| Error::custom("invalid timestamp"))?; + .map_err(|_| Error::custom("invalid date"))?; Ok(Some(datetime)) } else { Ok(None) From 27f90bbf6f8f31252b8aacbb19444ca85ef29a5e Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 11 Jul 2024 10:59:31 +0800 Subject: [PATCH 04/13] add support for specify push candlestick mode --- CMakeLists.txt | 1 + c/cbindgen.toml | 1 + c/csrc/include/longport.h | 19 +- c/src/config.rs | 12 +- c/src/types/mod.rs | 2 + c/src/types/push_candlestick_mode.rs | 15 + cpp/CMakeLists.txt | 2 +- cpp/include/config.hpp | 28 +- cpp/include/status.hpp | 6 +- cpp/include/types.hpp | 9 + cpp/src/config.cpp | 26 +- cpp/src/convert.hpp | 13 + cpp/src/status.cpp | 4 +- .../cpp/subscribe_candlesticks/CMakeLists.txt | 4 + examples/cpp/subscribe_candlesticks/main.cpp | 61 +++ .../rust/subscribe_candlesticks/src/main.rs | 9 +- java/c/com_longport_SdkNative.h | 4 +- .../src/main/java/com/longport/Config.java | 2 + .../main/java/com/longport/ConfigBuilder.java | 15 +- .../com/longport/PushCandlestickMode.java | 15 + .../src/main/java/com/longport/SdkNative.java | 3 +- java/src/config.rs | 8 +- java/src/init.rs | 1 + java/src/types/enum_types.rs | 6 + nodejs/Cargo.toml | 4 +- nodejs/index.d.ts | 16 + nodejs/index.js | 3 +- nodejs/src/config.rs | 14 +- nodejs/src/types.rs | 10 + python/pysrc/longport/openapi.pyi | 16 + python/src/config.rs | 9 +- python/src/lib.rs | 1 + python/src/types.rs | 11 + rust/crates/candlesticks/src/lib.rs | 4 +- rust/crates/candlesticks/src/market.rs | 9 + rust/crates/candlesticks/src/merger.rs | 377 ++++++++++++++++-- rust/crates/wsclient/Cargo.toml | 1 + rust/crates/wsclient/src/client.rs | 2 + rust/src/config.rs | 30 ++ rust/src/lib.rs | 2 +- rust/src/quote/core.rs | 187 +++++++-- rust/src/quote/mod.rs | 2 - rust/src/quote/store.rs | 21 +- rust/src/types.rs | 11 + 44 files changed, 880 insertions(+), 116 deletions(-) create mode 100644 c/src/types/push_candlestick_mode.rs create mode 100644 examples/cpp/subscribe_candlesticks/CMakeLists.txt create mode 100644 examples/cpp/subscribe_candlesticks/main.cpp create mode 100644 java/javasrc/src/main/java/com/longport/PushCandlestickMode.java diff --git a/CMakeLists.txt b/CMakeLists.txt index 178ebbda0..866f8f666 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ add_subdirectory(examples/c/http_client) add_subdirectory(examples/cpp/submit_order) add_subdirectory(examples/cpp/subscribe_quote) +add_subdirectory(examples/cpp/subscribe_candlesticks) add_subdirectory(examples/cpp/get_quote) add_subdirectory(examples/cpp/today_orders) add_subdirectory(examples/cpp/http_client) diff --git a/c/cbindgen.toml b/c/cbindgen.toml index 282faec32..554b3fee3 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -8,6 +8,7 @@ cpp_compat = true "CHeader" = "lb_http_header_t" "CHttpResult" = "lb_http_result_t" "CLanguage" = "lb_language_t" +"CPushCandlestickMode" = "lb_push_candlestick_mode_t" "CMarket" = "lb_market_t" "CDecimal" = "lb_decimal_t" "CAsyncCallback" = "lb_async_callback_t" diff --git a/c/csrc/include/longport.h b/c/csrc/include/longport.h index 4c6439a61..6e6b89c57 100644 --- a/c/csrc/include/longport.h +++ b/c/csrc/include/longport.h @@ -724,6 +724,20 @@ typedef enum lb_period_t { PeriodYear, } lb_period_t; +/** + * Language identifer + */ +typedef enum lb_push_candlestick_mode_t { + /** + * Real-time + */ + PushCandlestickMode_Realtime, + /** + * Confirmed + */ + PushCandlestickMode_Confirmed, +} lb_push_candlestick_mode_t; + /** * Trade session */ @@ -3606,6 +3620,8 @@ extern "C" { * `wss://openapi-trade.longportapp.com/v2`) * - `LONGPORT_ENABLE_OVERNIGHT` - Enable overnight quote, `true` or `false` * (Default: `false`) + * - `LONGPORT_PUSH_CANDLESTICK_MODE` - `realtime` or `confirmed` (Default: + * `realtime`) */ struct lb_config_t *lb_config_from_env(struct lb_error_t **error); @@ -3616,7 +3632,8 @@ struct lb_config_t *lb_config_new(const char *app_key, const char *quote_ws_url, const char *trade_ws_url, const enum lb_language_t *language, - bool enable_overight); + bool enable_overight, + const enum lb_push_candlestick_mode_t *push_candlestick_mode); /** * Free the config object diff --git a/c/src/config.rs b/c/src/config.rs index bffd235fe..3fa939be0 100644 --- a/c/src/config.rs +++ b/c/src/config.rs @@ -10,7 +10,7 @@ use time::OffsetDateTime; use crate::{ async_call::{execute_async, CAsyncCallback}, error::{set_error, CError}, - types::{CLanguage, CString}, + types::{CLanguage, CPushCandlestickMode, CString}, }; /// Configuration options for LongPort sdk @@ -33,6 +33,8 @@ pub struct CConfig(pub(crate) Arc); /// `wss://openapi-trade.longportapp.com/v2`) /// - `LONGPORT_ENABLE_OVERNIGHT` - Enable overnight quote, `true` or `false` /// (Default: `false`) +/// - `LONGPORT_PUSH_CANDLESTICK_MODE` - `realtime` or `confirmed` (Default: +/// `realtime`) #[no_mangle] pub unsafe extern "C" fn lb_config_from_env(error: *mut *mut CError) -> *mut CConfig { match Config::from_env() { @@ -57,6 +59,7 @@ pub unsafe extern "C" fn lb_config_new( trade_ws_url: *const c_char, language: *const CLanguage, enable_overight: bool, + push_candlestick_mode: *const CPushCandlestickMode, ) -> *mut CConfig { let app_key = CStr::from_ptr(app_key).to_str().expect("invalid app key"); let app_secret = CStr::from_ptr(app_secret) @@ -70,6 +73,7 @@ pub unsafe extern "C" fn lb_config_new( if !http_url.is_null() { config = config.http_url(CStr::from_ptr(http_url).to_str().expect("invalid http url")); } + if !quote_ws_url.is_null() { config = config.quote_ws_url( CStr::from_ptr(quote_ws_url) @@ -77,6 +81,7 @@ pub unsafe extern "C" fn lb_config_new( .expect("invalid quote websocket url"), ); } + if !trade_ws_url.is_null() { config = config.trade_ws_url( CStr::from_ptr(trade_ws_url) @@ -84,6 +89,7 @@ pub unsafe extern "C" fn lb_config_new( .expect("invalid trade websocket url"), ); } + if !language.is_null() { config = config.language((*language).into()); } @@ -92,6 +98,10 @@ pub unsafe extern "C" fn lb_config_new( config = config.enable_overnight(); } + if !push_candlestick_mode.is_null() { + config = config.push_candlestick_mode((*push_candlestick_mode).into()); + } + Box::into_raw(Box::new(CConfig(Arc::new(config)))) } diff --git a/c/src/types/mod.rs b/c/src/types/mod.rs index abc0689e9..5eb1bdb57 100644 --- a/c/src/types/mod.rs +++ b/c/src/types/mod.rs @@ -5,6 +5,7 @@ mod decimal; mod language; mod market; mod option; +mod push_candlestick_mode; mod string; use std::{ffi::CStr, os::raw::c_char}; @@ -16,6 +17,7 @@ pub(crate) use decimal::CDecimal; pub(crate) use language::CLanguage; pub(crate) use market::CMarket; pub(crate) use option::COption; +pub(crate) use push_candlestick_mode::CPushCandlestickMode; pub(crate) use string::CString; pub(crate) trait ToFFI { diff --git a/c/src/types/push_candlestick_mode.rs b/c/src/types/push_candlestick_mode.rs new file mode 100644 index 000000000..26053fd66 --- /dev/null +++ b/c/src/types/push_candlestick_mode.rs @@ -0,0 +1,15 @@ +use longport_c_macros::CEnum; + +/// Language identifer +#[derive(Debug, Copy, Clone, Eq, PartialEq, CEnum)] +#[c(remote = "longport::PushCandlestickMode")] +#[allow(clippy::enum_variant_names, non_camel_case_types)] +#[repr(C)] +pub enum CPushCandlestickMode { + /// Real-time + #[c(remote = "Realtime")] + PushCandlestickMode_Realtime, + /// Confirmed + #[c(remote = "Confirmed")] + PushCandlestickMode_Confirmed, +} diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index a9ca2c794..20d4fb24b 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -13,7 +13,7 @@ add_library( ${SOURCES} ) if(NOT CMAKE_HOST_LINUX) -target_link_libraries(longport_cpp longport-c) +target_link_libraries(longport_cpp longport_c) endif() add_subdirectory(test) diff --git a/cpp/include/config.hpp b/cpp/include/config.hpp index 9eb26c3fa..74bcc2aa8 100644 --- a/cpp/include/config.hpp +++ b/cpp/include/config.hpp @@ -35,6 +35,8 @@ class Config * @param trade_ws_url Trade websocket endpoint url (Default: * wss://openapi-trade.longportapp.com/v2) * @param language Language identifer (Default: Language::EN) + * @param push_candlestick_mode Push candlestick mode (Default: + * PushCandlestickMode::Realtime) */ Config(const std::string& app_key, const std::string& app_secret, @@ -43,7 +45,29 @@ class Config const std::optional& quote_ws_url, const std::optional& trade_ws_url, const std::optional& language, - bool enable_overnight); + bool enable_overnight, + const std::optional& push_candlestick_mode); + + /** Config + * + * @param app_key App key + * @param app_secret App secret + * @param access_token Access token + */ + Config(const std::string& app_key, + const std::string& app_secret, + const std::string& access_token) + : Config(app_key, + app_secret, + access_token, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + false, + std::nullopt) + { + } ~Config(); @@ -67,6 +91,8 @@ class Config /// `wss://openapi-trade.longportapp.com/v2`) /// - `LONGPORT_ENABLE_OVERNIGHT` - Enable overnight quote, `true` or /// `false` (Default: `false`) + /// - `LONGPORT_PUSH_CANDLESTICK_MODE` - `realtime` or `confirmed` (Default: + /// `realtime`) static Status from_env(Config& config); /// Gets a new `access_token` diff --git a/cpp/include/status.hpp b/cpp/include/status.hpp index ad680a43b..5dae8e4b9 100644 --- a/cpp/include/status.hpp +++ b/cpp/include/status.hpp @@ -1,5 +1,7 @@ #pragma once +#include + typedef struct lb_error_t lb_error_t; namespace longport { @@ -13,7 +15,7 @@ class Status Status(); Status(const lb_error_t* err); Status(lb_error_t* err); - Status(Status&& status); + Status(Status&& status) noexcept; ~Status(); inline operator bool() { return is_ok(); } @@ -25,7 +27,7 @@ class Status bool is_err() const; /// Returns the error code - int code() const; + int64_t code() const; /// Returns the error message const char* message() const; diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index 0a8fb04e5..a8cf990ec 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -37,6 +37,15 @@ enum class Language EN, }; +/// Push candlestick mode +enum class PushCandlestickMode +{ + /// Real-time + Realtime, + /// Confirmed + Confirmed, +}; + /// Market enum class Market { diff --git a/cpp/src/config.cpp b/cpp/src/config.cpp index a2cd2a5d5..c261fb2d4 100644 --- a/cpp/src/config.cpp +++ b/cpp/src/config.cpp @@ -28,21 +28,29 @@ Config::Config(const std::string& app_key, const std::optional& quote_ws_url, const std::optional& trade_ws_url, const std::optional& language, - bool enable_overnight) + bool enable_overnight, + const std::optional& push_candlestick_mode) { lb_language_t c_language; if (language) { c_language = convert::convert(*language); } - config_ = lb_config_new(app_key.c_str(), - app_secret.c_str(), - access_token.c_str(), - http_url ? http_url->c_str() : nullptr, - quote_ws_url ? quote_ws_url->c_str() : nullptr, - trade_ws_url ? trade_ws_url->c_str() : nullptr, - language ? &c_language : nullptr, - enable_overnight); + lb_push_candlestick_mode_t c_push_candlestick_mode; + if (push_candlestick_mode) { + c_push_candlestick_mode = convert::convert(*push_candlestick_mode); + } + + config_ = + lb_config_new(app_key.c_str(), + app_secret.c_str(), + access_token.c_str(), + http_url ? http_url->c_str() : nullptr, + quote_ws_url ? quote_ws_url->c_str() : nullptr, + trade_ws_url ? trade_ws_url->c_str() : nullptr, + language ? &c_language : nullptr, + enable_overnight, + push_candlestick_mode ? &c_push_candlestick_mode : nullptr); } Config::~Config() diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index f105d9737..5390391f3 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -112,6 +112,19 @@ convert(Language language) } } +inline lb_push_candlestick_mode_t +convert(PushCandlestickMode mode) +{ + switch (mode) { + case PushCandlestickMode::Realtime: + return PushCandlestickMode_Realtime; + case PushCandlestickMode::Confirmed: + return PushCandlestickMode_Confirmed; + default: + throw std::invalid_argument("unreachable"); + } +} + inline Market convert(lb_market_t market) { diff --git a/cpp/src/status.cpp b/cpp/src/status.cpp index 9ea8e8c33..87d8f966e 100644 --- a/cpp/src/status.cpp +++ b/cpp/src/status.cpp @@ -21,7 +21,7 @@ Status::Status(lb_error_t* err) need_free_ = true; } -Status::Status(Status&& status) +Status::Status(Status&& status) noexcept { err_ = status.err_; need_free_ = status.need_free_; @@ -51,7 +51,7 @@ Status::is_err() const } /// Returns the error code -int +int64_t Status::code() const { return lb_error_code(err_); diff --git a/examples/cpp/subscribe_candlesticks/CMakeLists.txt b/examples/cpp/subscribe_candlesticks/CMakeLists.txt new file mode 100644 index 000000000..27ba56faf --- /dev/null +++ b/examples/cpp/subscribe_candlesticks/CMakeLists.txt @@ -0,0 +1,4 @@ +include_directories(../../../cpp/include) + +add_executable(subscribe_candlesticks_cpp main.cpp) +target_link_libraries(subscribe_candlesticks_cpp longport_cpp) diff --git a/examples/cpp/subscribe_candlesticks/main.cpp b/examples/cpp/subscribe_candlesticks/main.cpp new file mode 100644 index 000000000..04e54e8cb --- /dev/null +++ b/examples/cpp/subscribe_candlesticks/main.cpp @@ -0,0 +1,61 @@ +#include +#include + +#ifdef WIN32 +#include +#endif + +using namespace longport; +using namespace longport::quote; + +int +main(int argc, char const* argv[]) +{ +#ifdef WIN32 + SetConsoleOutputCP(CP_UTF8); +#endif + + Config config; + Status status = Config::from_env(config); + + if (!status) { + std::cout << "failed to load configuration from environment: " + << status.message() << std::endl; + return -1; + } + + QuoteContext ctx; + + QuoteContext::create(config, [&](auto res) { + if (!res) { + std::cout << "failed to create quote context: " << res.status().message() + << std::endl; + return; + } + + ctx = res.context(); + + res.context().set_on_candlestick([](auto event) { + std::cout << event->symbol + << " timestamp=" << event->candlestick.timestamp + << " close=" << (double)event->candlestick.close + << " open=" << (double)event->candlestick.open + << " high=" << (double)event->candlestick.high + << " low=" << (double)event->candlestick.low + << " volume=" << event->candlestick.volume + << " turnover=" << (double)event->candlestick.turnover + << std::endl; + }); + + res.context().subscribe_candlesticks("AAPL.US", Period::Min1, [](auto res) { + if (!res) { + std::cout << "failed to subscribe quote: " << res.status().message() + << std::endl; + return; + } + }); + }); + + std::cin.get(); + return 0; +} diff --git a/examples/rust/subscribe_candlesticks/src/main.rs b/examples/rust/subscribe_candlesticks/src/main.rs index 34f8b0b3e..024c3b68b 100644 --- a/examples/rust/subscribe_candlesticks/src/main.rs +++ b/examples/rust/subscribe_candlesticks/src/main.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use longport::{ quote::{Period, QuoteContext}, - Config, + Config, PushCandlestickMode, }; use tracing_subscriber::EnvFilter; @@ -12,9 +12,12 @@ async fn main() -> Result<(), Box> { .with_env_filter(EnvFilter::from_default_env()) .init(); - let config = Arc::new(Config::from_env()?); + let config = + Arc::new(Config::from_env()?.push_candlestick_mode(PushCandlestickMode::Confirmed)); let (ctx, mut receiver) = QuoteContext::try_new(config).await?; - ctx.subscribe_candlesticks("AAPL.US", Period::Day).await?; + println!("member id: {}", ctx.member_id()); + ctx.subscribe_candlesticks("600000.SH", Period::FiveMinute) + .await?; while let Some(event) = receiver.recv().await { println!("{:?}", event); diff --git a/java/c/com_longport_SdkNative.h b/java/c/com_longport_SdkNative.h index 4aca21910..1d73bdd02 100644 --- a/java/c/com_longport_SdkNative.h +++ b/java/c/com_longport_SdkNative.h @@ -50,10 +50,10 @@ JNIEXPORT void JNICALL Java_com_longport_SdkNative_httpClientRequest /* * Class: com_longport_SdkNative * Method: newConfig - * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/longport/Language;Z)J + * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/longport/Language;ZLcom/longport/PushCandlestickMode;)J */ JNIEXPORT jlong JNICALL Java_com_longport_SdkNative_newConfig - (JNIEnv *, jclass, jstring, jstring, jstring, jstring, jstring, jstring, jobject, jboolean); + (JNIEnv *, jclass, jstring, jstring, jstring, jstring, jstring, jstring, jobject, jboolean, jobject); /* * Class: com_longport_SdkNative diff --git a/java/javasrc/src/main/java/com/longport/Config.java b/java/javasrc/src/main/java/com/longport/Config.java index b1b9dd699..b560e2b4b 100644 --- a/java/javasrc/src/main/java/com/longport/Config.java +++ b/java/javasrc/src/main/java/com/longport/Config.java @@ -35,6 +35,8 @@ public class Config implements AutoCloseable { * `wss://openapi-trade.longportapp.com/v2`) * - `LONGPORT_ENABLE_OVERNIGHT` - Enable overnight quote, `true` or * `false` (Default: `false`) + * - `LONGPORT_PUSH_CANDLESTICK_MODE` - `realtime` or `confirmed` (Default: + * `realtime`) * * @return Config object * @throws OpenApiException If an error occurs diff --git a/java/javasrc/src/main/java/com/longport/ConfigBuilder.java b/java/javasrc/src/main/java/com/longport/ConfigBuilder.java index 3b31ca211..96fa60731 100644 --- a/java/javasrc/src/main/java/com/longport/ConfigBuilder.java +++ b/java/javasrc/src/main/java/com/longport/ConfigBuilder.java @@ -12,6 +12,7 @@ public class ConfigBuilder { private String tradeWsUrl; private Language language; private boolean enableOvernight; + private PushCandlestickMode pushCandlestickMode; /** * Create a `Config` object builder @@ -75,6 +76,7 @@ public ConfigBuilder tradeWebsocketUrl(String tradeWsUrl) { * @return this object */ public ConfigBuilder language(Language language) { + this.language = language; return this; } @@ -88,6 +90,17 @@ public ConfigBuilder enableOvernight() { return this; } + /** + * Specifies the push candlestick mode + * + * @param mode Mode (Default: PushCandlestickMode.Realtime) + * @return this object + */ + public ConfigBuilder pushCandlestickMode(PushCandlestickMode mode) { + this.pushCandlestickMode = mode; + return this; + } + /** * Build a Config object * @@ -97,6 +110,6 @@ public ConfigBuilder enableOvernight() { public Config build() throws OpenApiException { return new Config( SdkNative.newConfig(appKey, appSecret, accessToken, httpUrl, quoteWsUrl, tradeWsUrl, language, - enableOvernight)); + enableOvernight, pushCandlestickMode)); } } diff --git a/java/javasrc/src/main/java/com/longport/PushCandlestickMode.java b/java/javasrc/src/main/java/com/longport/PushCandlestickMode.java new file mode 100644 index 000000000..d30e2e6fb --- /dev/null +++ b/java/javasrc/src/main/java/com/longport/PushCandlestickMode.java @@ -0,0 +1,15 @@ +package com.longport; + +/** + * Push candlestick mode + */ +public enum PushCandlestickMode { + /** + * Real-time + */ + Realtime, + /** + * Confirmed + */ + Confirmed, +} diff --git a/java/javasrc/src/main/java/com/longport/SdkNative.java b/java/javasrc/src/main/java/com/longport/SdkNative.java index 100a6ea63..54b8f263d 100644 --- a/java/javasrc/src/main/java/com/longport/SdkNative.java +++ b/java/javasrc/src/main/java/com/longport/SdkNative.java @@ -25,7 +25,8 @@ public class SdkNative { public static native void httpClientRequest(long httpClient, String request, AsyncCallback callback); public static native long newConfig(String appKey, String appSecret, String accessToken, String httpUrl, - String quoteWsUrl, String tradeWsUrl, Language language, boolean enableOvernight); + String quoteWsUrl, String tradeWsUrl, Language language, boolean enableOvernight, + PushCandlestickMode mode); public static native long newConfigFromEnv(); diff --git a/java/src/config.rs b/java/src/config.rs index 2a5eb333a..05a8d2289 100644 --- a/java/src/config.rs +++ b/java/src/config.rs @@ -3,7 +3,7 @@ use jni::{ sys::{jboolean, jlong}, JNIEnv, }; -use longport::{Config, Language}; +use longport::{Config, Language, PushCandlestickMode}; use time::OffsetDateTime; use crate::{async_util, error::jni_result, types::FromJValue}; @@ -20,6 +20,7 @@ pub extern "system" fn Java_com_longport_SdkNative_newConfig( trade_ws_url: JString, language: JObject, enable_overnight: jboolean, + push_candlestick_mode: JObject, ) -> jlong { jni_result(&mut env, 0, |env| { let app_key = String::from_jvalue(env, app_key.into())?; @@ -29,6 +30,8 @@ pub extern "system" fn Java_com_longport_SdkNative_newConfig( let quote_ws_url = >::from_jvalue(env, quote_ws_url.into())?; let trade_ws_url = >::from_jvalue(env, trade_ws_url.into())?; let language = >::from_jvalue(env, language.into())?; + let push_candlestick_mode = + >::from_jvalue(env, push_candlestick_mode.into())?; let mut config = Config::new(app_key, app_secret, access_token); @@ -47,6 +50,9 @@ pub extern "system" fn Java_com_longport_SdkNative_newConfig( if enable_overnight > 0 { config = config.enable_overnight(); } + if let Some(mode) = push_candlestick_mode { + config = config.push_candlestick_mode(mode); + } Ok(Box::into_raw(Box::new(config)) as jlong) }) diff --git a/java/src/init.rs b/java/src/init.rs index cf3e1e867..80dec7205 100644 --- a/java/src/init.rs +++ b/java/src/init.rs @@ -78,6 +78,7 @@ pub extern "system" fn Java_com_longport_SdkNative_init<'a>( init_class_by_classloader!( env, longport::Language, + longport::PushCandlestickMode, longport::Market, longport::quote::TradeStatus, longport::quote::TradeSession, diff --git a/java/src/types/enum_types.rs b/java/src/types/enum_types.rs index 05a4fa731..60eb05abd 100644 --- a/java/src/types/enum_types.rs +++ b/java/src/types/enum_types.rs @@ -18,6 +18,12 @@ impl_java_enum!( [ZH_CN, ZH_HK, EN] ); +impl_java_enum!( + "com/longport/PushCandlestickMode", + longport::PushCandlestickMode, + [Realtime, Confirmed] +); + impl_java_enum!( "com/longport/Market", longport::Market, diff --git a/nodejs/Cargo.toml b/nodejs/Cargo.toml index 8d3b88558..441ac28b5 100644 --- a/nodejs/Cargo.toml +++ b/nodejs/Cargo.toml @@ -10,13 +10,13 @@ crate-type = ["cdylib"] longport = { path = "../rust" } longport-nodejs-macros = { path = "crates/macros" } -napi = { version = "2.16.1", default-features = false, features = [ +napi = { version = "2.16.8", default-features = false, features = [ "napi4", "chrono_date", "async", "serde-json", ] } -napi-derive = "2.16.1" +napi-derive = "2.16.8" rust_decimal = { version = "1.23.1", features = ["maths"] } chrono = "0.4.19" time = { version = "0.3.9", features = ["macros", "formatting"] } diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 1c6a420b6..a79355c94 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -27,6 +27,8 @@ export interface ConfigParams { language?: Language /** Enable overnight (default: false) */ enableOvernight?: boolean + /** Push candlesticks mode (default: PushCandlestickMode.Realtime) */ + pushCandlestickMode?: PushCandlestickMode } /** An request to create a watchlist group */ export interface CreateWatchlistGroup { @@ -737,6 +739,12 @@ export const enum Language { /** en */ EN = 2 } +export const enum PushCandlestickMode { + /** Realtime mode */ + Realtime = 0, + /** Confirmed mode */ + Confirmed = 1 +} /** Configuration for LongPort sdk */ export class Config { /** Create a new `Config` */ @@ -757,6 +765,8 @@ export class Config { * - `LONGPORT_TRADE_WS_URL` - Trade websocket endpoint url * - `LONGPORT_ENABLE_OVERNIGHT` - Enable overnight quote, `true` or * `false` (Default: `false`) + * - `LONGPORT_PUSH_CANDLESTICK_MODE` - `realtime` or `confirmed` (Default: + * `realtime`) */ static fromEnv(): Config /** @@ -813,6 +823,12 @@ export class Decimal { * `6.5` -> `6`, `7.5` -> `8` */ round(): Decimal + /** + * Returns a new Decimal number with the specified number of decimal + * points for fractional portion. Rounding currently follows “Bankers + * Rounding” rules. e.g. 6.5 -> 6, 7.5 -> 8 + */ + roundDp(dp: number): Decimal /** * Returns a new Decimal integral with no fractional portion. This is a * true truncation whereby no rounding is performed. diff --git a/nodejs/index.js b/nodejs/index.js index ab70e42e0..53ff354e1 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -295,7 +295,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Config, Decimal, HttpClient, QuoteContext, PushQuoteEvent, PushDepthEvent, PushBrokersEvent, PushTradesEvent, PushCandlestickEvent, Subscription, DerivativeType, TradeStatus, TradeSession, SubType, TradeDirection, OptionType, OptionDirection, WarrantType, Period, AdjustType, SecurityBoard, SecurityStaticInfo, PrePostQuote, SecurityQuote, OptionQuote, WarrantQuote, Depth, SecurityDepth, Brokers, SecurityBrokers, ParticipantInfo, Trade, IntradayLine, Candlestick, StrikePriceInfo, IssuerInfo, SortOrderType, WarrantSortBy, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, WarrantInfo, WarrantStatus, TradingSessionInfo, MarketTradingSession, RealtimeQuote, PushQuote, PushDepth, PushBrokers, PushTrades, PushCandlestick, MarketTradingDays, CapitalFlowLine, CapitalDistribution, CapitalDistributionResponse, WatchlistGroup, WatchlistSecurity, SecuritiesUpdateMode, CalcIndex, SecurityCalcIndex, SecurityListCategory, Security, NaiveDate, Time, NaiveDatetime, TradeContext, TopicType, Execution, OrderStatus, OrderSide, OrderType, OrderTag, TimeInForceType, TriggerStatus, OutsideRTH, Order, CommissionFreeStatus, DeductionStatus, ChargeCategoryCode, OrderHistoryDetail, OrderChargeFee, OrderChargeItem, OrderChargeDetail, OrderDetail, PushOrderChanged, SubmitOrderResponse, CashInfo, AccountBalance, BalanceType, CashFlowDirection, CashFlow, FundPositionsResponse, FundPositionChannel, FundPosition, StockPositionsResponse, StockPositionChannel, StockPosition, MarginRatio, EstimateMaxPurchaseQuantityResponse, Market, Language } = nativeBinding +const { Config, Decimal, HttpClient, QuoteContext, PushQuoteEvent, PushDepthEvent, PushBrokersEvent, PushTradesEvent, PushCandlestickEvent, Subscription, DerivativeType, TradeStatus, TradeSession, SubType, TradeDirection, OptionType, OptionDirection, WarrantType, Period, AdjustType, SecurityBoard, SecurityStaticInfo, PrePostQuote, SecurityQuote, OptionQuote, WarrantQuote, Depth, SecurityDepth, Brokers, SecurityBrokers, ParticipantInfo, Trade, IntradayLine, Candlestick, StrikePriceInfo, IssuerInfo, SortOrderType, WarrantSortBy, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, WarrantInfo, WarrantStatus, TradingSessionInfo, MarketTradingSession, RealtimeQuote, PushQuote, PushDepth, PushBrokers, PushTrades, PushCandlestick, MarketTradingDays, CapitalFlowLine, CapitalDistribution, CapitalDistributionResponse, WatchlistGroup, WatchlistSecurity, SecuritiesUpdateMode, CalcIndex, SecurityCalcIndex, SecurityListCategory, Security, NaiveDate, Time, NaiveDatetime, TradeContext, TopicType, Execution, OrderStatus, OrderSide, OrderType, OrderTag, TimeInForceType, TriggerStatus, OutsideRTH, Order, CommissionFreeStatus, DeductionStatus, ChargeCategoryCode, OrderHistoryDetail, OrderChargeFee, OrderChargeItem, OrderChargeDetail, OrderDetail, PushOrderChanged, SubmitOrderResponse, CashInfo, AccountBalance, BalanceType, CashFlowDirection, CashFlow, FundPositionsResponse, FundPositionChannel, FundPosition, StockPositionsResponse, StockPositionChannel, StockPosition, MarginRatio, EstimateMaxPurchaseQuantityResponse, Market, Language, PushCandlestickMode } = nativeBinding module.exports.Config = Config module.exports.Decimal = Decimal @@ -397,3 +397,4 @@ module.exports.MarginRatio = MarginRatio module.exports.EstimateMaxPurchaseQuantityResponse = EstimateMaxPurchaseQuantityResponse module.exports.Market = Market module.exports.Language = Language +module.exports.PushCandlestickMode = PushCandlestickMode diff --git a/nodejs/src/config.rs b/nodejs/src/config.rs index 00831adc9..f190a27aa 100644 --- a/nodejs/src/config.rs +++ b/nodejs/src/config.rs @@ -1,7 +1,11 @@ use chrono::{DateTime, Utc}; use napi::Result; -use crate::{error::ErrorNewType, types::Language, utils::from_datetime}; +use crate::{ + error::ErrorNewType, + types::{Language, PushCandlestickMode}, + utils::from_datetime, +}; /// Configuration parameters #[napi_derive::napi(object)] @@ -24,6 +28,8 @@ pub struct ConfigParams { pub language: Option, /// Enable overnight (default: false) pub enable_overnight: Option, + /// Push candlesticks mode (default: PushCandlestickMode.Realtime) + pub push_candlestick_mode: Option, } /// Configuration for LongPort sdk @@ -58,6 +64,10 @@ impl Config { config = config.enable_overnight(); } + if let Some(mode) = params.push_candlestick_mode { + config = config.push_candlestick_mode(mode.into()); + } + Self(config) } @@ -76,6 +86,8 @@ impl Config { /// - `LONGPORT_TRADE_WS_URL` - Trade websocket endpoint url /// - `LONGPORT_ENABLE_OVERNIGHT` - Enable overnight quote, `true` or /// `false` (Default: `false`) + /// - `LONGPORT_PUSH_CANDLESTICK_MODE` - `realtime` or `confirmed` (Default: + /// `realtime`) #[napi(factory)] pub fn from_env() -> Result { Ok(Self(longport::Config::from_env().map_err(ErrorNewType)?)) diff --git a/nodejs/src/types.rs b/nodejs/src/types.rs index 26f1ad283..b308f2832 100644 --- a/nodejs/src/types.rs +++ b/nodejs/src/types.rs @@ -28,3 +28,13 @@ pub enum Language { /// en EN, } + +#[napi_derive::napi] +#[derive(Debug, JsEnum, Hash, Eq, PartialEq)] +#[js(remote = "longport::PushCandlestickMode")] +pub enum PushCandlestickMode { + /// Realtime mode + Realtime, + /// Confirmed mode + Confirmed, +} diff --git a/python/pysrc/longport/openapi.pyi b/python/pysrc/longport/openapi.pyi index 02404b68f..fa507095a 100644 --- a/python/pysrc/longport/openapi.pyi +++ b/python/pysrc/longport/openapi.pyi @@ -72,6 +72,20 @@ class HttpClient: """ ... +class PushCandlestickMode: + """ + Push candlestick mode + """ + + class Realtime(PushCandlestickMode): + """ + Real-time + """ + + class Confirmed(PushCandlestickMode): + """ + Confirmed + """ class Config: """ @@ -86,6 +100,7 @@ class Config: trade_ws_url: Websocket url for trade API language: Language identifier enable_overnight: Enable overnight quote + push_candlestick_mode: Push candlestick mode """ def __init__( @@ -98,6 +113,7 @@ class Config: trade_ws_url: Optional[str] = None, language: Optional[Type[Language]] = None, enable_overnight: bool = False, + push_candlestick_mode: Type[PushCandlestickMode] = PushCandlestickMode.Realtime, ) -> None: ... @classmethod diff --git a/python/src/config.rs b/python/src/config.rs index 9b5b0aaa3..a9dcc2047 100644 --- a/python/src/config.rs +++ b/python/src/config.rs @@ -1,6 +1,10 @@ use pyo3::{prelude::*, types::PyType}; -use crate::{error::ErrorNewType, time::PyOffsetDateTimeWrapper, types::Language}; +use crate::{ + error::ErrorNewType, + time::PyOffsetDateTimeWrapper, + types::{Language, PushCandlestickMode}, +}; #[pyclass(name = "Config")] pub(crate) struct Config(pub(crate) longport::Config); @@ -17,6 +21,7 @@ impl Config { trade_ws_url = None, language = None, enable_overnight = false, + push_candlestick_mode = PushCandlestickMode::Realtime, ))] #[allow(clippy::too_many_arguments)] fn py_new( @@ -28,6 +33,7 @@ impl Config { trade_ws_url: Option, language: Option, enable_overnight: bool, + push_candlestick_mode: PushCandlestickMode, ) -> Self { let mut config = longport::Config::new(app_key, app_secret, access_token); if let Some(http_url) = http_url { @@ -45,6 +51,7 @@ impl Config { if enable_overnight { config = config.enable_overnight(); } + config = config.push_candlestick_mode(push_candlestick_mode.into()); Self(config) } diff --git a/python/src/lib.rs b/python/src/lib.rs index b1a01db83..1e4e4e83c 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -16,6 +16,7 @@ fn longport(py: Python<'_>, m: Bound) -> PyResult<()> { openapi.add_class::()?; openapi.add_class::()?; openapi.add_class::()?; + openapi.add_class::()?; openapi.add_class::()?; quote::register_types(&openapi)?; trade::register_types(&openapi)?; diff --git a/python/src/types.rs b/python/src/types.rs index 606968827..f36b579a7 100644 --- a/python/src/types.rs +++ b/python/src/types.rs @@ -29,3 +29,14 @@ pub(crate) enum Language { /// en EN, } + +#[pyclass] +#[derive(Debug, PyEnum, Copy, Clone, Hash, Eq, PartialEq)] +#[allow(non_camel_case_types)] +#[py(remote = "longport::PushCandlestickMode")] +pub(crate) enum PushCandlestickMode { + /// Realtime mode + Realtime, + /// Confirmed mode + Confirmed, +} diff --git a/rust/crates/candlesticks/src/lib.rs b/rust/crates/candlesticks/src/lib.rs index 58c484e85..f92dd761d 100644 --- a/rust/crates/candlesticks/src/lib.rs +++ b/rust/crates/candlesticks/src/lib.rs @@ -3,5 +3,7 @@ mod merger; mod types; pub use market::Market; -pub use merger::{Candlestick, IsHalfTradeDay, Merger, Quote, Trade, UpdateAction}; +pub use merger::{ + Candlestick, InputCandlestick, IsHalfTradeDay, Merger, Quote, TickAction, Trade, UpdateAction, +}; pub use types::{Period, Type}; diff --git a/rust/crates/candlesticks/src/market.rs b/rust/crates/candlesticks/src/market.rs index 31519b27e..cdf817f84 100644 --- a/rust/crates/candlesticks/src/market.rs +++ b/rust/crates/candlesticks/src/market.rs @@ -17,6 +17,7 @@ pub enum Market { US, SH, SZ, + SG, } impl Market { @@ -28,6 +29,7 @@ impl Market { Market::HK => db::asia::HONG_KONG, Market::US => db::america::NEW_YORK, Market::SH | Market::SZ => db::asia::SHANGHAI, + Market::SG => db::asia::SINGAPORE, } } @@ -66,6 +68,7 @@ impl Market { _ => UpdateFields::empty(), }, Market::SH | Market::SZ => UpdateFields::all(), + Market::SG => UpdateFields::all(), } } @@ -82,6 +85,10 @@ impl Market { (time!(9:30:00), time!(11:30:00)), (time!(13:00:00), time!(15:00:00)), ], + (Market::SG, _) => &[ + (time!(9:00:00), time!(12:30:00)), + (time!(14:00:00), time!(17:00:00)), + ], } } @@ -92,6 +99,7 @@ impl Market { (Market::US, Type::USOQ) => &[(time!(9:30:00), time!(13:00:00))], (Market::US, _) => &[(time!(9:30:00), time!(13:00:00))], (Market::SH | Market::SZ, _) => unreachable!("does not supported"), + (Market::SG, _) => unreachable!("does not supported"), } } @@ -102,6 +110,7 @@ impl Market { Market::US => volume, Market::SH => volume * 100, Market::SZ => volume * 100, + Market::SG => volume, } } } diff --git a/rust/crates/candlesticks/src/merger.rs b/rust/crates/candlesticks/src/merger.rs index 129810313..7f5718403 100644 --- a/rust/crates/candlesticks/src/merger.rs +++ b/rust/crates/candlesticks/src/merger.rs @@ -1,9 +1,14 @@ -use rust_decimal::{prelude::FromPrimitive, Decimal}; +use rust_decimal::{ + prelude::{FromPrimitive, Zero}, + Decimal, +}; use time::{macros::time, Date, Duration, Month, OffsetDateTime, Time, Weekday}; use time_tz::OffsetDateTimeExt; use crate::{market::UpdateFields, Market, Period, Type}; +const TICK_TIMEOUT: Duration = Duration::seconds(3); + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Candlestick { pub time: OffsetDateTime, @@ -37,7 +42,18 @@ pub struct Quote { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum UpdateAction { UpdateLast(Candlestick), - AppendNew(Candlestick), + AppendNew { + confirmed: Option, + new: Candlestick, + }, + Confirm(Candlestick), + None, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum TickAction { + AppendNew(OffsetDateTime), + Confirm, None, } @@ -52,6 +68,13 @@ impl IsHalfTradeDay for bool { } } +#[derive(Debug)] +pub enum InputCandlestick { + Normal(Candlestick), + Confirmed(Candlestick), + None, +} + pub struct Merger { market: Market, period: Period, @@ -107,10 +130,11 @@ where market.half_trade_sessions(ty) }; match period { - Period::Min_1 => self - .round_time(time, trade_sessions) - .replace_second(0) - .unwrap(), + Period::Min_1 => { + let time = self.round_time(time, trade_sessions); + let t = time.time(); + time.replace_time(Time::from_hms(t.hour(), t.minute(), 0).unwrap()) + } Period::Min_5 | Period::Min_15 | Period::Min_30 => { let time = self.round_time(time, trade_sessions); let n = period.minutes() as i64; @@ -163,49 +187,65 @@ where } #[must_use] - pub fn merge_by_quote( - &self, - prev: Option<&Candlestick>, - ty: Type, - quote: Quote, - ) -> UpdateAction { + pub fn merge_by_quote(&self, input: InputCandlestick, ty: Type, quote: Quote) -> UpdateAction { assert_eq!(self.period, Period::Day); let Merger { market, .. } = self; let tz = market.timezone(); let time = self.candlestick_time(ty, quote.time.to_timezone(tz)); - match prev { - Some(prev) if time == prev.time => UpdateAction::UpdateLast(Candlestick { - time, - open: quote.open, - high: quote.high, - low: quote.low, - close: quote.lastdone, - volume: quote.volume, - turnover: quote.turnover, - }), - Some(prev) if time < prev.time => UpdateAction::None, - _ => UpdateAction::AppendNew(Candlestick { - time, - open: quote.open, - high: quote.high, - low: quote.low, - close: quote.lastdone, - volume: quote.volume, - turnover: quote.turnover, - }), + match input { + InputCandlestick::Normal(prev) if time == prev.time => { + UpdateAction::UpdateLast(Candlestick { + time: time.to_timezone(time_tz::timezones::db::UTC), + open: quote.open, + high: quote.high, + low: quote.low, + close: quote.lastdone, + volume: quote.volume, + turnover: quote.turnover, + }) + } + InputCandlestick::None => UpdateAction::AppendNew { + confirmed: None, + new: Candlestick { + time: time.to_timezone(time_tz::timezones::db::UTC), + open: quote.open, + high: quote.high, + low: quote.low, + close: quote.lastdone, + volume: quote.volume, + turnover: quote.turnover, + }, + }, + InputCandlestick::Normal(prev) | InputCandlestick::Confirmed(prev) + if time > prev.time => + { + UpdateAction::AppendNew { + confirmed: Some(prev), + new: Candlestick { + time: time.to_timezone(time_tz::timezones::db::UTC), + open: quote.open, + high: quote.high, + low: quote.low, + close: quote.lastdone, + volume: quote.volume, + turnover: quote.turnover, + }, + } + } + _ => UpdateAction::None, } } #[must_use] - pub fn merge(&self, ty: Type, prev: Option<&Candlestick>, trade: Trade<'_>) -> UpdateAction { + pub fn merge(&self, ty: Type, input: InputCandlestick, trade: Trade<'_>) -> UpdateAction { let Merger { market, .. } = self; let tz = market.timezone(); let time = self.candlestick_time(ty, trade.time.to_timezone(tz)); let update_fields = market.update_fields(trade.trade_type); - match prev { - Some(prev) if time == prev.time => { - let mut candlestick = *prev; + match input { + InputCandlestick::Normal(prev) if time == prev.time => { + let mut candlestick = prev; if update_fields.contains(UpdateFields::PRICE) { candlestick.high = candlestick.high.max(trade.price); @@ -222,8 +262,30 @@ where UpdateAction::UpdateLast(candlestick) } - Some(prev) if time < prev.time => UpdateAction::None, - _ => { + InputCandlestick::None => { + if update_fields.contains(UpdateFields::PRICE) { + let new_candlestick = Candlestick { + time: time.to_timezone(time_tz::timezones::db::UTC), + open: trade.price, + high: trade.price, + low: trade.price, + close: trade.price, + volume: trade.volume, + turnover: trade.price + * Decimal::from_i64(self.market.num_shares(trade.volume)) + .unwrap_or_default(), + }; + UpdateAction::AppendNew { + confirmed: None, + new: new_candlestick, + } + } else { + UpdateAction::None + } + } + InputCandlestick::Normal(prev) | InputCandlestick::Confirmed(prev) + if time > prev.time => + { if update_fields.contains(UpdateFields::PRICE) { let new_candlestick = Candlestick { time: time.to_timezone(time_tz::timezones::db::UTC), @@ -236,12 +298,162 @@ where * Decimal::from_i64(self.market.num_shares(trade.volume)) .unwrap_or_default(), }; - UpdateAction::AppendNew(new_candlestick) + UpdateAction::AppendNew { + confirmed: Some(prev), + new: new_candlestick, + } } else { UpdateAction::None } } + _ => UpdateAction::None, + } + } + + #[must_use] + pub fn tick(&self, ty: Type, prev: OffsetDateTime, time: OffsetDateTime) -> TickAction { + let Merger { market, .. } = self; + let tz = market.timezone(); + let time = time.to_timezone(tz); + let trade_sessions = market.trade_sessions(ty); + + match self.period { + Period::Min_1 => { + for (idx, (start, end)) in trade_sessions.iter().enumerate() { + if time.time() >= *start && time.time() < *end + Duration::minutes(1) { + let candlestick_time = + self.candlestick_time(ty, self.round_time(time, trade_sessions)); + + if candlestick_time > prev && time >= candlestick_time + TICK_TIMEOUT { + return TickAction::AppendNew( + candlestick_time.to_timezone(time_tz::timezones::db::UTC), + ); + } + break; + } else if time.time() >= *end + Duration::minutes(1) + TICK_TIMEOUT { + if idx == trade_sessions.len() - 1 + || time.time() < trade_sessions[idx + 1].0 + { + return TickAction::Confirm; + } + } + } + } + Period::Min_5 | Period::Min_15 | Period::Min_30 | Period::Min_60 => { + for (idx, (start, end)) in trade_sessions.iter().enumerate() { + if time.time() >= *start && time.time() < *end + Duration::minutes(1) { + let candlestick_time = + self.candlestick_time(ty, self.round_time(time, trade_sessions)); + + if candlestick_time > prev && time >= candlestick_time + TICK_TIMEOUT { + return TickAction::AppendNew( + candlestick_time.to_timezone(time_tz::timezones::db::UTC), + ); + } + break; + } else if time.time() >= *end + Duration::minutes(1) + TICK_TIMEOUT { + if idx == trade_sessions.len() - 1 + || time.time() < trade_sessions[idx + 1].0 + { + return TickAction::Confirm; + } + } + } + } + Period::Day | Period::Week | Period::Month | Period::Year => { + if time.time() + >= trade_sessions[trade_sessions.len() - 1].1 + + Duration::minutes(1) + + TICK_TIMEOUT + { + return TickAction::Confirm; + } + } } + + TickAction::None + } + + #[must_use] + pub fn tick2(&self, ty: Type, input: InputCandlestick, time: OffsetDateTime) -> UpdateAction { + const TIMEOUT: Duration = Duration::seconds(3); + + let Merger { market, .. } = self; + let tz = market.timezone(); + let time = time.to_timezone(tz); + let InputCandlestick::Normal(prev) = input else { + return UpdateAction::None; + }; + let trade_sessions = market.trade_sessions(ty); + + match self.period { + Period::Min_1 => { + for (idx, (start, end)) in trade_sessions.iter().enumerate() { + if time.time() >= *start && time.time() < *end { + let candlestick_time = + self.candlestick_time(ty, self.round_time(time, trade_sessions)); + + if candlestick_time > prev.time && time > candlestick_time + TIMEOUT { + let new = Candlestick { + time: candlestick_time.to_timezone(time_tz::timezones::db::UTC), + volume: 0, + turnover: Decimal::zero(), + ..prev + }; + return UpdateAction::AppendNew { + confirmed: Some(prev), + new, + }; + } + break; + } else if time.time() >= *end + TIMEOUT { + if idx == trade_sessions.len() - 1 + || time.time() < trade_sessions[idx + 1].0 + { + return UpdateAction::Confirm(prev); + } + } + } + } + Period::Min_5 | Period::Min_15 | Period::Min_30 | Period::Min_60 => { + for (idx, (start, end)) in trade_sessions.iter().enumerate() { + if time.time() >= *start && time.time() < *end { + let candlestick_time = + self.candlestick_time(ty, self.round_time(time, trade_sessions)); + + if candlestick_time > prev.time && time > prev.time + TIMEOUT { + let new = Candlestick { + time: candlestick_time.to_timezone(time_tz::timezones::db::UTC), + volume: 0, + turnover: Decimal::zero(), + ..prev + }; + return UpdateAction::AppendNew { + confirmed: Some(prev), + new, + }; + } + break; + } else if time.time() >= *end + TIMEOUT { + if idx == trade_sessions.len() - 1 + || time.time() < trade_sessions[idx + 1].0 + { + return UpdateAction::Confirm(prev); + } + } + } + } + Period::Day => { + if time.time() + > trade_sessions[trade_sessions.len() - 1].1 + Duration::minutes(1) + TIMEOUT + { + return UpdateAction::Confirm(prev); + } + } + Period::Week | Period::Month | Period::Year => {} + } + + UpdateAction::None } } @@ -617,4 +829,91 @@ mod tests { datetime!(2022-1-1 0:0:0 UTC) ); } + + #[test] + fn test_tick_min1() { + let merger = Merger::new(Market::SH, Period::Min_1, false); + + assert_eq!( + merger.tick( + Type::Normal, + datetime!(2024-1-1 9:30:58 +8), + datetime!(2024-1-1 9:31:00 +8) + ), + TickAction::None + ); + + assert_eq!( + merger.tick( + Type::Normal, + datetime!(2024-1-1 9:30:58 +8), + datetime!(2024-1-1 9:31:00 +8) + TICK_TIMEOUT + ), + TickAction::AppendNew(datetime!(2024-1-1 9:31:00 +8)) + ); + + assert_eq!( + merger.tick( + Type::Normal, + datetime!(2024-1-1 11:30:58 +8), + datetime!(2024-1-1 11:31:00 +8) + TICK_TIMEOUT + ), + TickAction::Confirm + ); + } + + #[test] + fn test_tick_min5() { + let merger = Merger::new(Market::SH, Period::Min_5, false); + + assert_eq!( + merger.tick( + Type::Normal, + datetime!(2024-1-1 9:30:58 +8), + datetime!(2024-1-1 9:31:00 +8) + ), + TickAction::None + ); + + assert_eq!( + merger.tick( + Type::Normal, + datetime!(2024-1-1 9:34:58 +8), + datetime!(2024-1-1 9:35:00 +8) + TICK_TIMEOUT - Duration::seconds(1) + ), + TickAction::None + ); + + assert_eq!( + merger.tick( + Type::Normal, + datetime!(2024-1-1 9:34:58 +8), + datetime!(2024-1-1 9:35:00 +8) + TICK_TIMEOUT + ), + TickAction::AppendNew(datetime!(2024-1-1 9:35:00 +8)) + ); + } + + #[test] + fn test_tick_day() { + let merger = Merger::new(Market::SH, Period::Day, false); + + assert_eq!( + merger.tick( + Type::Normal, + datetime!(2024-1-1 15:00:23 +8), + datetime!(2024-1-1 15:01:00 +8) + TICK_TIMEOUT - Duration::seconds(1) + ), + TickAction::None + ); + + assert_eq!( + merger.tick( + Type::Normal, + datetime!(2024-1-1 15:00:23 +8), + datetime!(2024-1-1 15:01:00 +8) + TICK_TIMEOUT + ), + TickAction::Confirm + ); + } } diff --git a/rust/crates/wsclient/Cargo.toml b/rust/crates/wsclient/Cargo.toml index cdd583a02..40c528230 100644 --- a/rust/crates/wsclient/Cargo.toml +++ b/rust/crates/wsclient/Cargo.toml @@ -26,3 +26,4 @@ num_enum = "0.5.7" url = "2.2.2" flate2 = "1.0.23" leaky-bucket = "1.0.1" +tracing = { version = "0.1.34", features = ["attributes"] } diff --git a/rust/crates/wsclient/src/client.rs b/rust/crates/wsclient/src/client.rs index 4b7bc91db..f359c1e47 100644 --- a/rust/crates/wsclient/src/client.rs +++ b/rust/crates/wsclient/src/client.rs @@ -139,6 +139,7 @@ impl<'a> Context<'a> { match item.transpose()? { Some(msg) => { if msg.is_ping() { + tracing::debug!("ping"); ping_time = Instant::now(); } self.handle_message(msg).await?; @@ -154,6 +155,7 @@ impl<'a> Context<'a> { } _ = checkout_timeout.tick() => { if (Instant::now() - ping_time) > HEARTBEAT_TIMEOUT { + tracing::error!("heartbeat timeout"); return Err(WsClientError::ConnectionClosed { reason: None }); } } diff --git a/rust/src/config.rs b/rust/src/config.rs index 3eec98f68..55974ce96 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -38,6 +38,15 @@ impl Language { } } +/// Push mode for candlestick +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PushCandlestickMode { + /// Realtime mode + Realtime, + /// Confirmed mode + Confirmed, +} + /// Configuration options for LongPort sdk #[derive(Debug, Clone)] pub struct Config { @@ -46,6 +55,7 @@ pub struct Config { pub(crate) trade_ws_url: String, pub(crate) language: Language, pub(crate) enable_overnight: bool, + pub(crate) push_candlestick_mode: PushCandlestickMode, } impl Config { @@ -71,6 +81,7 @@ impl Config { .to_string(), language: Language::EN, enable_overnight: false, + push_candlestick_mode: PushCandlestickMode::Realtime, } } @@ -91,6 +102,8 @@ impl Config { /// `wss://openapi-trade.longportapp.com/v2`) /// - `LONGPORT_ENABLE_OVERNIGHT` - Enable overnight quote, `true` or /// `false` (Default: `false`) + /// - `LONGPORT_PUSH_CANDLESTICK_MODE` - `realtime` or `confirmed` (Default: + /// `realtime`) pub fn from_env() -> Result { let _ = dotenv::dotenv(); @@ -119,6 +132,12 @@ impl Config { .ok() .map(|var| var == "true") .unwrap_or_default(); + let push_candlestick_mode = + if std::env::var("LONGPORT_PUSH_CANDLESTICK_MODE").as_deref() == Ok("confirmed") { + PushCandlestickMode::Confirmed + } else { + PushCandlestickMode::Realtime + }; Ok(Config { http_cli_config, @@ -126,6 +145,7 @@ impl Config { trade_ws_url, language: Language::EN, enable_overnight, + push_candlestick_mode, }) } @@ -183,6 +203,16 @@ impl Config { } } + /// Specifies the push candlestick mode + /// + /// Default: `PushCandlestickMode::Realtime` + pub fn push_candlestick_mode(self, mode: PushCandlestickMode) -> Self { + Self { + push_candlestick_mode: mode, + ..self + } + } + /// Create metadata for auth/reconnect request pub fn create_metadata(&self) -> HashMap { let mut metadata = HashMap::new(); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 8a4e3ebaa..b3d60125f 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -18,7 +18,7 @@ pub mod blocking; pub mod quote; pub mod trade; -pub use config::{Config, Language}; +pub use config::{Config, Language, PushCandlestickMode}; pub use error::{Error, Result, SimpleError}; pub use longport_httpcli as httpclient; pub use quote::QuoteContext; diff --git a/rust/src/quote/core.rs b/rust/src/quote/core.rs index 38ba0b674..998918256 100644 --- a/rust/src/quote/core.rs +++ b/rust/src/quote/core.rs @@ -3,7 +3,7 @@ use std::{ sync::Arc, }; -use longport_candlesticks::{IsHalfTradeDay, Type, UpdateAction}; +use longport_candlesticks::{IsHalfTradeDay, TickAction, Type, UpdateAction}; use longport_httpcli::HttpClient; use longport_proto::quote::{ self, AdjustType, MarketTradeDayRequest, MarketTradeDayResponse, MultiSecurityRequest, Period, @@ -13,6 +13,7 @@ use longport_proto::quote::{ use longport_wscli::{ CodecType, Platform, ProtocolVersion, RateLimit, WsClient, WsClientError, WsEvent, WsSession, }; +use rust_decimal::Decimal; use time::{Date, OffsetDateTime}; use tokio::{ sync::{mpsc, oneshot}, @@ -20,9 +21,10 @@ use tokio::{ }; use crate::{ + config::PushCandlestickMode, quote::{ cmd_code, - store::Store, + store::{Candlesticks, Store}, sub_flags::SubFlags, utils::{format_date, parse_date}, Candlestick, PushCandlestick, PushEvent, PushEventDetail, RealtimeQuote, SecurityBoard, @@ -91,6 +93,7 @@ pub(crate) enum Command { #[derive(Debug, Default)] struct CurrentTradeDays { + trade_day: HashMap>, half_days: HashMap>, } @@ -102,8 +105,17 @@ impl CurrentTradeDays { HK => HalfDays(self.half_days.get(&Market::HK)), US => HalfDays(self.half_days.get(&Market::US)), SH | SZ => HalfDays(None), + SG => HalfDays(None), } } + + fn is_trade_day(&self, market: Market, date: Date) -> bool { + self.trade_day + .get(&market) + .map(|days| days.contains(&date)) + .or(self.half_days.get(&market).map(|days| days.contains(&date))) + .unwrap_or_default() + } } #[derive(Debug, Copy, Clone)] @@ -135,6 +147,7 @@ pub(crate) struct Core { store: Store, member_id: i64, quote_level: String, + push_candlestick_mode: PushCandlestickMode, } impl Core { @@ -196,6 +209,7 @@ impl Core { ws_cli.set_rate_limit(rate_limit.clone()); let current_trade_days = fetch_current_trade_days(&ws_cli).await?; + let push_candlestick_mode = config.push_candlestick_mode; Ok(Self { config, @@ -213,6 +227,7 @@ impl Core { store: Store::default(), member_id, quote_level, + push_candlestick_mode, }) } @@ -321,6 +336,7 @@ impl Core { Instant::now() + Duration::from_secs(60 * 60 * 24), Duration::from_secs(60 * 60 * 24), ); + let mut ticker = tokio::time::interval(Duration::from_secs(1)); loop { tokio::select! { @@ -339,6 +355,7 @@ impl Core { } } } + _ = ticker.tick() => self.tick(), _ = update_trade_days_interval.tick() => { if let Ok(days) = fetch_current_trade_days(&self.ws_cli).await { self.current_trade_days = days; @@ -583,11 +600,18 @@ impl Core { ) .await?; - *security_data.candlesticks.entry(period).or_default() = resp + let candlesticks = resp .candlesticks .into_iter() .map(TryInto::try_into) .collect::>>()?; + security_data + .candlesticks + .entry(period) + .or_insert_with(|| Candlesticks { + candlesticks, + confirmed: false, + }); let sub_flags = if period == Period::Day { SubFlags::QUOTE @@ -683,7 +707,7 @@ impl Core { async fn handle_ws_event(&mut self, event: WsEvent) -> Result<()> { match event { WsEvent::Error(err) => Err(err.into()), - WsEvent::Push { command_code, body } => self.handle_push(command_code, body).await, + WsEvent::Push { command_code, body } => self.handle_push(command_code, body), } } @@ -728,10 +752,64 @@ impl Core { Ok(()) } - async fn handle_push(&mut self, command_code: u8, body: Vec) -> Result<()> { + fn tick(&mut self) { + let now = OffsetDateTime::now_utc(); + + for (symbol, data) in &mut self.store.securities { + let ty = get_merger_ty(data.board); + let Some(market) = parse_market_from_symbol(symbol) else { + continue; + }; + + if !self + .current_trade_days + .is_trade_day(market.into(), now.date()) + { + return; + } + + for (period, candlesticks) in &mut data.candlesticks { + let merger = longport_candlesticks::Merger::new( + market, + convert_period(*period), + self.current_trade_days.half_days(market), + ); + if candlesticks.confirmed { + continue; + } + let Some(prev_candlestick) = candlesticks.candlesticks.last() else { + continue; + }; + let prev_candlestick = longport_candlesticks::Candlestick::from(*prev_candlestick); + let action = match merger.tick(ty, prev_candlestick.time, now) { + TickAction::AppendNew(new_time) => UpdateAction::AppendNew { + confirmed: Some(prev_candlestick), + new: longport_candlesticks::Candlestick { + time: new_time, + volume: 0, + turnover: Decimal::ZERO, + ..prev_candlestick + }, + }, + TickAction::Confirm => UpdateAction::Confirm(prev_candlestick), + TickAction::None => UpdateAction::None, + }; + update_and_push_candlestick( + candlesticks, + &symbol, + *period, + action, + self.push_candlestick_mode, + &mut self.push_tx, + ); + } + } + } + + fn handle_push(&mut self, command_code: u8, body: Vec) -> Result<()> { match PushEvent::parse(command_code, &body) { Ok((mut event, tag)) => { - tracing::debug!(event = ?event, tag = ?tag,"push event"); + tracing::debug!(event = ?event, tag = ?tag, "push event"); if tag != Some(PushQuoteTag::Eod) { self.store.handle_push(&mut event); @@ -752,22 +830,9 @@ impl Core { continue; } - let period2 = match period { - Period::UnknownPeriod => unreachable!(), - Period::OneMinute => longport_candlesticks::Period::Min_1, - Period::FiveMinute => longport_candlesticks::Period::Min_5, - Period::FifteenMinute => longport_candlesticks::Period::Min_15, - Period::ThirtyMinute => longport_candlesticks::Period::Min_30, - Period::SixtyMinute => longport_candlesticks::Period::Min_60, - Period::Day => unreachable!(), - Period::Week => longport_candlesticks::Period::Week, - Period::Month => longport_candlesticks::Period::Month, - Period::Year => longport_candlesticks::Period::Year, - }; - let merger = longport_candlesticks::Merger::new( market, - period2, + convert_period(*period), self.current_trade_days.half_days(market), ); @@ -776,13 +841,9 @@ impl Core { continue; } - let prev = candlesticks - .last() - .map(|candlestick| (*candlestick).into()); - let action = merger.merge( merge_ty, - prev.as_ref(), + candlesticks.merge_input(), longport_candlesticks::Trade { time: trade.timestamp, price: trade.price, @@ -795,6 +856,7 @@ impl Core { &event.symbol, *period, action, + self.push_candlestick_mode, &mut self.push_tx, ); } @@ -831,10 +893,8 @@ impl Core { longport_candlesticks::Period::Day, self.current_trade_days.half_days(market), ); - let prev = - candlesticks.last().map(|candlestick| (*candlestick).into()); let action = merger.merge_by_quote( - prev.as_ref(), + candlesticks.merge_input(), merge_ty, longport_candlesticks::Quote { time: push_quote.timestamp, @@ -851,6 +911,7 @@ impl Core { &event.symbol, Period::Day, action, + self.push_candlestick_mode, &mut self.push_tx, ); } @@ -946,10 +1007,10 @@ impl Core { .map(|data| &data.candlesticks) .and_then(|periods| periods.get(&period)) .map(|candlesticks| { - let candlesticks = if candlesticks.len() >= count { - &candlesticks[candlesticks.len() - count..] + let candlesticks = if candlesticks.candlesticks.len() >= count { + &candlesticks.candlesticks[candlesticks.candlesticks.len() - count..] } else { - candlesticks + &candlesticks.candlesticks }; candlesticks.to_vec() }) @@ -970,7 +1031,7 @@ async fn fetch_current_trade_days(cli: &WsClient) -> Result { let begin_day = OffsetDateTime::now_utc().date() - time::Duration::days(1); let end_day = begin_day + time::Duration::days(30); - for market in [Market::HK, Market::US] { + for market in [Market::HK, Market::US, Market::SG, Market::CN] { let resp = cli .request::<_, MarketTradeDayResponse>( cmd_code::GET_TRADING_DAYS, @@ -982,6 +1043,17 @@ async fn fetch_current_trade_days(cli: &WsClient) -> Result { }, ) .await?; + + days.trade_day.insert( + market, + resp.trade_day + .iter() + .map(|value| { + parse_date(value).map_err(|err| Error::parse_field_error("half_trade_day", err)) + }) + .collect::>>()?, + ); + days.half_days.insert( market, resp.half_trade_day @@ -1008,28 +1080,43 @@ fn parse_market_from_symbol(symbol: &str) -> Option, + candlesticks: &mut Candlesticks, symbol: &str, period: Period, action: UpdateAction, + push_candlestick_mode: PushCandlestickMode, tx: &mut mpsc::UnboundedSender, ) { let candlestick = match action { UpdateAction::UpdateLast(candlestick) => { - let candlestick = candlestick.into(); - *candlesticks.last_mut().unwrap() = candlestick; - Some(candlestick) + *candlesticks.candlesticks.last_mut().unwrap() = candlestick.into(); + match push_candlestick_mode { + PushCandlestickMode::Realtime => Some(candlestick.into()), + PushCandlestickMode::Confirmed => None, + } + } + UpdateAction::AppendNew { confirmed, new } => { + candlesticks.candlesticks.push(new.into()); + candlesticks.confirmed = false; + if candlesticks.candlesticks.len() > MAX_CANDLESTICKS * 2 { + candlesticks.candlesticks.drain(..MAX_CANDLESTICKS); + } + + match push_candlestick_mode { + PushCandlestickMode::Realtime => Some(new.into()), + PushCandlestickMode::Confirmed => confirmed.map(Into::into), + } } - UpdateAction::AppendNew(candlestick) => { - let candlestick = candlestick.into(); - candlesticks.push(candlestick); - if candlesticks.len() > MAX_CANDLESTICKS * 2 { - candlesticks.drain(..MAX_CANDLESTICKS); + UpdateAction::Confirm(candlestick) => { + candlesticks.confirmed = true; + match push_candlestick_mode { + PushCandlestickMode::Realtime => None, + PushCandlestickMode::Confirmed => Some(candlestick.into()), } - Some(candlestick) } UpdateAction::None => None, }; + if let Some(candlestick) = candlestick { let _ = tx.send(PushEvent { sequence: 0, @@ -1041,3 +1128,19 @@ fn update_and_push_candlestick( }); } } + +#[inline] +fn convert_period(period: Period) -> longport_candlesticks::Period { + match period { + Period::UnknownPeriod => unreachable!(), + Period::OneMinute => longport_candlesticks::Period::Min_1, + Period::FiveMinute => longport_candlesticks::Period::Min_5, + Period::FifteenMinute => longport_candlesticks::Period::Min_15, + Period::ThirtyMinute => longport_candlesticks::Period::Min_30, + Period::SixtyMinute => longport_candlesticks::Period::Min_60, + Period::Day => longport_candlesticks::Period::Day, + Period::Week => longport_candlesticks::Period::Week, + Period::Month => longport_candlesticks::Period::Month, + Period::Year => longport_candlesticks::Period::Year, + } +} diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index df4544a6b..b9984902c 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -27,5 +27,3 @@ pub use types::{ TradeDirection, TradingSessionInfo, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, WatchlistSecurity, }; -// pub use types::{FilterWarrantExpiryDate, -// FilterWarrantStatus,Language,SortType}; diff --git a/rust/src/quote/store.rs b/rust/src/quote/store.rs index 681f72a7b..465839fde 100644 --- a/rust/src/quote/store.rs +++ b/rust/src/quote/store.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use longport_candlesticks::InputCandlestick; use longport_proto::quote::Period; use crate::quote::{ @@ -30,6 +31,24 @@ macro_rules! merge_i64 { }; } +#[derive(Debug)] +pub(crate) struct Candlesticks { + pub(crate) candlesticks: Vec, + pub(crate) confirmed: bool, +} + +impl Candlesticks { + #[inline] + pub(crate) fn merge_input(&self) -> InputCandlestick { + match (self.candlesticks.last(), self.confirmed) { + (None, true) => unreachable!(), + (None, false) => InputCandlestick::None, + (Some(prev), true) => InputCandlestick::Confirmed((*prev).into()), + (Some(prev), false) => InputCandlestick::Normal((*prev).into()), + } + } +} + #[derive(Debug, Default)] pub(crate) struct SecuritiesData { pub(crate) quote: PushQuote, @@ -43,7 +62,7 @@ pub(crate) struct SecuritiesData { pub(crate) trades: Vec, pub(crate) board: SecurityBoard, - pub(crate) candlesticks: HashMap>, + pub(crate) candlesticks: HashMap, } #[derive(Debug, Default)] diff --git a/rust/src/types.rs b/rust/src/types.rs index 390533581..8a5b07a34 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -18,3 +18,14 @@ pub enum Market { impl_default_for_enum_string!(Market); impl_serde_for_enum_string!(Market); + +impl From for Market { + fn from(market: longport_candlesticks::Market) -> Self { + match market { + longport_candlesticks::Market::HK => Market::HK, + longport_candlesticks::Market::US => Market::US, + longport_candlesticks::Market::SH | longport_candlesticks::Market::SZ => Market::CN, + longport_candlesticks::Market::SG => Market::SG, + } + } +} From 5976295d86471a32b07b2408005e0a47b812c084 Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 11 Jul 2024 11:32:22 +0800 Subject: [PATCH 05/13] add support for specify push candlestick mode 2 --- cpp/include/http_client.hpp | 2 ++ cpp/src/http_client.cpp | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cpp/include/http_client.hpp b/cpp/include/http_client.hpp index b91a21d71..ebd9148dd 100644 --- a/cpp/include/http_client.hpp +++ b/cpp/include/http_client.hpp @@ -14,6 +14,8 @@ namespace longport { struct HttpResult { const char* response_body; + + HttpResult(const char* response_body) {} }; class HttpClient diff --git a/cpp/src/http_client.cpp b/cpp/src/http_client.cpp index 55fdb7d3d..6cb0aef32 100644 --- a/cpp/src/http_client.cpp +++ b/cpp/src/http_client.cpp @@ -71,10 +71,7 @@ HttpClient::request( if (status) { const lb_http_result_t* result = (const lb_http_result_t*)res->data; - HttpResult http_res; - - http_res.response_body = lb_http_result_response_body(result); - + HttpResult http_res(lb_http_result_response_body(result)); (*callback_ptr)(AsyncResult( nullptr, std::move(status), &http_res)); } else { From 8298c82d3c9c750c8cc1bf7959a5a287466007e6 Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 11 Jul 2024 11:34:01 +0800 Subject: [PATCH 06/13] add support for specify push candlestick mode 3 --- cpp/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 20d4fb24b..dc2f81f43 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -12,7 +12,7 @@ add_library( longport_cpp SHARED ${SOURCES} ) -if(NOT CMAKE_HOST_LINUX) +if(WIN32) target_link_libraries(longport_cpp longport_c) endif() From 0a4aadd834e44cb3e787832fe5462a916ba34ecb Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 11 Jul 2024 11:37:23 +0800 Subject: [PATCH 07/13] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 866f8f666..1937a5941 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ include(FetchContent) fetchcontent_declare( Corrosion GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git - GIT_TAG v0.2.1 + GIT_TAG v0.5.0 ) fetchcontent_makeavailable(Corrosion) From c8913fb52aa70fbc8eff82eb72cd9a8089c83c5e Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 11 Jul 2024 11:43:48 +0800 Subject: [PATCH 08/13] Update CMakeLists.txt --- cpp/CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index dc2f81f43..31e9df056 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -12,8 +12,6 @@ add_library( longport_cpp SHARED ${SOURCES} ) -if(WIN32) target_link_libraries(longport_cpp longport_c) -endif() add_subdirectory(test) From 79243df83a85c539c8de0e220ce7b945088203b4 Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 11 Jul 2024 12:00:30 +0800 Subject: [PATCH 09/13] Update Makefile.toml --- c/Makefile.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/c/Makefile.toml b/c/Makefile.toml index dc8657778..407209ed9 100644 --- a/c/Makefile.toml +++ b/c/Makefile.toml @@ -1,21 +1,21 @@ [tasks.c] command = "make" -args = ["cargo-build_longport-c"] +args = ["cargo-build_longport_c"] cwd = "cmake.build" [tasks.c-release] command = "make" -args = ["cargo-build_longport-c"] +args = ["cargo-build_longport_c"] cwd = "cmake.build" [tasks.c.windows] command = "msbuild" -args = ["longport.sln", "-p:Configuration=Debug", "/t:cargo-build_longport-c"] +args = ["longport.sln", "-p:Configuration=Debug", "/t:cargo-build_longport_c"] cwd = "cmake.build" [tasks.c-release.windows] command = "msbuild" -args = ["longport.sln", "-p:Configuration=Release", "/t:cargo-build_longport-c"] +args = ["longport.sln", "-p:Configuration=Release", "/t:cargo-build_longport_c"] cwd = "cmake.build" [tasks.c-test] From d6e48078a70b40644c63a4b0c0b8b23e0c3b736d Mon Sep 17 00:00:00 2001 From: Sunli Date: Thu, 11 Jul 2024 14:19:13 +0800 Subject: [PATCH 10/13] Release 1.0.28 longport@1.0.28 longport-c@1.0.28 longport-c-macros@1.0.28 longport-candlesticks@1.0.28 longport-httpcli@1.0.28 longport-java@1.0.28 longport-java-macros@1.0.28 longport-nodejs@1.0.28 longport-nodejs-macros@1.0.28 longport-proto@1.0.28 longport-python@1.0.28 longport-python-macros@1.0.28 longport-wscli@1.0.28 Generated by cargo-workspaces --- .github/workflows/ci.yml | 1 - .github/workflows/release.yml | 1 - c/Cargo.toml | 2 +- c/crates/macros/Cargo.toml | 2 +- java/Cargo.toml | 2 +- java/crates/macros/Cargo.toml | 2 +- nodejs/Cargo.toml | 2 +- nodejs/crates/macros/Cargo.toml | 2 +- python/Cargo.toml | 2 +- python/Makefile.toml | 2 +- python/crates/macros/Cargo.toml | 2 +- rust/Cargo.toml | 10 +++++----- rust/crates/candlesticks/Cargo.toml | 2 +- rust/crates/httpclient/Cargo.toml | 2 +- rust/crates/proto/Cargo.toml | 2 +- rust/crates/wsclient/Cargo.toml | 4 ++-- 16 files changed, 19 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 158b3e078..013fc9783 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -244,7 +244,6 @@ jobs: manylinux: 2_28 args: -i python${{ matrix.python-version }} --release --out dist -m python/Cargo.toml - uses: uraimo/run-on-arch-action@v2.2.0 - if: matrix.target != 'ppc64' name: Install built wheel with: arch: ${{ matrix.target }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da9ac8352..138ab917e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -202,7 +202,6 @@ jobs: manylinux: 2_28 args: -i python${{ matrix.python-version }} --release --out dist -m python/Cargo.toml - uses: uraimo/run-on-arch-action@v2.2.0 - if: matrix.target != 'ppc64' name: Install built wheel with: arch: ${{ matrix.target }} diff --git a/c/Cargo.toml b/c/Cargo.toml index 9db249216..c76df17c1 100644 --- a/c/Cargo.toml +++ b/c/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-c" -version = "1.0.27" +version = "1.0.28" description = "LongPort OpenAPI SDK for C" homepage = "https://open.longportapp.com/en/" readme = "README.md" diff --git a/c/crates/macros/Cargo.toml b/c/crates/macros/Cargo.toml index 3c0fb4520..7e097585a 100644 --- a/c/crates/macros/Cargo.toml +++ b/c/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longport-c-macros" -version = "1.0.27" +version = "1.0.28" edition = "2021" [lib] diff --git a/java/Cargo.toml b/java/Cargo.toml index a6829fd3f..47fbfc684 100644 --- a/java/Cargo.toml +++ b/java/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-java" -version = "1.0.27" +version = "1.0.28" [lib] crate-type = ["cdylib"] diff --git a/java/crates/macros/Cargo.toml b/java/crates/macros/Cargo.toml index 69d953e28..631447c25 100644 --- a/java/crates/macros/Cargo.toml +++ b/java/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longport-java-macros" -version = "1.0.27" +version = "1.0.28" edition = "2021" [lib] diff --git a/nodejs/Cargo.toml b/nodejs/Cargo.toml index 441ac28b5..ff581a72d 100644 --- a/nodejs/Cargo.toml +++ b/nodejs/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-nodejs" -version = "1.0.27" +version = "1.0.28" [lib] crate-type = ["cdylib"] diff --git a/nodejs/crates/macros/Cargo.toml b/nodejs/crates/macros/Cargo.toml index f8e74412e..250aa8ecd 100644 --- a/nodejs/crates/macros/Cargo.toml +++ b/nodejs/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longport-nodejs-macros" -version = "1.0.27" +version = "1.0.28" edition = "2021" [lib] diff --git a/python/Cargo.toml b/python/Cargo.toml index ef8815605..612daa8b5 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-python" -version = "1.0.27" +version = "1.0.28" description = "LongPort OpenAPI SDK for Python" homepage = "https://open.longportapp.com/en/" readme = "README.md" diff --git a/python/Makefile.toml b/python/Makefile.toml index d55a9ec63..66cfc9678 100644 --- a/python/Makefile.toml +++ b/python/Makefile.toml @@ -12,7 +12,7 @@ args = ["install", "maturin>=1.0,<2.0"] command = "pip" args = [ "install", - "target/wheels/longport-1.0.27-cp311-none-win_amd64.whl", + "target/wheels/longport-1.0.28-cp311-none-win_amd64.whl", "-I", ] dependencies = ["python"] diff --git a/python/crates/macros/Cargo.toml b/python/crates/macros/Cargo.toml index 66548594a..593601fe6 100644 --- a/python/crates/macros/Cargo.toml +++ b/python/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longport-python-macros" -version = "1.0.27" +version = "1.0.28" edition = "2021" [lib] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9cbf23f7c..1bcab6e67 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport" -version = "1.0.27" +version = "1.0.28" description = "LongPort OpenAPI SDK for Rust" homepage = "https://open.longportapp.com/en/" readme = "README.md" @@ -14,10 +14,10 @@ categories = ["api-bindings"] blocking = ["flume"] [dependencies] -longport-wscli = { path = "crates/wsclient", version = "1.0.27" } -longport-httpcli = { path = "crates/httpclient", version = "1.0.27" } -longport-proto = { path = "crates/proto", version = "1.0.27" } -longport-candlesticks = { path = "crates/candlesticks", version = "1.0.27" } +longport-wscli = { path = "crates/wsclient", version = "1.0.28" } +longport-httpcli = { path = "crates/httpclient", version = "1.0.28" } +longport-proto = { path = "crates/proto", version = "1.0.28" } +longport-candlesticks = { path = "crates/candlesticks", version = "1.0.28" } tokio = { version = "1.18.2", features = [ "time", diff --git a/rust/crates/candlesticks/Cargo.toml b/rust/crates/candlesticks/Cargo.toml index 263c57dbb..1121d50e9 100644 --- a/rust/crates/candlesticks/Cargo.toml +++ b/rust/crates/candlesticks/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-candlesticks" -version = "1.0.27" +version = "1.0.28" description = "LongPort candlestick utils for Rust" license = "MIT OR Apache-2.0" diff --git a/rust/crates/httpclient/Cargo.toml b/rust/crates/httpclient/Cargo.toml index f09765083..00ea830ee 100644 --- a/rust/crates/httpclient/Cargo.toml +++ b/rust/crates/httpclient/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-httpcli" -version = "1.0.27" +version = "1.0.28" description = "LongPort HTTP SDK for Rust" license = "MIT OR Apache-2.0" diff --git a/rust/crates/proto/Cargo.toml b/rust/crates/proto/Cargo.toml index 471c57b2a..92da1437e 100644 --- a/rust/crates/proto/Cargo.toml +++ b/rust/crates/proto/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-proto" -version = "1.0.27" +version = "1.0.28" description = "LongPort Protocol" license = "MIT OR Apache-2.0" diff --git a/rust/crates/wsclient/Cargo.toml b/rust/crates/wsclient/Cargo.toml index 40c528230..14cd0970a 100644 --- a/rust/crates/wsclient/Cargo.toml +++ b/rust/crates/wsclient/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "longport-wscli" -version = "1.0.27" +version = "1.0.28" edition = "2021" description = "LongPort Websocket SDK for Rust" license = "MIT OR Apache-2.0" [dependencies] -longport-proto = { path = "../proto", version = "1.0.27" } +longport-proto = { path = "../proto", version = "1.0.28" } tokio = { version = "1.18.2", features = [ "time", From e239dffd714e4976d79352215d24ddfdafad1120 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 25 Jul 2024 14:49:31 +0800 Subject: [PATCH 11/13] fix: add missing field remark --- cpp/src/trade_context.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/trade_context.cpp b/cpp/src/trade_context.cpp index bda204234..450eb6589 100644 --- a/cpp/src/trade_context.cpp +++ b/cpp/src/trade_context.cpp @@ -426,7 +426,7 @@ TradeContext::submit_order( nullptr, nullptr, nullptr, - nullptr, + opts.remark.c_str(), }; lb_date_t expire_date; lb_outside_rth_t outside_rth; @@ -790,4 +790,4 @@ TradeContext::estimate_max_purchase_quantity( } } // namespace trade -} // namespace longport \ No newline at end of file +} // namespace longport From 046ad48f6ee242d0102c3861698b8b8c6d5eb461 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 26 Jul 2024 10:14:05 +0800 Subject: [PATCH 12/13] remark can be null --- cpp/src/trade_context.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/trade_context.cpp b/cpp/src/trade_context.cpp index 450eb6589..62480f8bd 100644 --- a/cpp/src/trade_context.cpp +++ b/cpp/src/trade_context.cpp @@ -426,7 +426,7 @@ TradeContext::submit_order( nullptr, nullptr, nullptr, - opts.remark.c_str(), + opts.remark ? opts.remark->c_str() : nullptr, }; lb_date_t expire_date; lb_outside_rth_t outside_rth; From 3b4cd4bdd7fa6bc5f53b7c3729b78c8bb4108703 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 26 Jul 2024 10:34:52 +0800 Subject: [PATCH 13/13] Release 1.0.29 longport@1.0.29 longport-c@1.0.29 longport-c-macros@1.0.29 longport-candlesticks@1.0.29 longport-httpcli@1.0.29 longport-java@1.0.29 longport-java-macros@1.0.29 longport-nodejs@1.0.29 longport-nodejs-macros@1.0.29 longport-proto@1.0.29 longport-python@1.0.29 longport-python-macros@1.0.29 longport-wscli@1.0.29 Generated by cargo-workspaces --- c/Cargo.toml | 2 +- c/crates/macros/Cargo.toml | 2 +- java/Cargo.toml | 2 +- java/crates/macros/Cargo.toml | 2 +- nodejs/Cargo.toml | 2 +- nodejs/crates/macros/Cargo.toml | 2 +- python/Cargo.toml | 2 +- python/Makefile.toml | 2 +- python/crates/macros/Cargo.toml | 2 +- rust/Cargo.toml | 10 +++++----- rust/crates/candlesticks/Cargo.toml | 2 +- rust/crates/httpclient/Cargo.toml | 2 +- rust/crates/proto/Cargo.toml | 2 +- rust/crates/wsclient/Cargo.toml | 4 ++-- 14 files changed, 19 insertions(+), 19 deletions(-) diff --git a/c/Cargo.toml b/c/Cargo.toml index c76df17c1..30a6cf260 100644 --- a/c/Cargo.toml +++ b/c/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-c" -version = "1.0.28" +version = "1.0.29" description = "LongPort OpenAPI SDK for C" homepage = "https://open.longportapp.com/en/" readme = "README.md" diff --git a/c/crates/macros/Cargo.toml b/c/crates/macros/Cargo.toml index 7e097585a..54749851b 100644 --- a/c/crates/macros/Cargo.toml +++ b/c/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longport-c-macros" -version = "1.0.28" +version = "1.0.29" edition = "2021" [lib] diff --git a/java/Cargo.toml b/java/Cargo.toml index 47fbfc684..0d05e70b7 100644 --- a/java/Cargo.toml +++ b/java/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-java" -version = "1.0.28" +version = "1.0.29" [lib] crate-type = ["cdylib"] diff --git a/java/crates/macros/Cargo.toml b/java/crates/macros/Cargo.toml index 631447c25..bb4084e09 100644 --- a/java/crates/macros/Cargo.toml +++ b/java/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longport-java-macros" -version = "1.0.28" +version = "1.0.29" edition = "2021" [lib] diff --git a/nodejs/Cargo.toml b/nodejs/Cargo.toml index ff581a72d..4b1abfae4 100644 --- a/nodejs/Cargo.toml +++ b/nodejs/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-nodejs" -version = "1.0.28" +version = "1.0.29" [lib] crate-type = ["cdylib"] diff --git a/nodejs/crates/macros/Cargo.toml b/nodejs/crates/macros/Cargo.toml index 250aa8ecd..ce1e89e2f 100644 --- a/nodejs/crates/macros/Cargo.toml +++ b/nodejs/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longport-nodejs-macros" -version = "1.0.28" +version = "1.0.29" edition = "2021" [lib] diff --git a/python/Cargo.toml b/python/Cargo.toml index 612daa8b5..aea9c7838 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-python" -version = "1.0.28" +version = "1.0.29" description = "LongPort OpenAPI SDK for Python" homepage = "https://open.longportapp.com/en/" readme = "README.md" diff --git a/python/Makefile.toml b/python/Makefile.toml index 66cfc9678..cb4279c7b 100644 --- a/python/Makefile.toml +++ b/python/Makefile.toml @@ -12,7 +12,7 @@ args = ["install", "maturin>=1.0,<2.0"] command = "pip" args = [ "install", - "target/wheels/longport-1.0.28-cp311-none-win_amd64.whl", + "target/wheels/longport-1.0.29-cp311-none-win_amd64.whl", "-I", ] dependencies = ["python"] diff --git a/python/crates/macros/Cargo.toml b/python/crates/macros/Cargo.toml index 593601fe6..1e2373e69 100644 --- a/python/crates/macros/Cargo.toml +++ b/python/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "longport-python-macros" -version = "1.0.28" +version = "1.0.29" edition = "2021" [lib] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 1bcab6e67..959539e65 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport" -version = "1.0.28" +version = "1.0.29" description = "LongPort OpenAPI SDK for Rust" homepage = "https://open.longportapp.com/en/" readme = "README.md" @@ -14,10 +14,10 @@ categories = ["api-bindings"] blocking = ["flume"] [dependencies] -longport-wscli = { path = "crates/wsclient", version = "1.0.28" } -longport-httpcli = { path = "crates/httpclient", version = "1.0.28" } -longport-proto = { path = "crates/proto", version = "1.0.28" } -longport-candlesticks = { path = "crates/candlesticks", version = "1.0.28" } +longport-wscli = { path = "crates/wsclient", version = "1.0.29" } +longport-httpcli = { path = "crates/httpclient", version = "1.0.29" } +longport-proto = { path = "crates/proto", version = "1.0.29" } +longport-candlesticks = { path = "crates/candlesticks", version = "1.0.29" } tokio = { version = "1.18.2", features = [ "time", diff --git a/rust/crates/candlesticks/Cargo.toml b/rust/crates/candlesticks/Cargo.toml index 1121d50e9..22dc0c5a1 100644 --- a/rust/crates/candlesticks/Cargo.toml +++ b/rust/crates/candlesticks/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-candlesticks" -version = "1.0.28" +version = "1.0.29" description = "LongPort candlestick utils for Rust" license = "MIT OR Apache-2.0" diff --git a/rust/crates/httpclient/Cargo.toml b/rust/crates/httpclient/Cargo.toml index 00ea830ee..821c426a0 100644 --- a/rust/crates/httpclient/Cargo.toml +++ b/rust/crates/httpclient/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-httpcli" -version = "1.0.28" +version = "1.0.29" description = "LongPort HTTP SDK for Rust" license = "MIT OR Apache-2.0" diff --git a/rust/crates/proto/Cargo.toml b/rust/crates/proto/Cargo.toml index 92da1437e..8ee2cc85e 100644 --- a/rust/crates/proto/Cargo.toml +++ b/rust/crates/proto/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "longport-proto" -version = "1.0.28" +version = "1.0.29" description = "LongPort Protocol" license = "MIT OR Apache-2.0" diff --git a/rust/crates/wsclient/Cargo.toml b/rust/crates/wsclient/Cargo.toml index 14cd0970a..2bf9d79eb 100644 --- a/rust/crates/wsclient/Cargo.toml +++ b/rust/crates/wsclient/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "longport-wscli" -version = "1.0.28" +version = "1.0.29" edition = "2021" description = "LongPort Websocket SDK for Rust" license = "MIT OR Apache-2.0" [dependencies] -longport-proto = { path = "../proto", version = "1.0.28" } +longport-proto = { path = "../proto", version = "1.0.29" } tokio = { version = "1.18.2", features = [ "time",