package forex import ( "encoding/json" "errors" "fmt" "github.com/google/uuid" "github.com/gorilla/websocket" "github.com/shopspring/decimal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "log" "net/http" "strings" "sync" "time" "wss-pool/config" "wss-pool/internal" "wss-pool/internal/data" "wss-pool/logging/applogger" "wss-pool/pkg/model" ) var topic = "forex" // 实时报价 var topic_d = "forexDay" // 实时天报价 var topic_t = "trade" // 成交报价 var topic_s = "tradeStorage" // 成交报价存储 var topic_c = "quotes" // 盘口(买一卖一)报价 var upGrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, // Verification request` CheckOrigin: func(r *http.Request) bool { // If it is not a get request, return an error if r.Method != "GET" { fmt.Println("Request method error") return false } // If chat is not included in the path, an error is returned if r.URL.Path != "/forexWs" { fmt.Println("Request path error") return false } // Verification rules can also be customized according to other needs return true }, } var forexLastKline = make(chan []model.ForexJsonData) var forexDayLastKline = make(chan []model.ForexJsonData) // 订阅数据类型 type Symbol struct { Code string `json:"code"` DepthLevel int `json:"depth_level"` } type Request struct { CmdID int `json:"cmd_id"` SeqID int `json:"seq_id"` Trace string `json:"trace"` Data DataList `json:"data"` } type DataList struct { SymbolList []Symbol `json:"symbol_list"` } type ResultBbo struct { CmdID int `json:"cmd_id"` Data struct { Code string `json:"code"` Seq string `json:"seq"` TickTime string `json:"tick_time"` Bids []struct { Price string `json:"price"` Volume string `json:"volume"` } `json:"bids"` Asks []struct { Price string `json:"price"` Volume string `json:"volume"` } `json:"asks"` } `json:"data"` } type ResultsTrade struct { CmdID int `json:"cmd_id"` Data struct { Code string `json:"code"` Seq string `json:"seq"` TickTime string `json:"tick_time"` Price string `json:"price"` Volume string `json:"volume"` Turnover string `json:"turnover"` TradeDirection int `json:"trade_direction"` } `json:"data"` } // Client represents a WebSocket client. type Client struct { conn *websocket.Conn subscriptions map[string]bool // Tracks which topics the client is subscribed to } // Hub maintains the set of active clients and broadcasts messages to the clients. type Hub struct { clients map[*Client]bool // All connected clients broadcast chan Message // Broadcast channel for messages topics map[string][]*Client // Topic to clients mapping for subscriptions mu sync.Mutex // Mutex for safe concurrent access } // Message structure to hold the message content and the topic type Message struct { Topic string `json:"topic"` Content string `json:"content"` } // newHub Initialize a new Hub func newHub() *Hub { return &Hub{ clients: make(map[*Client]bool), broadcast: make(chan Message), topics: make(map[string][]*Client), } } // run Start the Hub to listen for messages func (h *Hub) run() { for { msg := <-h.broadcast // Lock the hub to safely access the topics map h.mu.Lock() if clients, ok := h.topics[msg.Topic]; ok { for _, client := range clients { err := client.conn.WriteJSON(msg) if err != nil { log.Printf("Error writing message to client: %v", err) client.conn.Close() delete(h.clients, client) } } } h.mu.Unlock() } } // Handle WebSocket connections func handleConnection(hub *Hub, w http.ResponseWriter, r *http.Request) { conn, err := upGrader.Upgrade(w, r, nil) if err != nil { log.Println("Error during connection upgrade:", err) return } client := &Client{conn: conn, subscriptions: make(map[string]bool)} hub.clients[client] = true go func() { defer func() { delete(hub.clients, client) for topic := range client.subscriptions { hub.mu.Lock() hub.topics[topic] = removeClientFromTopic(hub.topics[topic], client) hub.mu.Unlock() } }() for { var msg Message if err = conn.ReadJSON(&msg); err != nil { log.Println("Error reading message:", err) break } applogger.Info("message info:%v---%v", msg.Topic, msg.Content) if msg.Content == "ping" || msg.Content == "subscribe" { client.subscriptions[msg.Topic] = true hub.mu.Lock() hub.topics[msg.Topic] = append(hub.topics[msg.Topic], client) hub.mu.Unlock() } switch msg.Content { case "subscribe": hub.broadcast <- Message{Topic: msg.Topic, Content: "subscribe success"} case "ping": hub.broadcast <- Message{Topic: msg.Topic, Content: "pong"} default: // TODO: 广播客户端订阅消息 //hub.broadcast <- msg } } }() } // subscribeTradeSwitcher content new func subscribeTradeSwitcher() *websocket.Conn { url := "wss://quote.tradeswitcher.com/quote-b-ws-api?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" applogger.Info("subscribePolygon info Url:%v", url) conn, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { applogger.Error("Failed to link to wss server:%v", err) return nil } return conn } func (h *Hub) ForexMarketBBOSwitcher(codeList []string) { var dataList DataList // 构造订阅参数 for _, value := range codeList { dataList.SymbolList = append(dataList.SymbolList, Symbol{ Code: value, DepthLevel: 1, }) } ticker := time.NewTicker(time.Second * 1) defer ticker.Stop() for { select { case <-ticker.C: applogger.Info("Execute automatic subscription........") if conn := subscribeTradeSwitcher(); conn != nil { // Send heartbeat every 10 seconds go func() { for range time.NewTicker(10 * time.Second).C { req := Request{ CmdID: 22000, SeqID: 1447560092, Trace: "b9e14618-b43f-4fb3-bf08-0bd1b60f285d", Data: DataList{}, } messageBytes, err := json.Marshal(req) if err != nil { applogger.Error("json.Marshal err:%v", err) return } applogger.Debug("Message send info:%v", string(messageBytes)) err = conn.WriteMessage(websocket.TextMessage, messageBytes) if err != nil { applogger.Error("write heartbeat err:%v", err) } } }() // Construct subscription parameters req := Request{ CmdID: 22002, SeqID: 1447560092, Trace: uuid.New().String(), Data: dataList, } messageBytes, err := json.Marshal(req) if err != nil { applogger.Error("json.Marshal err:%v", err) return } applogger.Debug("Message send info:%v", string(messageBytes)) err = conn.WriteMessage(websocket.TextMessage, messageBytes) if err != nil { applogger.Debug("write err:%v", err) return } // 客户端broadcast,消息推送 h.SendAllClientBBOSwitcher(conn) } } } } func (h *Hub) ForexMarketTradeSwitcher(codeList []string) { var dataList DataList // 构造订阅参数 for _, value := range codeList { dataList.SymbolList = append(dataList.SymbolList, Symbol{ Code: value, DepthLevel: 1, }) } ticker := time.NewTicker(time.Second * 1) defer ticker.Stop() for { select { case <-ticker.C: applogger.Info("Execute automatic subscription........") if conn := subscribeTradeSwitcher(); conn != nil { // Send heartbeat every 10 seconds go func() { for range time.NewTicker(10 * time.Second).C { req := Request{ CmdID: 22000, SeqID: 1148509458, Trace: "a5e37d07-168f-403a-b4cb-08be813e0b91", Data: DataList{}, } messageBytes, err := json.Marshal(req) if err != nil { applogger.Error("json.Marshal err:%v", err) return } applogger.Debug("Message send info:%v", string(messageBytes)) err = conn.WriteMessage(websocket.TextMessage, messageBytes) if err != nil { applogger.Error("heartbeat write err:%v", err) } } }() // Construct subscription parameters req := Request{ CmdID: 22004, SeqID: 1148509458, Trace: uuid.New().String(), Data: dataList, } messageBytes, err := json.Marshal(req) if err != nil { applogger.Error("json.Marshal err:%v", err) return } applogger.Debug("Message send info:%v", string(messageBytes)) err = conn.WriteMessage(websocket.TextMessage, messageBytes) if err != nil { applogger.Debug("write err:%v", err) return } // 客户端broadcast,消息推送 h.SendAllClientTradeSwitcher(conn) } } } } func (h *Hub) ForexMarketBatchPrice(codeList []string) { var dataList model.ConstructParametersPost for _, value := range codeList { dataList.Trace = uuid.New().String() dataList.Data.DataList = append(dataList.Data.DataList, model.DataParameters{ Code: value, KlineType: 1, KlineTimestampEnd: 0, QueryKlineNum: 1, AdjustType: 0, }) } go func() { for { UrlBatchKline := "https://quote.tradeswitcher.com/quote-b-api/batch-kline?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" queryStr, err := json.Marshal(&dataList) if err != nil { return } bodyStr, err := internal.HttpPost(UrlBatchKline, string(queryStr)) if err != nil { return } //applogger.Debug("klineBatchPrice data:%v", bodyStr) var klineNew model.KlinePostReturnStruct if err = json.Unmarshal([]byte(bodyStr), &klineNew); err != nil { time.Sleep(3 * time.Second) continue } //applogger.Debug("klineNew count:%v", len(klineNew.Data.KlineList)) // 将数据写入缓存用来推送K线 for _, value := range klineNew.Data.KlineList { if len(value.KlineData) == 0 { continue } code := model.Check_Code[value.Code] if len(code) == 0 { code = value.Code } openPrice := decimal.RequireFromString(value.KlineData[0].OpenPrice).InexactFloat64() closePrice := decimal.RequireFromString(value.KlineData[0].ClosePrice).InexactFloat64() highPrice := decimal.RequireFromString(value.KlineData[0].HighPrice).InexactFloat64() lowPrice := decimal.RequireFromString(value.KlineData[0].LowPrice).InexactFloat64() volume := decimal.RequireFromString(value.KlineData[0].Volume).InexactFloat64() timestamp := decimal.RequireFromString(value.KlineData[0].Timestamp).IntPart() forexJson := []model.ForexJsonData{ { Event: "CAS", Pair: code, Open: openPrice, Close: closePrice, High: highPrice, Low: lowPrice, Volume: int(volume), Timestamp: timestamp, }, } forexLastKline <- forexJson } time.Sleep(200 * time.Millisecond) } }() h.ForexMarketBatchPriceBatch() } func (h *Hub) ForexMarketBatchDayPrice(codeList []string) { var dataList model.ConstructParametersPost for _, value := range codeList { dataList.Trace = uuid.New().String() dataList.Data.DataList = append(dataList.Data.DataList, model.DataParameters{ Code: value, KlineType: 8, KlineTimestampEnd: 0, QueryKlineNum: 1, AdjustType: 0, }) } go func() { for { UrlBatchKline := "https://quote.tradeswitcher.com/quote-b-api/batch-kline?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" queryStr, err := json.Marshal(&dataList) if err != nil { return } bodyStr, err := internal.HttpPost(UrlBatchKline, string(queryStr)) if err != nil { return } //applogger.Debug("klineBatchPrice data:%v", bodyStr) var klineNew model.KlinePostReturnStruct if err = json.Unmarshal([]byte(bodyStr), &klineNew); err != nil { time.Sleep(3 * time.Second) continue } //applogger.Debug("klineNew count:%v", len(klineNew.Data.KlineList)) // 将数据写入缓存用来推送K线 for _, value := range klineNew.Data.KlineList { if len(value.KlineData) == 0 { continue } code := model.Check_Code[value.Code] if len(code) == 0 { code = value.Code } openPrice := decimal.RequireFromString(value.KlineData[0].OpenPrice).InexactFloat64() closePrice := decimal.RequireFromString(value.KlineData[0].ClosePrice).InexactFloat64() highPrice := decimal.RequireFromString(value.KlineData[0].HighPrice).InexactFloat64() lowPrice := decimal.RequireFromString(value.KlineData[0].LowPrice).InexactFloat64() volume := decimal.RequireFromString(value.KlineData[0].Volume).InexactFloat64() timestamp := decimal.RequireFromString(value.KlineData[0].Timestamp).IntPart() forexJson := []model.ForexJsonData{ { Event: "CAS-D", Pair: code, Open: openPrice, Close: closePrice, High: highPrice, Low: lowPrice, Volume: int(volume), Timestamp: timestamp, }, } forexDayLastKline <- forexJson } time.Sleep(200 * time.Millisecond) } }() h.ForexMarketBatchDayPriceBatch() } // SendAllClientTradeSwitcher send message to all clients func (h *Hub) SendAllClientBBOSwitcher(conn *websocket.Conn) { for { _, msg, err := conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { applogger.Error("IsUnexpectedCloseError err:%v", err) } break } var messageToJson ResultBbo if err = json.Unmarshal([]byte(string(msg)), &messageToJson); err != nil { applogger.Error("SendAllClientTradeSwitcher err:%v", err) time.Sleep(5 * time.Second) continue } switch messageToJson.CmdID { case 22001: // 心跳 // {"ret":200,"msg":"ok","cmd_id":22001,"seq_id":123,"trace":"3380a7a-3e1f-c3a5-5ee3-9e5be0ec8c241692805462"} applogger.Debug("Heartbeat results:%v", string(msg)) case 22999: // 处理订阅数据 // {"cmd_id":22999,"data":{"code":"EURUSD","seq":"114101723","tick_time":"1732168153591","bids":[{"price":"1.05478","volume":"100000.00"}],"asks":[{"price":"1.05479","volume":"100000.00"}]}} code := model.Check_Code[messageToJson.Data.Code] if len(code) == 0 { code = messageToJson.Data.Code } tickTime := decimal.RequireFromString(messageToJson.Data.TickTime).IntPart() bids := decimal.RequireFromString(messageToJson.Data.Bids[0].Price).InexactFloat64() asks := decimal.RequireFromString(messageToJson.Data.Asks[0].Price).InexactFloat64() volume := decimal.RequireFromString(messageToJson.Data.Asks[0].Volume).IntPart() quotesMsg := &[]model.ForexLastQuote{ { Ev: "C", P: code, T: tickTime, B: bids, A: asks, X: int(volume), }, } msgStr, err := json.Marshal(quotesMsg) if err != nil { applogger.Error("json.Marshal err:%v", err) time.Sleep(5 * time.Second) continue } applogger.Info("Message result:%v", string(msgStr)) // 广播数据 h.broadcast <- Message{Topic: topic_c, Content: string(msgStr)} default: applogger.Debug("Message result:%v", string(msg)) } } } func (h *Hub) SendAllClientTradeSwitcher(conn *websocket.Conn) { for { _, msg, err := conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { applogger.Error("IsUnexpectedCloseError err:%v", err) } break } var messageToJson ResultsTrade if err = json.Unmarshal(msg, &messageToJson); err != nil { applogger.Error("json.Unmarshal err:%v", err) continue } switch messageToJson.CmdID { case 22001: // 处理心跳 // {"ret":200,"msg":"ok","cmd_id":22001,"seq_id":123456,"trace":"3380a7a-3e1f-c3a5-5ee3-9e5be0ec8c241692805462787878"} applogger.Debug("Heartbeat results:%v", string(msg)) case 22998: // 处理订阅数据 // {"cmd_id":22998,"data":{"code":"XAUUSD","seq":"65087341","tick_time":"1732267727182","price":"2694.84","volume":"95.00","turnover":"0.00","trade_direction":0}} code := model.Check_Code[messageToJson.Data.Code] if len(code) == 0 { code = messageToJson.Data.Code } tradeMsg := &[]model.ForexTrade{ { Ev: "T", Code: code, Seq: messageToJson.Data.Seq, TickTime: messageToJson.Data.TickTime, Price: messageToJson.Data.Price, Volume: messageToJson.Data.Volume, Turnover: messageToJson.Data.Turnover, TradeDirection: messageToJson.Data.TradeDirection, }, } msgStr, err := json.Marshal(tradeMsg) if err != nil { applogger.Error("json.Marshal err:%v", err) time.Sleep(5 * time.Second) continue } applogger.Info("Message result:%v", string(msgStr)) // 广播数据 h.broadcast <- Message{ Topic: topic_t, Content: string(msgStr), } h.broadcast <- Message{ Topic: topic_s, Content: string(msgStr), } default: applogger.Debug("Message result:%v", string(msg)) } } } func (h *Hub) ForexMarketBatchPriceBatch() { for dataValue := range forexLastKline { applogger.Info("Message result:%v", dataValue) message, err := json.Marshal(&dataValue) if err != nil { time.Sleep(5 * time.Second) continue } // 广播数据 h.broadcast <- Message{Topic: topic, Content: string(message)} } } func (h *Hub) ForexMarketBatchDayPriceBatch() { for dataValue := range forexDayLastKline { applogger.Info("Message result:%v", dataValue) message, err := json.Marshal(&dataValue) if err != nil { time.Sleep(5 * time.Second) continue } // 广播数据 h.broadcast <- Message{Topic: topic_d, Content: string(message)} } } // ForexMarket content func (h *Hub) ForexMarket() { ticker := time.NewTicker(time.Second * 10) defer ticker.Stop() for { select { case <-ticker.C: applogger.Info("Execute automatic subscription........") if conn := subscribePolygon(); conn != nil { h.SendAllClientNew(conn) // 客户端broadcast,消息推送 } } } } // subscribePolygon Link to third-party services func subscribePolygon() *websocket.Conn { url := fmt.Sprintf("wss://%v/forex", config.Config.ShareGather.PolygonWss) applogger.Info("subscribePolygon info Url:%v", url) conn, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { applogger.Error("Failed to link to wss server:%v", err) return nil } return conn } // SendAllClientNew send message to all clients func (h *Hub) SendAllClientNew(conn *websocket.Conn) { for { _, msg, err := conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { applogger.Error("IsUnexpectedCloseError error: %v", err) } break } applogger.Debug("ReadMessage data info:%v", string(msg)) message := string(msg) if strings.Contains(message, "Connected Successfully") { // 鉴权 // {"action":"auth","params":"vG4tCD5emAFPkS4kWtXxJntMASyN4dnv"} keyStr := model.SendAuthority{ Action: "auth", Params: config.Config.ShareGather.PolygonKey, } authStr, err := JsonMarshal(keyStr) if err != nil { break } if err = Send(conn, string(authStr)); err != nil { break } } else if strings.Contains(message, "authenticated") { // 发起实时价格订阅 // {"action":"subscribe", "params":"CAS.*"} subStr := model.SendAuthority{Action: "subscribe", Params: "CAS.*"} resultStr, err := JsonMarshal(subStr) if err != nil { break } if err = Send(conn, string(resultStr)); err != nil { break } // 发起报价(卖一|买一)订阅 // {"action":"subscribe", "params":"C.*"} subStrC := model.SendAuthority{Action: "subscribe", Params: "C.*"} resultStrC, err := JsonMarshal(subStrC) if err != nil { break } if err = Send(conn, string(resultStrC)); err != nil { break } } else { // 广播数据 //[{"ev":"CAS","sym":"C.C:EUR-USD","i":"50578","x":4,"p":215.9721,"s":100,"t":1611082428813,"z":3}] //[{"ev":"C","p":"PLN/NOK","i":0,"a":2.7225,"b":2.7214,"x":48,"t":1730194133000}] var topicTop string if strings.Contains(message, "\"ev\":\"CAS\"") { topicTop = topic } else if strings.Contains(message, "\"ev\":\"C\"") { topicTop = topic_c } h.broadcast <- Message{Topic: topicTop, Content: message} } } } // Send Subscribe send message func Send(conn *websocket.Conn, data string) error { if conn == nil { applogger.Error("WebSocket sent error: no connection available") return errors.New("WebSocket sent error: no connection available") } if err := conn.WriteMessage(websocket.TextMessage, []byte(data)); err != nil { applogger.Error("WebSocket sent error: data=%s, error=%s", data, err) return err } return nil } // JsonMarshal marshal json func JsonMarshal(v interface{}) ([]byte, error) { str, err := json.Marshal(v) if err != nil { applogger.Error("json.Marshal error: %v", err) return []byte{}, err } return str, nil } // removeClientFromTopic Remove a client from a topic func removeClientFromTopic(clients []*Client, c *Client) []*Client { for i, client := range clients { if client == c { return append(clients[:i], clients[i+1:]...) } } return clients } // GetMongodbForexCode select forex code func GetMongodbForexCode() []string { dateList, err := data.MgoFind(data.ForexListBak, bson.M{}) if err != nil { applogger.Error("MgoFind info err: %v", err) return []string{} } //applogger.Info("MgoFind info len: %v", dateList) var dateListStr []string for _, value := range dateList.([]primitive.M) { code := value["code"].(string) dateListStr = append(dateListStr, code) } return dateListStr }