diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ede6a267..7fb51abf99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking changes +- **All SDKs:** `QuoteContext.option_chain_info_by_date` moves off quote socket business command `21` onto the plain HTTP endpoint `GET /v1/gemini/option/option_chain_list` (longbridge/developers#1244), and its return type changes shape. The paired `StrikePriceInfo` (`price` + `call_symbol` + `put_symbol` + `standard`) is **removed** in favour of a flat, one-entry-per-contract `OptionChainContract` (`symbol`, `expiry_date`, `strike_price`, `direction`, `option_type`, `standard_attr`, `days_to_expiry`). Calls and puts are no longer paired by strike, so a strike listed on one side only now yields a single entry instead of an entry with an empty `call_symbol`/`put_symbol`, and callers must filter on `direction` where they used to read the two symbol fields. Two new enums come with it: `OptionExpiryCycleType` (`Unknown` / `Monthly` / `Weekly` / `Quarterly`) for the special expiration cycle — the server's empty `option_type` means a standard monthly option — and `OptionStandardAttr` (`Unknown` / `Normal` / `Old`) marking the legacy contracts left over from a corporate action (e.g. `BABA2261218C10000.US`); both fall back to `Unknown` for an unrecognized server value. The method also gains a `standard_only` parameter that filters those legacy contracts out server-side (positional `bool` in Rust incl. blocking, C, C++ and Java; optional `standardOnly?: boolean` in Node.js; `standard_only: bool = False` in Python) — it is omitted from the query string when false, which the endpoint treats as "return everything". The 30-minute client-side cache of the chain is gone, matching the other HTTP quote endpoints. New types per layer: C `lb_option_chain_contract_t` / `lb_option_expiry_cycle_type_t` / `lb_option_standard_attr_t` (`lb_strike_price_info_t` removed, and `lb_quote_context_option_chain_info_by_date` gains a `bool standard_only` argument before the callback), C++ `quote::OptionChainContract` / `OptionExpiryCycleType` / `OptionStandardAttr`, Java `com.longbridge.quote.OptionChainContract` / `OptionExpiryCycleType` / `OptionStandardAttr` (`StrikePriceInfo` deleted, and `getOptionChainInfoByDate` becomes `getOptionChainInfoByDate(String, LocalDate, boolean)`), Node.js `OptionChainContract` / `OptionExpiryCycleType` / `OptionStandardAttr`, and Python the same three (incl. the `openapi.pyi` stub). `QuoteContext.option_chain_expiry_date_list` is **unchanged** and still uses socket command `20`: the new endpoint requires `expiry_date`, so it cannot enumerate expiry dates, and the gateway path for the replacement expiry-date-list endpoint is not settled upstream yet - **All SDKs:** removed `GridContext.submit_strategy_questionnaire` (`POST /v1/record/questionnaire`) and its `SubmitStrategyQuestionnaireOptions` type. The endpoint has been retired; the strategy risk-disclosure record is no longer submitted through the OpenAPI SDK. Removed across Rust (incl. blocking), C, C++, Java, Node.js, and Python bindings ### Fixed diff --git a/c/cbindgen.toml b/c/cbindgen.toml index 92778e1d6c..f8df46c97a 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -24,6 +24,8 @@ cpp_compat = true "CTradeStatus" = "lb_trade_status_t" "CTradeDirection" = "lb_trade_direction_t" "COptionDirection" = "lb_option_direction_t" +"COptionExpiryCycleType" = "lb_option_expiry_cycle_type_t" +"COptionStandardAttr" = "lb_option_standard_attr_t" "COptionType" = "lb_option_type_t" "CWarrantType" = "lb_warrant_type_t" "CAdjustType" = "lb_adjust_type_t" @@ -52,7 +54,7 @@ cpp_compat = true "CSecurityBrokers" = "lb_security_brokers_t" "CParticipantInfo" = "lb_participant_info_t" "CIntradayLine" = "lb_intraday_line_t" -"CStrikePriceInfo" = "lb_strike_price_info_t" +"COptionChainContract" = "lb_option_chain_contract_t" "CIssuerInfo" = "lb_issuer_info_t" "CTradingSessionInfo" = "lb_trading_session_info_t" "CMarketTradingSession" = "lb_market_trading_session_t" @@ -413,7 +415,7 @@ include = [ "CSecurityBrokers", "CParticipantInfo", "CIntradayLine", - "CStrikePriceInfo", + "COptionChainContract", "CIssuerInfo", "CTradingSessionInfo", "CMarketTradingSession", diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index a458d19b9f..4e1322c1e4 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -1633,6 +1633,47 @@ typedef enum lb_option_direction_t { OptionDirectionCall, } lb_option_direction_t; +/** + * Special expiration cycle of an option contract + */ +typedef enum lb_option_expiry_cycle_type_t { + /** + * Unknown + */ + OptionExpiryCycleTypeUnknown, + /** + * Standard monthly option + */ + OptionExpiryCycleTypeMonthly, + /** + * Weekly option, expires weekly + */ + OptionExpiryCycleTypeWeekly, + /** + * Quarterly option, expires quarterly + */ + OptionExpiryCycleTypeQuarterly, +} lb_option_expiry_cycle_type_t; + +/** + * Whether an option contract is a legacy contract left over from a corporate + * action + */ +typedef enum lb_option_standard_attr_t { + /** + * Unknown + */ + OptionStandardAttrUnknown, + /** + * A normal, active contract + */ + OptionStandardAttrNormal, + /** + * A legacy contract produced by a corporate action + */ + OptionStandardAttrOld, +} lb_option_standard_attr_t; + /** * Cash flow direction */ @@ -5028,26 +5069,40 @@ typedef struct lb_intraday_line_t { } lb_intraday_line_t; /** - * Strike price info + * A single option contract of an option chain */ -typedef struct lb_strike_price_info_t { +typedef struct lb_option_chain_contract_t { + /** + * Option contract code, in `ticker.region` format + */ + const char *symbol; + /** + * Expiry date, in US Eastern time + */ + struct lb_date_t expiry_date; /** * Strike price */ - const struct lb_decimal_t *price; + const struct lb_decimal_t *strike_price; + /** + * Contract direction + */ + enum lb_option_direction_t direction; /** - * Security code of call option + * Special expiration cycle of the contract */ - const char *call_symbol; + enum lb_option_expiry_cycle_type_t option_type; /** - * Security code of put option + * Whether the contract is a legacy contract left over from a corporate + * action */ - const char *put_symbol; + enum lb_option_standard_attr_t standard_attr; /** - * Is standard + * Number of days remaining until the option expires, `0` on the expiry + * day and negative once expired */ - bool standard; -} lb_strike_price_info_t; + int32_t days_to_expiry; +} lb_option_chain_contract_t; /** * Issuer info @@ -13319,11 +13374,20 @@ void lb_quote_context_option_chain_expiry_date_list(const struct lb_quote_contex void *userdata); /** - * Get option chain info by date + * Get the option contract list of an underlying security for a given expiry + * date + * + * Every contract is an independent entry: calls and puts are not paired, so a + * strike price that is listed on one side only yields a single entry. + * + * `standard_only` filters out the legacy contracts produced by corporate + * actions. `true` returns standard contracts only; `false` returns everything, + * including the contracts carrying `OptionStandardAttrOld`. */ void lb_quote_context_option_chain_info_by_date(const struct lb_quote_context_t *ctx, const char *symbol, const struct lb_date_t *expiry_date, + bool standard_only, lb_async_callback_t callback, void *userdata); diff --git a/c/src/quote_context/context.rs b/c/src/quote_context/context.rs index a0a1babde2..5f6fa6b58a 100644 --- a/c/src/quote_context/context.rs +++ b/c/src/quote_context/context.rs @@ -23,12 +23,12 @@ use crate::{ CCandlestickOwned, CCapitalDistributionResponseOwned, CCapitalFlowLineOwned, CCreateWatchlistGroup, CFilingItemOwned, CHistoryMarketTemperatureResponseOwned, CIntradayLineOwned, CIssuerInfoOwned, CMarketTemperatureOwned, CMarketTradingDaysOwned, - CMarketTradingSessionOwned, COptionQuoteOwned, CParticipantInfoOwned, CPushBrokers, - CPushBrokersOwned, CPushCandlestick, CPushCandlestickOwned, CPushDepth, - CPushDepthOwned, CPushQuote, CPushQuoteOwned, CPushTrades, CPushTradesOwned, - CQuotePackageDetailOwned, CRealtimeQuoteOwned, CSecurityBrokersOwned, - CSecurityCalcIndexOwned, CSecurityDepthOwned, CSecurityOwned, CSecurityQuoteOwned, - CSecurityStaticInfoOwned, CStrikePriceInfoOwned, CSubscriptionOwned, CTradeOwned, + CMarketTradingSessionOwned, COptionChainContractOwned, COptionQuoteOwned, + CParticipantInfoOwned, CPushBrokers, CPushBrokersOwned, CPushCandlestick, + CPushCandlestickOwned, CPushDepth, CPushDepthOwned, CPushQuote, CPushQuoteOwned, + CPushTrades, CPushTradesOwned, CQuotePackageDetailOwned, CRealtimeQuoteOwned, + CSecurityBrokersOwned, CSecurityCalcIndexOwned, CSecurityDepthOwned, CSecurityOwned, + CSecurityQuoteOwned, CSecurityStaticInfoOwned, CSubscriptionOwned, CTradeOwned, CUpdateWatchlistGroup, CWarrantInfoOwned, CWarrantQuoteOwned, CWatchlistGroupOwned, LB_WATCHLIST_GROUP_NAME, LB_WATCHLIST_GROUP_SECURITIES, }, @@ -725,12 +725,21 @@ pub unsafe extern "C" fn lb_quote_context_option_chain_expiry_date_list( }); } -/// Get option chain info by date +/// Get the option contract list of an underlying security for a given expiry +/// date +/// +/// Every contract is an independent entry: calls and puts are not paired, so a +/// strike price that is listed on one side only yields a single entry. +/// +/// `standard_only` filters out the legacy contracts produced by corporate +/// actions. `true` returns standard contracts only; `false` returns everything, +/// including the contracts carrying `OptionStandardAttrOld`. #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_quote_context_option_chain_info_by_date( ctx: *const CQuoteContext, symbol: *const c_char, expiry_date: *const CDate, + standard_only: bool, callback: CAsyncCallback, userdata: *mut c_void, ) { @@ -738,8 +747,8 @@ pub unsafe extern "C" fn lb_quote_context_option_chain_info_by_date( let symbol = cstr_to_rust(symbol); let expiry_date = (*expiry_date).into(); execute_async(callback, ctx, userdata, async move { - let rows: CVec = ctx_inner - .option_chain_info_by_date(symbol, expiry_date) + let rows: CVec = ctx_inner + .option_chain_info_by_date(symbol, expiry_date, standard_only) .await? .into(); Ok(rows) diff --git a/c/src/quote_context/enum_types.rs b/c/src/quote_context/enum_types.rs index 2bd3ac7d48..3e4f05888c 100644 --- a/c/src/quote_context/enum_types.rs +++ b/c/src/quote_context/enum_types.rs @@ -112,6 +112,44 @@ pub enum COptionDirection { OptionDirectionCall, } +/// Special expiration cycle of an option contract +#[derive(Debug, Copy, Clone, Eq, PartialEq, CEnum)] +#[c(remote = "longbridge::quote::OptionExpiryCycleType")] +#[allow(clippy::enum_variant_names)] +#[repr(C)] +pub enum COptionExpiryCycleType { + /// Unknown + #[c(remote = "Unknown")] + OptionExpiryCycleTypeUnknown, + /// Standard monthly option + #[c(remote = "Monthly")] + OptionExpiryCycleTypeMonthly, + /// Weekly option, expires weekly + #[c(remote = "Weekly")] + OptionExpiryCycleTypeWeekly, + /// Quarterly option, expires quarterly + #[c(remote = "Quarterly")] + OptionExpiryCycleTypeQuarterly, +} + +/// Whether an option contract is a legacy contract left over from a corporate +/// action +#[derive(Debug, Copy, Clone, Eq, PartialEq, CEnum)] +#[c(remote = "longbridge::quote::OptionStandardAttr")] +#[allow(clippy::enum_variant_names)] +#[repr(C)] +pub enum COptionStandardAttr { + /// Unknown + #[c(remote = "Unknown")] + OptionStandardAttrUnknown, + /// A normal, active contract + #[c(remote = "Normal")] + OptionStandardAttrNormal, + /// A legacy contract produced by a corporate action + #[c(remote = "Old")] + OptionStandardAttrOld, +} + /// Warrant type #[derive(Debug, Copy, Clone, Eq, PartialEq, CEnum)] #[c(remote = "longbridge::quote::WarrantType")] diff --git a/c/src/quote_context/types.rs b/c/src/quote_context/types.rs index 7b82938ba9..f4c04fc587 100644 --- a/c/src/quote_context/types.rs +++ b/c/src/quote_context/types.rs @@ -3,20 +3,21 @@ use std::os::raw::c_char; use longbridge::quote::{ Brokers, Candlestick, CapitalDistribution, CapitalDistributionResponse, CapitalFlowLine, Depth, FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionDirection, OptionQuote, OptionType, - OptionVolumeDaily, OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, Period, - PrePostQuote, PushBrokers, PushCandlestick, PushDepth, PushQuote, PushTrades, + MarketTradingDays, MarketTradingSession, OptionChainContract, OptionDirection, OptionQuote, + OptionType, OptionVolumeDaily, OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, + Period, PrePostQuote, PushBrokers, PushCandlestick, PushDepth, PushQuote, PushTrades, QuotePackageDetail, RealtimeQuote, Security, SecurityBoard, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPositionsItem, ShortPositionsResponse, - ShortTradesItem, ShortTradesResponse, StrikePriceInfo, Subscription, Trade, TradeDirection, - TradeSession, TradeStatus, TradingSessionInfo, WarrantInfo, WarrantQuote, WarrantType, - WatchlistGroup, WatchlistSecurity, + ShortTradesItem, ShortTradesResponse, Subscription, Trade, TradeDirection, TradeSession, + TradeStatus, TradingSessionInfo, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, + WatchlistSecurity, }; use crate::{ quote_context::enum_types::{ - CGranularity, COptionDirection, COptionType, CPeriod, CSecuritiesUpdateMode, - CSecurityBoard, CTradeDirection, CTradeSession, CTradeStatus, CWarrantStatus, CWarrantType, + CGranularity, COptionDirection, COptionExpiryCycleType, COptionStandardAttr, COptionType, + CPeriod, CSecuritiesUpdateMode, CSecurityBoard, CTradeDirection, CTradeSession, + CTradeStatus, CWarrantStatus, CWarrantType, }, types::{CDate, CDecimal, CMarket, COption, CString, CTime, CVec, ToFFI}, }; @@ -1559,59 +1560,82 @@ impl ToFFI for CIntradayLineOwned { } } -/// Strike price info +/// A single option contract of an option chain #[repr(C)] -pub struct CStrikePriceInfo { +pub struct COptionChainContract { + /// Option contract code, in `ticker.region` format + pub symbol: *const c_char, + /// Expiry date, in US Eastern time + pub expiry_date: CDate, /// Strike price - pub price: *const CDecimal, - /// Security code of call option - pub call_symbol: *const c_char, - /// Security code of put option - pub put_symbol: *const c_char, - /// Is standard - pub standard: bool, + pub strike_price: *const CDecimal, + /// Contract direction + pub direction: COptionDirection, + /// Special expiration cycle of the contract + pub option_type: COptionExpiryCycleType, + /// Whether the contract is a legacy contract left over from a corporate + /// action + pub standard_attr: COptionStandardAttr, + /// Number of days remaining until the option expires, `0` on the expiry + /// day and negative once expired + pub days_to_expiry: i32, } #[derive(Debug)] -pub(crate) struct CStrikePriceInfoOwned { - price: CDecimal, - call_symbol: CString, - put_symbol: CString, - standard: bool, +pub(crate) struct COptionChainContractOwned { + symbol: CString, + expiry_date: CDate, + strike_price: CDecimal, + direction: COptionDirection, + option_type: COptionExpiryCycleType, + standard_attr: COptionStandardAttr, + days_to_expiry: i32, } -impl From for CStrikePriceInfoOwned { - fn from(info: StrikePriceInfo) -> Self { - let StrikePriceInfo { - price, - call_symbol, - put_symbol, - standard, +impl From for COptionChainContractOwned { + fn from(info: OptionChainContract) -> Self { + let OptionChainContract { + symbol, + expiry_date, + strike_price, + direction, + option_type, + standard_attr, + days_to_expiry, } = info; - CStrikePriceInfoOwned { - price: price.into(), - call_symbol: call_symbol.into(), - put_symbol: put_symbol.into(), - standard, + COptionChainContractOwned { + symbol: symbol.into(), + expiry_date: expiry_date.into(), + strike_price: strike_price.into(), + direction: direction.into(), + option_type: option_type.into(), + standard_attr: standard_attr.into(), + days_to_expiry, } } } -impl ToFFI for CStrikePriceInfoOwned { - type FFIType = CStrikePriceInfo; +impl ToFFI for COptionChainContractOwned { + type FFIType = COptionChainContract; fn to_ffi_type(&self) -> Self::FFIType { - let CStrikePriceInfoOwned { - price, - call_symbol, - put_symbol, - standard, + let COptionChainContractOwned { + symbol, + expiry_date, + strike_price, + direction, + option_type, + standard_attr, + days_to_expiry, } = self; - CStrikePriceInfo { - price, - call_symbol: call_symbol.to_ffi_type(), - put_symbol: put_symbol.to_ffi_type(), - standard: *standard, + COptionChainContract { + symbol: symbol.to_ffi_type(), + expiry_date: *expiry_date, + strike_price, + direction: *direction, + option_type: *option_type, + standard_attr: *standard_attr, + days_to_expiry: *days_to_expiry, } } } diff --git a/cpp/include/quote_context.hpp b/cpp/include/quote_context.hpp index 01644f3321..b4f2af9b8f 100644 --- a/cpp/include/quote_context.hpp +++ b/cpp/include/quote_context.hpp @@ -167,11 +167,21 @@ class QuoteContext const std::string& symbol, AsyncCallback> callback) const; - /// Get option chain expiry date list + /// Get the option contract list of an underlying security for a given expiry + /// date + /// + /// Every contract is an independent entry: calls and puts are not paired, so + /// a strike price that is listed on one side only yields a single entry. + /// + /// `standard_only` filters out the legacy contracts produced by corporate + /// actions. `true` returns standard contracts only; `false` returns + /// everything, including the contracts carrying `OptionStandardAttr::Old`. void option_chain_info_by_date( const std::string& symbol, Date expiry_date, - AsyncCallback> callback) const; + bool standard_only, + AsyncCallback> callback) + const; /// Get warrant issuers void warrant_issuers( diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index 81fb3eabd0..eacab7dd3f 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -473,6 +473,31 @@ enum class OptionDirection Call, }; +/// Special expiration cycle of an option contract +enum class OptionExpiryCycleType +{ + /// Unknown + Unknown, + /// Standard monthly option + Monthly, + /// Weekly option, expires weekly + Weekly, + /// Quarterly option, expires quarterly + Quarterly, +}; + +/// Whether an option contract is a legacy contract left over from a corporate +/// action +enum class OptionStandardAttr +{ + /// Unknown + Unknown, + /// A normal, active contract + Normal, + /// A legacy contract produced by a corporate action + Old, +}; + /// Quote of option struct OptionQuote { /// Security code @@ -694,17 +719,28 @@ enum class AdjustType ForwardAdjust }; -/// Strike price info -struct StrikePriceInfo +/// A single option contract of an option chain +/// +/// Every contract is an independent entry: calls and puts are not paired, so a +/// strike price that is listed on one side only yields a single entry. +struct OptionChainContract { + /// Option contract code, in `ticker.region` format + std::string symbol; + /// Expiry date, in US Eastern time + Date expiry_date; /// Strike price - Decimal price; - /// Security code of call option - std::string call_symbol; - /// Security code of put option - std::string put_symbol; - /// Is standard - bool standard; + Decimal strike_price; + /// Contract direction + OptionDirection direction; + /// Special expiration cycle of the contract + OptionExpiryCycleType option_type; + /// Whether the contract is a legacy contract left over from a corporate + /// action + OptionStandardAttr standard_attr; + /// Number of days remaining until the option expires, `0` on the expiry day + /// and negative once expired + int32_t days_to_expiry; }; /// Issuer info diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index f18c05ff96..fa9ccaa599 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -30,8 +30,11 @@ using longbridge::quote::IssuerInfo; using longbridge::quote::MarketTemperature; using longbridge::quote::MarketTradingDays; using longbridge::quote::MarketTradingSession; +using longbridge::quote::OptionChainContract; using longbridge::quote::OptionDirection; +using longbridge::quote::OptionExpiryCycleType; using longbridge::quote::OptionQuote; +using longbridge::quote::OptionStandardAttr; using longbridge::quote::OptionType; using longbridge::quote::ParticipantInfo; using longbridge::quote::Period; @@ -53,7 +56,6 @@ using longbridge::quote::SecurityListCategory; using longbridge::quote::SecurityQuote; using longbridge::quote::SecurityStaticInfo; using longbridge::quote::SortOrderType; -using longbridge::quote::StrikePriceInfo; using longbridge::quote::SubFlags; using longbridge::quote::Subscription; using longbridge::quote::Trade; @@ -511,6 +513,38 @@ convert(lb_option_direction_t ty) } } +inline OptionExpiryCycleType +convert(lb_option_expiry_cycle_type_t ty) +{ + switch (ty) { + case OptionExpiryCycleTypeUnknown: + return OptionExpiryCycleType::Unknown; + case OptionExpiryCycleTypeMonthly: + return OptionExpiryCycleType::Monthly; + case OptionExpiryCycleTypeWeekly: + return OptionExpiryCycleType::Weekly; + case OptionExpiryCycleTypeQuarterly: + return OptionExpiryCycleType::Quarterly; + default: + throw std::invalid_argument("unreachable"); + } +} + +inline OptionStandardAttr +convert(lb_option_standard_attr_t ty) +{ + switch (ty) { + case OptionStandardAttrUnknown: + return OptionStandardAttr::Unknown; + case OptionStandardAttrNormal: + return OptionStandardAttr::Normal; + case OptionStandardAttrOld: + return OptionStandardAttr::Old; + default: + throw std::invalid_argument("unreachable"); + } +} + inline Date convert(const lb_date_t* date) { @@ -873,14 +907,17 @@ convert(AdjustType ty) } } -inline StrikePriceInfo -convert(const lb_strike_price_info_t* info) +inline OptionChainContract +convert(const lb_option_chain_contract_t* info) { - return StrikePriceInfo{ - Decimal(info->price), - info->call_symbol, - info->put_symbol, - info->standard, + return OptionChainContract{ + info->symbol, + convert(&info->expiry_date), + Decimal(info->strike_price), + convert(info->direction), + convert(info->option_type), + convert(info->standard_attr), + info->days_to_expiry, }; } diff --git a/cpp/src/quote_context.cpp b/cpp/src/quote_context.cpp index ab5e4f2b5b..5e1785b0a9 100644 --- a/cpp/src/quote_context.cpp +++ b/cpp/src/quote_context.cpp @@ -860,7 +860,8 @@ void QuoteContext::option_chain_info_by_date( const std::string& symbol, Date expiry_date, - AsyncCallback> callback) const + bool standard_only, + AsyncCallback> callback) const { auto expiry_date2 = convert(&expiry_date); @@ -868,30 +869,33 @@ QuoteContext::option_chain_info_by_date( ctx_, symbol.c_str(), &expiry_date2, + standard_only, [](auto res) { auto callback_ptr = callback::get_async_callback>( + std::vector>( res->userdata); QuoteContext ctx((const lb_quote_context_t*)res->ctx); Status status(res->error); if (status) { - auto rows = (const lb_strike_price_info_t*)res->data; - std::vector rows2; + auto rows = (const lb_option_chain_contract_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)); + (*callback_ptr)( + AsyncResult>( + ctx, std::move(status), &rows2)); } else { - (*callback_ptr)(AsyncResult>( - ctx, std::move(status), nullptr)); + (*callback_ptr)( + AsyncResult>( + ctx, std::move(status), nullptr)); } }, - new AsyncCallback>(callback)); + new AsyncCallback>(callback)); } void diff --git a/java/Makefile.toml b/java/Makefile.toml index 8388758cce..fa1a566b54 100644 --- a/java/Makefile.toml +++ b/java/Makefile.toml @@ -66,6 +66,9 @@ args = [ "javasrc/src/main/java/com/longbridge/quote/IssuerInfo.java", "javasrc/src/main/java/com/longbridge/quote/MarketTradingDays.java", "javasrc/src/main/java/com/longbridge/quote/MarketTradingSession.java", + "javasrc/src/main/java/com/longbridge/quote/OptionChainContract.java", + "javasrc/src/main/java/com/longbridge/quote/OptionExpiryCycleType.java", + "javasrc/src/main/java/com/longbridge/quote/OptionStandardAttr.java", "javasrc/src/main/java/com/longbridge/quote/OptionDirection.java", "javasrc/src/main/java/com/longbridge/quote/OptionQuote.java", "javasrc/src/main/java/com/longbridge/quote/OptionType.java", @@ -85,7 +88,6 @@ args = [ "javasrc/src/main/java/com/longbridge/quote/SecurityDepth.java", "javasrc/src/main/java/com/longbridge/quote/SecurityQuote.java", "javasrc/src/main/java/com/longbridge/quote/SecurityStaticInfo.java", - "javasrc/src/main/java/com/longbridge/quote/StrikePriceInfo.java", "javasrc/src/main/java/com/longbridge/quote/SubFlags.java", "javasrc/src/main/java/com/longbridge/quote/Subscription.java", "javasrc/src/main/java/com/longbridge/quote/Trade.java", diff --git a/java/c/com_longbridge_SdkNative.h b/java/c/com_longbridge_SdkNative.h index f2e15f1acc..5297822a52 100644 --- a/java/c/com_longbridge_SdkNative.h +++ b/java/c/com_longbridge_SdkNative.h @@ -306,10 +306,10 @@ JNIEXPORT void JNICALL Java_com_longbridge_SdkNative_quoteContextOptionChainExpi /* * Class: com_longbridge_SdkNative * Method: quoteContextOptionChainInfoByDate - * Signature: (JLjava/lang/String;Ljava/time/LocalDate;Lcom/longbridge/AsyncCallback;)V + * Signature: (JLjava/lang/String;Ljava/time/LocalDate;ZLcom/longbridge/AsyncCallback;)V */ JNIEXPORT void JNICALL Java_com_longbridge_SdkNative_quoteContextOptionChainInfoByDate - (JNIEnv *, jclass, jlong, jstring, jobject, jobject); + (JNIEnv *, jclass, jlong, jstring, jobject, jboolean, jobject); /* * Class: com_longbridge_SdkNative diff --git a/java/javasrc/src/main/java/com/longbridge/SdkNative.java b/java/javasrc/src/main/java/com/longbridge/SdkNative.java index 9e80117822..64eb3d6674 100644 --- a/java/javasrc/src/main/java/com/longbridge/SdkNative.java +++ b/java/javasrc/src/main/java/com/longbridge/SdkNative.java @@ -153,7 +153,7 @@ public static native void quoteContextOptionChainExpiryDateList(long context, St AsyncCallback callback); public static native void quoteContextOptionChainInfoByDate(long context, String symbol, LocalDate expiryDate, - AsyncCallback callback); + boolean standardOnly, AsyncCallback callback); public static native void quoteContextWarrantIssuers(long context, AsyncCallback callback); diff --git a/java/javasrc/src/main/java/com/longbridge/quote/OptionChainContract.java b/java/javasrc/src/main/java/com/longbridge/quote/OptionChainContract.java new file mode 100644 index 0000000000..6099dd0948 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/OptionChainContract.java @@ -0,0 +1,97 @@ +package com.longbridge.quote; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * A single option contract of an option chain. + * + *

+ * Every contract is an independent entry: calls and puts are not paired, so a + * strike price that is listed on one side only yields a single entry. + */ +public class OptionChainContract { + private String symbol; + private LocalDate expiryDate; + private BigDecimal strikePrice; + private OptionDirection direction; + private OptionExpiryCycleType optionType; + private OptionStandardAttr standardAttr; + private int daysToExpiry; + + /** + * Returns the option contract code, in {@code ticker.region} format. + * + * @return the option contract code + */ + public String getSymbol() { + return symbol; + } + + /** + * Returns the expiry date, in US Eastern time. + * + * @return the expiry date + */ + public LocalDate getExpiryDate() { + return expiryDate; + } + + /** + * Returns the strike price. + * + * @return the strike price + */ + public BigDecimal getStrikePrice() { + return strikePrice; + } + + /** + * Returns the contract direction. + * + * @return the contract direction + */ + public OptionDirection getDirection() { + return direction; + } + + /** + * Returns the special expiration cycle of the contract. + * + * @return the expiration cycle + */ + public OptionExpiryCycleType getOptionType() { + return optionType; + } + + /** + * Returns whether the contract is a legacy contract left over from a + * corporate action. + * + * @return the standard attribute + */ + public OptionStandardAttr getStandardAttr() { + return standardAttr; + } + + /** + * Returns the number of days remaining until the option expires, updated + * daily at midnight ET. + * + *

+ * The value is {@code 0} for options expiring today, and negative for + * already-expired options. + * + * @return the number of days remaining until expiry + */ + public int getDaysToExpiry() { + return daysToExpiry; + } + + @Override + public String toString() { + return "OptionChainContract [symbol=" + symbol + ", expiryDate=" + expiryDate + ", strikePrice=" + strikePrice + + ", direction=" + direction + ", optionType=" + optionType + ", standardAttr=" + standardAttr + + ", daysToExpiry=" + daysToExpiry + "]"; + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/OptionExpiryCycleType.java b/java/javasrc/src/main/java/com/longbridge/quote/OptionExpiryCycleType.java new file mode 100644 index 0000000000..ab9fa6ecf3 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/OptionExpiryCycleType.java @@ -0,0 +1,15 @@ +package com.longbridge.quote; + +/** + * Special expiration cycle of an option contract + */ +public enum OptionExpiryCycleType { + /** Unknown */ + Unknown, + /** Standard monthly option */ + Monthly, + /** Weekly option, expires weekly */ + Weekly, + /** Quarterly option, expires quarterly */ + Quarterly, +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/OptionStandardAttr.java b/java/javasrc/src/main/java/com/longbridge/quote/OptionStandardAttr.java new file mode 100644 index 0000000000..b872bdd10a --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/OptionStandardAttr.java @@ -0,0 +1,14 @@ +package com.longbridge.quote; + +/** + * Whether an option contract is a legacy contract left over from a corporate + * action (e.g. a stock split or a merger) + */ +public enum OptionStandardAttr { + /** Unknown */ + Unknown, + /** A normal, active contract */ + Normal, + /** A legacy contract produced by a corporate action */ + Old, +} 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 c93809aea4..3347340879 100644 --- a/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java +++ b/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java @@ -656,7 +656,12 @@ public synchronized CompletableFuture getOptionChainExpiryDateList( } /** - * Get option chain info by date + * Get the option contract list of an underlying security for a given expiry + * date + * + *

+ * Every contract is an independent entry: calls and puts are not paired, so + * a strike price that is listed on one side only yields a single entry. * *

      * {@code
@@ -669,8 +674,8 @@ public synchronized CompletableFuture getOptionChainExpiryDateList(
      *         OAuth oauth = new OAuthBuilder("your-client-id")
      *             .build(url -> System.out.println("Visit: " + url)).get();
      *         try (Config config = Config.fromOAuth(oauth); QuoteContext ctx = QuoteContext.create(config)) {
-     *             StrikePriceInfo[] resp = ctx.getOptionChainInfoByDate("AAPL.US", LocalDate.of(2023, 1, 20)).get();
-     *             for (StrikePriceInfo obj : resp) {
+     *             OptionChainContract[] resp = ctx.getOptionChainInfoByDate("AAPL.US", LocalDate.of(2023, 1, 20), false).get();
+     *             for (OptionChainContract obj : resp) {
      *                 System.out.println(obj);
      *             }
      *         }
@@ -679,15 +684,21 @@ public synchronized CompletableFuture getOptionChainExpiryDateList(
      * }
      * 
* - * @param symbol Security symbol - * @param expiryDate Option expiry date + * @param symbol Security symbol + * @param expiryDate Option expiry date + * @param standardOnly Whether to filter out the legacy contracts produced by + * corporate actions. {@code true} returns standard + * contracts only; {@code false} returns everything, + * including the contracts carrying + * {@link OptionStandardAttr#Old} * @return A Future representing the result of the operation * @throws OpenApiException If an error occurs */ - public synchronized CompletableFuture getOptionChainInfoByDate(String symbol, LocalDate expiryDate) + public synchronized CompletableFuture getOptionChainInfoByDate(String symbol, + LocalDate expiryDate, boolean standardOnly) throws OpenApiException { return AsyncCallback.executeTask((callback) -> { - SdkNative.quoteContextOptionChainInfoByDate(raw(), symbol, expiryDate, callback); + SdkNative.quoteContextOptionChainInfoByDate(raw(), symbol, expiryDate, standardOnly, callback); }); } diff --git a/java/javasrc/src/main/java/com/longbridge/quote/StrikePriceInfo.java b/java/javasrc/src/main/java/com/longbridge/quote/StrikePriceInfo.java deleted file mode 100644 index ea7b97d4d6..0000000000 --- a/java/javasrc/src/main/java/com/longbridge/quote/StrikePriceInfo.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.longbridge.quote; - -import java.math.BigDecimal; - -/** - * Strike price information for an option chain. - */ -public class StrikePriceInfo { - private BigDecimal price; - private String callSymbol; - private String putSymbol; - private boolean standard; - - /** - * Returns the strike price. - * - * @return the strike price - */ - public BigDecimal getPrice() { - return price; - } - - /** - * Returns the symbol of the call option at this strike. - * - * @return the call option symbol - */ - public String getCallSymbol() { - return callSymbol; - } - - /** - * Returns the symbol of the put option at this strike. - * - * @return the put option symbol - */ - public String getPutSymbol() { - return putSymbol; - } - - /** - * Returns whether this is a standard strike price. - * - * @return {@code true} if this is a standard strike price - */ - public boolean isStandard() { - return standard; - } - - @Override - public String toString() { - return "StrikePriceInfo [callSymbol=" + callSymbol + ", price=" + price + ", putSymbol=" + putSymbol - + ", standard=" + standard + "]"; - } -} diff --git a/java/src/init.rs b/java/src/init.rs index 8ffd4d903b..7a969fbc1c 100644 --- a/java/src/init.rs +++ b/java/src/init.rs @@ -92,6 +92,8 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::quote::TradeDirection, longbridge::quote::OptionType, longbridge::quote::OptionDirection, + longbridge::quote::OptionExpiryCycleType, + longbridge::quote::OptionStandardAttr, longbridge::quote::WarrantType, longbridge::quote::WarrantStatus, longbridge::quote::SortOrderType, @@ -167,7 +169,7 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::quote::ParticipantInfo, longbridge::quote::IntradayLine, longbridge::quote::Candlestick, - longbridge::quote::StrikePriceInfo, + longbridge::quote::OptionChainContract, longbridge::quote::IssuerInfo, longbridge::quote::WarrantInfo, longbridge::quote::MarketTradingSession, diff --git a/java/src/quote_context.rs b/java/src/quote_context.rs index 67ea1831f3..d32713d2a6 100644 --- a/java/src/quote_context.rs +++ b/java/src/quote_context.rs @@ -731,6 +731,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextOptionCh context: i64, symbol: JString, expiry_date: JObject, + standard_only: jboolean, callback: JObject, ) { jni_result(&mut env, (), |env| { @@ -741,7 +742,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextOptionCh async_util::execute(env, callback, async move { Ok(ObjectArray( __owned_ctx - .option_chain_info_by_date(symbol, expiry_date) + .option_chain_info_by_date(symbol, expiry_date, standard_only > 0) .await?, )) })?; diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index 272c82045d..fd9453a649 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -270,9 +270,17 @@ impl_java_class!( ); impl_java_class!( - "com/longbridge/quote/StrikePriceInfo", - longbridge::quote::StrikePriceInfo, - [price, call_symbol, put_symbol, standard] + "com/longbridge/quote/OptionChainContract", + longbridge::quote::OptionChainContract, + [ + symbol, + expiry_date, + strike_price, + direction, + option_type, + standard_attr, + days_to_expiry + ] ); impl_java_class!( diff --git a/java/src/types/enum_types.rs b/java/src/types/enum_types.rs index 38ceb4fcac..7e0ac7a882 100644 --- a/java/src/types/enum_types.rs +++ b/java/src/types/enum_types.rs @@ -128,6 +128,18 @@ impl_java_enum!( [Unknown, Put, Call] ); +impl_java_enum!( + "com/longbridge/quote/OptionExpiryCycleType", + longbridge::quote::OptionExpiryCycleType, + [Unknown, Monthly, Weekly, Quarterly] +); + +impl_java_enum!( + "com/longbridge/quote/OptionStandardAttr", + longbridge::quote::OptionStandardAttr, + [Unknown, Normal, Old] +); + impl_java_enum!( "com/longbridge/quote/WarrantType", longbridge::quote::WarrantType, diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index e40e0e0258..a5e580273b 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -1466,6 +1466,37 @@ export declare class OAuth { static build(clientId: string, onOpenUrl: ((err: Error | null, arg: string) => void), callbackPort?: number | undefined | null): Promise } +/** + * A single option contract of an option chain + * + * Every contract is an independent entry: calls and puts are not paired, so a + * strike price that is listed on one side only yields a single entry. + */ +export declare class OptionChainContract { + toString(): string + toJSON(): any + /** Option contract code, in `ticker.region` format */ + get symbol(): string + /** Expiry date, in US Eastern time */ + get expiryDate(): NaiveDate + /** Strike price */ + get strikePrice(): Decimal + /** Contract direction */ + get direction(): OptionDirection + /** Special expiration cycle of the contract */ + get optionType(): OptionExpiryCycleType + /** + * Whether the contract is a legacy contract left over from a corporate + * action + */ + get standardAttr(): OptionStandardAttr + /** + * Number of days remaining until the option expires, `0` on the expiry + * day and negative once expired + */ + get daysToExpiry(): number +} + /** Quote of option */ export declare class OptionQuote { toString(): string @@ -2345,7 +2376,16 @@ export declare class QuoteContext { */ optionChainExpiryDateList(symbol: string): Promise> /** - * Get option chain info by date + * Get the option contract list of an underlying security for a given + * expiry date + * + * Every contract is an independent entry: calls and puts are not paired, + * so a strike price that is listed on one side only yields a single entry. + * + * `standardOnly` filters out the legacy contracts produced by corporate + * actions. `true` returns standard contracts only; omitted or `false` + * returns everything, including the contracts carrying a `standardAttr` + * of `Old`. * * #### Example * @@ -2360,7 +2400,7 @@ export declare class QuoteContext { * } * ``` */ - optionChainInfoByDate(symbol: string, expiryDate: NaiveDate): Promise> + optionChainInfoByDate(symbol: string, expiryDate: NaiveDate, standardOnly?: boolean | undefined | null): Promise> /** * Get warrant issuers * @@ -3034,20 +3074,6 @@ export declare class StockPositionsResponse { get channels(): Array } -/** Strike price info */ -export declare class StrikePriceInfo { - toString(): string - toJSON(): any - /** Strike price */ - get price(): Decimal - /** Security code of call option */ - get callSymbol(): string - /** Security code of put option */ - get putSymbol(): string - /** Is standard */ - get standard(): boolean -} - /** Response for submit grid trading order request */ export declare class SubmitGridOrderResponse { toString(): string @@ -6164,6 +6190,31 @@ export declare const enum OptionDirection { Call = 2 } +/** Special expiration cycle of an option contract */ +export declare const enum OptionExpiryCycleType { + /** Unknown */ + Unknown = 0, + /** Standard monthly option */ + Monthly = 1, + /** Weekly option, expires weekly */ + Weekly = 2, + /** Quarterly option, expires quarterly */ + Quarterly = 3 +} + +/** + * Whether an option contract is a legacy contract left over from a corporate + * action (e.g. a stock split or a merger) + */ +export declare const enum OptionStandardAttr { + /** Unknown */ + Unknown = 0, + /** A normal, active contract */ + Normal = 1, + /** A legacy contract produced by a corporate action */ + Old = 2 +} + /** Option type */ export declare const enum OptionType { /** Unknown */ diff --git a/nodejs/index.js b/nodejs/index.js index 88789ebfbe..a8f040323b 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -3,7 +3,7 @@ // @ts-nocheck /* auto-generated by NAPI-RS */ -const { readFileSync } = require('node:fs') +const { readFileSync } = require('fs') let nativeBinding = null const loadErrors = [] @@ -33,7 +33,7 @@ const isMuslFromFilesystem = () => { const isMuslFromReport = () => { let report = null - if (typeof process.report?.getReport === 'function') { + if (process.report && typeof process.report.getReport === 'function') { process.report.excludeNetwork = true report = process.report.getReport() } @@ -105,7 +105,7 @@ function requireNative() { } } else if (process.platform === 'win32') { if (process.arch === 'x64') { - if (process.config?.variables?.shlib_suffix === 'dll.a' || process.config?.variables?.node_target_type === 'shared_library') { + if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { try { return require('./longbridge.win32-x64-gnu.node') } catch (e) { @@ -523,54 +523,178 @@ function requireNative() { } } -nativeBinding = requireNative() +function createLoadErrorChain(errors) { + return errors.reduce((previous, current) => { + let message + try { + message = + current && typeof current.message === 'string' + ? current.message + : String(current) + } catch { + message = 'Unknown error' + } + const error = new Error(message) + error.cause = previous + return error + }, null) +} + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value → native binding preferred, WASI is only a fallback +// 'true' → prefer WASI, but retain native as a lazy fallback +// 'error' → require WASI without initializing a native fallback +// Treating any non-empty string as truthy (the historical behavior) meant +// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered +// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. +// +// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict +// WASI loading. It never crosses into another flavor or falls back to native. +const __napiWasiFlavors = ["wasm32-wasi"] +const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR +const __napiWasiFlavorRequested = + typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 +if ( + __napiWasiFlavorRequested && + __napiWasiFlavors.indexOf(__napiWasiFlavor) === -1 +) { + throw new Error( + 'Unsupported WASI flavor "' + + __napiWasiFlavor + + '". Available flavors: ' + + __napiWasiFlavors.join(', '), + ) +} +const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error' +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === 'true' || + forceWasiError || + __napiWasiFlavorRequested + +if (!forceWasi) { + nativeBinding = requireNative() +} -if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { +if (!nativeBinding || forceWasi) { let wasiBinding = null - let wasiBindingError = null - try { - wasiBinding = require('./longbridge.wasi.cjs') - nativeBinding = wasiBinding - } catch (err) { - if (process.env.NAPI_RS_FORCE_WASI) { - wasiBindingError = err + let wasiBindingLoaded = false + const wasiBindingErrors = [] + const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => { + try { + require.resolve(specifier) + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + if (isPackage) { + try { + require.resolve(specifier + '/package.json') + } catch (packageError) { + if (packageError && packageError.code === 'MODULE_NOT_FOUND') { + return resolveError + } + // An exports restriction proves the package exists even when its + // package.json is not public. Preserve the root resolution failure. + throw resolveError + } + // The package exists but its main/export target is broken. + throw resolveError + } + return resolveError } + if (localArtifacts) { + let artifactError = null + for (let i = 0; i < localArtifacts.length; i++) { + try { + require.resolve(localArtifacts[i]) + return null + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + artifactError = resolveError + } + } + return artifactError + } + return null } - if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { + let candidateError = null + let candidateFailed = false try { - wasiBinding = require('longbridge-wasm32-wasi') - nativeBinding = wasiBinding + candidateError = __napiWasiResolveCandidate('./longbridge.wasi.cjs', false, ["./longbridge.wasm32-wasi.debug.wasm","./longbridge.wasm32-wasi.wasm"]) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./longbridge.wasi.cjs') + nativeBinding = wasiBinding + wasiBindingLoaded = true + } } catch (err) { - if (process.env.NAPI_RS_FORCE_WASI) { - if (!wasiBindingError) { - wasiBindingError = err - } else { - wasiBindingError.cause = err + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('longbridge-wasm32-wasi', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('longbridge-wasm32-wasi/package.json').version + if (bindingPackageVersion !== '0.0.0') { + throw new Error(`WASI binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } } - loadErrors.push(err) + wasiBinding = require('longbridge-wasm32-wasi') + nativeBinding = wasiBinding + wasiBindingLoaded = true } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) } } - if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) { - const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error') - error.cause = wasiBindingError + if ( + !wasiBindingLoaded && + forceWasi && + !forceWasiError && + !__napiWasiFlavorRequested + ) { + nativeBinding = requireNative() + } + if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) { + const error = new Error( + __napiWasiFlavorRequested + ? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found' + : 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error', + ) + error.cause = createLoadErrorChain(wasiBindingErrors) throw error } } if (!nativeBinding) { if (loadErrors.length > 0) { - throw new Error( + const error = new Error( `Cannot find native binding. ` + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', - { - cause: loadErrors.reduce((err, cur) => { - cur.cause = err - return cur - }), - }, ) + // assign instead of the `new Error(message, { cause })` options form, + // which Node < 16.9 silently ignores + error.cause = createLoadErrorChain(loadErrors) + throw error } throw new Error(`Failed to load native binding`) } @@ -629,6 +753,7 @@ module.exports.NaiveDate = nativeBinding.NaiveDate module.exports.NaiveDatetime = nativeBinding.NaiveDatetime module.exports.NewsItem = nativeBinding.NewsItem module.exports.OAuth = nativeBinding.OAuth +module.exports.OptionChainContract = nativeBinding.OptionChainContract module.exports.OptionQuote = nativeBinding.OptionQuote module.exports.OptionVolumeDaily = nativeBinding.OptionVolumeDaily module.exports.OptionVolumeDailyStat = nativeBinding.OptionVolumeDailyStat @@ -668,7 +793,6 @@ module.exports.SharelistContext = nativeBinding.SharelistContext module.exports.StockPosition = nativeBinding.StockPosition module.exports.StockPositionChannel = nativeBinding.StockPositionChannel module.exports.StockPositionsResponse = nativeBinding.StockPositionsResponse -module.exports.StrikePriceInfo = nativeBinding.StrikePriceInfo module.exports.SubmitGridOrderResponse = nativeBinding.SubmitGridOrderResponse module.exports.SubmitOrderResponse = nativeBinding.SubmitOrderResponse module.exports.Subscription = nativeBinding.Subscription @@ -720,6 +844,8 @@ module.exports.Market = nativeBinding.Market module.exports.MultiLegPosition = nativeBinding.MultiLegPosition module.exports.MultiLegStrategy = nativeBinding.MultiLegStrategy module.exports.OptionDirection = nativeBinding.OptionDirection +module.exports.OptionExpiryCycleType = nativeBinding.OptionExpiryCycleType +module.exports.OptionStandardAttr = nativeBinding.OptionStandardAttr module.exports.OptionType = nativeBinding.OptionType module.exports.OrderSide = nativeBinding.OrderSide module.exports.OrderStatus = nativeBinding.OrderStatus diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index bd945db671..20d7009c47 100644 --- a/nodejs/src/quote/context.rs +++ b/nodejs/src/quote/context.rs @@ -16,13 +16,13 @@ use crate::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, - OptionVolumeStats, ParticipantInfo, Period, PinnedMode, QuotePackageDetail, - RealtimeQuote, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, - SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, - ShortTradesResponse, SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, - Trade, TradeSessions, USCryptoOverview, WarrantInfo, WarrantQuote, WarrantSortBy, - WarrantStatus, WarrantType, WatchlistGroup, + MarketTradingDays, MarketTradingSession, OptionChainContract, OptionQuote, + OptionVolumeDaily, OptionVolumeStats, ParticipantInfo, Period, PinnedMode, + QuotePackageDetail, RealtimeQuote, Security, SecurityBrokers, SecurityCalcIndex, + SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, + ShortPositionsResponse, ShortTradesResponse, SortOrderType, SubType, SubTypes, + Subscription, Trade, TradeSessions, USCryptoOverview, WarrantInfo, WarrantQuote, + WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, }, }, time::{NaiveDate, NaiveDatetime}, @@ -692,7 +692,16 @@ impl QuoteContext { .collect()) } - /// Get option chain info by date + /// Get the option contract list of an underlying security for a given + /// expiry date + /// + /// Every contract is an independent entry: calls and puts are not paired, + /// so a strike price that is listed on one side only yields a single entry. + /// + /// `standardOnly` filters out the legacy contracts produced by corporate + /// actions. `true` returns standard contracts only; omitted or `false` + /// returns everything, including the contracts carrying a `standardAttr` + /// of `Old`. /// /// #### Example /// @@ -711,9 +720,10 @@ impl QuoteContext { &self, symbol: String, expiry_date: &NaiveDate, - ) -> Result> { + standard_only: Option, + ) -> Result> { self.ctx - .option_chain_info_by_date(symbol, expiry_date.0) + .option_chain_info_by_date(symbol, expiry_date.0, standard_only.unwrap_or(false)) .await .map_err(ErrorNewType)? .into_iter() diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index 9d585d0ed0..1abfa0ca6b 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -208,6 +208,35 @@ pub enum OptionDirection { Call, } +/// Special expiration cycle of an option contract +#[napi_derive::napi] +#[derive(JsEnum, Debug, Hash, Eq, PartialEq, Copy, Clone)] +#[js(remote = "longbridge::quote::OptionExpiryCycleType")] +pub enum OptionExpiryCycleType { + /// Unknown + Unknown, + /// Standard monthly option + Monthly, + /// Weekly option, expires weekly + Weekly, + /// Quarterly option, expires quarterly + Quarterly, +} + +/// Whether an option contract is a legacy contract left over from a corporate +/// action (e.g. a stock split or a merger) +#[napi_derive::napi] +#[derive(JsEnum, Debug, Hash, Eq, PartialEq, Copy, Clone)] +#[js(remote = "longbridge::quote::OptionStandardAttr")] +pub enum OptionStandardAttr { + /// Unknown + Unknown, + /// A normal, active contract + Normal, + /// A legacy contract produced by a corporate action + Old, +} + /// Warrant type #[napi_derive::napi] #[derive(JsEnum, Debug, Hash, Eq, PartialEq, Copy, Clone)] @@ -690,19 +719,30 @@ pub struct Candlestick { trade_session: TradeSession, } -/// Strike price info +/// A single option contract of an option chain +/// +/// Every contract is an independent entry: calls and puts are not paired, so a +/// strike price that is listed on one side only yields a single entry. #[napi_derive::napi] #[derive(Debug, JsObject)] -#[js(remote = "longbridge::quote::StrikePriceInfo")] -pub struct StrikePriceInfo { +#[js(remote = "longbridge::quote::OptionChainContract")] +pub struct OptionChainContract { + /// Option contract code, in `ticker.region` format + symbol: String, + /// Expiry date, in US Eastern time + expiry_date: NaiveDate, /// Strike price - price: Decimal, - /// Security code of call option - call_symbol: String, - /// Security code of put option - put_symbol: String, - /// Is standard - standard: bool, + strike_price: Decimal, + /// Contract direction + direction: OptionDirection, + /// Special expiration cycle of the contract + option_type: OptionExpiryCycleType, + /// Whether the contract is a legacy contract left over from a corporate + /// action + standard_attr: OptionStandardAttr, + /// Number of days remaining until the option expires, `0` on the expiry + /// day and negative once expired + days_to_expiry: i32, } /// Issuer info diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 09c16f65dc..b4d3ec5ba0 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -1149,6 +1149,52 @@ class OptionDirection: Call """ +class OptionExpiryCycleType: + """ + Special expiration cycle of an option contract + """ + + class Unknown(OptionExpiryCycleType): + """ + Unknown + """ + + class Monthly(OptionExpiryCycleType): + """ + Standard monthly option + """ + + class Weekly(OptionExpiryCycleType): + """ + Weekly option, expires weekly + """ + + class Quarterly(OptionExpiryCycleType): + """ + Quarterly option, expires quarterly + """ + +class OptionStandardAttr: + """ + Whether an option contract is a legacy contract left over from a corporate + action (e.g. a stock split or a merger) + """ + + class Unknown(OptionStandardAttr): + """ + Unknown + """ + + class Normal(OptionStandardAttr): + """ + A normal, active contract + """ + + class Old(OptionStandardAttr): + """ + A legacy contract produced by a corporate action + """ + class OptionQuote: """ Quote of option @@ -1800,29 +1846,48 @@ class Period: Yearly """ -class StrikePriceInfo: +class OptionChainContract: """ - Strike price info + A single option contract of an option chain + + Every contract is an independent entry: calls and puts are not paired, so a + strike price that is listed on one side only yields a single entry. """ - price: Decimal + symbol: str + """ + Option contract code, in `ticker.region` format + """ + + expiry_date: date + """ + Expiry date, in US Eastern time + """ + + strike_price: Decimal """ Strike price """ - call_symbol: str + direction: OptionDirection """ - Security code of call option + Contract direction """ - put_symbol: str + option_type: OptionExpiryCycleType """ - Security code of put option + Special expiration cycle of the contract """ - standard: bool + standard_attr: OptionStandardAttr """ - Is standard + Whether the contract is a legacy contract left over from a corporate action + """ + + days_to_expiry: int + """ + Number of days remaining until the option expires, `0` on the expiry day + and negative once expired """ class IssuerInfo: @@ -3475,17 +3540,26 @@ class QuoteContext: """ def option_chain_info_by_date( - self, symbol: str, expiry_date: date - ) -> List[StrikePriceInfo]: + self, symbol: str, expiry_date: date, standard_only: bool = False + ) -> List[OptionChainContract]: """ - Get option chain info by date + Get the option contract list of an underlying security for a given + expiry date + + Every contract is an independent entry: calls and puts are not paired, + so a strike price that is listed on one side only yields a single + entry. Args: symbol: Security code expiry_date: Expiry date + standard_only: Whether to filter out the legacy contracts produced + by corporate actions. `True` returns standard contracts only; + `False` returns everything, including the contracts carrying a + `standard_attr` of `Old` Returns: - Option chain info + Option contract list Examples: :: @@ -4816,14 +4890,24 @@ class AsyncQuoteContext: ... def option_chain_info_by_date( - self, symbol: str, expiry_date: date - ) -> Awaitable[List[StrikePriceInfo]]: + self, symbol: str, expiry_date: date, standard_only: bool = False + ) -> Awaitable[List[OptionChainContract]]: """ - Get option chain info by date. Returns an awaitable that resolves to strike price info list. + Get the option contract list of an underlying security for a given + expiry date. Returns an awaitable that resolves to the option contract + list. + + Every contract is an independent entry: calls and puts are not paired, + so a strike price that is listed on one side only yields a single + entry. Args: symbol: Security code. expiry_date: Expiry date. + standard_only: Whether to filter out the legacy contracts produced + by corporate actions. `True` returns standard contracts only; + `False` returns everything, including the contracts carrying a + `standard_attr` of `Old`. Examples: :: diff --git a/python/src/quote/context.rs b/python/src/quote/context.rs index a87ee29be4..6cb6b4ea2c 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -17,12 +17,12 @@ use crate::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, - PinnedMode, QuotePackageDetail, RealtimeQuote, SecuritiesUpdateMode, Security, - SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, - SecurityStaticInfo, SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, - Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, - WarrantType, WatchlistGroup, + MarketTradingDays, MarketTradingSession, OptionChainContract, OptionQuote, + ParticipantInfo, Period, PinnedMode, QuotePackageDetail, RealtimeQuote, + SecuritiesUpdateMode, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, + SecurityListCategory, SecurityQuote, SecurityStaticInfo, SortOrderType, SubType, + SubTypes, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, + WarrantStatus, WarrantType, WatchlistGroup, }, }, time::{PyDateWrapper, PyOffsetDateTimeWrapper}, @@ -354,14 +354,17 @@ impl QuoteContext { .collect()) } - /// Get option chain info by date + /// Get the option contract list of an underlying security for a given + /// expiry date + #[pyo3(signature = (symbol, expiry_date, standard_only = false))] fn option_chain_info_by_date( &self, symbol: String, expiry_date: PyDateWrapper, - ) -> PyResult> { + standard_only: bool, + ) -> PyResult> { self.ctx - .option_chain_info_by_date(symbol, expiry_date.0) + .option_chain_info_by_date(symbol, expiry_date.0, standard_only) .map_err(ErrorNewType)? .into_iter() .map(TryInto::try_into) diff --git a/python/src/quote/context_async.rs b/python/src/quote/context_async.rs index 7d6e3cbe9e..5223d48dce 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -20,12 +20,12 @@ use crate::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, - PinnedMode, QuotePackageDetail, RealtimeQuote, SecuritiesUpdateMode, Security, - SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, - SecurityStaticInfo, SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, - Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, - WarrantType, WatchlistGroup, + MarketTradingDays, MarketTradingSession, OptionChainContract, OptionQuote, + ParticipantInfo, Period, PinnedMode, QuotePackageDetail, RealtimeQuote, + SecuritiesUpdateMode, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, + SecurityListCategory, SecurityQuote, SecurityStaticInfo, SortOrderType, SubType, + SubTypes, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, + WarrantStatus, WarrantType, WatchlistGroup, }, }, time::{PyDateWrapper, PyOffsetDateTimeWrapper}, @@ -466,22 +466,25 @@ impl AsyncQuoteContext { .map(|b| b.unbind()) } - /// Get option chain info by date. Returns awaitable. + /// Get the option contract list of an underlying security for a given + /// expiry date. Returns awaitable. + #[pyo3(signature = (symbol, expiry_date, standard_only = false))] fn option_chain_info_by_date( &self, py: Python<'_>, symbol: String, expiry_date: PyDateWrapper, + standard_only: bool, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let v = ctx - .option_chain_info_by_date(symbol, expiry_date.0) + .option_chain_info_by_date(symbol, expiry_date.0, standard_only) .await .map_err(ErrorNewType)?; v.into_iter() - .map(|x| -> PyResult { x.try_into() }) - .collect::>>() + .map(|x| -> PyResult { x.try_into() }) + .collect::>>() }) .map(|b| b.unbind()) } diff --git a/python/src/quote/mod.rs b/python/src/quote/mod.rs index 0c89ee2c29..d877d8edce 100644 --- a/python/src/quote/mod.rs +++ b/python/src/quote/mod.rs @@ -27,7 +27,7 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; - parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; @@ -63,6 +63,8 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index db27da0241..5372c4f2a4 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -156,6 +156,35 @@ pub(crate) enum OptionDirection { Call, } +/// Special expiration cycle of an option contract +#[pyclass(eq, eq_int, skip_from_py_object)] +#[derive(Debug, PyEnum, Copy, Clone, Hash, Eq, PartialEq)] +#[py(remote = "longbridge::quote::OptionExpiryCycleType")] +pub(crate) enum OptionExpiryCycleType { + /// Unknown + Unknown, + /// Standard monthly option + Monthly, + /// Weekly option, expires weekly + Weekly, + /// Quarterly option, expires quarterly + Quarterly, +} + +/// Whether an option contract is a legacy contract left over from a corporate +/// action (e.g. a stock split or a merger) +#[pyclass(eq, eq_int, skip_from_py_object)] +#[derive(Debug, PyEnum, Copy, Clone, Hash, Eq, PartialEq)] +#[py(remote = "longbridge::quote::OptionStandardAttr")] +pub(crate) enum OptionStandardAttr { + /// Unknown + Unknown, + /// A normal, active contract + Normal, + /// A legacy contract produced by a corporate action + Old, +} + /// Warrant type #[pyclass(eq, eq_int, from_py_object)] #[derive(Debug, PyEnum, Copy, Clone, Hash, Eq, PartialEq)] @@ -656,19 +685,30 @@ pub(crate) struct Candlestick { trade_session: TradeSession, } -/// Strike price info +/// A single option contract of an option chain +/// +/// Every contract is an independent entry: calls and puts are not paired, so a +/// strike price that is listed on one side only yields a single entry. #[pyclass(skip_from_py_object)] #[derive(Debug, PyObject)] -#[py(remote = "longbridge::quote::StrikePriceInfo")] -pub(crate) struct StrikePriceInfo { +#[py(remote = "longbridge::quote::OptionChainContract")] +pub(crate) struct OptionChainContract { + /// Option contract code, in `ticker.region` format + symbol: String, + /// Expiry date, in US Eastern time + expiry_date: PyDateWrapper, /// Strike price - price: PyDecimal, - /// Security code of call option - call_symbol: String, - /// Security code of put option - put_symbol: String, - /// Is standard - standard: bool, + strike_price: PyDecimal, + /// Contract direction + direction: OptionDirection, + /// Special expiration cycle of the contract + option_type: OptionExpiryCycleType, + /// Whether the contract is a legacy contract left over from a corporate + /// action + standard_attr: OptionStandardAttr, + /// Number of days remaining until the option expires, `0` on the expiry + /// day and negative once expired + days_to_expiry: i32, } /// Issuer info diff --git a/rust/src/blocking/quote.rs b/rust/src/blocking/quote.rs index 61c4bd3ecd..7d68940ef4 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -9,13 +9,14 @@ use crate::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, - ParticipantInfo, Period, PinnedMode, PushEvent, QuotePackageDetail, RealtimeQuote, - RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, - SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, - ShortPositionsResponse, ShortTradesResponse, SortOrderType, StrikePriceInfo, SubFlags, - Subscription, Trade, TradeSessions, USCryptoOverview, WarrantInfo, WarrantQuote, - WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, + MarketTradingDays, MarketTradingSession, OptionChainContract, OptionQuote, + OptionVolumeDaily, OptionVolumeStats, ParticipantInfo, Period, PinnedMode, PushEvent, + QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, + RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, + SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, + ShortTradesResponse, SortOrderType, SubFlags, Subscription, Trade, TradeSessions, + USCryptoOverview, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, WarrantType, + WatchlistGroup, }, }; @@ -578,7 +579,16 @@ impl QuoteContextSync { .call(move |ctx| async move { ctx.option_chain_expiry_date_list(symbol).await }) } - /// Get option chain info by date + /// Get the option contract list of an underlying security for a given + /// expiry date + /// + /// Every contract is an independent entry: calls and puts are not paired, + /// so a strike price that is listed on one side only yields a single entry. + /// + /// `standard_only` filters out the legacy contracts produced by corporate + /// actions. `true` returns standard contracts only; `false` returns + /// everything, including the contracts carrying + /// [`OptionStandardAttr::Old`](crate::quote::OptionStandardAttr::Old). /// /// # Examples /// @@ -594,7 +604,7 @@ impl QuoteContextSync { /// let config = Arc::new(Config::from_oauth(oauth)); /// let ctx = QuoteContextSync::new(config, |_| ()); /// - /// let resp = ctx.option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20))?; + /// let resp = ctx.option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20), false)?; /// println!("{:?}", resp); /// # Ok(()) /// # } @@ -603,10 +613,12 @@ impl QuoteContextSync { &self, symbol: impl Into + Send + 'static, expiry_date: Date, - ) -> Result> { - self.rt.call( - move |ctx| async move { ctx.option_chain_info_by_date(symbol, expiry_date).await }, - ) + standard_only: bool, + ) -> Result> { + self.rt.call(move |ctx| async move { + ctx.option_chain_info_by_date(symbol, expiry_date, standard_only) + .await + }) } /// Get warrant issuers diff --git a/rust/src/quote/cmd_code.rs b/rust/src/quote/cmd_code.rs index 21181af8fd..62563b5ff8 100644 --- a/rust/src/quote/cmd_code.rs +++ b/rust/src/quote/cmd_code.rs @@ -46,9 +46,6 @@ pub(crate) const GET_SECURITY_CANDLESTICKS: u8 = 19; /// Get Option Chain Expiry Date List pub(crate) const GET_OPTION_CHAIN_EXPIRY_DATE_LIST: u8 = 20; -/// Get Option Chain Info By Date -pub(crate) const GET_OPTION_CHAIN_INFO_BY_DATE: u8 = 21; - /// Get Warrant Issuer IDs pub(crate) const GET_WARRANT_ISSUER_IDS: u8 = 22; diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index b9f916259b..d95843378c 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -1,4 +1,5 @@ use std::{ + str::FromStr, sync::{Arc, RwLock}, time::Duration, }; @@ -6,6 +7,7 @@ use std::{ use longbridge_httpcli::{DcRegion, HttpClient, Json, Method}; use longbridge_proto::quote; use longbridge_wscli::WsClientError; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use time::{Date, PrimitiveDateTime}; use tokio::sync::{mpsc, oneshot}; @@ -16,13 +18,14 @@ use crate::{ quote::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, + MarketTradingDays, MarketTradingSession, OptionChainContract, OptionDirection, + OptionExpiryCycleType, OptionQuote, OptionStandardAttr, OptionVolumeDaily, OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsItem, - ShortPositionsResponse, ShortTradesItem, ShortTradesResponse, StrikePriceInfo, - Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, + ShortPositionsResponse, ShortTradesItem, ShortTradesResponse, Subscription, Trade, + TradeSessions, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, cache::{Cache, CacheWithKey}, cmd_code, core::{Command, Core, UserProfile}, @@ -53,7 +56,6 @@ fn unix_secs_to_rfc3339(s: &str) -> String { } const ISSUER_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); const OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); -const OPTION_CHAIN_STRIKE_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); const TRADING_SESSION_CACHE_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 2); struct InnerQuoteContext { @@ -66,7 +68,6 @@ struct InnerQuoteContext { cache_participants: Cache>, cache_issuers: Cache>, cache_option_chain_expiry_date_list: CacheWithKey>, - cache_option_chain_strike_info: CacheWithKey<(String, Date), Vec>, cache_trading_session: Cache>, user_profile: Arc>>, log_subscriber: Arc, @@ -126,9 +127,6 @@ impl QuoteContext { cache_option_chain_expiry_date_list: CacheWithKey::new( OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT, ), - cache_option_chain_strike_info: CacheWithKey::new( - OPTION_CHAIN_STRIKE_INFO_CACHE_TIMEOUT, - ), cache_trading_session: Cache::new(TRADING_SESSION_CACHE_TIMEOUT), user_profile, log_subscriber, @@ -1049,9 +1047,18 @@ impl QuoteContext { .await } - /// Get option chain info by date + /// Get the option contract list of an underlying security for a given + /// expiry date + /// + /// Every contract is an independent entry: calls and puts are not paired, + /// so a strike price that is listed on one side only yields a single entry. /// - /// Reference: + /// `standard_only` filters out the legacy contracts produced by corporate + /// actions. `true` returns standard contracts only; `false` returns + /// everything, including the contracts carrying + /// [`OptionStandardAttr::Old`]. + /// + /// Path: `GET /v1/gemini/option/option_chain_list` /// /// # Examples /// @@ -1069,7 +1076,7 @@ impl QuoteContext { /// let (ctx, _) = QuoteContext::new(config); /// /// let resp = ctx - /// .option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20)) + /// .option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20), false) /// .await?; /// println!("{:?}", resp); /// # Ok::<_, Box>(()) @@ -1079,28 +1086,72 @@ impl QuoteContext { &self, symbol: impl Into, expiry_date: Date, - ) -> Result> { + standard_only: bool, + ) -> Result> { + #[derive(Debug, Serialize)] + struct Request { + symbol: String, + expiry_date: String, + // `false` and an omitted parameter mean the same thing to the + // endpoint, so send nothing rather than `standard_only=false`. + #[serde(skip_serializing_if = "std::ops::Not::not")] + standard_only: bool, + } + + #[derive(Debug, Deserialize)] + struct RawOptionChainContract { + #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")] + symbol: String, + expiry_date: String, + #[serde(with = "crate::serde_utils::decimal_empty_is_0")] + strike_price: Decimal, + direction: String, + // Both of these are documented as an empty string for the common + // case, so tolerate an explicit `null` as well. + #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")] + option_type: String, + #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")] + standard_attr: String, + #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")] + days_to_expiry: i32, + } + + #[derive(Debug, Deserialize)] + struct Response { + #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")] + list: Vec, + } + self.0 - .cache_option_chain_strike_info - .get_or_update( - (symbol.into(), expiry_date), - |(symbol, expiry_date)| async move { - let resp: quote::OptionChainDateStrikeInfoResponse = self - .request( - cmd_code::GET_OPTION_CHAIN_INFO_BY_DATE, - quote::OptionChainDateStrikeInfoRequest { - symbol, - expiry_date: format_date(expiry_date), - }, - ) - .await?; - resp.strike_price_info - .into_iter() - .map(TryInto::try_into) - .collect::>>() - }, - ) - .await + .http_cli + .request(Method::GET, "/v1/gemini/option/option_chain_list") + .query_params(Request { + symbol: symbol.into(), + expiry_date: format_date(expiry_date), + standard_only, + }) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0 + .list + .into_iter() + .map(|row| { + Ok(OptionChainContract { + symbol: row.symbol, + expiry_date: parse_date(&row.expiry_date) + .map_err(|err| Error::parse_field_error("expiry_date", err))?, + strike_price: row.strike_price, + direction: OptionDirection::from_str(&row.direction).unwrap_or_default(), + option_type: OptionExpiryCycleType::from_str(&row.option_type) + .unwrap_or_default(), + standard_attr: OptionStandardAttr::from_str(&row.standard_attr) + .unwrap_or_default(), + days_to_expiry: row.days_to_expiry, + }) + }) + .collect() } /// Get warrant issuers diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index 39d4260e1a..82cb006276 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -35,8 +35,11 @@ pub use types::{ MarketTemperature, MarketTradingDays, MarketTradingSession, + OptionChainContract, OptionDirection, + OptionExpiryCycleType, OptionQuote, + OptionStandardAttr, OptionType, // New in Step 3 OptionVolumeDaily, @@ -63,7 +66,6 @@ pub use types::{ ShortTradesItem, ShortTradesResponse, SortOrderType, - StrikePriceInfo, Subscription, Trade, TradeDirection, diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index f9d0b5d7a4..dd3a851c58 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -499,6 +499,36 @@ pub enum OptionDirection { Call, } +/// Special expiration cycle of an option contract +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)] +pub enum OptionExpiryCycleType { + /// Unknown + Unknown, + /// Standard monthly option + #[strum(serialize = "")] + Monthly, + /// Weekly option, expires weekly + #[strum(serialize = "W")] + Weekly, + /// Quarterly option, expires quarterly + #[strum(serialize = "Q")] + Quarterly, +} + +/// Whether an option contract is a legacy contract left over from a corporate +/// action (e.g. a stock split or a merger) +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)] +pub enum OptionStandardAttr { + /// Unknown + Unknown, + /// A normal, active contract + #[strum(serialize = "")] + Normal, + /// A legacy contract produced by a corporate action + #[strum(serialize = "old")] + Old, +} + /// Quote of option #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OptionQuote { @@ -940,30 +970,31 @@ impl longbridge_candlesticks::CandlestickType for Candlestick { } } -/// Strike price info +/// A single option contract of an option chain +/// +/// Every contract is an independent entry: calls and puts are not paired, so a +/// strike price that is listed on one side only yields a single entry. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StrikePriceInfo { +pub struct OptionChainContract { + /// Option contract code, in `ticker.region` format + pub symbol: String, + /// Expiry date, in US Eastern time + pub expiry_date: Date, /// Strike price - pub price: Decimal, - /// Security code of call option - pub call_symbol: String, - /// Security code of put option - pub put_symbol: String, - /// Is standard - pub standard: bool, -} - -impl TryFrom for StrikePriceInfo { - type Error = Error; - - fn try_from(value: quote::StrikePriceInfo) -> Result { - Ok(Self { - price: value.price.parse().unwrap_or_default(), - call_symbol: value.call_symbol, - put_symbol: value.put_symbol, - standard: value.standard, - }) - } + pub strike_price: Decimal, + /// Contract direction + pub direction: OptionDirection, + /// Special expiration cycle of the contract + pub option_type: OptionExpiryCycleType, + /// Whether the contract is a legacy contract left over from a corporate + /// action + pub standard_attr: OptionStandardAttr, + /// Number of days remaining until the option expires, updated daily at + /// midnight ET + /// + /// `0` for options expiring today, and negative for already-expired + /// options. + pub days_to_expiry: i32, } /// Issuer info @@ -2026,6 +2057,8 @@ impl_serde_for_enum_string!(Granularity); impl_default_for_enum_string!( OptionType, OptionDirection, + OptionExpiryCycleType, + OptionStandardAttr, WarrantType, SecurityBoard, Granularity