Commit c1cf3bd2 authored by vicotor's avatar vicotor

add kline generator

parent 7b933c41
...@@ -56,7 +56,6 @@ func (s *server) GetBlockByHash(ctx context.Context, request *nodev1.GetBlockReq ...@@ -56,7 +56,6 @@ func (s *server) GetBlockByHash(ctx context.Context, request *nodev1.GetBlockReq
return &nodev1.GetBlockResponse{ return &nodev1.GetBlockResponse{
Block: blk, Block: blk,
}, nil }, nil
} }
func (s *server) GetTransactionByHash(ctx context.Context, request *nodev1.GetTransactionRequest) (*nodev1.GetTransactionResponse, error) { func (s *server) GetTransactionByHash(ctx context.Context, request *nodev1.GetTransactionRequest) (*nodev1.GetTransactionResponse, error) {
......
package exmonitor
import (
"log"
"time"
"github.com/robfig/cron/v3"
)
const (
K_FIELD_MIN = time.Minute
K_FIELD_HOUR = time.Hour
K_FIELD_DAY = 24 * time.Hour
K_FIELD_WEEK = 7 * 24 * time.Hour
K_FIELD_MONTH = 30 * 24 * time.Hour
K_FIELD_YEAR = 365 * 24 * time.Hour
)
type CoinProcessorFactory interface {
GetCoinProcessor(symbol, baseCoin string) *DefaultCoinProcessor
GetProcessorMap() map[string]*DefaultCoinProcessor
}
type KLineGeneratorJob struct {
ProcessorFactory CoinProcessorFactory
}
func NewKLineGeneratorJob(factory CoinProcessorFactory, service MarketService) *KLineGeneratorJob {
return &KLineGeneratorJob{
ProcessorFactory: factory,
}
}
func (job *KLineGeneratorJob) Handle5MinKLine() {
now := time.Now()
log.Printf("Minute KLine: %v", now)
// Set seconds and milliseconds to 0
truncatedTime := now.Truncate(time.Minute)
minute := truncatedTime.Minute()
hour := truncatedTime.Hour()
for symbol, processor := range job.ProcessorFactory.GetProcessorMap() {
if !processor.IsStopKline() {
log.Printf("Generating 1-minute KLine for %s", symbol)
processor.AutoGenerate()
processor.Update24HVolume(truncatedTime.UnixMilli())
if minute%5 == 0 {
processor.GenerateKLine(5, K_FIELD_MIN, truncatedTime.UnixMilli())
}
if minute%10 == 0 {
processor.GenerateKLine(10, K_FIELD_MIN, truncatedTime.UnixMilli())
}
if minute%15 == 0 {
processor.GenerateKLine(15, K_FIELD_MIN, truncatedTime.UnixMilli())
}
if minute%30 == 0 {
processor.GenerateKLine(30, K_FIELD_MIN, truncatedTime.UnixMilli())
}
if hour == 0 && minute == 0 {
processor.ResetThumb()
}
}
}
}
func (job *KLineGeneratorJob) HandleHourKLine() {
now := time.Now()
log.Printf("Hour KLine: %v", now)
// Set minutes, seconds, and milliseconds to 0
truncatedTime := now.Truncate(time.Hour)
for _, processor := range job.ProcessorFactory.GetProcessorMap() {
if !processor.IsStopKline() {
processor.GenerateKLine(1, K_FIELD_HOUR, truncatedTime.UnixMilli())
}
}
}
func (job *KLineGeneratorJob) HandleDayKLine() {
now := time.Now()
log.Printf("Day KLine: %v", now)
// Set hours, minutes, seconds, and milliseconds to 0
truncatedTime := now.Truncate(24 * time.Hour)
week := int(truncatedTime.Weekday())
dayOfMonth := truncatedTime.Day()
for _, processor := range job.ProcessorFactory.GetProcessorMap() {
if !processor.IsStopKline() {
if week == 0 { // Sunday
processor.GenerateKLine(1, K_FIELD_WEEK, truncatedTime.UnixMilli())
}
if dayOfMonth == 1 {
processor.GenerateKLine(1, K_FIELD_MONTH, truncatedTime.UnixMilli())
}
processor.GenerateKLine(1, K_FIELD_YEAR, truncatedTime.UnixMilli())
}
}
}
func (job *KLineGeneratorJob) Start() {
c := cron.New()
// Schedule tasks
c.AddFunc("0 * * * *", job.Handle5MinKLine) // Every minute
c.AddFunc("0 0 * * *", job.HandleHourKLine) // Every hour
c.AddFunc("0 0 0 * *", job.HandleDayKLine) // Every day at midnight
// Start the cron scheduler
c.Start()
}
package exmonitor
import (
"context"
"fmt"
"go.mongodb.org/mongo-driver/mongo"
)
// MongoMarketHandler handles market data operations
type mongoMarketHandler struct {
client *mongo.Client
db *mongo.Database
}
// HandleTrade inserts an ExchangeTrade into the corresponding collection
func (h *mongoMarketHandler) HandleTrade(symbol string, trade *ExchangeTrade) error {
collection := h.db.Collection("exchange_trade_" + symbol)
_, err := collection.InsertOne(context.Background(), trade)
if err != nil {
return fmt.Errorf("failed to insert trade: %v", err)
}
return nil
}
// HandleKLine inserts a KLine into the corresponding collection
func (h *mongoMarketHandler) HandleKLine(symbol string, kline *KLine) error {
collection := h.db.Collection("exchange_kline_" + symbol + "_" + kline.Period)
_, err := collection.InsertOne(context.Background(), kline)
if err != nil {
return fmt.Errorf("failed to insert KLine: %v", err)
}
return nil
}
package exmonitor
import (
"context"
"github.com/exchain/go-exchain/op-supervisor/config"
"log"
"math/big"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
mongodbName = "exmarket"
klinePrefix = "exchange_kline_"
tradePrefix = "exchange_trade_"
)
type marketService struct {
mongoClient *mongo.Client
}
func NewMarketService(conf *config.Config) MarketService {
// Initialize MongoDB client
mongoURI := "" // conf.MongoDBURI
clientOptions := options.Client().ApplyURI(mongoURI)
client, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
log.Fatalf("Failed to connect to MongoDB: %v", err)
}
return &marketService{mongoClient: client}
}
func (s *marketService) FindAllKLine(symbol, period string) []*KLine {
collection := s.mongoClient.Database(mongodbName).Collection(klinePrefix + symbol + "_" + period)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
opts := options.Find().SetSort(bson.D{{Key: "time", Value: -1}}).SetLimit(1000)
cursor, err := collection.Find(ctx, bson.M{}, opts)
if err != nil {
log.Fatalf("Error finding KLine: %v", err)
}
defer cursor.Close(ctx)
var kLines []*KLine
if err := cursor.All(ctx, &kLines); err != nil {
log.Fatalf("Error decoding KLine: %v", err)
}
return kLines
}
func (s *marketService) FindAllKLineByTimeRange(symbol string, fromTime, toTime int64, period string) []*KLine {
collection := s.mongoClient.Database(mongodbName).Collection(klinePrefix + symbol + "_" + period)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
filter := bson.M{
"time": bson.M{
"$gte": fromTime,
"$lte": toTime,
},
}
opts := options.Find().SetSort(bson.D{{Key: "time", Value: 1}})
cursor, err := collection.Find(ctx, filter, opts)
if err != nil {
log.Fatalf("Error finding KLine by time range: %v", err)
}
defer cursor.Close(ctx)
var kLines []*KLine
if err := cursor.All(ctx, &kLines); err != nil {
log.Fatalf("Error decoding KLine: %v", err)
}
return kLines
}
func (s *marketService) FindFirstTrade(symbol string, fromTime, toTime int64) *ExchangeTrade {
return s.findTrade(symbol, fromTime, toTime, bson.D{{Key: "time", Value: 1}})
}
func (s *marketService) FindLastTrade(symbol string, fromTime, toTime int64) *ExchangeTrade {
return s.findTrade(symbol, fromTime, toTime, bson.D{{Key: "time", Value: -1}})
}
func (s *marketService) findTrade(symbol string, fromTime, toTime int64, sortOrder bson.D) *ExchangeTrade {
collection := s.mongoClient.Database(mongodbName).Collection(tradePrefix + symbol)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
filter := bson.M{
"time": bson.M{
"$gte": fromTime,
"$lte": toTime,
},
}
opts := options.FindOne().SetSort(sortOrder)
var trade ExchangeTrade
err := collection.FindOne(ctx, filter, opts).Decode(&trade)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil
}
log.Fatalf("Error finding trade: %v", err)
}
return &trade
}
func (s *marketService) FindTradeByTimeRange(symbol string, timeStart, timeEnd int64) []*ExchangeTrade {
collection := s.mongoClient.Database(mongodbName).Collection(tradePrefix + symbol)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
filter := bson.M{
"time": bson.M{
"$gte": timeStart,
"$lt": timeEnd,
},
}
opts := options.Find().SetSort(bson.D{{Key: "time", Value: 1}})
cursor, err := collection.Find(ctx, filter, opts)
if err != nil {
log.Fatalf("Error finding trades by time range: %v", err)
}
defer cursor.Close(ctx)
var trades []*ExchangeTrade
if err := cursor.All(ctx, &trades); err != nil {
log.Fatalf("Error decoding trades: %v", err)
}
return trades
}
func (s *marketService) SaveKLine(symbol string, kLine *KLine) {
collection := s.mongoClient.Database(mongodbName).Collection(klinePrefix + symbol + "_" + kLine.Period)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err := collection.InsertOne(ctx, kLine)
if err != nil {
log.Fatalf("Error saving KLine: %v", err)
}
}
func (s *marketService) FindTradeVolume(symbol string, timeStart, timeEnd int64) *big.Float {
collection := s.mongoClient.Database(mongodbName).Collection(klinePrefix + symbol + "_1min")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
filter := bson.M{
"time": bson.M{
"$gt": timeStart,
"$lte": timeEnd,
},
}
opts := options.Find().SetSort(bson.D{{Key: "time", Value: 1}})
cursor, err := collection.Find(ctx, filter, opts)
if err != nil {
log.Fatalf("Error finding trade volume: %v", err)
}
defer cursor.Close(ctx)
totalVolume := big.NewFloat(0)
for cursor.Next(ctx) {
var kLine KLine
if err := cursor.Decode(&kLine); err != nil {
log.Fatalf("Error decoding KLine: %v", err)
}
totalVolume = totalVolume.Add(totalVolume, kLine.Volume)
}
return totalVolume
}
func (s *marketService) NewHandler(symbol string) MarketHandler {
return &mongoMarketHandler{
client: s.mongoClient,
db: s.mongoClient.Database(mongodbName),
}
}
package exmonitor
import "github.com/exchain/go-exchain/op-supervisor/config"
type Monitor struct {
kline *KLineGeneratorJob
service MarketService
}
func NewMonitor(conf *config.Config) *Monitor {
service := NewMarketService(conf)
kline := NewKLineGeneratorJob(&coinProcessorFactory{}, service)
return &Monitor{
kline: kline,
service: service,
}
}
func (m *Monitor) Start() {
// Initialize the monitor
// Start the monitoring process
m.kline.Start()
}
package exmonitor
import (
"fmt"
"math/big"
"sync"
"time"
)
type KLine struct {
Time int64
Period string
Count int
OpenPrice *big.Float
ClosePrice *big.Float
HighestPrice *big.Float
LowestPrice *big.Float
Volume *big.Float
Turnover *big.Float
}
type CoinThumb struct {
Symbol string
Open *big.Float
Close *big.Float
High *big.Float
Low *big.Float
Volume *big.Float
Turnover *big.Float
Change *big.Float
Chg *big.Float
BaseUsdRate *big.Float
UsdRate *big.Float
LastDayClose *big.Float
}
type ExchangeTrade struct {
Price *big.Float
Amount *big.Float
}
type MarketHandler interface {
HandleTrade(symbol string, trade *ExchangeTrade) error
HandleKLine(symbol string, kline *KLine)
}
type MarketService interface {
//FindAllKLine(symbol string, start, end int64, period string) []*KLine
FindTradeVolume(symbol string, start, end int64) *big.Float
FindTradeByTimeRange(symbol string, start, end int64) []*ExchangeTrade
FindAllKLineByTimeRange(symbol string, fromTime, toTime int64, period string) []*KLine
SaveKLine(symbol string, kline *KLine)
}
type DefaultCoinProcessor struct {
symbol string
baseCoin string
currentKLine *KLine
handlers []MarketHandler
coinThumb *CoinThumb
service MarketService
coinExchangeRate map[string]*big.Float
isHalt bool
stopKLine bool
mutex sync.Mutex
}
type coinProcessorFactory struct {
mux sync.Mutex
coinProcessors map[string]*DefaultCoinProcessor
}
func (cpf *coinProcessorFactory) GetCoinProcessor(symbol, baseCoin string) *DefaultCoinProcessor {
cpf.mux.Lock()
defer cpf.mux.Unlock()
if processor, exists := cpf.coinProcessors[symbol]; exists {
return processor
} else {
processor = &DefaultCoinProcessor{
symbol: symbol,
baseCoin: baseCoin,
currentKLine: createNewKLine(),
handlers: []MarketHandler{},
coinThumb: &CoinThumb{},
isHalt: true,
stopKLine: false,
}
cpf.coinProcessors[symbol] = processor
return processor
}
}
func (cpf *coinProcessorFactory) GetProcessorMap() map[string]*DefaultCoinProcessor {
cpf.mux.Lock()
defer cpf.mux.Unlock()
// copy the map to avoid concurrent map writes
// and return a new map
processorMap := make(map[string]*DefaultCoinProcessor)
for k, v := range cpf.coinProcessors {
processorMap[k] = v
}
return processorMap
}
func createNewKLine() *KLine {
now := time.Now()
nextMinute := now.Add(time.Minute)
return &KLine{
Time: nextMinute.UnixMilli(),
Period: "1min",
Count: 0,
OpenPrice: big.NewFloat(0),
ClosePrice: big.NewFloat(0),
HighestPrice: big.NewFloat(0),
LowestPrice: big.NewFloat(0),
Volume: big.NewFloat(0),
Turnover: big.NewFloat(0),
}
}
func (p *DefaultCoinProcessor) InitializeThumb() {
p.mutex.Lock()
defer p.mutex.Unlock()
now := time.Now()
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
lines := p.service.FindAllKLineByTimeRange(p.symbol, startOfDay.UnixMilli(), now.UnixMilli(), "1min")
p.coinThumb = &CoinThumb{
Symbol: p.symbol,
Open: big.NewFloat(0),
High: big.NewFloat(0),
Low: big.NewFloat(0),
Close: big.NewFloat(0),
Volume: big.NewFloat(0),
Turnover: big.NewFloat(0),
}
for _, line := range lines {
if line.OpenPrice.Cmp(big.NewFloat(0)) == 0 {
continue
}
if p.coinThumb.Open.Cmp(big.NewFloat(0)) == 0 {
p.coinThumb.Open = line.OpenPrice
}
if p.coinThumb.High.Cmp(line.HighestPrice) < 0 {
p.coinThumb.High = line.HighestPrice
}
if line.LowestPrice.Cmp(big.NewFloat(0)) > 0 && p.coinThumb.Low.Cmp(line.LowestPrice) > 0 {
p.coinThumb.Low = line.LowestPrice
}
if line.ClosePrice.Cmp(big.NewFloat(0)) > 0 {
p.coinThumb.Close = line.ClosePrice
}
p.coinThumb.Volume.Add(p.coinThumb.Volume, line.Volume)
p.coinThumb.Turnover.Add(p.coinThumb.Turnover, line.Turnover)
}
change := new(big.Float).Sub(p.coinThumb.Close, p.coinThumb.Open)
p.coinThumb.Change = change
if p.coinThumb.Low.Cmp(big.NewFloat(0)) > 0 {
p.coinThumb.Chg = new(big.Float).Quo(change, p.coinThumb.Low)
}
}
func (p *DefaultCoinProcessor) IsStopKline() bool {
return p.stopKLine
}
func (p *DefaultCoinProcessor) ResetThumb() {
p.mutex.Lock()
defer p.mutex.Unlock()
p.coinThumb.Open = big.NewFloat(0)
p.coinThumb.High = big.NewFloat(0)
p.coinThumb.Low = big.NewFloat(0)
p.coinThumb.Close = big.NewFloat(0)
p.coinThumb.Change = big.NewFloat(0)
p.coinThumb.Chg = big.NewFloat(0)
p.coinThumb.LastDayClose = p.coinThumb.Close
}
func (p *DefaultCoinProcessor) AutoGenerate() {
p.mutex.Lock()
defer p.mutex.Unlock()
if p.coinThumb != nil {
if p.currentKLine.OpenPrice.Cmp(big.NewFloat(0)) == 0 {
p.currentKLine.OpenPrice = p.coinThumb.Close
p.currentKLine.LowestPrice = p.coinThumb.Close
p.currentKLine.HighestPrice = p.coinThumb.Close
p.currentKLine.ClosePrice = p.coinThumb.Close
}
p.currentKLine.Time = time.Now().UnixMilli()
p.handleKLineStorage(p.currentKLine)
p.currentKLine = createNewKLine()
}
}
func (p *DefaultCoinProcessor) handleKLineStorage(kline *KLine) {
for _, handler := range p.handlers {
handler.HandleKLine(p.symbol, kline)
}
}
func (p *DefaultCoinProcessor) AddHandler(handler MarketHandler) {
p.handlers = append(p.handlers, handler)
}
func (p *DefaultCoinProcessor) GenerateKLine(rangeValue int, field time.Duration, timestamp int64) {
p.stopKLine = false
defer func() { p.stopKLine = true }()
endTime := time.UnixMilli(timestamp)
startTime := endTime.Add(-field * time.Duration(rangeValue))
trades := p.service.FindTradeByTimeRange(p.symbol, startTime.UnixMilli(), endTime.UnixMilli())
kline := &KLine{
Time: endTime.UnixMilli(),
Period: formatPeriod(rangeValue, field),
OpenPrice: big.NewFloat(0),
ClosePrice: big.NewFloat(0),
HighestPrice: big.NewFloat(0),
LowestPrice: big.NewFloat(0),
Volume: big.NewFloat(0),
Turnover: big.NewFloat(0),
}
for _, trade := range trades {
p.processTrade(kline, trade)
}
if kline.OpenPrice.Cmp(big.NewFloat(0)) == 0 {
kline.OpenPrice = p.coinThumb.Close
kline.ClosePrice = p.coinThumb.Close
kline.LowestPrice = p.coinThumb.Close
kline.HighestPrice = p.coinThumb.Close
}
p.service.SaveKLine(p.symbol, kline)
}
func (p *DefaultCoinProcessor) processTrade(kline *KLine, trade *ExchangeTrade) {
if kline.OpenPrice.Cmp(big.NewFloat(0)) == 0 {
kline.OpenPrice = trade.Price
kline.HighestPrice = trade.Price
kline.LowestPrice = trade.Price
kline.ClosePrice = trade.Price
} else {
if trade.Price.Cmp(kline.HighestPrice) > 0 {
kline.HighestPrice = trade.Price
}
if trade.Price.Cmp(kline.LowestPrice) < 0 {
kline.LowestPrice = trade.Price
}
kline.ClosePrice = trade.Price
}
kline.Count++
kline.Volume.Add(kline.Volume, trade.Amount)
turnover := new(big.Float).Mul(trade.Price, trade.Amount)
kline.Turnover.Add(kline.Turnover, turnover)
}
func formatPeriod(rangeValue int, field time.Duration) string {
switch field {
case K_FIELD_MIN:
return fmt.Sprintf("%dmin", rangeValue)
case K_FIELD_HOUR:
return fmt.Sprintf("%dhour", rangeValue)
case K_FIELD_DAY:
return fmt.Sprintf("%dday", rangeValue)
case K_FIELD_WEEK:
return fmt.Sprintf("%dweek", rangeValue)
case K_FIELD_MONTH:
return fmt.Sprintf("%dmonth", rangeValue)
case K_FIELD_YEAR:
return fmt.Sprintf("%dyear", rangeValue)
default:
return "unknown"
}
}
func (p *DefaultCoinProcessor) Update24HVolume(currentTime int64) {
if p.coinThumb != nil {
p.mutex.Lock()
defer p.mutex.Unlock()
// Calculate the start time (24 hours ago)
startTime := time.UnixMilli(currentTime).Add(-24 * time.Hour).UnixMilli()
// Fetch the trade volume from the service
volume := p.service.FindTradeVolume(p.symbol, startTime, currentTime)
// Set the volume in the coinThumb, rounded to 4 decimal places
p.coinThumb.Volume = new(big.Float).SetPrec(4).SetMode(big.ToZero).Set(volume)
}
}
...@@ -7,6 +7,12 @@ option go_package = "github.com/exchain/go-exchain/exchain/protocol/orderbook/v1 ...@@ -7,6 +7,12 @@ option go_package = "github.com/exchain/go-exchain/exchain/protocol/orderbook/v1
import "nebula/v1/transaction.proto"; import "nebula/v1/transaction.proto";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
option java_multiple_files = true;
option java_package = "com.exchain.protocol.orderbook.v1";
option java_outer_classname = "ExChainOrderBookProto";
import "google/protobuf/timestamp.proto";
message PlaceLimitOrderRequest { message PlaceLimitOrderRequest {
exchain.nebula.v1.Transaction transaction = 1; exchain.nebula.v1.Transaction transaction = 1;
...@@ -55,3 +61,782 @@ service Orderbook { ...@@ -55,3 +61,782 @@ service Orderbook {
} }
} }
message Symbol {
string base_asset = 1;
string quote_asset = 2;
}
message PaginationRequest {
int32 page = 1;
int32 limit = 2;
}
message PaginationResponse {
int32 total = 1;
int32 page = 2;
int32 limit = 3;
}
message PriceLevel {
string price = 1;
string quantity = 2;
}
message OrderBookEntry {
string price = 1;
string quantity = 2;
int64 order_count = 3; // 可选,此价格的订单数量
}
// 订单簿快照
message OrderBookSnapshot {
string symbol = 1;
int64 last_update_id = 2;
google.protobuf.Timestamp time = 3;
repeated OrderBookEntry bids = 4; // 按价格从高到低排序
repeated OrderBookEntry asks = 5; // 按价格从低到高排序
}
// 订单簿增量更新
message OrderBookDelta {
string symbol = 1;
int64 first_update_id = 2;
int64 last_update_id = 3;
google.protobuf.Timestamp time = 4;
repeated OrderBookEntry bids = 5; // 价格为0表示删除此价格级别
repeated OrderBookEntry asks = 6; // 价格为0表示删除此价格级别
}
// 获取订单簿快照请求
message GetOrderBookRequest {
string symbol = 1;
int32 limit = 2; // 可选,每侧显示的价格级别数,默认100,最大1000
}
// 获取订单簿快照响应
message GetOrderBookResponse {
OrderBookSnapshot snapshot = 1;
}
// 订阅订单簿增量更新请求
message SubscribeOrderBookRequest {
string symbol = 1;
string update_speed = 2; // 可选,如"100ms"、"1000ms"等
}
// 订阅订单簿增量更新响应(流式)
message SubscribeOrderBookResponse {
oneof update {
OrderBookSnapshot snapshot = 1; // 初始快照
OrderBookDelta delta = 2; // 后续增量更新
}
}
// 获取订单簿顶部价位请求
message GetOrderBookTickerRequest {
string symbol = 1; // 可选,不提供则返回所有交易对
}
// 订单簿顶部价位
message OrderBookTicker {
string symbol = 1;
string bid_price = 2;
string bid_qty = 3;
string ask_price = 4;
string ask_qty = 5;
google.protobuf.Timestamp time = 6;
}
// 获取订单簿顶部价位响应
message GetOrderBookTickerResponse {
repeated OrderBookTicker tickers = 1;
}
// ======================== 系统API消息定义 ========================
message PingRequest {}
message PingResponse {}
message ServerTimeRequest {}
message ServerTimeResponse {
google.protobuf.Timestamp server_time = 1;
}
message ExchangeInfoRequest {
string symbol = 1; // 可选,指定交易对
}
message RateLimit {
string rate_limit_type = 1;
string interval = 2;
int32 interval_num = 3;
int64 limit = 4;
}
message SymbolFilter {
string filter_type = 1;
string min_price = 2;
string max_price = 3;
string tick_size = 4;
string min_qty = 5;
string max_qty = 6;
string step_size = 7;
string min_notional = 8;
bool apply_to_market = 9;
int32 avg_price_mins = 10;
string multiplier_up = 11;
string multiplier_down = 12;
int32 limit = 13;
}
message SymbolInfo {
string symbol = 1;
string status = 2;
string base_asset = 3;
int32 base_asset_precision = 4;
string quote_asset = 5;
int32 quote_asset_precision = 6;
repeated string order_types = 7;
bool iceberg_allowed = 8;
bool oco_allowed = 9;
bool quote_order_qty_market_allowed = 10;
bool allow_trailing_stop = 11;
bool cancel_replace_allowed = 12;
bool is_spot_trading_allowed = 13;
bool is_margin_trading_allowed = 14;
repeated SymbolFilter filters = 15;
repeated string permissions = 16;
}
message ExchangeInfoResponse {
string timezone = 1;
google.protobuf.Timestamp server_time = 2;
repeated RateLimit rate_limits = 3;
repeated SymbolInfo symbols = 4;
}
// ======================== 市场数据API消息定义 ========================
message TradesRequest {
string symbol = 1;
int32 limit = 2; // 可选,默认500
}
message Trade {
int64 id = 1;
string price = 2;
string qty = 3;
string quote_qty = 4;
google.protobuf.Timestamp time = 5;
bool is_buyer_maker = 6;
bool is_best_match = 7;
}
message TradesResponse {
repeated Trade trades = 1;
}
message HistoricalTradesRequest {
string symbol = 1;
int64 from_id = 2; // 可选,从哪个交易ID开始
int32 limit = 3; // 可选,默认500
}
message HistoricalTradesResponse {
repeated Trade trades = 1;
}
message AggTradesRequest {
string symbol = 1;
int64 from_id = 2; // 可选
google.protobuf.Timestamp start_time = 3; // 可选
google.protobuf.Timestamp end_time = 4; // 可选
int32 limit = 5; // 可选,默认500
}
message AggTrade {
int64 id = 1;
string price = 2;
string qty = 3;
int64 first_id = 4;
int64 last_id = 5;
google.protobuf.Timestamp time = 6;
bool is_buyer_maker = 7;
bool is_best_match = 8;
}
message AggTradesResponse {
repeated AggTrade agg_trades = 1;
}
enum KlineInterval {
KLINE_INTERVAL_UNSPECIFIED = 0;
KLINE_INTERVAL_1m = 1;
KLINE_INTERVAL_3m = 2;
KLINE_INTERVAL_5m = 3;
KLINE_INTERVAL_15m = 4;
KLINE_INTERVAL_30m = 5;
KLINE_INTERVAL_1h = 6;
KLINE_INTERVAL_2h = 7;
KLINE_INTERVAL_4h = 8;
KLINE_INTERVAL_6h = 9;
KLINE_INTERVAL_8h = 10;
KLINE_INTERVAL_12h = 11;
KLINE_INTERVAL_1d = 12;
KLINE_INTERVAL_3d = 13;
KLINE_INTERVAL_1w = 14;
KLINE_INTERVAL_1M = 15;
}
message KlinesRequest {
string symbol = 1;
KlineInterval interval = 2;
google.protobuf.Timestamp start_time = 3; // 可选
google.protobuf.Timestamp end_time = 4; // 可选
int32 limit = 5; // 可选,默认500
}
message Kline {
google.protobuf.Timestamp open_time = 1;
string open = 2;
string high = 3;
string low = 4;
string close = 5;
string volume = 6;
google.protobuf.Timestamp close_time = 7;
string quote_asset_volume = 8;
int32 trade_count = 9;
string taker_buy_base_asset_volume = 10;
string taker_buy_quote_asset_volume = 11;
}
message KlinesResponse {
repeated Kline klines = 1;
}
message TickerPriceRequest {
string symbol = 1; // 可选,不提供则返回所有交易对
}
message TickerPrice {
string symbol = 1;
string price = 2;
google.protobuf.Timestamp time = 3;
}
message TickerPriceResponse {
repeated TickerPrice tickers = 1;
}
message Ticker24hrRequest {
string symbol = 1; // 可选,不提供则返回所有交易对
}
message Ticker24hr {
string symbol = 1;
string price_change = 2;
string price_change_percent = 3;
string weighted_avg_price = 4;
string prev_close_price = 5;
string last_price = 6;
string last_qty = 7;
string bid_price = 8;
string bid_qty = 9;
string ask_price = 10;
string ask_qty = 11;
string open_price = 12;
string high_price = 13;
string low_price = 14;
string volume = 15;
string quote_volume = 16;
google.protobuf.Timestamp open_time = 17;
google.protobuf.Timestamp close_time = 18;
int64 first_id = 19;
int64 last_id = 20;
int64 count = 21;
}
message Ticker24hrResponse {
repeated Ticker24hr tickers = 1;
}
// ======================== 交易API消息定义 ========================
enum OrderSide {
ORDER_SIDE_UNSPECIFIED = 0;
ORDER_SIDE_BUY = 1;
ORDER_SIDE_SELL = 2;
}
enum OrderType {
ORDER_TYPE_UNSPECIFIED = 0;
ORDER_TYPE_LIMIT = 1;
ORDER_TYPE_MARKET = 2;
ORDER_TYPE_STOP_LOSS = 3;
ORDER_TYPE_STOP_LOSS_LIMIT = 4;
ORDER_TYPE_TAKE_PROFIT = 5;
ORDER_TYPE_TAKE_PROFIT_LIMIT = 6;
ORDER_TYPE_LIMIT_MAKER = 7;
}
enum TimeInForce {
TIME_IN_FORCE_UNSPECIFIED = 0;
TIME_IN_FORCE_GTC = 1; // Good Till Cancel
TIME_IN_FORCE_IOC = 2; // Immediate or Cancel
TIME_IN_FORCE_FOK = 3; // Fill or Kill
}
enum OrderResponseType {
ORDER_RESPONSE_TYPE_UNSPECIFIED = 0;
ORDER_RESPONSE_TYPE_ACK = 1;
ORDER_RESPONSE_TYPE_RESULT = 2;
ORDER_RESPONSE_TYPE_FULL = 3;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_NEW = 1;
ORDER_STATUS_PARTIALLY_FILLED = 2;
ORDER_STATUS_FILLED = 3;
ORDER_STATUS_CANCELED = 4;
ORDER_STATUS_PENDING_CANCEL = 5;
ORDER_STATUS_REJECTED = 6;
ORDER_STATUS_EXPIRED = 7;
}
message OrderRequest {
string symbol = 1;
OrderSide side = 2;
OrderType type = 3;
TimeInForce time_in_force = 4; // 可选
string quantity = 5; // 可选
string quote_order_qty = 6; // 可选
string price = 7; // 可选
string new_client_order_id = 8; // 可选
string stop_price = 9; // 可选
string iceberg_qty = 10; // 可选
OrderResponseType new_order_resp_type = 11; // 可选
int32 recv_window = 12; // 可选
google.protobuf.Timestamp timestamp = 13;
}
message Fill {
string price = 1;
string qty = 2;
string commission = 3;
string commission_asset = 4;
int64 trade_id = 5;
}
message OrderResponse {
string symbol = 1;
int64 order_id = 2;
string client_order_id = 3;
google.protobuf.Timestamp transact_time = 4;
string price = 5;
string orig_qty = 6;
string executed_qty = 7;
string cummulative_quote_qty = 8;
OrderStatus status = 9;
TimeInForce time_in_force = 10;
OrderType type = 11;
OrderSide side = 12;
repeated Fill fills = 13; // 仅在FULL响应类型中
}
message TestOrderRequest {
string symbol = 1;
OrderSide side = 2;
OrderType type = 3;
TimeInForce time_in_force = 4; // 可选
string quantity = 5; // 可选
string quote_order_qty = 6; // 可选
string price = 7; // 可选
string new_client_order_id = 8; // 可选
string stop_price = 9; // 可选
string iceberg_qty = 10; // 可选
int32 recv_window = 11; // 可选
google.protobuf.Timestamp timestamp = 12;
}
message TestOrderResponse {}
message OrderStatusRequest {
string symbol = 1;
int64 order_id = 2; // order_id 和 orig_client_order_id 必须二选一
string orig_client_order_id = 3;
int32 recv_window = 4; // 可选
google.protobuf.Timestamp timestamp = 5;
}
message OrderStatusResponse {
string symbol = 1;
int64 order_id = 2;
string client_order_id = 3;
string price = 4;
string orig_qty = 5;
string executed_qty = 6;
string cummulative_quote_qty = 7;
OrderStatus status = 8;
TimeInForce time_in_force = 9;
OrderType type = 10;
OrderSide side = 11;
string stop_price = 12;
string iceberg_qty = 13;
google.protobuf.Timestamp time = 14;
google.protobuf.Timestamp update_time = 15;
bool is_working = 16;
string orig_quote_order_qty = 17;
}
message CancelOrderRequest {
string symbol = 1;
int64 order_id = 2; // order_id 和 orig_client_order_id 必须二选一
string orig_client_order_id = 3;
string new_client_order_id = 4; // 可选
int32 recv_window = 5; // 可选
google.protobuf.Timestamp timestamp = 6;
}
message CancelOrderResponse {
string symbol = 1;
string orig_client_order_id = 2;
int64 order_id = 3;
string client_order_id = 4;
}
message CancelAllOrdersRequest {
string symbol = 1;
int32 recv_window = 2; // 可选
google.protobuf.Timestamp timestamp = 3;
}
message CancelAllOrdersResponse {
repeated CancelOrderResponse orders = 1;
}
message OpenOrdersRequest {
string symbol = 1; // 可选
int32 recv_window = 2; // 可选
google.protobuf.Timestamp timestamp = 3;
}
message OpenOrdersResponse {
repeated OrderStatusResponse orders = 1;
}
message AllOrdersRequest {
string symbol = 1;
int64 order_id = 2; // 可选
google.protobuf.Timestamp start_time = 3; // 可选
google.protobuf.Timestamp end_time = 4; // 可选
int32 limit = 5; // 可选, 默认500
int32 recv_window = 6; // 可选
google.protobuf.Timestamp timestamp = 7;
}
message AllOrdersResponse {
repeated OrderStatusResponse orders = 1;
}
// OCO (One-Cancels-Other) 订单
message NewOCORequest {
string symbol = 1;
string list_client_order_id = 2; // 可选
OrderSide side = 3;
string quantity = 4;
string limit_client_order_id = 5; // 可选
string price = 6;
string limit_iceberg_qty = 7; // 可选
string stop_client_order_id = 8; // 可选
string stop_price = 9;
string stop_limit_price = 10; // 可选
string stop_iceberg_qty = 11; // 可选
TimeInForce stop_limit_time_in_force = 12; // 可选
int32 recv_window = 13; // 可选
google.protobuf.Timestamp timestamp = 14;
}
message OCOOrder {
string symbol = 1;
int64 order_id = 2;
string client_order_id = 3;
}
message OCOResponse {
int64 order_list_id = 1;
string contingency_type = 2;
string list_status_type = 3;
string list_order_status = 4;
string list_client_order_id = 5;
google.protobuf.Timestamp transaction_time = 6;
repeated OCOOrder orders = 7;
}
message GetOCORequest {
int64 order_list_id = 1; // order_list_id 和 orig_client_order_id 二选一
string orig_client_order_id = 2;
int32 recv_window = 3; // 可选
google.protobuf.Timestamp timestamp = 4;
}
message GetOCOResponse {
OCOResponse oco = 1;
}
message CancelOCORequest {
string symbol = 1;
int64 order_list_id = 2; // order_list_id, list_client_order_id, 和 new_client_order_id 三选一
string list_client_order_id = 3;
string new_client_order_id = 4; // 可选
int32 recv_window = 5; // 可选
google.protobuf.Timestamp timestamp = 6;
}
message CancelOCOResponse {
OCOResponse oco = 1;
}
// ======================== 账户API消息定义 ========================
message AccountInfoRequest {
int32 recv_window = 1; // 可选
google.protobuf.Timestamp timestamp = 2;
}
message AssetBalance {
string asset = 1;
string free = 2;
string locked = 3;
}
message AccountInfoResponse {
int32 maker_commission = 1;
int32 taker_commission = 2;
int32 buyer_commission = 3;
int32 seller_commission = 4;
bool can_trade = 5;
bool can_withdraw = 6;
bool can_deposit = 7;
google.protobuf.Timestamp update_time = 8;
string account_type = 9;
repeated AssetBalance balances = 10;
repeated string permissions = 11;
}
message MyTradesRequest {
string symbol = 1;
int64 order_id = 2; // 可选
google.protobuf.Timestamp start_time = 3; // 可选
google.protobuf.Timestamp end_time = 4; // 可选
int64 from_id = 5; // 可选
int32 limit = 6; // 可选, 默认500
int32 recv_window = 7; // 可选
google.protobuf.Timestamp timestamp = 8;
}
message MyTrade {
string symbol = 1;
int64 id = 2;
int64 order_id = 3;
string price = 4;
string qty = 5;
string quote_qty = 6;
string commission = 7;
string commission_asset = 8;
google.protobuf.Timestamp time = 9;
bool is_buyer = 10;
bool is_maker = 11;
bool is_best_match = 12;
}
message MyTradesResponse {
repeated MyTrade trades = 1;
}
// ======================== 服务定义 ========================
// 系统API服务 - 提供基本系统功能
service SystemApi {
// 测试连接
rpc Ping(PingRequest) returns (PingResponse) {
option (google.api.http) = {
get: "/exchain/v1/ping"
};
}
// 获取服务器时间
rpc GetServerTime(ServerTimeRequest) returns (ServerTimeResponse) {
option (google.api.http) = {
get: "/exchain/v1/time"
};
}
// 获取交易所信息
rpc GetExchangeInfo(ExchangeInfoRequest) returns (ExchangeInfoResponse) {
option (google.api.http) = {
get: "/exchain/v1/exchangeInfo"
};
}
}
// 市场数据API - 提供市场数据查询
service MarketApi {
// 获取订单簿
rpc GetOrderBook(GetOrderBookRequest) returns (GetOrderBookResponse) {
option (google.api.http) = {
get: "/exchain/v1/depth"
};
}
// 订阅订单簿更新(流)
rpc SubscribeOrderBook(SubscribeOrderBookRequest) returns (stream SubscribeOrderBookResponse) {}
// 获取订单簿顶部价格
rpc GetOrderBookTicker(GetOrderBookTickerRequest) returns (GetOrderBookTickerResponse) {
option (google.api.http) = {
get: "/exchain/v1/ticker/bookTicker"
};
}
// 获取最近成交
rpc GetTrades(TradesRequest) returns (TradesResponse) {
option (google.api.http) = {
get: "/exchain/v1/trades"
};
}
// 获取历史成交
rpc GetHistoricalTrades(HistoricalTradesRequest) returns (HistoricalTradesResponse) {
option (google.api.http) = {
get: "/exchain/v1/historicalTrades"
};
}
// 获取聚合成交
rpc GetAggTrades(AggTradesRequest) returns (AggTradesResponse) {
option (google.api.http) = {
get: "/exchain/v1/aggTrades"
};
}
// 获取K线数据
rpc GetKlines(KlinesRequest) returns (KlinesResponse) {
option (google.api.http) = {
get: "/exchain/v1/klines"
};
}
// 获取24小时价格变动统计
rpc Get24hrTicker(Ticker24hrRequest) returns (Ticker24hrResponse) {
option (google.api.http) = {
get: "/exchain/v1/ticker/24hr"
};
}
// 获取最新价格
rpc GetTickerPrice(TickerPriceRequest) returns (TickerPriceResponse) {
option (google.api.http) = {
get: "/exchain/v1/ticker/price"
};
}
}
// 交易API服务 - 提供订单操作
service TradeApi {
// 测试下单
rpc TestOrder(TestOrderRequest) returns (TestOrderResponse) {
option (google.api.http) = {
post: "/exchain/v1/order/test"
body: "*"
};
}
// 下单
rpc CreateOrder(OrderRequest) returns (OrderResponse) {
option (google.api.http) = {
post: "/exchain/v1/order"
body: "*"
};
}
// 查询订单
rpc GetOrder(OrderStatusRequest) returns (OrderStatusResponse) {
option (google.api.http) = {
get: "/exchain/v1/order"
};
}
// 撤销订单
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse) {
option (google.api.http) = {
delete: "/exchain/v1/order"
};
}
// 撤销单一交易对的所有订单
rpc CancelAllOrders(CancelAllOrdersRequest) returns (CancelAllOrdersResponse) {
option (google.api.http) = {
delete: "/exchain/v1/openOrders"
};
}
// 查询当前挂单
rpc GetOpenOrders(OpenOrdersRequest) returns (OpenOrdersResponse) {
option (google.api.http) = {
get: "/exchain/v1/openOrders"
};
}
// 查询所有订单
rpc GetAllOrders(AllOrdersRequest) returns (AllOrdersResponse) {
option (google.api.http) = {
get: "/exchain/v1/allOrders"
};
}
}
// 账户API服务 - 提供账户信息
service AccountApi {
// 获取账户信息
rpc GetAccountInfo(AccountInfoRequest) returns (AccountInfoResponse) {
option (google.api.http) = {
get: "/exchain/v1/account"
};
}
// 获取账户交易历史
rpc GetMyTrades(MyTradesRequest) returns (MyTradesResponse) {
option (google.api.http) = {
get: "/exchain/v1/myTrades"
};
}
}
// 订单簿专用服务 - 提供订单簿功能
service OrderBookApi {
// 获取完整订单簿
rpc GetFullOrderBook(GetOrderBookRequest) returns (GetOrderBookResponse) {
option (google.api.http) = {
get: "/exchain/v1/orderbook/depth"
};
}
// 获取聚合订单簿(合并相似价格)
rpc GetAggregatedOrderBook(GetOrderBookRequest) returns (GetOrderBookResponse) {
option (google.api.http) = {
get: "/exchain/v1/orderbook/aggregated"
};
}
// 订阅实时订单簿更新
rpc StreamOrderBook(SubscribeOrderBookRequest) returns (stream SubscribeOrderBookResponse) {}
// 获取最近订单簿更新记录
rpc GetOrderBookUpdates(GetOrderBookRequest) returns (stream OrderBookDelta) {
option (google.api.http) = {
get: "/exchain/v1/orderbook/updates"
};
}
}
...@@ -59,6 +59,8 @@ require ( ...@@ -59,6 +59,8 @@ require (
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
go.mongodb.org/mongo-driver v1.17.3 // indirect
) )
require ( require (
......
...@@ -697,6 +697,8 @@ github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtD ...@@ -697,6 +697,8 @@ github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtD
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
...@@ -803,6 +805,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo ...@@ -803,6 +805,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment