You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

726 lines
21 KiB

2 months ago
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
}