diff --git a/CHANGELOG.md b/CHANGELOG.md index 8909da0..6279b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [Unreleased] + +### Added + +- `market.TradeStatus` models `/v1/quote/market-status` trade status codes, + including display names, labels, normalization helpers, and status code `2001`. + +### Changed + +- `market.MarketTimeItem.TradeStatus` and `DelayTradeStatus` now use + `market.TradeStatus` instead of raw `int32` values. + ## [v0.25.1] - 2026-06-13 ### Added diff --git a/market/context.go b/market/context.go index 9e41de0..4309b71 100644 --- a/market/context.go +++ b/market/context.go @@ -16,8 +16,8 @@ import ( "github.com/shopspring/decimal" "github.com/longbridge/openapi-go/config" - "github.com/longbridge/openapi-go/market/jsontypes" httplib "github.com/longbridge/openapi-go/http" + "github.com/longbridge/openapi-go/market/jsontypes" ) // MarketContext is a client for the Longbridge Market OpenAPI. @@ -63,9 +63,9 @@ func (m *MarketContext) MarketStatus(ctx context.Context) (*MarketStatusResponse for _, item := range resp.MarketTime { out.MarketTime = append(out.MarketTime, MarketTimeItem{ Market: item.Market, - TradeStatus: item.TradeStatus, + TradeStatus: TradeStatusFromCode(item.TradeStatus), Timestamp: parseTimestampString(item.Timestamp), - DelayTradeStatus: item.DelayTradeStatus, + DelayTradeStatus: TradeStatusFromCode(item.DelayTradeStatus), DelayTimestamp: parseTimestampString(item.DelayTimestamp), SubStatus: item.SubStatus, DelaySubStatus: item.DelaySubStatus, diff --git a/market/jsontypes/types.go b/market/jsontypes/types.go index dfd7039..1e0a4e5 100644 --- a/market/jsontypes/types.go +++ b/market/jsontypes/types.go @@ -11,8 +11,10 @@ type MarketStatusResponse struct { // MarketTimeItem is the raw JSON representation of one market's trading status. type MarketTimeItem struct { Market string `json:"market"` + // Raw market trade status code. See market.TradeStatus for the code table. TradeStatus int32 `json:"trade_status"` Timestamp string `json:"timestamp"` + // Raw delayed market trade status code. DelayTradeStatus int32 `json:"delay_trade_status"` DelayTimestamp string `json:"delay_timestamp"` SubStatus int32 `json:"sub_status"` diff --git a/market/trade_status.go b/market/trade_status.go new file mode 100644 index 0000000..3e6c9d1 --- /dev/null +++ b/market/trade_status.go @@ -0,0 +1,320 @@ +package market + +import "encoding/json" + +// TradeStatus is a market trading status code returned by /v1/quote/market-status. +type TradeStatus int32 + +const ( + // TradeStatusUnknown is an unknown or unsupported market trading status. + TradeStatusUnknown TradeStatus = -1 + // TradeStatusNoRegisterQuote means quote is not registered. + TradeStatusNoRegisterQuote TradeStatus = 0 + // TradeStatusClean is clearing before the market opens. + TradeStatusClean TradeStatus = 101 + // TradeStatusOpenBid is the opening auction. + TradeStatusOpenBid TradeStatus = 102 + // TradeStatusMorningClosing is the morning break, currently used by VIX indexes. + TradeStatusMorningClosing TradeStatus = 103 + // TradeStatusTrading is regular trading. + TradeStatusTrading TradeStatus = 105 + // TradeStatusNoonClosing is the midday break. + TradeStatusNoonClosing TradeStatus = 106 + // TradeStatusCloseBid is the closing auction. + TradeStatusCloseBid TradeStatus = 107 + // TradeStatusClosing means the market is closed. + TradeStatusClosing TradeStatus = 108 + // TradeStatusDarkWait is dark trading waiting to open. + TradeStatusDarkWait TradeStatus = 110 + // TradeStatusDarkTrading is dark trading. + TradeStatusDarkTrading TradeStatus = 111 + // TradeStatusDarkClosing is dark trading closed. + TradeStatusDarkClosing TradeStatus = 112 + // TradeStatusAfterFix is after-hours fixed-price trading. + TradeStatusAfterFix TradeStatus = 120 + // TradeStatusHalfClosing is half-day market closed. + TradeStatusHalfClosing TradeStatus = 121 + // TradeStatusNotOpened means the exchange is waiting to open under special conditions. + TradeStatusNotOpened TradeStatus = 122 + // TradeStatusRealtimeQuote is a temporary intraday break. + TradeStatusRealtimeQuote TradeStatus = 123 + // TradeStatusUSPrev is US pre-market. + TradeStatusUSPrev TradeStatus = 201 + // TradeStatusUSTrading is US regular trading. + TradeStatusUSTrading TradeStatus = 202 + // TradeStatusUSAfter is US post-market. + TradeStatusUSAfter TradeStatus = 203 + // TradeStatusUSClosing is US closed. + TradeStatusUSClosing TradeStatus = 204 + // TradeStatusUSStop is US halted. + TradeStatusUSStop TradeStatus = 205 + // TradeStatusUSClean is US clearing plus pre-market. + TradeStatusUSClean TradeStatus = 206 + // TradeStatusUSNight is US overnight trading. + TradeStatusUSNight TradeStatus = 207 + // TradeStatusUSPrevMarketClean is a US pre-market clearing alias returned by the quote engine. + TradeStatusUSPrevMarketClean TradeStatus = 209 + // TradeStatusUSAfterMarketClean is a US post-market clearing alias returned by the quote engine. + TradeStatusUSAfterMarketClean TradeStatus = 210 + // TradeStatusRefresh is stock refresh. It is deprecated in the status definition. + TradeStatusRefresh TradeStatus = 1000 + // TradeStatusDelist is delisted. + TradeStatusDelist TradeStatus = 1001 + // TradeStatusPrepare is preparing to list. + TradeStatusPrepare TradeStatus = 1002 + // TradeStatusCodeChange is code changed. + TradeStatusCodeChange TradeStatus = 1003 + // TradeStatusStop is halted. + TradeStatusStop TradeStatus = 1004 + // TradeStatusWillOpen is waiting to open, typically for a US IPO auction. + TradeStatusWillOpen TradeStatus = 1005 + // TradeStatusCommonSuspend is split or merge suspended. + TradeStatusCommonSuspend TradeStatus = 1006 + // TradeStatusExpire is expired. + TradeStatusExpire TradeStatus = 1007 + // TradeStatusNoQuote means no quote data. + TradeStatusNoQuote TradeStatus = 1008 + // TradeStatusUnited is not listed. The historical variant name is kept for compatibility. + TradeStatusUnited TradeStatus = 1009 + // TradeStatusTradingHalt is terminated trading, usually for warrants. + TradeStatusTradingHalt TradeStatus = 1010 + // TradeStatusWaitListing is waiting to list, usually for new warrants. + TradeStatusWaitListing TradeStatus = 1011 + // TradeStatusFuse is fuse. + TradeStatusFuse TradeStatus = 2001 +) + +// TradeStatusFromCode converts a raw market trading status code to TradeStatus. +func TradeStatusFromCode(code int32) TradeStatus { + status := TradeStatus(code) + switch status { + case TradeStatusUnknown, + TradeStatusNoRegisterQuote, + TradeStatusClean, + TradeStatusOpenBid, + TradeStatusMorningClosing, + TradeStatusTrading, + TradeStatusNoonClosing, + TradeStatusCloseBid, + TradeStatusClosing, + TradeStatusDarkWait, + TradeStatusDarkTrading, + TradeStatusDarkClosing, + TradeStatusAfterFix, + TradeStatusHalfClosing, + TradeStatusNotOpened, + TradeStatusRealtimeQuote, + TradeStatusUSPrev, + TradeStatusUSTrading, + TradeStatusUSAfter, + TradeStatusUSClosing, + TradeStatusUSStop, + TradeStatusUSClean, + TradeStatusUSNight, + TradeStatusUSPrevMarketClean, + TradeStatusUSAfterMarketClean, + TradeStatusRefresh, + TradeStatusDelist, + TradeStatusPrepare, + TradeStatusCodeChange, + TradeStatusStop, + TradeStatusWillOpen, + TradeStatusCommonSuspend, + TradeStatusExpire, + TradeStatusNoQuote, + TradeStatusUnited, + TradeStatusTradingHalt, + TradeStatusWaitListing, + TradeStatusFuse: + return status + default: + return TradeStatusUnknown + } +} + +// UnmarshalJSON decodes numeric market trading status codes. +func (s *TradeStatus) UnmarshalJSON(data []byte) error { + var code int32 + if err := json.Unmarshal(data, &code); err != nil { + return err + } + *s = TradeStatusFromCode(code) + return nil +} + +// Code returns the raw numeric status code. +func (s TradeStatus) Code() int32 { + return int32(s) +} + +// String returns the full English status name. +func (s TradeStatus) String() string { + return s.Name() +} + +// Label returns a simplified label for key display states. +func (s TradeStatus) Label() string { + status := s.Normalize() + switch status { + case TradeStatusUSPrev, + TradeStatusUSTrading, + TradeStatusUSAfter, + TradeStatusUSNight, + TradeStatusUSClosing, + TradeStatusTrading, + TradeStatusClosing: + return status.Name() + default: + return "" + } +} + +// Name returns the full English status name. +func (s TradeStatus) Name() string { + switch s.Normalize() { + case TradeStatusUnknown, TradeStatusNoRegisterQuote: + return "Unknown" + case TradeStatusOpenBid: + return "Open Bid" + case TradeStatusMorningClosing: + return "Morning Break" + case TradeStatusTrading, TradeStatusUSTrading, TradeStatusUSAfterMarketClean: + return "Trading" + case TradeStatusNoonClosing: + return "Mid-Day Break" + case TradeStatusCloseBid: + return "Close Bid" + case TradeStatusClosing, TradeStatusClean, TradeStatusHalfClosing, TradeStatusUSClosing, TradeStatusUSPrevMarketClean: + return "Closed" + case TradeStatusDarkWait: + return "Dark Wait" + case TradeStatusDarkTrading: + return "Dark Trading" + case TradeStatusDarkClosing: + return "Closing" + case TradeStatusAfterFix: + return "After Fix" + case TradeStatusNotOpened: + return "Not Open" + case TradeStatusRealtimeQuote: + return "Temporary Break" + case TradeStatusUSPrev, TradeStatusUSClean: + return "Pre-Market" + case TradeStatusUSAfter: + return "Post-Market" + case TradeStatusUSStop, TradeStatusStop: + return "Stop" + case TradeStatusUSNight: + return "Overnight" + case TradeStatusRefresh: + return "Refresh" + case TradeStatusDelist: + return "Delist" + case TradeStatusPrepare: + return "Prepare" + case TradeStatusCodeChange: + return "Code Change" + case TradeStatusWillOpen: + return "Will Open" + case TradeStatusCommonSuspend: + return "Common Suspend" + case TradeStatusExpire: + return "Expire" + case TradeStatusNoQuote: + return "No Quote" + case TradeStatusUnited: + return "Not Listed" + case TradeStatusTradingHalt: + return "Terminated" + case TradeStatusWaitListing: + return "Wait Listing" + case TradeStatusFuse: + return "Fuse" + default: + return "Unknown" + } +} + +// IsUSMarket reports whether this is a US market status. +func (s TradeStatus) IsUSMarket() bool { + return s.Code() >= 200 && s.Code() < 300 +} + +// IsUSPrePost reports whether this is a US pre/post-market status. +func (s TradeStatus) IsUSPrePost() bool { + return s.IsUSPreMarket() || s.IsUSPostMarket() +} + +// IsUSNight reports whether this is a US overnight status. +func (s TradeStatus) IsUSNight() bool { + return s == TradeStatusUSNight +} + +// IsUSClosing reports whether this is a US closed status. +func (s TradeStatus) IsUSClosing() bool { + return s == TradeStatusUSClosing || s == TradeStatusUSPrevMarketClean +} + +// IsClosing reports whether this is a closed status. +func (s TradeStatus) IsClosing() bool { + return s == TradeStatusUSClosing || + s == TradeStatusUSPrevMarketClean || + s == TradeStatusClosing || + s == TradeStatusHalfClosing +} + +// IsUSPreMarket reports whether this is a US pre-market status. +func (s TradeStatus) IsUSPreMarket() bool { + return s == TradeStatusUSPrev || s == TradeStatusUSClean +} + +// IsUSPostMarket reports whether this is a US post-market status. +func (s TradeStatus) IsUSPostMarket() bool { + return s == TradeStatusUSAfter +} + +// IsTrading reports whether this is a trading status. +func (s TradeStatus) IsTrading() bool { + return s == TradeStatusTrading || + s == TradeStatusUSTrading || + s == TradeStatusUSAfterMarketClean +} + +// IsDark reports whether this is a dark-pool status. +func (s TradeStatus) IsDark() bool { + return s == TradeStatusDarkWait || + s == TradeStatusDarkTrading || + s == TradeStatusDarkClosing +} + +// AllowTrading reports whether this status allows trading. +func (s TradeStatus) AllowTrading() bool { + return s == TradeStatusOpenBid || + s == TradeStatusTrading || + s == TradeStatusCloseBid || + s == TradeStatusNotOpened || + s == TradeStatusNoonClosing || + s == TradeStatusUSTrading || + s == TradeStatusUSAfterMarketClean +} + +// Normalize maps quote-engine aliases to their display-equivalent status. +func (s TradeStatus) Normalize() TradeStatus { + switch s { + case TradeStatusClean: + return TradeStatusClosing + case TradeStatusUSPrevMarketClean: + return TradeStatusUSClosing + case TradeStatusUSClean: + return TradeStatusUSPrev + case TradeStatusUSAfterMarketClean: + return TradeStatusUSTrading + default: + return s + } +} + +// IsSpecial reports whether this is a special non-regular status. +func (s TradeStatus) IsSpecial() bool { + return s.Code() < 100 || s == TradeStatusUSStop || s.Code() >= 1000 +} diff --git a/market/trade_status_test.go b/market/trade_status_test.go new file mode 100644 index 0000000..b2fa77f --- /dev/null +++ b/market/trade_status_test.go @@ -0,0 +1,120 @@ +package market + +import ( + "encoding/json" + "testing" +) + +func TestTradeStatusJSONDeserializesNumericCodes(t *testing.T) { + var status TradeStatus + + if err := json.Unmarshal([]byte("202"), &status); err != nil { + t.Fatalf("unmarshal known status: %v", err) + } + if status != TradeStatusUSTrading { + t.Fatalf("known status = %v, want %v", status, TradeStatusUSTrading) + } + + if err := json.Unmarshal([]byte("456"), &status); err != nil { + t.Fatalf("unmarshal unknown status: %v", err) + } + if status != TradeStatusUnknown { + t.Fatalf("unknown status = %v, want %v", status, TradeStatusUnknown) + } + + data, err := json.Marshal(TradeStatusUSClean) + if err != nil { + t.Fatalf("marshal status: %v", err) + } + if string(data) != "206" { + t.Fatalf("marshal status = %s, want 206", data) + } +} + +func TestTradeStatusHelpersMatchOpenAPIDefinition(t *testing.T) { + cases := []struct { + code int32 + want TradeStatus + name string + }{ + {101, TradeStatusClean, "Closed"}, + {123, TradeStatusRealtimeQuote, "Temporary Break"}, + {202, TradeStatusUSTrading, "Trading"}, + {1009, TradeStatusUnited, "Not Listed"}, + {1010, TradeStatusTradingHalt, "Terminated"}, + {2001, TradeStatusFuse, "Fuse"}, + } + + for _, tc := range cases { + status := TradeStatusFromCode(tc.code) + if status != tc.want { + t.Fatalf("TradeStatusFromCode(%d) = %v, want %v", tc.code, status, tc.want) + } + if status.Code() != tc.code { + t.Fatalf("TradeStatusFromCode(%d).Code() = %d, want %d", tc.code, status.Code(), tc.code) + } + if status.Name() != tc.name { + t.Fatalf("TradeStatusFromCode(%d).Name() = %q, want %q", tc.code, status.Name(), tc.name) + } + } + + if got := TradeStatusFromCode(456); got != TradeStatusUnknown { + t.Fatalf("TradeStatusFromCode(456) = %v, want %v", got, TradeStatusUnknown) + } +} + +func TestTradeStatusNormalizesEngineAliases(t *testing.T) { + cases := []struct { + status TradeStatus + want TradeStatus + }{ + {TradeStatusClean, TradeStatusClosing}, + {TradeStatusUSClean, TradeStatusUSPrev}, + {TradeStatusUSPrevMarketClean, TradeStatusUSClosing}, + {TradeStatusUSAfterMarketClean, TradeStatusUSTrading}, + } + + for _, tc := range cases { + if got := tc.status.Normalize(); got != tc.want { + t.Fatalf("%v.Normalize() = %v, want %v", tc.status, got, tc.want) + } + } +} + +func TestTradeStatusLabelMatchesEngineDisplay(t *testing.T) { + cases := []struct { + status TradeStatus + want string + }{ + {TradeStatusUSPrev, "Pre-Market"}, + {TradeStatusUSClean, "Pre-Market"}, + {TradeStatusUSAfter, "Post-Market"}, + {TradeStatusUSClosing, "Closed"}, + {TradeStatusUSAfterMarketClean, "Trading"}, + {TradeStatusUSTrading, "Trading"}, + {TradeStatusTrading, "Trading"}, + {TradeStatusClean, "Closed"}, + {TradeStatusOpenBid, ""}, + {TradeStatusNoonClosing, ""}, + } + + for _, tc := range cases { + if got := tc.status.Label(); got != tc.want { + t.Fatalf("%v.Label() = %q, want %q", tc.status, got, tc.want) + } + } +} + +func TestMarketTimeItemUsesTradeStatusType(t *testing.T) { + item := MarketTimeItem{ + TradeStatus: TradeStatusUSTrading, + DelayTradeStatus: TradeStatusUSClosing, + } + + if item.TradeStatus != TradeStatusUSTrading { + t.Fatalf("TradeStatus = %v, want %v", item.TradeStatus, TradeStatusUSTrading) + } + if item.DelayTradeStatus != TradeStatusUSClosing { + t.Fatalf("DelayTradeStatus = %v, want %v", item.DelayTradeStatus, TradeStatusUSClosing) + } +} diff --git a/market/types.go b/market/types.go index 3e810f5..10f4a0b 100644 --- a/market/types.go +++ b/market/types.go @@ -66,14 +66,12 @@ type MarketStatusResponse struct { type MarketTimeItem struct { // Market code, e.g. "HK", "US", "CN" Market string - // Raw trade status code: - // 101=PreOpen, 102/103/105=Trading, 104=LunchBreak, - // 106=PostTrading, 108=Closed, 201=PreMarket, 204=PostMarket - TradeStatus int32 + // Market trade status. See TradeStatus for the code table. + TradeStatus TradeStatus // Current market time Timestamp time.Time - // Delayed-quote trade status code - DelayTradeStatus int32 + // Delayed-quote market trade status. See TradeStatus for the code table. + DelayTradeStatus TradeStatus // Delayed-quote market time DelayTimestamp time.Time // Sub-status code