From e71bae6f149710029cb1803177be8d861a7ab94b Mon Sep 17 00:00:00 2001 From: "1447560092@qq.com" <1447560092@qq.com> Date: Thu, 2 Jan 2025 13:13:59 +0800 Subject: [PATCH] fix:add code --- .gitignore | 27 + Makefile | 8 + README.md | 132 ++ ServerDeployment | 208 +++ ServerList | 43 + ServiceUpdateLog | 128 ++ api/api.go | 357 +++++ cmd/closingMarket/closingMarket.go | 521 +++++++ cmd/common/base.go | 223 +++ cmd/common/common.go | 1122 +++++++++++++++ cmd/common/gzip.go | 71 + cmd/common/notStock.go | 671 +++++++++ cmd/main.go | 115 ++ cmd/marketwsscliert/marketcontract.go | 278 ++++ cmd/marketwsscliert/marketforex.go | 430 ++++++ cmd/marketwsscliert/marketshare.go | 216 +++ cmd/marketwsscliert/marketspots.go | 401 ++++++ cmd/marketwsscliert/marketwssclient.go | 270 ++++ cmd/marketwsscliert/marketwssclient_test.go | 589 ++++++++ cmd/marketwsscliert/modifycontract.go | 217 +++ cmd/marketwsscliert/modifyforex.go | 421 ++++++ cmd/selfContract/aggregation.go | 517 +++++++ cmd/selfContract/mongo.go | 57 + cmd/selfContract/virtualContract.go | 723 ++++++++++ cmd/selfMarketSpot/aggregation.go | 300 ++++ cmd/selfMarketSpot/model.go | 59 + cmd/selfMarketSpot/virtualContract.go | 698 ++++++++++ cmd/servicemanager/currencyWss.go | 13 + cmd/servicemanager/gather.go | 31 + cmd/servicemanager/gin.go | 68 + cmd/servicemanager/selfContract.go | 38 + cmd/servicemanager/selfMarketSpot.go | 38 + cmd/servicemanager/shareWss.go | 79 ++ cmd/servicemanager/shareus.go | 20 + cmd/servicemanager/tickdb.go | 55 + cmd/websocketcollect/cache/leveldb.go | 161 +++ cmd/websocketcollect/db/forex/CURRENT | 1 + cmd/websocketcollect/db/forex/CURRENT.bak | 1 + cmd/websocketcollect/db/forex/LOCK | 0 cmd/websocketcollect/db/forex/LOG | 15 + cmd/websocketcollect/db/forex/MANIFEST-000003 | Bin 0 -> 41 bytes cmd/websocketcollect/db/us/000016.ldb | Bin 0 -> 120276 bytes cmd/websocketcollect/db/us/000033.ldb | Bin 0 -> 185 bytes cmd/websocketcollect/db/us/CURRENT | 1 + cmd/websocketcollect/db/us/CURRENT.bak | 1 + cmd/websocketcollect/db/us/LOCK | 0 cmd/websocketcollect/db/us/LOG | 161 +++ cmd/websocketcollect/db/us/MANIFEST-000037 | Bin 0 -> 103 bytes cmd/websocketcollect/forex/route.go | 36 + cmd/websocketcollect/forex/wss.go | 725 ++++++++++ cmd/websocketcollect/us/route.go | 75 + cmd/websocketcollect/us/wss.go | 241 ++++ cmd/websocketservice/websocketclient.go | 540 ++++++++ cmd/websocketservice/websocketusclient.go | 412 ++++++ config/config.go | 25 + dictionary/publickey.go | 84 ++ internal/Publicmethods.go | 106 ++ internal/common.go | 58 + internal/data/business/forextoexcel.go | 48 + internal/data/business/mgocontract.go | 245 ++++ internal/data/business/mgohistoricalus.go | 119 ++ internal/data/business/mgoinitdata.go | 993 ++++++++++++++ internal/data/business/mgolistdataus.go | 1175 ++++++++++++++++ internal/data/business/mgolistdatemys.go | 638 +++++++++ internal/data/business/mgomanager.go | 67 + internal/data/business/mgoshareus.go | 86 ++ internal/data/business/mgospots.go | 381 ++++++ internal/data/business/mgostockus.go | 331 +++++ internal/data/gorm.go | 24 + internal/data/index.go | 501 +++++++ internal/data/mongo.go | 767 +++++++++++ internal/data/mysql.go | 48 + internal/data/mysqlbusiness/addinfo.go | 65 + internal/data/mysqlbusiness/getinfo.go | 112 ++ internal/data/mysqlbusiness/updateinfo.go | 102 ++ internal/gzip/gzipanalysis.go | 61 + internal/httprequest.go | 195 +++ internal/model/contractMarket.go | 194 +++ internal/model/forexMarket.go | 193 +++ internal/model/pingmessage.go | 44 + internal/model/pingv1message.go | 22 + internal/model/pingv2message.go | 24 + .../model/websocketv1authenticationrequest.go | 18 + .../model/websocketv2authenticationrequest.go | 28 + internal/mq/redis.go | 48 + internal/mq/redis_test.go | 17 + internal/paramstr.go | 89 ++ internal/pubsub/pubsub.go | 85 ++ internal/pubsub/pubsub_test.go | 42 + internal/redis/redis.go | 233 ++++ internal/requestbuilder/privateurlbuilder.go | 60 + internal/requestbuilder/publicurlbuilder.go | 25 + internal/requestbuilder/signer.go | 41 + .../websocketv1requestbuilder.go | 65 + .../websocketv2requestbuilder.go | 65 + logging/applogger/applogger.go | 55 + logging/perflogger/performancelogger.go | 84 ++ pkg/bawssclient/candlestickwebsocketclient.go | 137 ++ pkg/bawssclient/depthwebsocketclient.go | 114 ++ pkg/bawssclient/tickerwebsocketclient.go | 122 ++ pkg/client/bawebsocketclientbase/common.go | 13 + .../websocketclientbase.go | 245 ++++ .../websocketclientbase.go | 282 ++++ .../websocketv2clientbase.go | 263 ++++ .../bestbidofferwebsocketclient.go | 53 + .../candlestickwebsocketclient.go | 63 + .../marketwssclient/contractbboclient.go | 63 + .../marketwssclient/contractdepthclient.go | 63 + .../contractdepthsizeclient.go | 63 + .../marketwssclient/contractdetailclient.go | 63 + .../marketwssclient/contractklineclient.go | 63 + .../contracttradedetailclient.go | 63 + .../marketwssclient/depthwebsocketclient.go | 63 + .../last24hcandlestickwebsocketclient.go | 63 + .../marketbypricetickwebsocketclient.go | 63 + .../marketbypricewebsocketclient.go | 83 ++ .../marketwssclient/tickerwebsocketclient.go | 63 + .../marketwssclient/tradewebsocketclient.go | 63 + pkg/memory/cache.go | 61 + pkg/methods/methods.go | 23 + .../auth/websocketv1authenticationresponse.go | 26 + .../auth/websocketv2authenticationresponse.go | 20 + pkg/model/bamodel/klineResponse.go | 34 + pkg/model/base/bawebsocketresponsebase.go | 7 + pkg/model/base/websocketresponsebase.go | 7 + pkg/model/base/websocketv2responsebase.go | 24 + pkg/model/config.go | 147 ++ pkg/model/forex.go | 261 ++++ pkg/model/getrequest.go | 39 + pkg/model/global.go | 41 + pkg/model/market/contractbboresponse.go | 23 + pkg/model/market/contractdepthresponse.go | 35 + pkg/model/market/contractdepthsizeresponse.go | 23 + pkg/model/market/contractdetailresponse.go | 28 + pkg/model/market/contractklineresponse.go | 27 + .../market/contracttradedetailresponse.go | 29 + pkg/model/market/getcandlestickresponse.go | 20 + pkg/model/market/getdepthoptionalrequest.go | 20 + pkg/model/market/getdepthresponse.go | 17 + .../market/subscribebestbidofferresponse.go | 18 + .../market/subscribecandlestickresponse.go | 25 + pkg/model/market/subscribedepthresponse.go | 11 + .../subscribelast24hcandlestickresponse.go | 11 + .../market/subscribemarketbypriceresponse.go | 19 + pkg/model/market/subscribetraderesponse.go | 26 + .../market/subtickerwebsocketresponse.go | 39 + pkg/model/request.go | 470 +++++++ .../sqlmodel/bo_user_fund_account_spots.go | 17 + pkg/model/sqlmodel/bo_user_fund_accounts.go | 16 + pkg/model/sqlmodel/bo_user_optional_stocks.go | 15 + pkg/model/sqlmodel/bo_user_sms.go | 16 + .../sqlmodel/bo_user_terminal_equipments.go | 15 + pkg/model/sqlmodel/bo_users.go | 53 + pkg/model/stock/forex.go | 54 + pkg/model/stock/stockpublic.go | 182 +++ pkg/model/stock/stockus.go | 180 +++ pkg/model/tojson.go | 88 ++ pkg/model/tojson_test.go | 1 + pkg/msg/aliyun.go | 217 +++ pkg/msg/msg_test.go | 28 + pkg/msg/sms.go | 27 + pkg/processor/contract_api.go | 503 +++++++ pkg/processor/forex_api.go | 948 +++++++++++++ pkg/processor/history.go | 657 +++++++++ pkg/processor/intro.go | 194 +++ pkg/processor/msg_api.go | 258 ++++ pkg/processor/option.go | 350 +++++ pkg/processor/share_api.go | 1213 +++++++++++++++++ pkg/processor/spots_api.go | 281 ++++ pkg/processor/stock.go | 813 +++++++++++ pkg/processor/stock_index.go | 414 ++++++ 171 files changed, 29316 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 ServerDeployment create mode 100644 ServerList create mode 100644 ServiceUpdateLog create mode 100644 api/api.go create mode 100644 cmd/closingMarket/closingMarket.go create mode 100644 cmd/common/base.go create mode 100644 cmd/common/common.go create mode 100644 cmd/common/gzip.go create mode 100644 cmd/common/notStock.go create mode 100644 cmd/main.go create mode 100644 cmd/marketwsscliert/marketcontract.go create mode 100644 cmd/marketwsscliert/marketforex.go create mode 100644 cmd/marketwsscliert/marketshare.go create mode 100644 cmd/marketwsscliert/marketspots.go create mode 100644 cmd/marketwsscliert/marketwssclient.go create mode 100644 cmd/marketwsscliert/marketwssclient_test.go create mode 100644 cmd/marketwsscliert/modifycontract.go create mode 100644 cmd/marketwsscliert/modifyforex.go create mode 100644 cmd/selfContract/aggregation.go create mode 100644 cmd/selfContract/mongo.go create mode 100644 cmd/selfContract/virtualContract.go create mode 100644 cmd/selfMarketSpot/aggregation.go create mode 100644 cmd/selfMarketSpot/model.go create mode 100644 cmd/selfMarketSpot/virtualContract.go create mode 100644 cmd/servicemanager/currencyWss.go create mode 100644 cmd/servicemanager/gather.go create mode 100644 cmd/servicemanager/gin.go create mode 100644 cmd/servicemanager/selfContract.go create mode 100644 cmd/servicemanager/selfMarketSpot.go create mode 100644 cmd/servicemanager/shareWss.go create mode 100644 cmd/servicemanager/shareus.go create mode 100644 cmd/servicemanager/tickdb.go create mode 100644 cmd/websocketcollect/cache/leveldb.go create mode 100644 cmd/websocketcollect/db/forex/CURRENT create mode 100644 cmd/websocketcollect/db/forex/CURRENT.bak create mode 100644 cmd/websocketcollect/db/forex/LOCK create mode 100644 cmd/websocketcollect/db/forex/LOG create mode 100644 cmd/websocketcollect/db/forex/MANIFEST-000003 create mode 100644 cmd/websocketcollect/db/us/000016.ldb create mode 100644 cmd/websocketcollect/db/us/000033.ldb create mode 100644 cmd/websocketcollect/db/us/CURRENT create mode 100644 cmd/websocketcollect/db/us/CURRENT.bak create mode 100644 cmd/websocketcollect/db/us/LOCK create mode 100644 cmd/websocketcollect/db/us/LOG create mode 100644 cmd/websocketcollect/db/us/MANIFEST-000037 create mode 100644 cmd/websocketcollect/forex/route.go create mode 100644 cmd/websocketcollect/forex/wss.go create mode 100644 cmd/websocketcollect/us/route.go create mode 100644 cmd/websocketcollect/us/wss.go create mode 100644 cmd/websocketservice/websocketclient.go create mode 100644 cmd/websocketservice/websocketusclient.go create mode 100644 config/config.go create mode 100644 dictionary/publickey.go create mode 100644 internal/Publicmethods.go create mode 100644 internal/common.go create mode 100644 internal/data/business/forextoexcel.go create mode 100644 internal/data/business/mgocontract.go create mode 100644 internal/data/business/mgohistoricalus.go create mode 100644 internal/data/business/mgoinitdata.go create mode 100644 internal/data/business/mgolistdataus.go create mode 100644 internal/data/business/mgolistdatemys.go create mode 100644 internal/data/business/mgomanager.go create mode 100644 internal/data/business/mgoshareus.go create mode 100644 internal/data/business/mgospots.go create mode 100644 internal/data/business/mgostockus.go create mode 100644 internal/data/gorm.go create mode 100644 internal/data/index.go create mode 100644 internal/data/mongo.go create mode 100644 internal/data/mysql.go create mode 100644 internal/data/mysqlbusiness/addinfo.go create mode 100644 internal/data/mysqlbusiness/getinfo.go create mode 100644 internal/data/mysqlbusiness/updateinfo.go create mode 100644 internal/gzip/gzipanalysis.go create mode 100644 internal/httprequest.go create mode 100644 internal/model/contractMarket.go create mode 100644 internal/model/forexMarket.go create mode 100644 internal/model/pingmessage.go create mode 100644 internal/model/pingv1message.go create mode 100644 internal/model/pingv2message.go create mode 100644 internal/model/websocketv1authenticationrequest.go create mode 100644 internal/model/websocketv2authenticationrequest.go create mode 100644 internal/mq/redis.go create mode 100644 internal/mq/redis_test.go create mode 100644 internal/paramstr.go create mode 100644 internal/pubsub/pubsub.go create mode 100644 internal/pubsub/pubsub_test.go create mode 100644 internal/redis/redis.go create mode 100644 internal/requestbuilder/privateurlbuilder.go create mode 100644 internal/requestbuilder/publicurlbuilder.go create mode 100644 internal/requestbuilder/signer.go create mode 100644 internal/requestbuilder/websocketv1requestbuilder.go create mode 100644 internal/requestbuilder/websocketv2requestbuilder.go create mode 100644 logging/applogger/applogger.go create mode 100644 logging/perflogger/performancelogger.go create mode 100644 pkg/bawssclient/candlestickwebsocketclient.go create mode 100644 pkg/bawssclient/depthwebsocketclient.go create mode 100644 pkg/bawssclient/tickerwebsocketclient.go create mode 100644 pkg/client/bawebsocketclientbase/common.go create mode 100644 pkg/client/bawebsocketclientbase/websocketclientbase.go create mode 100644 pkg/client/hbwebsocketclientbase/websocketclientbase.go create mode 100644 pkg/client/hbwebsocketclientbase/websocketv2clientbase.go create mode 100644 pkg/hbwssclient/marketwssclient/bestbidofferwebsocketclient.go create mode 100644 pkg/hbwssclient/marketwssclient/candlestickwebsocketclient.go create mode 100644 pkg/hbwssclient/marketwssclient/contractbboclient.go create mode 100644 pkg/hbwssclient/marketwssclient/contractdepthclient.go create mode 100644 pkg/hbwssclient/marketwssclient/contractdepthsizeclient.go create mode 100644 pkg/hbwssclient/marketwssclient/contractdetailclient.go create mode 100644 pkg/hbwssclient/marketwssclient/contractklineclient.go create mode 100644 pkg/hbwssclient/marketwssclient/contracttradedetailclient.go create mode 100644 pkg/hbwssclient/marketwssclient/depthwebsocketclient.go create mode 100644 pkg/hbwssclient/marketwssclient/last24hcandlestickwebsocketclient.go create mode 100644 pkg/hbwssclient/marketwssclient/marketbypricetickwebsocketclient.go create mode 100644 pkg/hbwssclient/marketwssclient/marketbypricewebsocketclient.go create mode 100644 pkg/hbwssclient/marketwssclient/tickerwebsocketclient.go create mode 100644 pkg/hbwssclient/marketwssclient/tradewebsocketclient.go create mode 100644 pkg/memory/cache.go create mode 100644 pkg/methods/methods.go create mode 100644 pkg/model/auth/websocketv1authenticationresponse.go create mode 100644 pkg/model/auth/websocketv2authenticationresponse.go create mode 100644 pkg/model/bamodel/klineResponse.go create mode 100644 pkg/model/base/bawebsocketresponsebase.go create mode 100644 pkg/model/base/websocketresponsebase.go create mode 100644 pkg/model/base/websocketv2responsebase.go create mode 100644 pkg/model/config.go create mode 100644 pkg/model/forex.go create mode 100644 pkg/model/getrequest.go create mode 100644 pkg/model/global.go create mode 100644 pkg/model/market/contractbboresponse.go create mode 100644 pkg/model/market/contractdepthresponse.go create mode 100644 pkg/model/market/contractdepthsizeresponse.go create mode 100644 pkg/model/market/contractdetailresponse.go create mode 100644 pkg/model/market/contractklineresponse.go create mode 100644 pkg/model/market/contracttradedetailresponse.go create mode 100644 pkg/model/market/getcandlestickresponse.go create mode 100644 pkg/model/market/getdepthoptionalrequest.go create mode 100644 pkg/model/market/getdepthresponse.go create mode 100644 pkg/model/market/subscribebestbidofferresponse.go create mode 100644 pkg/model/market/subscribecandlestickresponse.go create mode 100644 pkg/model/market/subscribedepthresponse.go create mode 100644 pkg/model/market/subscribelast24hcandlestickresponse.go create mode 100644 pkg/model/market/subscribemarketbypriceresponse.go create mode 100644 pkg/model/market/subscribetraderesponse.go create mode 100644 pkg/model/market/subtickerwebsocketresponse.go create mode 100644 pkg/model/request.go create mode 100644 pkg/model/sqlmodel/bo_user_fund_account_spots.go create mode 100644 pkg/model/sqlmodel/bo_user_fund_accounts.go create mode 100644 pkg/model/sqlmodel/bo_user_optional_stocks.go create mode 100644 pkg/model/sqlmodel/bo_user_sms.go create mode 100644 pkg/model/sqlmodel/bo_user_terminal_equipments.go create mode 100644 pkg/model/sqlmodel/bo_users.go create mode 100644 pkg/model/stock/forex.go create mode 100644 pkg/model/stock/stockpublic.go create mode 100644 pkg/model/stock/stockus.go create mode 100644 pkg/model/tojson.go create mode 100644 pkg/model/tojson_test.go create mode 100644 pkg/msg/aliyun.go create mode 100644 pkg/msg/msg_test.go create mode 100644 pkg/msg/sms.go create mode 100644 pkg/processor/contract_api.go create mode 100644 pkg/processor/forex_api.go create mode 100644 pkg/processor/history.go create mode 100644 pkg/processor/intro.go create mode 100644 pkg/processor/msg_api.go create mode 100644 pkg/processor/option.go create mode 100644 pkg/processor/share_api.go create mode 100644 pkg/processor/spots_api.go create mode 100644 pkg/processor/stock.go create mode 100644 pkg/processor/stock_index.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b013b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +.idea/ +# Test binary, built with `go test -c` +*.test +cmd/config.yaml +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ +cmd/config.yaml +# Go workspace file +go.work +*.log +config/config.yaml +go.* + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6aace8e --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +GOHOSTOS:=$(shell go env GOHOSTOS) +GOPATH:=$(shell go env GOPATH) +VERSION=$(shell git describe --tags --always) + +.PHONY: win_build +# win_build services +win_build: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X main.Version=$(VERSION)" -o ./bin/wssPool ./cmd/main.go \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a1bb3c --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +### wss-pool +## 项目说明 + +根据撮合系统需求,实现一个广播分发的webSocket数据服务; + +* 对接第三方数据服务[火币,币安,OKX,股票...]. +* 基于go-websocket提供高性能的、稳定的、时效性的数据服务. +* 实现用户订阅功能 +* 实现数据分发功能 + +## Benchmarks + +#现货数据ws调用规则 +*ws://127.0.0.1:端口/quotes-wss +- (1)客户端ping: +{ + "type":"ping", + "symbol":"ping" +} +- (2)订阅: +{ + "type":"subscribe", + "symbol":"market.btcusdt.kline.1min" +} +- (3)取消订阅: +{ + "type":"unSubscribe", + "symbol":"market.btcusdt.kline.1min" +} + +| Benchmark name | 服务端端口| +| ------------------------------ | ---------:| +| wssPool服务 | :8861| +| 数采集服务 | :8852| + +*服务启动说明 +- wssPool服务启动:./服务名称 --check=server --hostS 127.0.0.1 --addrS 服务端端口号 +- 数采集服务端启动:./服务名称 --check=gather --hostG 127.0.0.1 --addrG 服务端端口号 + +#现货-合约-股票静态数据服务 +| Static Services name | 服务名称 | +| ------------------------------ | --------------------:| +| 现货-K线数据(蜡烛图) | spots/kline| +| 现货-聚合行情(Ticker) | spots/merged| +| 现货-所有交易对的最新 | spots/tickers| +| 现货-最近市场成交记录 | spots/trade| +| 现货-市场深度数据 | spots/depth| +| 现货-最近24小时行情数据 | spots/detail| +| 现货-获得近期交易记录 | spots/history/trade| +| 现货列表数据服务 | spots/merged/list| +| 合约-获取行情深度数据 | contract/depth| +| 合约-获取市场最优挂单 | contract/bbo| +| 合约-K线数据获取 | contract/history/kline| +| 合约-行情数据信息 | contract/merged | +| 合约-获取标记价格的K线数据 | contract/history/price_kline | +| 合约-批量获取聚合行情(V2) | contract/batch_merged | +| 合约-获取市场最近成交记录 | contract/trade| +| 合约-批量获取最近的交易记录 | contract/history/trade| +| 合约-平台历史持仓量查询 | contract/swap_his_open_interest| +| 合约-获取合约的溢价指数K线 | contract/history/linear_swap_premium_index_kline| +| 合约-获取实时预测资金费率的K线数据 | contract/history/linear_swap_estimated_rate_kline| +| 合约-获取基差数据 | contract/history/linear_swap_basis | + +*服务启动说明 +- 服务端启动:./staticS --check=gin --hostC 127.0.0.1 --addrC :8851 + +*端口配置 +- 静态服务端口:8851 + +#现货-合约-股票静态服务调用规则 +- http://127.0.0.1:8851/服务名称 + +# 股票服务 +### supervisor 管理服务 +``` +1、http静态服务 +WEB主服务端启动:服务名称 --check gin --hostS 0.0.0.0 --addrS :88 + +2、(美股|外汇[实时|买一卖一])行情服务 +外汇分发服务:服务名称 --check collectForex --hostS 0.0.0.0 --addrS :7778 --config /home/ubuntu/wss-server/config/config.yaml +外汇采集服务:服务名称 --check gatherForex --hostS 0.0.0.0 --addrS :8965 --model forex --config /home/ubuntu/wss-server/config/config.yaml +美股分发服务:服务名称 --check collectUs --hostS 0.0.0.0 --addrS :7777 --config /home/ubuntu/wss-server/config/config.yaml +美股采集服务:服务名称 --check gatherUs --hostS 0.0.0.0 --addrS :8964 --model usShare --config /home/ubuntu/wss-server/config/config.yaml + +3、股票采集和更新服务 +印度期权股票服务端启动:服务名称 --check indiaOption --hostS 0.0.0.0 --addrS :95 +指数股票服务端启动:服务名称 --check stockIndex --hostS 0.0.0.0 --addrS :92 +日本股票服务端启动:服务名称 --check japanStock --hostS 0.0.0.0 --addrS :86 +印尼股票服务端启动:服务名称 --check indonesiaStock --hostS 0.0.0.0 --addrS :89 +泰国股票服务端启动:服务名称 --check thailandStock --hostS 0.0.0.0 --addrS :90 +印度股票服务端启动:服务名称 --check indiaStock --hostS 0.0.0.0 --addrS :91 +马来股票服务端启动:服务名称 --check malaysiaStock --hostS 0.0.0.0 --addrS :93 +新加坡股票服务端启动:服务名称 --check singaporeStock --hostS 0.0.0.0 --addrS :94 +港股票服务端启动:服务名称 --check hongkongStock --hostS 0.0.0.0 --addrS :96 +英股票服务端启动:服务名称 --check ukStock --hostS 0.0.0.0 --addrS :97 +德股票服务端启动:服务名称 --check germanyStock --hostS 0.0.0.0 --addrS :98 +巴西股票服务端启动:服务名称 --check brazilStock --hostS 0.0.0.0 --addrS :103 +美股票服务端启动:服务名称 --check usStock --hostS 0.0.0.0 --addrS :102 +``` + +### cron 管理服务(行情报警、指数、泰股、马股、港股、印度股、印尼股、新加坡股、英股、德股、法股、巴西、日本) +``` +*/20 * * * 1-5 root /home/ubuntu/wss-server/checkStock --check tickDB --model checkStock --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/checkStock.log 2>&1 & +*/5 * * * 1-6 root /home/ubuntu/wss-server/stockIndex --check tickDB --model stockIndex --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/stockIndex.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/ukStock --check tickDB --model southAsiaStock --contract UK --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/ukStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/indiaStock --check tickDB --model southAsiaStock --contract India --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/indiaStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/thailandStock --check tickDB --model southAsiaStock --contract Thailand --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/thailandStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/malaysiaStock --check tickDB --model southAsiaStock --contract Malaysia --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/malaysiaStock.log 2>&1 +*/5 * * * 1-5 root /home/ubuntu/wss-server/hongkongStock --check tickDB --model southAsiaStock --contract HongKong --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/hongkongStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/indonesiaStock --check tickDB --model southAsiaStock --contract Indonesia --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/indonesiaStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/singaporeStock --check tickDB --model southAsiaStock --contract Singapore --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/singaporeStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/germanyStock --check tickDB --model southAsiaStock --contract Germany --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/germanyStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/franceStock --check tickDB --model southAsiaStock --contract France --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/franceStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/japanStock --check tickDB --model southAsiaStock --contract Japan --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/japanStock.log 2>&1 & +``` + +### 更新美股上一次行情价格 +``` +8 9 * * * root /home/ubuntu/wss-server/preClose --check tickDB --model previousClose --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/preClose.log 2>&1 & +``` + +### K线数据优化 +``` +12 22 * * 2-5 root /home/ubuntu/wss-server/deleteSpot --check tickDB --model deleteSpot --contract false --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/deleteSpot.log 2>&1 & +``` + +### 插针数据推送 +``` +*/1 * * * * root /home/ubuntu/wss-server/stockCloseData --check tickDB --model stockCloseData --config /home/ubuntu/wss-server/config/config.yaml>>/var/log/stockCloseData.log 2>&1 & +``` + + diff --git a/ServerDeployment b/ServerDeployment new file mode 100644 index 0000000..6918a81 --- /dev/null +++ b/ServerDeployment @@ -0,0 +1,208 @@ +公司无线密码: + 账号1:H3C_2202-5G-1 + 密码:A13b142202) + 账号2:ssid + 密码:Meetingyou0) +------------------------------------------------------------------------------------------------------------------------------------------------- + +1、美股数据接入账号: + https://polygon.io/dashboard/api-keys + 账号:rnldburn@gmail.com + 密码: Meetingyou0 + key: CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + +2、亚马逊oss接入: + myaccesspoint + 桶名称:log-aws-bucket-2023 + S3: s3://arn:aws:s3:ap-southeast-1:297182325232:accesspoint/myaccesspoint + ARN: arn:aws:s3:ap-southeast-1:297182325232:accesspoint/myaccesspoint + 别名:myaccesspoint-9kcrj5icrgdejw1fydhda6n6rfcjkaps1a-s3alias + +3、区域:ap-southeast-1 + aws_access_key_id:AKIAUKMLSNHYAP7EOBDE + aws_secret_access_key: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 + endpoint:s3.ap-southeast-1.amazonaws.com +------------------------------------------------------------------------------------------------------------------------------------------------ + +1、域名解析: + cotelaiamelia@gmail.com + Meetingyou0) + +2、GCP-谷歌云-测试环境: + 项目名称:SC Project 47850 + LoginAccount:zajdelvipondespq9931@gmail.com + 邮箱密码:7ae8b5w0u0z + 访问地址:https://cloud.google.com/?hl=zh-CN + +3、GCP-谷歌云-正式环境: + 项目名称:SC Project 51416 + 登录邮箱:cloudrun40@gmail.com + 邮箱密码:Meetingyou0 + 辅助邮箱:rnldburn@gmail.com + 访问地址:https://cloud.google.com/?hl=zh-CN + +4、跳板机 + ec2-13-212-72-30.ap-southeast-1.compute.amazonaws.com + 用户名:Administrator + 密码:J.RUhgy8hHf?QsaK50cckzv3ynl7X.W= +------------------------------------------------------------------------------------------------------------------------------------------------ + +1、测试服务接口是否正常 + curl -X POST -H 'Content-Type: application/json' -d '{ "status": "0","pageSize": "10","pageCount": "1"}' http://10.160.0.2:8003/order_shareus/share_list + curl -X POST -H 'Content-Type: application/json' -d '{ "status": "0","pageSize": "10","pageCount": "1"}' http://trade.lazardinvestgroup.net/order_shareus/share_list + curl -X POST -H 'Content-Type: application/json' -d '{ "status": "0","pageSize": "10","pageCount": "1"}' http://10.160.0.2:8002/order_contract/contract_list + curl -X POST -H 'Content-Type: application/json' -d '{ "status": "0","pageSize": "10","pageCount": "1"}' http://trade.chdh.me/order_contract/contract_list + curl -X POST -H 'Content-Type: application/json' -d '{ "status": "0","pageSize": "10","pageCount": "1"}' https://172.23.48.59:8004/order_sharemys/share_list + curl -X POST -H 'Content-Type: application/json' -d '{ }' http://10.148.0.6:8000/order_sharepre/update_all_stock_id + curl -X POST -H 'Content-Type: application/json' -d '{ "code": "BSE:IXIGO","id": "80","stock": "7"}' http://10.160.0.17:8000/order_sharepre/share_pre_trade + +2、初始化股票数据 34.100.189.47 + ./shareUs -conf /home/ubuntu/service/config/shareUs.yaml -check shareUs -network onLine + ./digitalInit -conf /home/ubuntu/service/config/digitalInit.yaml -check digitalInit -network onLine + ./optionInr -conf /home/ubuntu/service/config/optionInr.yaml -check optionInr -network onLine + ./shareClearCache -conf /home/ubuntu/service/config/shareInit.yaml -check shareClearCache -network onLine + ./contract -conf /home/ubuntu/service/config/contract.yaml -check contract -network onLine + ./second -conf /home/ubuntu/service/config/second.yaml -check second -network onLine + ./shareCache -conf /home/ubuntu/service/config/shareCache.yaml -check shareCache -network onLine + ./wssPool --check tickDB --hostS 0.0.0.0 --addrS :1000 --model allUs --config /home/ubuntu/wss-server/config/config06.yaml + ./wssPool --check tickDB --hostS 0.0.0.0 --addrS :1000 --model Us --config /home/ubuntu/wss-server/config/config06.yaml + ./wssPool --check tickDB --hostS 0.0.0.0 --addrS :1000 --model allUs --config /home/ubuntu/wss-server/config/config06.yaml + ./wssPool --check stockDataUs --hostS 0.0.0.0 --addrS :1000 --project US --config /home/ubuntu/wss-server/config/config06.yaml + ./wssPool --check tickDB --hostS 0.0.0.0 --addrS :1000 --model updateStockUsCode --config /home/ubuntu/wss-server/config/config06.yaml + ./wssPool --check stockCode --hostS 0.0.0.0 --addrS :7777 --project US --config /home/ubuntu/wss-server/config/config06.yaml + ./wssPool --check gatherUs --hostS 0.0.0.0 --addrS :7777 +3、mysql-生成model + ./xorm.exe reverse mysql admin:Meetingyou0@\(dbtest.crsocbk1nt38.ap-southeast-1.rds.amazonaws.com:3306\)/bourse?charset=utf8 templates/goxorm + ./xorm.exe reverse mysql admin:Meetingyou0@\(ubsfim.c59brkvf12hq.ap-southeast-3.rds.amazonaws.com:3306\)/bourse?charset=utf8 templates/goxorm + ./xorm.exe reverse mysql root:'q7%B/$o>ck5r]{x<'@\(35.186.154.125:3306\)/bourse?charset=utf8 templates/goxorm + ./xorm.exe reverse mysql root:123456789@\(127.0.0.1:13306\)/bourse?charset=utf8 templates/goxorm + +4、本地环境-docker启动服务 + docker run --name mysql -p 13306:3306 -e MYSQL_ROOT_PASSWORD=12345678 mysql:8.0.28 + docker run --name f2ad9f23df82a3e5efabd1574b862a94c0657c73a6179efec07d5cf9ae5a307f -p 13306:3306 -e MYSQL_ROOT_PASSWORD=123456789 -d mysql:8.0.28 + docker run --name my-redis -p 6379:6379 -d redis --requirepass "123456" +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + +服务器环境部署: +1、redis部署 + sudo apt update + sudo apt-get install redis-server + +2、mongod部署 + sudo apt update + wget -qO- https://get.docker.com/ | sh + apt install docker-compose + docker-compose up -d + +3、安装supervisor + sudo apt update + apt install supervisor + +4、安装nginxs + sudo apt update + sudo apt install nginx + sudo systemctl start nginx + sudo service nginx reload {start|stop|restart|reload|force-reload|status|configtest|rotate|upgrade) + +5、设置最大链接数 + vim /etc/profile + ulimit -n 1000000 + source /etc/profile + +6、初始化服务器 + 切换用户:sudo -i + 修改登录权限:vim /etc/ssh/sshd_config + 修改配置:PasswordAuthentication yes + 修改配置: ChallengeResponseAuthentication yes + 重启ssh服务:service ssh restart + 修改密码:passwd + +7、修改服务器时区 + sudo timedatectl set-timezone Asia/Shanghai + +8、查看redis链接数 + netstat -an | grep :6379 | wc -l + netstat -tuln | grep :80 + +9、corn 定时器 + sudo systemctl status cron + sudo systemctl stop cron + sudo systemctl enable cron + sudo service cron restart +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + +1、市场总资产: 冻结 + 可用 + 持仓市值 +2、市场可用资产: 可用 +3、市场累计盈亏(订单表-->平仓状态): + 1、买涨:订单量 * (平仓价 - 开仓价) + 2、买跌:订单量 * (开仓价 - 平仓价) +4、市场冻结资产: 冻结 +5、市场总手续费(统计订单表-->【持仓和平仓】状态): + 1、交易手续费:bot_stock_fur_trade ------ sum(service_cost + closing_cost) + 2、申购手续费:bot_user_fur_pre_stock_order ----- sum(get_fee) +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + +目前杠杆设置注解: + 1、全市场杠杆倍数设置——》针对所有市场股票给的默认杠杆倍数(目前设置的是1,可修改),且不需要关闭,当然也不影响用户单独设置其他杠杆倍数; + 2、用户杠杆倍数设置——》如果用户通过了申请且满足了后台设置触发杠杆的条件(例如:1、是否开启杠杆,2、是否达到最小面值,3、是否满足设置杠杆的范围(最大和最小)),就会使用这个用户单独设置的杠杆倍数; +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + +docker save liuqingzheng/yapi:latest > yapi_latest.tar +docker save mongo:latest > mongo_latest.tar + +docker save gitea/gitea:latest > gitea_latest.tar +docker save mysql:5.7 > mysql.tar + +scp -P 31544 mongo_latest.tar mysql.tar gitea_latest.tar yapi_latest.tar root@154.86.0.30:/root +docker load < mongo_latest.tar mysql.tar gitea_latest.tar yapi_latest.tar + +docker run -d --name yapi-mongo -e MONGO_INITDB_ROOT_USERNAME=admin@admin.com -e MONGO_INITDB_ROOT_PASSWORD=admin mongo:latest +docker run -d --name yapi-mongo mongo:latest +docker run -d --name yapi-web -p 3001:3000 liuqingzheng/yapi:latest + +find / -name config.json 2>/dev/null + +docker run -d --name gitea_server_1 -p 3000:3000 -p 222:31544 gitea/gitea:latest +docker run -d --name gitea_db_1 -p 3306:3306 -p 33060:33060 mysql/mysql:5.7 + +文档|git +yapi:http://154.86.0.30:3001/ +git:http://103.71.254.42:3000/ +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + +cron 执行定时任务; cat /etc/crontab +// 行情报警、指数、泰股、马股、港股、印度股、印尼股、新加坡股、英股、德股、法股、日本 +*/20 * * * 1-5 root /home/ubuntu/wss-server/checkStock --check tickDB --model checkStock --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/checkStock.log 2>&1 & +*/5 * * * 1-6 root /home/ubuntu/wss-server/stockIndex --check tickDB --model stockIndex --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/stockIndex.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/thailandStock --check tickDB --model southAsiaStock --contract Thailand --hostS 0.0.0.0.0 --addrS :289 --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/thailandStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/malaysiaStock --check tickDB --model southAsiaStock --contract Malaysia --hostS 0.0.0.0.0 --addrS :299 --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/malaysiaStock.log 2>&1 +*/5 * * * 1-5 root /home/ubuntu/wss-server/hongkongStock --check tickDB --model southAsiaStock --contract HongKong --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/hongkongStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/indiaStock --check tickDB --model southAsiaStock --contract India --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/indiaStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/indonesiaStock --check tickDB --model southAsiaStock --contract Indonesia --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/indonesiaStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/singaporeStock --check tickDB --model southAsiaStock --contract Singapore --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/singaporeStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/ukStock --check tickDB --model southAsiaStock --contract UK --config=/home/ubuntu/wss-server/config/config06.yaml>>/var/log/ukStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/germanyStock --check tickDB --model southAsiaStock --contract Germany --config=/home/ubuntu/wss-server/config/config06.yaml>>/var/log/germanyStock.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/franceStock --check tickDB --model southAsiaStock --contract France --config=/home/ubuntu/wss-server/config/config06.yaml>>/var/log/franceStock.log 2>&1 & +59 59 23 * * root /home/ubuntu/wss-server/forexClosePrice --check tickDB --model forexClosePrice --config=/home/ubuntu/wss-server/config/config.yaml>>/var/log/forexClosePrice.log 2>&1 & +*/5 * * * 1-5 root /home/ubuntu/wss-server/deleteForexTrade --check tickDB --model deleteForexTrade --config=/home/ubuntu/wss-server/config/config.yaml>>/var/log/forexClosePrice.log 2>&1 & +// 更新上一次行情价格 +8 9 * * * root /home/ubuntu/wss-server/preClose --check=tickDB --model=previousClose --config=/home/ubuntu/wss-server/config/config06.yaml>>/var/log/preClose.log 2>&1 & + +// 数据清理 +12 22 * * 2-5 root /home/ubuntu/wss-server/deleteSpot --check=tickDB --model=deleteSpot --contract=false --config /home/ubuntu/wss-server/config/config06.yaml>>/var/log/deleteSpot.log 2>&1 & + +// 插针数据推送 +*/1 * * * * root /home/ubuntu/wss-server/stockCloseData --check=tickDB --model=stockCloseData --config=/home/ubuntu/wss-server/config/config06.yaml>>/var/log/stockCloseData.log 2>&1 & +----------------------------------------------------------------------------------------------------------------------------------------------------------------- + +[program:collectUs] +command=/home/ubuntu/wss-server/collectUs --check collectUs --hostS 0.0.0.0 --addrS :7777 --config /home/ubuntu/wss-server/config/config06.yaml +startsecs=30 +autostart=true +autorestart=true + +stderr_logfile=/var/log/supervisor/collectUs_err.log +stdout_logfile=/var/log/supervisor/collectUs_info.log +stdout_logfile_maxbytes = 5MB +stdout_logfile_backups = 3 +----------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/ServerList b/ServerList new file mode 100644 index 0000000..b715807 --- /dev/null +++ b/ServerList @@ -0,0 +1,43 @@ +------------------------------------------------------------------------------------------------------------------------------------------------ + +test(谷歌云)服务器(域名:orbisimg.com) +web-mysql 47.237.29.68(公) 172.26.45.216(私有) +trade-quotes 47.237.64.60(公) 172.26.45.215(私有) +mongo-redis 8.222.169.172(公) 172.26.45.217(私有) + +msyql +host:47.237.29.68(公) 172.26.45.216(私有) +user:root +密码:Meetingyou0)) + +redis +host:47.237.29.68(公) 172.26.45.216(私有) +密码:MRrfvtyujnb&hg56 +端口:6379 + +mongodb数据库 +host:8.222.169.172(公) 172.26.45.217(私有) +账号:pqRRVamndJ +密码:35LlW3pXF76&WD!OOlnI +------------------------------------------------------------------------------------------------------------------------------------------------ + +project06-新服务器(域名:yrsig.com) +p6-mongo 10.154.0.10 (nic0) 35.189.116.242 (nic0) +p6-quotes 10.154.0.8 (nic0) 35.246.61.201 (nic0) +p6-trade 10.154.0.7 (nic0) 34.147.138.200 (nic0) +p6-web 10.154.0.9 (nic0) 34.105.182.222 (nic0) + +用户名:root +密码:Meetingyou0)) + +mysql +IP: 10.154.0.9 (nic0) 34.105.132.51 (nic0) +port: 23306 +USER: root +PWD: Meetingyou0))2024$ + +redis +IP: 10.154.0.9 (nic0) 34.105.132.51 (nic0) +port :26379 +PWD: 7d00cb62-1d1c-4c86-b50a-ebf9f00cc9fd +------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/ServiceUpdateLog b/ServiceUpdateLog new file mode 100644 index 0000000..ba1561b --- /dev/null +++ b/ServiceUpdateLog @@ -0,0 +1,128 @@ + +------------------------------------------------------------------------------------------------------------------------------------------------ +p7正式环境,美股|泰股|马股|港股|IPO交易服务更新 +服务部署完毕(2024-06-21) +此次更新功能包含: +1、部署美股、泰股、马股、港股、IPO交易服务 +2、访问地址 + 1>交易订单域名: https://trade.tdcowengroup.com + 2>交易订单Wss: wss://trade.tdcowengroup.com +3、相关API文档参见 + 1>http://103.71.254.42:3001/project/56/interface/api + 2>http://103.71.254.42:3001/project/56/interface/api/cat_88 +------------------------------------------------------------------------------------------------------------------------------------------------ + +测试|正式环境,德股|法股交易服务更新 +服务部署完毕(2024-06-24) +此次更新功能包含: +1、部署德股、法股交易服务 +2、测试访问地址 + 1>交易订单域名: https://trade.jdtest88.com + 2>交易订单Wss: wss://trade.jdtest88.com +3、线上访问地址 + 1>交易订单域名: https://trade.twinim.com + 2>交易订单Wss: wss://trade.twinim.com +4、相关API文档参见 + 1>http://103.71.254.42:3001/project/56/interface/api/cat_807 + 2>http://103.71.254.42:3001/project/56/interface/api/cat_799 + 3>http://103.71.254.42:3001/project/56/interface/api/cat_88 +------------------------------------------------------------------------------------------------------------------------------------------------ + +测试|p6正式环境,股票交易服务更新 +服务部署完毕(2024-06-27) +此次更新功能包含: +1、统一股票订单列表时间格式 +------------------------------------------------------------------------------------------------------------------------------------------------ + +p2|p7正式环境,股票交易服务更新 +服务部署完毕(2024-07-01) +此次更新功能包含: +1、更新通过配置股票插针进行后台交易 +------------------------------------------------------------------------------------------------------------------------------------------------ + +p8正式环境,数字币交易服务更新 +服务部署完毕(2024-07-02) +此次更新功能包含: +1、部署合约、现货、秒合约交易服务 +2、线上访问地址 + 1>交易订单域名: https://trade.chdh.me + 2>交易订单Wss: wss://trade.chdh.me +3、相关API文档参见 + 1>http://103.71.254.42:3001/project/56/interface/api/cat_519 + 2>http://103.71.254.42:3001/project/56/interface/api/cat_74 + 3>http://103.71.254.42:3001/project/56/interface/api/cat_81 +------------------------------------------------------------------------------------------------------------------------------------------------ + +关于插针改动(p2\p6\p7): +1、前端/后端-(订单wss订阅|浮动盈亏wss订阅|市场总金额浮动盈亏wss订阅)取值为:实时价、闭盘价(优先级:实时价 > 闭盘价) +2、盘中插针(优先级:插针价 > 实时价) + 1>设置时:交易开仓取值为:插针价 + 2>未设置时:交易开仓取值为:实时价 +3、盘前|盘后插针 (在设置容许下单的前提下:例如调整开盘时间等) + 1>设置时:交易开仓取值为:插针价 +------------------------------------------------------------------------------------------------------------------------------------------------ + +测试环境,股票交易统计服务更新 +服务部署完毕(2024-07-10) +此次更新功能包含: +1、新增股票各个市场订阅统计服务(包含:用户市场总资产、用户市场总可用余额、用户市场冻结、用户市场累计盈亏、用户市场总手续费、用户市场总浮动盈亏) +2、相关API文档参见 + 1>http://103.71.254.42:3001/project/56/interface/api/cat_88 +------------------------------------------------------------------------------------------------------------------------------------------------ + +FB +应用编号:489884953731337 +应用密钥:77fcf7fe8f8b1ba26ad622b537e321c9 +gg +客户端ID: 220504529176-bl8cfsr1dktbebl1qo1km6mu02lfdjaa.apps.googleusercontent.com +客户端密钥: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 +------------------------------------------------------------------------------------------------------------------------------------------------ + +p6正式环境,股票交易服务更新 +服务部署完毕(2024-08-02) +此次更新功能包含: +1、新增股票市场IPO欠款功能 +------------------------------------------------------------------------------------------------------------------------------------------------ + +正式环境,p9股票交易服务更新 +服务部署完毕(2024-08-13) +此次更新功能包含: +1、部署美股、泰股、巴西股交易服务 +2、线上访问地址 + 1>交易订单域名: https://trade.wedbushig.com + 2>交易订单Wss: wss://trade.wedbushig.com +3、相关API文档参见 + 1>订单api:http://103.71.254.42:3001/project/56/interface/api/cat_808 + 2>订单订阅:http://103.71.254.42:3001/project/56/interface/api/4543 + 3>管理员|浮动盈亏订阅:http://103.71.254.42:3001/project/56/interface/api/519 +------------------------------------------------------------------------------------------------------------------------------------------------ + +正式|测试(美股\德股\法股\英股)环境,p6股票交易服务更新 +服务部署完毕(2024-08-14) +此次更新功能包含: +1、修改股票交易杠杆取值逻辑 +------------------------------------------------------------------------------------------------------------------------------------------------ + +目前杠杆开启的条件: +1、申请杠杆(开启杠杆) +2、是否满足设置杠杆的最小购买量 +3、设置杠杆倍数是否满足区间范围(最小~最大) + +杠杆优先级: +1、设置默认杠杆 +2、设置单个用户杠杆 +3、优先级:设置单个用户杠杆 > 默认值 +------------------------------------------------------------------------------------------------------------------------------------------------ + +p9测试环境,数字币交易服务更新 +服务部署完毕(2024-09-19) +此次更新功能包含: +1、部署合约、现货、秒合约交易服务 +2、交易访问地址 + 1>交易订单域名: https://trade.jdtest88.com + 2>交易订单Wss: wss://trade.jdtest88.com +3、行情访问地址 + 1>行情订单域名: https://quotes.jdtest88.com + 2>行情订单Wss: wss://quotes.jdtest88.com + +------------------------------------------------------------------------------------------------------------------------------------------------ \ No newline at end of file diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..c647359 --- /dev/null +++ b/api/api.go @@ -0,0 +1,357 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "net/http" + "wss-pool/pkg/processor" +) + +// 处理跨域 +func Core() gin.HandlerFunc { + return func(c *gin.Context) { + method := c.Request.Method + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token,Authorization,Token") + c.Header("Access-Control-Allow-Methods", "POST,GET,OPTIONS") + c.Header("Access-Control-Expose-Headers", "Content-Length,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Content-Type") + c.Header("Access-Control-Allow-Credentials", "True") + // Release Index Options + if method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + } + // Process Request + c.Next() + } +} + +// 数据API服务路由 +func RouterApiServer(project string) *gin.Engine { + routers := gin.Default() + routers.Use(Core()) + router := routers.Group("/") + // excel 导出 + router.GET("/spots/excel", processor.SymbolToExcel) + router.GET("/spots/excel/forex", processor.ExcelToForexCode) + router.GET("/spots/excel/japan", processor.ExcelToSymbolByJapanJson) + router.GET("/spots/excel/japanJson", processor.ExcelToSymbolByJapan) + // 现货数据API服务 + router.GET("/main/list", processor.MainSpotList) + router.GET("/main/free-list", processor.MainFreeSpotList) + router.GET("/spots/kline", processor.SpotsKline) + router.GET("/spots/merged", processor.SpotsMerged) + router.GET("/spots/tickers", processor.SpotsTickers) + router.GET("/spots/trade", processor.SpotsTrade) + router.GET("/spots/intro", processor.IntroList) + router.GET("/spots/img/visit", processor.Visit) + router.GET("/spots/detail", processor.SpotsDetail) + router.GET("/spots/history/trade", processor.SpotsHistoryTrade) + router.GET("/spots/depth", processor.SpotsDepth) + router.GET("/spots/merged/list", processor.SpotsMergedList) + router.GET("/spots/index/list", processor.ExchangeSymbolIndexList) + router.GET("/spots/index/info", processor.StockIndexInfo) + router.GET("/spots/index/kline/list", processor.StockIndexKLineList) + // 合约数据API服务 + router.GET("/contract/bbo", processor.ContractBbo) + router.GET("/contract/history/kline", processor.ContractHistoryKline) + router.GET("/contract/history/price_kline", processor.ContractHistoryPriceKline) + router.GET("/contract/batch_merged", processor.ContractBatchMerged) + router.GET("/contract/trade", processor.ContractTrade) + router.GET("/contract/swap_his_open_interest", processor.ContractsWapHisOpenInterest) + router.GET("/contract/history/linear_swap_premium_index_kline", processor.ContractHistoryLinearSwapPremiumIndexKline) + router.GET("/contract/history/linear_swap_estimated_rate_kline", processor.ContractHistoryLinearSwapEstimatedRateKline) + router.GET("/contract/history/linear_swap_basis", processor.ContractHistoryLinearSwapBasis) + router.GET("/contract/merged/list", processor.ContractMergedList) + router.GET("/contract/merged", processor.ContractMerged) + router.GET("/contract/depth", processor.ContractDepth) + router.GET("/contract/history/trade", processor.ContractHistoryTrade) + router.POST("/encryption/spots/news/add", processor.StockNewAdd) + // 股票基本信息查询 + router.GET("share/fundamentals", processor.Fundamentals) + router.GET("share/fundamentals_new", processor.FundamentalsNew) + router.GET("share/eod", processor.Eod) + router.GET("share/get-list-optional-stock", processor.FindShareBySymbol) + router.GET("share/intradiscal", processor.IntraDisCal) + router.GET("share/exchange-symbol-list", processor.ExchangeSymbolList) + router.GET("share/exchange-free-symbol-list", processor.ExchangeFreeSymbolList) + router.GET("share/intraday", processor.Intraday) + router.GET("/spots/news/list", processor.StockNewsList) + router.GET("/spots/kline/list", processor.StockKLineList) + router.GET("/spots/southAsia/info", processor.StockSouthAsiaInfo) + router.GET("/spots/us/info", processor.StockUsInfo) + router.GET("/spots/kline/us/list", processor.StockKLineUsList) + router.GET("/spots/ticker_to_excel", processor.TickerToExcel) + router.POST("/spots/update/img", processor.UpdateImg) + router.POST("/spots/update/keep", processor.UpdateKeepDecimal) + router.POST("/spots/list/new/add", processor.StockListAddToPHP) + router.POST("/spots/php/update", processor.StockListUpdateToPHP) + router.POST("/spots/index/list/new/add", processor.StockIndexListUpdateToPHP) + // 美股股票静态数据查询 + router.GET("/market/grouped", processor.Grouped) + router.GET("/market/trades", processor.Trades) + router.GET("/market/last-trade", processor.LastTrade) + router.GET("/market/quotes", processor.Quotes) + router.GET("/market/last-quote", processor.LastQuote) + router.GET("/market/snapshot-all-tickers", processor.SnapshotAllTickers) + router.GET("/market/snapshot-gainers-losers", processor.SnapshotGainersLosers) + router.GET("/market/snapshot-one-ticker", processor.SnapshotOneTicker) + router.GET("/market/reference-ticker", processor.ReferenceTicker) + router.GET("/market/contract-price-kline", processor.ContractPriceKLineList) + router.GET("/market/history-us", processor.HistoryUsList) + router.GET("/market/inquiry/price", processor.InquiryPrice) + router.GET("/market/spot-kline", processor.SpotKLineList) + router.GET("/market/contract-kline", processor.ContractKLineList) + router.GET("/market/reference-ticker-details", processor.ReferenceTickerDetails) + router.GET("/market/aggregates", processor.Aggregates) + router.GET("/market/open-close", processor.OpenClose) + router.GET("/market/previous-close", processor.PreviousClose) + router.GET("/market/reference-ticker-news", processor.ReferenceTickerNews) + router.POST("/market/msg", processor.MsgSend) + router.POST("/market/mobilelogin", processor.MobileLogin) + router.POST("/market/phonenumberbypassword", processor.PhoneNumberByPassWord) + router.POST("/market/registration", processor.Registration) + router.POST("/market/forgetpasswore", processor.ForgetPassWore) + router.POST("/market/setphonenumber", processor.SetPhoneNumber) + // 期权数据API服务 + router.GET("/option/list", processor.ExchangeOptionList) + router.GET("/option/info", processor.OptionInfo) + router.GET("/option/php/list", processor.OptionPHPList) + router.GET("/option/excel", processor.OptionToExcel) + // 外汇数据API服务 + router.GET("/forex/kline", processor.ForexAggregates) + router.GET("/forex/tickers/list", processor.ForexAllTickers) + router.GET("/forex/ticker", processor.ForexTicker) + router.GET("/forex/previous_close", processor.ForexPreviousClose) + router.GET("/forex/grouped_daily", processor.ForexGroupedDaily) + router.GET("/forex/ticker_search_list", processor.ForexSymbolList) + router.GET("/forex/ticker_free_list", processor.ForexFreeSymbolList) + router.GET("/forex/quotes_bbo", processor.ForexQuotesBBO) + router.GET("/forex/last_quote_bbo", processor.ForexLastQuote) + router.GET("/forex/real_time_currency", processor.ForexRealTimeCurrency) + // 新版外汇API服务 + router.GET("/forex/ticker_new_search_list", processor.ForexSymbolListNew) + router.GET("/forex/ticker_new_free_list", processor.ForexFreeSymbolListNew) + router.GET("/forex/kline_history", processor.ForexAggregatesNewGet) + router.GET("/forex/trade_tick_list", processor.ForexTradeList) + router.POST("/forex/depth_tick", processor.ForexAggregatesDepthTick) + router.POST("/forex/trade_tick", processor.ForexAggregatesTradeTick) + router.POST("/forex/kline_new", processor.ForexAggregatesNewPost) + router.POST("/forex/spots/news/add", processor.StockNewAdd) + + return routers +} + +// 指数股票API服务 +func RouterStockIndexApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/stockIndex") + { + group.POST("/new/add", processor.StockIndexInfoAdd) + group.POST("/list/add", processor.StockIndexListAdd) + group.POST("/info/add", processor.StockIndexInfoMon) + group.POST("/list/update", processor.StockIndexListUpdate) + group.GET("/list/get", processor.StockIndexListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 印度期权股票API服务 +func RouterIndiaOptionApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/indiaOption") + { + group.POST("/info/add", processor.OptionInfoAdd) + group.POST("/list/add", processor.OptionListAdd) + } + return router +} + +// 美股股票API服务 +func RouterUSApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/us") + { + group.POST("/message/add", processor.UsMessage) + } + return router +} + +// 印尼股票API服务 +func RouterIndonesiaApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/indonesia") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 泰国股票API服务 +func RouterThailandApiServer() *gin.Engine { + router := gin.Default() + + router.Use(Core()) + group := router.Group("/thailand") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 印度股票API服务 +func RouterIndiaApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/india") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 马来西亚股票API服务 +func RouterMalaysiaApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/malaysia") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 新加坡股票API服务 +func RouterSingaporeApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/singapore") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 港股股票API服务 +func RouterHongKongApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/hongkong") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 英国股票API服务 +func RouterUKApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/uk") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 法国股票API服务 +func RouterFranceApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/france") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 德国股票API服务 +func RouterGermanyApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/germany") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 巴西股票API服务 +func RouterBrazilApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/brazil") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} + +// 日本股票API服务 +func RouterJapanApiServer() *gin.Engine { + router := gin.Default() + router.Use(Core()) + group := router.Group("/japan") + { + group.POST("/spots/new/add", processor.StockInfoAdd) + group.POST("/spots/list/add", processor.StockListAdd) + group.POST("/spots/info/add", processor.StockInfoMon) + group.POST("/spots/list/update", processor.StockListUpdate) + group.GET("/spots/list/get", processor.StockListGet) + group.POST("/spots/news/add", processor.StockNewAdd) + } + return router +} diff --git a/cmd/closingMarket/closingMarket.go b/cmd/closingMarket/closingMarket.go new file mode 100644 index 0000000..0ea95c9 --- /dev/null +++ b/cmd/closingMarket/closingMarket.go @@ -0,0 +1,521 @@ +package closingMarket + +import ( + "encoding/json" + "fmt" + "github.com/gorilla/websocket" + "github.com/satori/go.uuid" + "github.com/shopspring/decimal" + "go.uber.org/zap" + "log" + "math" + "net/http" + "strconv" + "strings" + "sync" + "time" + "wss-pool/cmd/common" + "wss-pool/dictionary" + "wss-pool/internal/data/business" + "wss-pool/internal/model" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + models "wss-pool/pkg/model" +) + +// Define a websocket connection object that contains information for each connection +type Client struct { + Id string // Client ID + conn *websocket.Conn // Define websocket link objects + msg chan []byte // Define messages received and distributed + symbol sync.Map // Concurrent Security - Manage User Subscription Types + mux sync.Mutex +} + +type StockMessage struct { + S string `json:"s,omitempty"` // 股票代码 + Country string `json:"country"` //国家 + StockCode string `json:"stock_code" bson:"stock_code"` // 股票代码 + Symbol string `json:"symbol"` + Stock string `json:"stock"` // 期权代码 + IsStockIndex bool `json:"is_stock_index"` + IsOptionList bool `json:"is_option_list"` + IsOptionInfo bool `json:"is_option_info"` +} + +var ( + wsStockConMap = map[string][]*websocket.Conn{} + mutexStock = sync.RWMutex{} + msgStockChan = make(chan []byte) + mutexConn = sync.RWMutex{} + TotalNum int + mutexTotal = sync.RWMutex{} + countryMap = make(map[string][]string) + mutexCountry = sync.RWMutex{} + clearClientChan = make(chan *websocket.Conn) + pinStockMap = make(map[string]bool) + mutexPinMap = sync.RWMutex{} +) + +const ( + stockConnNum int = 20 + writeWait = 10 * time.Second +) + +// Define an UpGrader to upgrade a regular HTTP connection to a websocket connection +var upServer = &websocket.Upgrader{ + // Define read/write buffer size + WriteBufferSize: 1024, + ReadBufferSize: 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 != "/quotes-pin-wss" { + fmt.Println("Request path error") + return false + } + token := r.URL.Query().Get("token") + if !common.CheckToken(token) { + applogger.Debug("token expired") + return false + } + // Verification rules can also be customized according to other needs + return true + }, +} + +// ShareConnect +func ShareConnect(host, addr string) { + go writeShare() + go offLineStock() + go pinStock() + http.HandleFunc("/quotes-pin-wss", wsHandleShare) + url := fmt.Sprintf("%v%v", host, addr) + + err := http.ListenAndServe(url, nil) + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} + +// 读取插针数据 +func pinStock() { + for { + stockPinData() + time.Sleep(20 * time.Second) + } +} + +func getPinMap(symbol string) bool { + mutexStock.RLock() + defer mutexStock.RUnlock() + return pinStockMap[symbol] + +} +func setPinMap(symbol string) { + mutexPinMap.Lock() + defer mutexPinMap.Unlock() + pinStockMap[symbol] = true + applogger.Info("set pinMap", symbol) +} + +func initPinMap() { + mutexPinMap.Lock() + defer mutexPinMap.Unlock() + pinStockMap = make(map[string]bool) +} + +// 针对资产插针 +func stockPinData() { + //清理上一次数据 + initPinMap() + for k, v := range business.StockClosedDataList { + hashListName := fmt.Sprintf("STOCK_PRICES:%d", v) + keys := red.Scan(hashListName) + for _, key := range keys { + res, _ := red.HGetAll(key) + status, _ := strconv.Atoi(res["status"]) + code := res["stock_code"] + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), res) + if status != business.StockStatusOn { + continue + } + symbol := fmt.Sprintf("%s.%s", code, k) + setPinMap(symbol) + mutexStock.RLock() + _, ok := wsStockConMap[symbol] + mutexStock.RUnlock() + //没有订阅没必要推送 + if !ok { + applogger.Info("No subscription ", symbol) + continue + } + if k == "US" { + UsPinStock(code, res["price"], k) + } else { + stockCode := common.GetOldCode(code) + SouthAsiaPinSpot(code, stockCode, res["price"], k) + } + } + } + +} + +func SouthAsiaPinSpot(symbol, stockCode, price, country string) { + prices, _ := strconv.ParseFloat(price, 64) + param := models.StockParam{ + Symbol: symbol, + StockCode: stockCode, + StockName: "", + Price: prices, + UpDownRate: decimal.NewFromInt(0), + UpDown: decimal.NewFromInt(0), + TradeV: decimal.NewFromInt(0), + TradeK: "买入", + Country: strings.ToLower(country), + Ts: time.Now().UnixMilli(), + ClosingMarket: true, + } + param.Token = "" + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return + } + msgStockChan <- msgStr +} + +func UsPinStock(code, price, country string) { + message := &models.ClientMessage{ + S: code, // 股票代码 + C: []decimal.Decimal{decimal.NewFromInt(0), decimal.NewFromFloat(1)}, // 条件,有关更多信息,请参阅贸易条件术语表 + V: common.CalculateContractPrices(decimal.NewFromInt(int64(100)), float64(0.02), 0, 1)[0].IntPart(), // 交易量,代表在相应时间戳处交易的股票数量 -- 报价交易量 + Dp: true, // 暗池真/假 + Ms: "open", // 市场状态,指示股票市场的当前状态(“开盘”、“收盘”、“延长交易时间”) + T: time.Now().UnixMilli(), // 以毫秒为单位的时间戳 -- 此聚合窗口的结束时钟周期的时间戳(以 Unix 毫秒为单位) + Cl: decimal.RequireFromString(price), // 此聚合窗口的收盘价 + A: decimal.NewFromInt(11), // 今天的成交量加权平均价格 + Se: time.Now().UnixMilli(), + H: decimal.RequireFromString(price), // 此聚合窗口的最高逐笔报价 + L: decimal.RequireFromString(price), // 此聚合窗口的最低价格变动价格 + Op: decimal.RequireFromString(price), // 今天正式开盘价格 + // P: decimal.RequireFromString(price), + ClosingMarket: true, + } + msgStr, err := json.Marshal(message) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return + } + msgStockChan <- msgStr +} + +func wsHandleShare(w http.ResponseWriter, r *http.Request) { + // Obtain a link through the upgraded upgrade tool + conn, err := upServer.Upgrade(w, r, nil) + if err != nil { + applogger.Info("Failed to obtain connection:%v", err) + return + } + // Register users after successful connection + client := &Client{ + Id: uuid.NewV4().String(), + conn: conn, + msg: make(chan []byte), + symbol: sync.Map{}, + mux: sync.Mutex{}, + } + readShare(client) +} + +func setTotalNum(num int) { + mutexTotal.Lock() + defer mutexTotal.Unlock() + TotalNum += num +} + +func getTotalNum() { + mutexTotal.RLock() + defer mutexTotal.RUnlock() + applogger.Debug("number of colleagues online :%v", TotalNum) +} + +// Read the message sent by the client and process the return response +func readShare(cl *Client) { + defer cl.conn.Close() + setTotalNum(1) + getTotalNum() + for { + _, msg, err := cl.conn.ReadMessage() + if err != nil { + clearClientChan <- cl.conn + applogger.Debug("user exit:%v", cl.conn.RemoteAddr().String()) + return + } + // Process business logic + psgMsg := model.SubMessage(string(msg)) + if psgMsg != nil { + switch psgMsg.Type { + case "ping": // Receiving ping + aloneSendStock(cl.conn, []byte(model.ReturnValue("pong"))) + case "subscribe": // Receive subscription + country, stock := getCountry(psgMsg.Symbol) + //applogger.Info(country, stock) + if !dictionary.StockCountryMap[country] { + applogger.Error(country, "incorrect subscription information 不属于合规的股票市场") + aloneSendStock(cl.conn, []byte(model.ReturnValue("incorrect subscription information"))) + clearClientChan <- cl.conn + return + } + aloneSendStock(cl.conn, []byte(model.ReturnValue("subscribe success"))) + //获取 服务是否 订阅该数据 + mutexStock.RLock() + conns, ok := wsStockConMap[psgMsg.Symbol] + mutexStock.RUnlock() + if ok { + //查询client是否订阅 + conns = checkClient(conns, cl.conn) + } else { + //添加订阅 + conns = make([]*websocket.Conn, 0) + conns = append(conns, cl.conn) + } + //applogger.Info("psgMsg.Symbol", psgMsg.Symbol) + mutexStock.Lock() + wsStockConMap[psgMsg.Symbol] = conns + mutexStock.Unlock() + if !ok { + go cl.userPSubscribeUs(country, stock) + } + case "unSubscribe": // Receive unsubscribe + applogger.Info("Received unsubscribe message body:", string(msg)) + mutexStock.RLock() + conns, ok := wsStockConMap[psgMsg.Symbol] + mutexStock.RUnlock() + if ok { + //取消订阅 + removeWsconnStock(conns, cl.conn, psgMsg.Symbol) + } + aloneSendStock(cl.conn, []byte(model.ReturnValue("unSubscribe success"))) + applogger.Debug("Subscription type after current user deletion:%v", ok) + default: + // TODO: Handling other situations transmitted by customers + applogger.Debug("Please provide accurate instructions......") + } + } + } +} + +// 废弃 客户端 +func offLineStock() { + for userConn := range clearClientChan { + setTotalNum(-1) + getTotalNum() + for key, v := range wsStockConMap { + removeWsconnStock(v, userConn, key) + } + } +} + +// send message +func aloneSendStock(conn *websocket.Conn, message []byte) error { + mutexConn.Lock() + defer mutexConn.Unlock() + //applogger.Debug("aloneSendStock :%v",conn,message) + conn.SetWriteDeadline(time.Now().Add(writeWait)) + w, err := conn.NextWriter(websocket.TextMessage) // Write data in the form of io, with parameters of data type + if err != nil { + //发送失败 + applogger.Error("Failed to conn.NextWriter :%v", err) + return err + } + if _, err := w.Write(message); err != nil { // Write data, this function is truly used to transmit data to the foreground + applogger.Error("Failed Write message :%v", err) + return err + } + if err := w.Close(); err != nil { // Close write stream + applogger.Error("Failed to close write stream:%v", err) + return nil + } + return nil +} + +// 清理客户端 +func removeWsconnStock(conn []*websocket.Conn, userConn *websocket.Conn, symbol string) error { + index := -1 + for i, v := range conn { + if v == userConn { + index = i + break + } + } + if index >= 0 { + conn = append(conn[:index], conn[index+1:]...) + mutexStock.Lock() + wsStockConMap[symbol] = conn + mutexStock.Unlock() + } + return nil +} + +func deleteWsconnStock(symbol string) { + mutexStock.Lock() + defer mutexStock.Unlock() + delete(wsStockConMap, symbol) +} + +// 广播 +func broadcastWebSocketStock(msg []byte, symbol string) { + mutexStock.RLock() + conns, ok := wsStockConMap[symbol] + mutexStock.RUnlock() + if !ok { + applogger.Error("Parsing data information:%v") + return + } + total := len(conns) + if total <= 0 { + return + } + start := time.Now() + connDiv := int(math.Ceil(float64(total) / float64(stockConnNum))) + //分批并发推送 + for i := 0; i < connDiv; i++ { + startIndex := i * stockConnNum + endIndex := (i + 1) * stockConnNum + if endIndex > total { + endIndex = total + } + wg := sync.WaitGroup{} + for _, val := range conns[startIndex:endIndex] { + wg.Add(1) + go func(val *websocket.Conn, msg []byte) { + defer wg.Done() + aloneSendStock(val, msg) + }(val, msg) + } + wg.Wait() + } + applogger.Info("broadcast WebSocket info : %v ;total:%v;time-consuming %v ", symbol, total, time.Since(start)) +} + +func writeShare() { + for message := range msgStockChan { + var subMsg StockMessage + if err := json.Unmarshal(message, &subMsg); err != nil { + applogger.Error(err.Error()) + } + if subMsg.S == "" { + if subMsg.IsStockIndex { //指数 + broadcastWebSocketStock(message, fmt.Sprintf("%s.%s", subMsg.StockCode, common.StockIndexPrefix)) + } else if subMsg.IsOptionList { //期权列表 + broadcastWebSocketStock(message, fmt.Sprintf("%s.%s", subMsg.Stock, fmt.Sprintf("%s%s%s", common.StockOption, common.CapitalizeFirstLetter(subMsg.Country), common.StockOptionList))) + } else if subMsg.IsOptionInfo { //期权详情 + broadcastWebSocketStock(message, fmt.Sprintf("%s.%s", subMsg.Stock, fmt.Sprintf("%s%s%s", common.StockOption, common.CapitalizeFirstLetter(subMsg.Country), common.StockOptionInfo))) + } else { //tradingview + broadcastWebSocketStock(message, fmt.Sprintf("%s.%s", subMsg.Symbol, common.CapitalizeFirstLetter(subMsg.Country))) + } + //applogger.Info("broadcast WebSocket Sub message %v info:%v", subMsg.Country, subMsg.StockCode) + continue + } + //美股 + broadcastWebSocketStock(message, fmt.Sprintf("%s.US", subMsg.S)) + //applogger.Info("broadcast WebSocket Sub message US info:%v", subMsg.S) + } +} + +func checkClient(conns []*websocket.Conn, conn *websocket.Conn) []*websocket.Conn { + for _, v := range conns { + if v == conn { + return conns + } + } + conns = append(conns, conn) + return conns +} + +func getCountry(symbol string) (string, string) { + symbolArr := strings.Split(symbol, ".") + if len(symbolArr) < 2 { + applogger.Error("symbol 有误") + return "", "" + } + county := symbolArr[len(symbolArr)-1] + return county, symbol[0 : strings.Index(symbol, county)-1] +} + +// 按市场订阅 +func (cl *Client) userPSubscribeUs(country, symbol string) { + mutexCountry.RLock() + symbols, ok := countryMap[country] + mutexCountry.RUnlock() + //提加 + mutexCountry.Lock() + countryMap[country] = append(symbols, symbol) + mutexCountry.Unlock() + if ok { + //applogger.Error(country, "已订阅") + return + } + applogger.Debug(country, "start a stock subscription") + pubSub := red.RedisClient.PSubscribe(fmt.Sprintf("*.%s", country)) + defer func() { + pubSub.Close() + }() + + _, err := pubSub.Receive() + if err != nil { + applogger.Error("failed to receive from control PubSub,%v", zap.Error(err)) + return + } + ch := pubSub.Channel() + for msg := range ch { + mutexStock.RLock() + conns, ok := wsStockConMap[msg.Channel] + mutexStock.RUnlock() + //未订阅股票跳出 + if !ok { + continue + } + // TODO: 是否还有客户端 + if len(conns) > 0 { + //是否有插针 必须放在下面判断,不然会被误判 客户端 断开链接 + if !getPinMap(msg.Channel) { + msgStockChan <- []byte(msg.Payload) + } + } else { + deleteWsconnStock(msg.Channel) + mutexCountry.RLock() + symbols := countryMap[country] + mutexCountry.RUnlock() + index := -1 + msgChannel := strings.Split(msg.Channel, ".") + for i, v := range symbols { + if v == msgChannel[0] { + index = i + break + } + } + if index >= 0 { + symbols = append(symbols[:index], symbols[index+1:]...) + mutexCountry.Lock() + countryMap[country] = symbols + mutexCountry.Unlock() + } + // 退订 + if len(symbols) <= 0 { + mutexCountry.Lock() + delete(countryMap, country) + mutexCountry.Unlock() + applogger.Debug("Starting unsubscribe.....", country) + pubSub.PUnsubscribe(fmt.Sprintf("*.%s", country)) + return + } + } + } +} diff --git a/cmd/common/base.go b/cmd/common/base.go new file mode 100644 index 0000000..65ed90e --- /dev/null +++ b/cmd/common/base.go @@ -0,0 +1,223 @@ +package common + +import ( + "fmt" + "github.com/gin-gonic/gin" + "net/http" + "regexp" + "strconv" + "strings" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/internal/redis" + "wss-pool/logging/applogger" +) + +const ( + StockProject string = "stock" + CoinProject string = "coin" + StockOn string = "1" + StockIndexOn int = 1 + StockIndexPrefix string = "StockIndex" + StockOption string = "Option" + StockOptionList string = "List" + StockOptionInfo string = "Info" +) + +var CountryStartTime = map[string]int64{ + "India": 42300000, + "Thailand": 39000000, + "Indonesia": 36000000, + "Malaysia": 32400000, + "Singapore": 32400000, + "UK": 54000000, + "France": 54000000, + "Germany": 54000000, + "Brazil": 75600000, + "Japan": 42300000, +} + +var StockToPHPMap = map[string]string{ + "US": "US", + "Thailand": "THA", + "India": "IN", + "Indonesia": "IDN", + "Malaysia": "MYS", + "Singapore": "SGD", + "HongKong": "HKD", + "UK": "UK", + "Germany": "EUR", + "France": "FUR", + "Brazil": "BR", + "Japan": "JP", +} + +var path = map[string]bool{ + "/spots/update/keep": true, + "/stock/spots/update/keep": true, + "/stock/spots/list/new/add": true, + "/spots/list/new/add": true, + "/spots/news/add": true, + "/spots/index/list/new/add": true, + "/stock/spots/index/list/new/add": true, + "/option/php/list": true, + "/stock/option/php/list": true, + "/spots/php/update": true, + "/stock/spots/php/update": true, +} + +type Response struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +// JWTAuthMiddleware 基于JWT的认证中间件 +func JWTAuthMiddleware() func(c *gin.Context) { + return func(c *gin.Context) { + if path[c.FullPath()] { + c.Next() + return + } + token := strings.TrimSpace(c.Request.Header.Get("token")) + if token == "" { + c.Abort() + JsonResult(401, "can't find the token", "", c) + return + } + //applogger.Debug("鉴权Token用户:%v", fmt.Sprintf("TOKEN:USER:%s", token)) + userId, _ := redis.Get_Cache_Data(fmt.Sprintf("TOKEN:USER:%s", token)) + if userId == "" || userId == "0" { + c.Abort() + JsonResult(401, "token expired", "", c) + return + } + } +} + +func CheckToken(token string) bool { + if token == "" { + return false + } + userId, _ := redis.Get_Cache_Data(fmt.Sprintf("TOKEN:USER:%s", token)) + if userId == "" || userId == "0" { + return false + } + return true +} + +func JsonResult(code int, msg string, data interface{}, c *gin.Context) { + // 开始时间 + c.JSON(http.StatusOK, Response{ + code, + msg, + data, + }) +} + +func IsLetter(str string) bool { + reg := regexp.MustCompile(`^[a-zA-Z0-9&-]+$`) + return reg.MatchString(str) +} + +func DelRes(hashListName, str string) { + redis.RedisClient = redis.RedisInit(config.Config.Redis.DbEleven) + keys, _ := redis.HGetAll(hashListName) + for key, _ := range keys { + if strings.Contains(key, str) { + redis.HDel(hashListName, key) + fmt.Println(key) + } + } + //if len(k)> 0 { + // redis.HDel(hashListName, strings.Join(k, ",")) + // k = make([]string,0) + //} +} + +func IsExistStock(country, code string) bool { + key := fmt.Sprintf("%s:STOCK:LIST:%s", StockToPHPMap[country], code) + status, _ := redis.Hget(key, "status") + if status == StockOn { + return true + } + return false +} + +func IsExistStockNew(country, code string, redisIp string) bool { + red := redis.RedisClientMap[redisIp] + key := fmt.Sprintf("%s:STOCK:LIST:%s", StockToPHPMap[country], code) + status, _ := redis.HGetNew(key, "status", red) + if status == StockOn { + return true + } + return false +} + +func IsExistOption(country, code string) (bool, float64) { + key := fmt.Sprintf("%s:OPTION:LIST:%s", StockToPHPMap[country], code) + status, _ := redis.Hget(key, "status") + //fmt.Println(country,code,status) + if status == StockOn { + rate, _ := redis.Hget(key, "rate") + rateFloat, _ := strconv.ParseFloat(rate, 64) + return true, rateFloat + } + return false, 0 +} + +func TgBotSendMsg(msg string) { + if _, err := internal.HttpPost(config.Config.TgBot.URL, fmt.Sprintf(`{"text":"%s","chat_id":%d}`, msg, config.Config.TgBot.ChatId)); err != nil { + applogger.Error("TgBotSendMsg", err) + } +} + +func UPdateAll(country string) { + data.Mgo_init(config.Config.Mongodb) + var per = []string{ + "1day", + "1week", + "1mon", + } + for _, v := range per { + UPdateTime(1696089600000, 1700796517000, country, v) + } +} + +func UPdateTime(from, to int64, country, period string) { + //filter := bson.M{"timestamp": bson.M{"$gte": from, "$lte": to}} + //tableName := data.GetStockSouthAsiaTableName(country, period) + //res := make([]model.StockMogoParam, 0) + //projection := bson.M{"symbol": 1, "stock_code": 1, "country": 1, "timestamp": 1} + //sort := bson.M{"timestamp": 1} + //data.MgoFindProjectionRes(tableName, filter, projection, sort, &res, 0) + //if len(res) <= 0 { + // applogger.Error(" no data", period) + // return + //} + //var key = make(map[int64]bool) + //for _, v := range res { + // utcTime := time.Unix(v.Ts/1000, 0) + // location, _ := time.LoadLocation("Asia/Singapore") + // t := utcTime.In(location) + // if t.Hour() != 0 { + // continue + // } + // l := len(key) + // key[v.Ts] = true + // if l == len(key){ + // continue + // } + // fmt.Println(v.Ts) + // filter = bson.M{"timestamp": bson.M{"$eq": v.Ts}} + // update := bson.D{{"$set", bson.D{ + // {"timestamp", v.Ts + CountryStartTime[country]}, + // }}} + // applogger.Info("GetTimeNewPriceAll info: %v %v", update,v) + // fmt.Println(data.GetStockSouthAsiaTableName(country, period)) + // if err := data.MgoUpdateMany(data.GetStockSouthAsiaTableName(country, period), filter,update); err != nil { + // applogger.Error("stock MgoInsertMany err:%v", err) + // } + //} +} diff --git a/cmd/common/common.go b/cmd/common/common.go new file mode 100644 index 0000000..70b64f5 --- /dev/null +++ b/cmd/common/common.go @@ -0,0 +1,1122 @@ +package common + +import ( + "bytes" + "compress/zlib" + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/shopspring/decimal" + "io" + "io/ioutil" + "math/rand" + "net/http" + "strconv" + "strings" + "time" + "wss-pool/pkg/model" +) + +const ( + Rregion string = "ap-southeast-1" + AwsAccessKeyId string = "AKIAUKMLSNHYAP7EOBDE" + AwsSecretAccessKey string = "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" + Endpoint string = "s3.ap-southeast-1.amazonaws.com" + Bucket string = "workerawsbucket" // 替换为你的S3桶名称 + Path string = "path/stock/" //路径 +) + +var TradingDayOff = map[string]map[string]bool{ + "India": map[string]bool{ + "2024-01-26": true, + "2024-03-08": true, + "2024-03-25": true, + "2024-03-29": true, + "2024-04-11": true, + "2024-04-17": true, + "2024-05-01": true, + "2024-06-17": true, + "2024-07-17": true, + "2024-08-15": true, + "2024-10-02": true, + "2024-11-01": true, + "2024-11-15": true, + "2024-12-25": true, + }, + "UK": map[string]bool{ + "2024-08-26": true, + "2024-12-25": true, + "2024-12-26": true, + }, + "Thailand": map[string]bool{ + "2024-01-01": true, + "2024-02-26": true, + "2024-04-08": true, + "2024-04-15": true, + "2024-04-16": true, + "2024-05-01": true, + "2024-05-06": true, + "2024-05-22": true, + "2024-06-03": true, + "2024-07-22": true, + "2024-07-29": true, + "2024-08-12": true, + "2024-10-14": true, + "2024-10-23": true, + "2024-12-05": true, + "2024-12-10": true, + "2024-12-31": true, + }, + "Indonesia": map[string]bool{ + "2024-01-01": true, + "2024-02-08": true, + "2024-02-09": true, + "2024-03-11": true, + "2024-03-12": true, + "2024-03-29": true, + "2024-04-08": true, + "2024-04-09": true, + "2024-04-10": true, + "2024-04-11": true, + "2024-04-12": true, + "2024-04-15": true, + "2024-05-01": true, + "2024-05-09": true, + "2024-05-10": true, + "2024-05-23": true, + "2024-05-24": true, + "2024-06-17": true, + "2024-06-18": true, + "2024-09-16": true, + "2024-12-25": true, + "2024-12-26": true, + }, + "Malaysia": map[string]bool{ + "2024-01-01": true, + "2024-01-25": true, + "2024-02-01": true, + "2024-02-12": true, + "2024-03-28": true, + "2024-04-10": true, + "2024-04-11": true, + "2024-05-01": true, + "2024-05-22": true, + "2024-06-03": true, + "2024-06-17": true, + "2024-07-08": true, + "2024-09-16": true, + "2024-09-17": true, + "2024-10-31": true, + "2024-12-25": true, + }, + "Singapore": map[string]bool{ + "2024-01-01": true, + "2024-02-10": true, + "2024-02-11": true, + "2024-02-12": true, + "2024-03-29": true, + "2024-04-10": true, + "2024-05-01": true, + "2024-05-22": true, + "2024-06-17": true, + "2024-08-09": true, + "2024-10-31": true, + "2024-12-25": true, + }, + "HongKong": map[string]bool{ + "2024-01-01": true, + "2024-02-12": true, + "2024-02-13": true, + "2024-03-29": true, + "2024-04-01": true, + "2024-04-04": true, + "2024-05-01": true, + "2024-05-15": true, + "2024-06-10": true, + "2024-07-01": true, + "2024-09-18": true, + "2024-10-01": true, + "2024-10-11": true, + "2024-12-25": true, + }, + "US": map[string]bool{ + "2024-01-01": true, + "2024-01-15": true, + "2024-02-19": true, + "2024-03-29": true, + "2024-05-27": true, + "2024-06-19": true, + "2024-07-04": true, + "2024-09-02": true, + "2024-11-28": true, + "2024-12-25": true, + }, + "Germany": map[string]bool{ + "2024-12-24": true, + "2024-12-25": true, + "2024-12-26": true, + "2024-12-31": true, + }, + "France": map[string]bool{ + "2024-12-25": true, + "2024-12-26": true, + }, + "Brazil": map[string]bool{ + "2024-11-15": true, + "2024-11-20": true, + "2024-12-24": true, + "2024-12-25": true, + "2024-12-31": true, + }, + "Japan": map[string]bool{ + "2024-01-01": true, + "2024-01-02": true, + "2024-01-03": true, + "2024-01-08": true, + "2024-02-12": true, + "2024-02-23": true, + "2024-03-20": true, + "2024-04-29": true, + "2024-05-03": true, + "2024-05-06": true, + "2024-07-15": true, + "2024-08-12": true, + "2024-09-16": true, + "2024-09-23": true, + "2024-10-14": true, + "2024-11-04": true, + "2024-12-31": true, + }, +} + +// 压缩 +func CompressData(data []byte) ([]byte, error) { + var buf bytes.Buffer + compressor, err := zlib.NewWriterLevel(&buf, zlib.BestCompression) + if err != nil { + return nil, err + } + _, err = compressor.Write(data) + compressor.Close() + + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// 解压 +func DecompressData(compressedData []byte) ([]byte, error) { + compressedDataReader := bytes.NewReader(compressedData) + decompressor, err := zlib.NewReader(compressedDataReader) + if err != nil { + return nil, err + } + decompressedData, err := ioutil.ReadAll(decompressor) + decompressor.Close() + + if err != nil { + return nil, err + } + return decompressedData, nil +} +func TimeStrToTimestamp(timeStr string) (int64, error) { + loc, _ := time.LoadLocation("Asia/Singapore") + t, err := time.ParseInLocation("2006-01-02 15:04:05", timeStr, loc) + if err != nil { + fmt.Println("解析时间字符串错误:", err) + return 0, err + } + return t.Unix(), nil +} + +// 新增五分钟-用来恢复真实价格 +func TimeStrAddFiveTime(timeStr string) (string, error) { + // 获取当前时间 + currentTime := TimeStringToTime(timeStr) + // 创建一个表示5分钟的Duration + fiveMinutes := 5 * time.Minute + // 给当前时间加上5分钟 + newTime := currentTime.Add(fiveMinutes) + // 定义时间格式 + const layout = "2006-01-02 15:04:05" + // 将时间转换为字符串 + timeS := newTime.Format(layout) + + return timeS, nil +} + +// 时间转换 +func TimeStringToTime(t string) time.Time { + loc, err := time.LoadLocation("Local") + if err != nil { + return time.Time{} + } + theTime, err := time.ParseInLocation("2006-01-02 15:04:05", t, loc) + if err != nil { + return time.Time{} + } + + return theTime +} + +func ConvertToUSTime(timestamp int64) int64 { + t := time.Unix(timestamp, 0) + location, _ := time.LoadLocation("America/New_York") + ustime := t.In(location) + // 计算美股时间戳 + ustimestamp := ustime.Unix() + return ustimestamp +} + +func ConvertUSTime() time.Time { + t := time.Now() + location, _ := time.LoadLocation("America/New_York") + return t.In(location) +} + +func ConvertToTimeStr(timestamp int64) string { + t := time.Unix(timestamp, 0) + location, _ := time.LoadLocation("Asia/Singapore") + ustime := t.In(location) + return ustime.Format("2006-01-02 15:04:05") +} + +// 美股 9:30 - 16:00 // New_York 时间 +func IsOpeningUS() bool { + location, _ := time.LoadLocation("America/New_York") + now := time.Now().In(location) // 获取当前时间 + weekday := now.Weekday() + if weekday == time.Sunday || weekday == time.Saturday { + return false + } + if TradingDayOff["US"][now.Format("2006-01-02")] { + return false + } + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 10:30:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:00:03", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + return false +} + +// 美股 9:30 - 16:00 // New_York 时间 提前二十分钟开盘 +func IsFinnhubOpeningUS() bool { + location, _ := time.LoadLocation("America/New_York") + now := time.Now().In(location) // 获取当前时间 + weekday := now.Weekday() + if weekday == time.Sunday || weekday == time.Saturday { + return false + } + if TradingDayOff["US"][now.Format("2006-01-02")] { + return false + } + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 10:10:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:00:03", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + return false +} + +func IsPullOpen(country string, amOpenTime, amCloseTime, pmOpenTime, pmCloseTime string) bool { + if amOpenTime == "" || amCloseTime == "" || pmOpenTime == "" || pmCloseTime == "" { + return false + } + if country == "US" { + location, _ := time.LoadLocation("America/New_York") + now := time.Now().In(location) // 获取当前时间 + weekday := now.Weekday() + if weekday == time.Sunday || weekday == time.Saturday { + return false + } + if TradingDayOff["US"][now.Format("2006-01-02")] { + return false + } + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 10:30:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:00:03", location) + //当前正常开盘 + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return false + } + // TODO: 因为后台是新加披时间 + location, _ = time.LoadLocation("Asia/Singapore") + now = time.Now().In(location) // 获取当前时间 + openTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", amOpenTime), location) + closeTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", amCloseTime), location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", pmOpenTime), location) + afternoonOpenTimeEnd, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 23:59:59", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", pmCloseTime), location) + afternoonCloseTimeBegin, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 00:00:00", location) + if (now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonOpenTimeEnd.Unix()) || (now.Unix() >= afternoonCloseTimeBegin.Unix() && now.Unix() <= afternoonCloseTime.Unix()) { + return true + } + return false + } + location, err := time.LoadLocation("Asia/Singapore") + if err != nil { + location = time.FixedZone("CST", 8*3600) //替换新加坡时区方式 + } + now := time.Now().In(location) // 获取当前时间 + weekday := now.Weekday() + if weekday == time.Sunday || weekday == time.Saturday { + return false + } + if TradingDayOff[country][now.Format("2006-01-02")] { + return false + } + switch country { + case "Thailand": + //上午开盘(正常开盘) + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 10:55:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 13:30:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return false + } + openTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", amOpenTime), location) + closeTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", amCloseTime), location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + //下午开盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 15:25:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:30:59", location) + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return false + } + afternoonOpenTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", pmOpenTime), location) + afternoonCloseTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", pmCloseTime), location) + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + case "Malaysia": + //上午开盘 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 09:00:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 12:30:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return false + } + openTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", amOpenTime), location) + closeTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", amCloseTime), location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + //下午开盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 14:30:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:00:59", location) + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return false + } + afternoonOpenTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", pmOpenTime), location) + afternoonCloseTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", pmCloseTime), location) + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + case "HongKong": + //上午开盘 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 09:30:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 12:00:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return false + } + openTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", amOpenTime), location) + closeTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", amCloseTime), location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + //下午开盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 13:00:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 16:00:59", location) + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return false + } + afternoonOpenTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", pmOpenTime), location) + afternoonCloseTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+fmt.Sprintf(" %s:00", pmCloseTime), location) + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + } + + return false + +} + +func GetToTime() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 04:00:00", location) + return openTime.UnixMilli() +} + +// 印尼 周一到周四(10:00 - 13:00)(14:30 - 17:10) 周五(10:00 - 12:30)(15:00 - 17:10) +// 泰國 (北京时间): 10:55 - 13:30(上午盘) 15:25 - 17:30(下午盘) +// 印度 北京时间(11:45–18:00) 印度时间(9:15–15:30) +// 马来 (北京时间): 09:00 - 12:30(上午盘) 14:30 - 17:00(下午盘) +// 新加坡 (北京时间): 09:00 - 12:00(上午盘) 12:55:00 - 17:00(下午盘) +// 港股 上午9:30开始至中午12:00结束 段:下午1:00开始至下午4:00结束。 +// 英国 开盘15:00 闭盘 23:30 +// 巴西 (北京)开盘 21:00 4:00 圣保罗 10:00 17:00 +// 德国 15:00 - 23:30 +// 法国 15:00 - 23:30 +// 日本 (北京时间): 08:00 - 10:30(上午盘) 11:30 - 14:00(下午盘) +func IsOpening(country string) bool { + if country == "Brazil" { + // TODO: 巴西特殊处理因为与东八区时间相差太大,因此转圣保罗时间 + locationB, _ := time.LoadLocation("America/Sao_Paulo") + nowB := time.Now().In(locationB) // 获取当前时间 + weekdayB := nowB.Weekday() + if weekdayB == time.Sunday || weekdayB == time.Saturday { + return false + } + if TradingDayOff[country][nowB.Format("2006-01-02")] { + return false + } + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", nowB.Format("2006-01-02")+" 10:00:00", locationB) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", nowB.Format("2006-01-02")+" 17:00:59", locationB) + if nowB.Unix() >= openTime.Unix() && nowB.Unix() <= closeTime.Unix() { + return true + } + return false + } + + location, err := time.LoadLocation("Asia/Singapore") + if err != nil { + location = time.FixedZone("CST", 8*3600) //替换新加坡时区方式 + } + now := time.Now().In(location) + weekday := now.Weekday() + if weekday == time.Sunday || weekday == time.Saturday { + return false + } + if TradingDayOff[country][now.Format("2006-01-02")] { + return false + } + switch country { + case "India": + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 11:45:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 18:00:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + case "UK": + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 15:00:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 23:30:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + case "France": + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 15:00:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 23:30:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + case "Germany": + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 15:00:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 23:30:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + case "Thailand": + //上午开盘 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 10:55:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 13:30:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + //下午开盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 15:25:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:30:59", location) + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + case "Japan": + // 上午盘 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 08:00:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 10:30:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + // 下午盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 11:30:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 14:00:59", location) + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + case "Indonesia": + //上午开盘 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 10:00:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 13:00:59", location) + //下午开盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 14:30:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:10:59", location) + if weekday == time.Friday { + openTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 10:00:00", location) + closeTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 12:30:59", location) + + afternoonOpenTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 15:00:00", location) + afternoonCloseTime, _ = time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:10:59", location) + } + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + case "Malaysia": + //上午开盘 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 09:00:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 12:30:59", location) + //下午开盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 14:30:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:00:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + case "Singapore": + //上午开盘 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 09:00:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 12:00:59", location) + //下午开盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 12:55:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 17:00:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + case "HongKong": + //上午开盘 + openTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 09:30:00", location) + closeTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 12:00:59", location) + //下午开盘 + afternoonOpenTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 13:00:00", location) + afternoonCloseTime, _ := time.ParseInLocation("2006-01-02 15:04:05", now.Format("2006-01-02")+" 16:00:59", location) + if now.Unix() >= openTime.Unix() && now.Unix() <= closeTime.Unix() { + return true + } + if now.Unix() >= afternoonOpenTime.Unix() && now.Unix() <= afternoonCloseTime.Unix() { + return true + } + default: + } + return false +} + +func ConvertToTimezone(utcTimestamp int64) string { + // 将UTC时间戳转换为time.Time对象 + utcTime := time.Unix(utcTimestamp/1000, 0) + location, _ := time.LoadLocation("America/New_York") + // 将时间转换为指定时区的时间 + localTime := utcTime.In(location) + + // 将时间转换为字符串,使用指定的格式 + localTimeString := localTime.Format("2006-01-02 15:04:05") + + return localTimeString +} + +func NewsUsTime(day int) string { + location, _ := time.LoadLocation("America/New_York") + now := time.Now().In(location) // 获取当前时间 + // 将时间转换为字符串,使用指定的格式 + return now.AddDate(0, 0, day).Format("2006-01-02") +} + +func ConvertToTimezones(utcTimestamp int64) time.Time { + // 将UTC时间戳转换为time.Time对象 + utcTime := time.Unix(utcTimestamp/1000, 0) + location, _ := time.LoadLocation("America/New_York") + // 将时间转换为指定时区的时间 + return utcTime.In(location) +} + +func TimeStrToTimes(timeStr string) (time.Time, error) { + loc, _ := time.LoadLocation("Asia/Singapore") + return time.ParseInLocation("2006-01-02 15:04:05", timeStr, loc) +} + +// 获取合约面值 +func GetFaceValue(price decimal.Decimal) decimal.Decimal { + return decimal.NewFromFloat(30176).Div(price).Round(0) +} + +// 1min +func GenerateSingaporeMinuteTimestamp() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前新加坡时区时间 + rounded := now.Round(time.Minute) // 将时间戳下取整到分钟 + minuteTimestamp := rounded.Unix() // 将时间转换为秒级的时间戳 + return minuteTimestamp +} + +// 30min +func GenerateSingaporeThirtyMinTimestamp() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 30 // 取当前分钟数对5的余数 + nearestMinute := minute - mod + 30 // 计算最近的整五分钟时间 + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime.Unix() +} + +// 15min +func GenerateSingaporeFifteenMinTimestamp() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 15 + nearestMinute := minute - mod + 15 // 计算最近的整五分钟时间 + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime.Unix() +} + +// 5min +func GenerateSingaporeFiveMinTimestamp() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 5 // 取当前分钟数对5的余数 + nearestMinute := minute - mod + 5 // 计算最近的整五分钟时间 + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime.Unix() +} + +// 4 hour +func GenerateSingaporeFourHourTimestamp() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) + minute := now.Hour() + mod := minute % 4 + nearestMinute := minute - mod + 4 + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), nearestMinute, 0, 0, 0, now.Location()) + return nearestTime.Unix() +} + +// 1 hour +func GenerateSingaporeHourTimestampOrigin() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) + midnightTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) + if now.Minute() == 0 { //准点的话 + return midnightTime.Unix() - int64(60*60) + } + return midnightTime.Unix() +} + +func GenerateSingaporeHourTimestamp() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) + midnightTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) + return midnightTime.Unix() +} + +// 1day +func GenerateSingaporeDayTimestamp(country string) (midnightTime int64) { + // TODO: 巴西 东八区时间会跨天 + if country == "Brazil" { + location, _ := time.LoadLocation("America/Sao_Paulo") + singaporeLoc, _ := time.LoadLocation("Asia/Singapore") + t, _ := time.ParseInLocation("2006-01-02 15:04:05", fmt.Sprintf("%s 00:00:00", time.Now().In(location).Format("2006-01-02")), singaporeLoc) + midnightTime = t.Unix() + CountryStartTime[country]/1000 + } else { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) + midnightTime = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() + } + + return midnightTime +} + +// 1mon +func GenerateSingaporeMonTimestamp() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) + midnightTime := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + + return midnightTime.Unix() +} + +func GetWeeHours() bool { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) + hour, minute, _ := now.Clock() + if hour == 0 && minute == 0 { + return true + } + return false +} + +func ToLower(str string) string { + return fmt.Sprintf("%susdt", strings.ToLower(str)) +} + +// week +func GetWeekTimestamp() int64 { + // TODO: 周日不会开盘 ,不用处理跨天问题 + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + weekday := now.Weekday() // 获取当前是星期几 + daysSinceMonday := int(weekday-time.Monday+7) % 7 // 计算当前与星期一的差值 + mondayZero := now.AddDate(0, 0, -daysSinceMonday) + monday, _ := TimeStrToTimes(mondayZero.Format("2006-01-02") + " 00:00:00") + return monday.Unix() +} + +func RandFloats(min, max float64) float64 { + value := min + rand.Float64()*(max-min) + value, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", value), 64) + return value +} + +func TimeToNow() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + return time.Now().In(location).Unix() +} +func TimeToNows() time.Time { + location, _ := time.LoadLocation("Asia/Singapore") + return time.Now().In(location) +} + +func CapitalizeFirstLetter(str string) string { + if str == "hongkong" || str == "Hongkong" || str == "HongKong" { + return "HongKong" + } + if str == "US" || str == "us" || str == "Us" || str == "uS" { + return "US" + } + if str == "uk" || str == "UK" || str == "Uk" || str == "uK" { + return "UK" + } + // 将字符串的第一个字符转换为大写 + firstLetter := strings.ToUpper(string(str[0])) + + // 将剩余的字符拼接起来 + remaining := strings.ToLower(str[1:]) + + // 返回首字母大写的字符串 + return firstLetter + remaining +} + +func GenerateRandomSteps(min, max decimal.Decimal) decimal.Decimal { + rand.New(rand.NewSource(time.Now().UnixNano())) + return min.Add(decimal.NewFromFloat(rand.Float64()).Mul(max.Sub(min))) +} + +func CalculateContractPrices(basePrice decimal.Decimal, defaultStep float64, digits int32, numPrices int) []decimal.Decimal { + prices := make([]decimal.Decimal, 0) + max := basePrice.Mul(decimal.NewFromFloat(defaultStep)).Round(digits) + min := max.Neg() + for i := 0; i < numPrices; i++ { + price := basePrice.Add(GenerateRandomSteps(max, min)).Round(digits) + prices = append(prices, price) + } + return prices +} + +func GetMaxPrice(highPrice decimal.Decimal, prices []decimal.Decimal) decimal.Decimal { + for _, price := range prices { + if price.GreaterThan(highPrice) { + highPrice = price + } + } + return highPrice +} + +// 获取最低价 +func GetMinPrices(lowPrice decimal.Decimal, prices []decimal.Decimal) decimal.Decimal { + for _, price := range prices { + if price.LessThan(lowPrice) { + lowPrice = price + } + } + return lowPrice +} + +// 上传图片 +func UpdateImage(fileName string, src io.ReadSeeker) error { + // 创建一个新的S3会话 + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(Rregion), // 替换为你的AWS区域 + Credentials: credentials.NewStaticCredentials( + AwsAccessKeyId, // 替换为您的访问密钥 ID + AwsSecretAccessKey, // 替换为您的访问密钥 + ""), // 提供一个可选的令牌 (token),如果您使用 MFA + Endpoint: aws.String(Endpoint), // 替换为您的自定义别名 + DisableSSL: aws.Bool(false), // 通过 HTTPS 进行连接 + S3ForcePathStyle: aws.Bool(true), // 使用路径样式的 URL + HTTPClient: &http.Client{}, // 自定义 HTTP 客户端,可选 + }) + if err != nil { + return err + } + // 创建S3服务客户端 + svc := s3.New(sess) + // 创建S3上传请求参数 + params := &s3.PutObjectInput{ + Bucket: aws.String(Bucket), + Key: aws.String(Path + fileName), + Body: src, + } + // 执行S3上传请求 + _, err = svc.PutObject(params) + if err != nil { + return err + } + return nil +} + +// 访问图片 +func VisitImage(fileName string) (string, error) { + // 创建一个新的S3会话 + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(Rregion), // 替换为你的AWS区域 + Credentials: credentials.NewStaticCredentials( + AwsAccessKeyId, // 替换为您的访问密钥 ID + AwsSecretAccessKey, // 替换为您的访问密钥 + ""), // 提供一个可选的令牌 (token),如果您使用 MFA + Endpoint: aws.String(Endpoint), // 替换为您的自定义别名 + DisableSSL: aws.Bool(false), // 通过 HTTPS 进行连接 + S3ForcePathStyle: aws.Bool(true), // 使用路径样式的 URL + HTTPClient: &http.Client{}, // 自定义 HTTP 客户端,可选 + }) + if err != nil { + return "", err + } + // 创建S3服务客户端 + svc := s3.New(sess) + // 获取图片的预签名URL + req, _ := svc.GetObjectRequest(&s3.GetObjectInput{ + Bucket: aws.String(Bucket), + Key: aws.String(fileName), + }) + url, err := req.Presign(48 * time.Hour) // URL的有效期限为48小时 + if err != nil { + return "", err + } + return url, nil +} + +func GenerateSingaporeFiveMinTimestampOrigin() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 5 // 取当前分钟数对5的余数 + nearestMinute := minute - mod + 5 + if mod == 0 { + nearestMinute = minute + } + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime.Unix() +} + +func GenerateSingaporeFiveMinTimestampOrigins() time.Time { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 5 // 取当前分钟数对5的余数 + nearestMinute := minute - mod + 5 + if mod == 0 { + nearestMinute = minute + } + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime +} + +func GenerateSingaporeThirtyMinTimestampOrigin() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 30 // 取当前分钟数对5的余数 + nearestMinute := minute - mod + 30 // 计算最近的整五分钟时间 + if mod == 0 { + nearestMinute = minute + } + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime.Unix() +} + +func GenerateSingaporeThirtyMinTimestampOrigins() time.Time { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 30 // 取当前分钟数对5的余数 + nearestMinute := minute - mod + 30 // 计算最近的整五分钟时间 + if mod == 0 { + nearestMinute = minute + } + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime +} + +// 15min +func GenerateSingaporeFifteenMinTimestampOrigin() int64 { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 15 + nearestMinute := minute - mod + 15 // 计算最近的整五分钟时间 + if mod == 0 { + nearestMinute = minute + } + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + fmt.Println(nearestTime.Format("2006-01-02 15:04:05")) + return nearestTime.Unix() +} + +func GenerateSingaporeFifteenMinTimestampOrigins() time.Time { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % 15 + nearestMinute := minute - mod + 15 // 计算最近的整五分钟时间 + if mod == 0 { + nearestMinute = minute + } + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime +} + +func GenerateSingaporeMinTimestamp(div int) time.Time { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) // 获取当前时间 + minute := now.Minute() // 获取当前分钟数 + mod := minute % div + nearestMinute := minute - mod // 计算最近的整五分钟时间 + nearestTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), nearestMinute, 0, 0, now.Location()) + return nearestTime +} + +func isWeekend(day time.Weekday) bool { + return day == time.Saturday || day == time.Sunday +} + +func GenerateSingaporeMonTimestampStock(country string) int64 { + // TODO: 巴西 东八区时间会跨天 + var midnightTime time.Time + if country == "Brazil" { + location, _ := time.LoadLocation("America/Sao_Paulo") + singaporeLoc, _ := time.LoadLocation("Asia/Singapore") + midnightTime, _ = time.ParseInLocation("2006-01-02 15:04:05", fmt.Sprintf("%s-01 00:00:00", time.Now().In(location).Format("2006-01")), singaporeLoc) + } else { + location, _ := time.LoadLocation("Asia/Singapore") + now := time.Now().In(location) + midnightTime = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + } +Loop: + if !isWeekend(midnightTime.Weekday()) { + if country == "Brazil" { + return midnightTime.Unix() + CountryStartTime[country]/1000 + } + return midnightTime.Unix() + } + midnightTime = midnightTime.AddDate(0, 0, 1) + goto Loop +} + +func OptionTime(beforeTime string) int64 { + location, _ := time.LoadLocation("Asia/Singapore") + afterTime, _ := time.ParseInLocation("2006-01-02", beforeTime, location) + return afterTime.Unix() +} + +func QuickSort(slice []model.StrikeInfo) []model.StrikeInfo { + n := len(slice) + for i := 0; i < n-1; i++ { + minIdx := i + for j := i + 1; j < n; j++ { + strikeMin, _ := strconv.ParseFloat(slice[minIdx].Strike, 64) + strikeJ, _ := strconv.ParseFloat(slice[j].Strike, 64) + if strikeJ < strikeMin { + minIdx = j + } + } + slice[i], slice[minIdx] = slice[minIdx], slice[i] + } + return slice +} + +func GetNewCode(exchange, code, country string) string { + if country == "US" { + return code + } + if strings.Contains(code, ":") { + return code + } + return fmt.Sprintf("%s:%s", exchange, code) +} + +func GetRedisDBMore(dbMore string) []string { + if strings.Contains(dbMore, ":") { + return strings.Split(dbMore, ":") + } + dbs := make([]string, 0) + return append(dbs, dbMore) +} + +func GetRedisAddrList(addrList string) map[string]string { + // ip:port$password + redMap := make(map[string]string) + redList := strings.Split(addrList, ",") + for _, value := range redList { + redPassword := strings.Split(value, "$") + redMap[redPassword[0]] = redPassword[1] + } + return redMap +} + +func GetMgoDbToRedisMap(redisToMongodb string) map[string]string { + // mongodb-redis:port + redMap := make(map[string]string) + redList := strings.Split(redisToMongodb, ",") + for _, value := range redList { + redPassword := strings.Split(value, "-") + redMap[redPassword[0]] = redPassword[1] + } + return redMap +} + +func GetMongodbAddrList(addrList string) []string { + // ip + if strings.Contains(addrList, ",") { + return strings.Split(addrList, ",") + } + dbs := make([]string, 0) + return append(dbs, addrList) +} + +func GetRedisNoPin(noPin string) map[string]bool { + fmt.Println("no Pin", noPin) + dbs := make(map[string]bool, 0) + if noPin == "" { + return dbs + } + if strings.Contains(noPin, ":") { + value := strings.Split(noPin, ":") + for _, v := range value { + dbs[v] = true + } + return dbs + } + dbs[noPin] = true + return dbs +} + +func GetOldCode(code string) string { + if !strings.Contains(code, ":") { + return code + } + return strings.Split(code, ":")[1] +} diff --git a/cmd/common/gzip.go b/cmd/common/gzip.go new file mode 100644 index 0000000..ef94ac9 --- /dev/null +++ b/cmd/common/gzip.go @@ -0,0 +1,71 @@ +package common + +import ( + "bytes" + "compress/gzip" + "encoding/json" +) + +// 压缩 与json搭配使用 +func MarshalToJsonWithGzip(jsonData interface{}) []byte { + dataAfterMarshal, _ := json.Marshal(jsonData) + dataAfterGzip, err := Encode(dataAfterMarshal) + if err != nil { + return nil + } + return dataAfterGzip +} + +// 解压 与json搭配使用 +func UnmarshalDataFromJsonWithGzip(msg []byte) (*struct{}, error) { + dataAfterDecode, err := Decode(msg) + if err != nil { + return nil, err + } + data := &struct { + }{} + err = json.Unmarshal(dataAfterDecode, data) + if err != nil { + return nil, err + } + return data, nil +} + +// Gzip用法 压缩数据 +func Encode(input []byte) ([]byte, error) { + // 创建一个新的 byte 输出流 + var buf bytes.Buffer + // 创建一个新的 gzip 输出流 + gzipWriter := gzip.NewWriter(&buf) + // 将 input byte 数组写入到此输出流中 + _, err := gzipWriter.Write(input) + if err != nil { + _ = gzipWriter.Close() + return nil, err + } + if err := gzipWriter.Close(); err != nil { + return nil, err + } + // 返回压缩后的 bytes 数组 + return buf.Bytes(), nil +} + +// Gzip用法 解压数据 +func Decode(input []byte) ([]byte, error) { + // 创建一个新的 gzip.Reader + bytesReader := bytes.NewReader(input) + gzipReader, err := gzip.NewReader(bytesReader) + if err != nil { + return nil, err + } + defer func() { + // defer 中关闭 gzipReader + _ = gzipReader.Close() + }() + buf := new(bytes.Buffer) + // 从 Reader 中读取出数据 + if _, err := buf.ReadFrom(gzipReader); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/cmd/common/notStock.go b/cmd/common/notStock.go new file mode 100644 index 0000000..b0859aa --- /dev/null +++ b/cmd/common/notStock.go @@ -0,0 +1,671 @@ +package common + +import ( + "bufio" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "os" + "strings" + "sync" + "time" + "wss-pool/config" + "wss-pool/internal/data" + "wss-pool/logging/applogger" +) + +var validStockCodeMutex = sync.RWMutex{} +var validStockCode = map[string]bool{} +var NotStockCode = map[string]bool{ + "BSE:20MICRONS": true, + "BSE:21STCENMGM": true, + "BSE:360ONE": true, + "BSE:3IINFOLTD": true, + "BSE:3MINDIA": true, + "BSE:3PLAND": true, + "BSE:5PAISA": true, + "BSE:63MOONS": true, + "BSE:A2ZINFRA": true, + "BSE:AAATECH": true, + "BSE:AAREYDRUGS": true, + "BSE:AARTECH": true, + "BSE:AARTIDRUGS": true, + "BSE:AARTIIND": true, + "BSE:AARTIPHARM": true, + "BSE:AARVEEDEN": true, + "BSE:AAVAS": true, + "BSE:ABAN": true, + "BSE:ABB": true, + "BSE:ABBOTINDIA": true, + "BSE:ABCAPITAL": true, + "BSE:ABFRL": true, + "BSE:ABSLAMC": true, + "BSE:ACC": true, + "BSE:ACCELYA": true, + "BSE:ACE": true, + "BSE:ACI": true, + "BSE:ACL": true, + "BSE:ADANIENSOL": true, + "BSE:ADANIENT": true, + "BSE:ADANIGREEN": true, + "BSE:ADANIPORTS": true, + "BSE:ADANIPOWER": true, + "BSE:ADFFOODS": true, + "BSE:ADL": true, + "BSE:ADORWELD": true, + "BSE:ADROITINFO": true, + "BSE:ADSL": true, + "BSE:ADVANIHOTR": true, + "BSE:ADVENZYMES": true, + "BSE:AEROFLEX": true, + "BSE:AETHER": true, + "BSE:AFFLE": true, + "BSE:AGARIND": true, + "BSE:AGI": true, + "BSE:AGRITECH": true, + "BSE:AGSTRA": true, + "BSE:AHL": true, + "BSE:AHLEAST": true, + "BSE:AHLUCONT": true, + "BSE:AIAENG": true, + "BSE:AIRAN": true, + "BSE:AJANTPHARM": true, + "BSE:AJMERA": true, + "BSE:AKI": true, + "BSE:AKSHAR": true, + "BSE:AKSHARCHEM": true, + "BSE:AKSHOPTFBR": true, + "BSE:AKZOINDIA": true, + "BSE:ALANKIT": true, + "BSE:ALBERTDAVD": true, + "BSE:ALEMBICLTD": true, + "BSE:ALICON": true, + "BSE:ALKALI": true, + "BSE:ALKEM": true, + "BSE:ALKYLAMINE": true, + "BSE:ALLCARGO": true, + "BSE:ALLSEC": true, + "BSE:ALMONDZ": true, + "BSE:ALOKINDS": true, + "BSE:ALPA": true, + "BSE:ALPHAGEO": true, + "BSE:ALPSINDUS": true, + "BSE:AMBER": true, + "BSE:AMBICAAGAR": true, + "BSE:AMBIKCO": true, + "BSE:AMBUJACEM": true, + "BSE:AMDIND": true, + "BSE:AMIORG": true, + "BSE:AMJLAND": true, + "NSE:AMNPLST": true, + "BSE:AMRUTANJAN": true, + "BSE:ANANDRATHI": true, + "BSE:ANANTRAJ": true, + "BSE:ANDHRAPAP": true, + "BSE:ANDHRSUGAR": true, + "BSE:ANDREWYU": true, + "BSE:ANGELONE": true, + "BSE:ANIKINDS": true, + "BSE:ANKITMETAL": true, + "BSE:ANMOL": true, + "BSE:ANUP": true, + "BSE:ANURAS": true, + "BSE:APARINDS": true, + "BSE:APCL": true, + "BSE:APCOTEXIND": true, + "BSE:APEX": true, + "BSE:APLAPOLLO": true, + "BSE:APLLTD": true, + "BSE:APOLLO": true, + "BSE:APOLLOHOSP": true, + "BSE:APOLLOPIPE": true, + "BSE:APOLLOTYRE": true, + "BSE:APTECHT": true, + "BSE:APTUS": true, + "BSE:ARCHIDPLY": true, + "BSE:ARCHIES": true, + "BSE:ARENTERP": true, + "BSE:ARIES": true, + "BSE:ARIHANTCAP": true, + "BSE:ARIHANTSUP": true, + "BSE:ARMANFIN": true, + "BSE:AROGRANITE": true, + "BSE:ARROWGREEN": true, + "BSE:ARSHIYA": true, + "BSE:ARTEMISMED": true, + "BSE:ARVIND": true, + "BSE:ARVINDFASN": true, + "BSE:ARVSMART": true, + "BSE:ASAHIINDIA": true, + "BSE:ASAHISONG": true, + "BSE:ASAL": true, + "BSE:ASALCBR": true, + "BSE:ASHAPURMIN": true, + "BSE:ASHIANA": true, + "BSE:ASHIMASYN": true, + "BSE:ASHOKA": true, + "BSE:ASHOKLEY": true, + "BSE:ASIANENE": true, + "BSE:ASIANHOTNR": true, + "BSE:ASIANPAINT": true, + "BSE:ASIANTILES": true, + "BSE:ASMS": true, + "BSE:ASTEC": true, + "BSE:ASTERDM": true, + "BSE:ASTRAL": true, + "BSE:ASTRAMICRO": true, + "BSE:ASTRAZEN": true, + "BSE:ASTRON": true, + "BSE:ATALREAL": true, + "BSE:ATAM": true, + "BSE:ATFL": true, + "BSE:ATGL": true, + "BSE:ATL": true, + "BSE:ATUL": true, + "BSE:ATULAUTO": true, + "BSE:AUBANK": true, + "BSE:AURIONPRO": true, + "BSE:AUROPHARMA": true, + "BSE:AURUM": true, + "BSE:AUSOMENT": true, + "BSE:AUTOAXLES": true, + "BSE:AUTOIND": true, + "BSE:AVADHSUGAR": true, + "BSE:AVALON": true, + "BSE:AVANTIFEED": true, + "BSE:AVG": true, + "BSE:AVONMORE": true, + "BSE:AVROIND": true, + "BSE:AVTNPL": true, + "BSE:AWHCL": true, + "BSE:AWL": true, + "BSE:AXISBANK": true, + "BSE:AXISCADES": true, + "BSE:AXITA": true, + "BSE:AYMSYNTEX": true, + "BSE:BAFNAPH": true, + "BSE:BAGFILMS": true, + "BSE:BAIDFIN": true, + "BSE:BAJAJCON": true, + "BSE:BAJAJELEC": true, + "BSE:BAJAJFINSV": true, + "BSE:BAJAJHCARE": true, + "BSE:BAJAJHIND": true, + "BSE:BAJAJHLDNG": true, + "BSE:BAJFINANCE": true, + "BSE:BALAJITELE": true, + "BSE:BALAMINES": true, + "BSE:BALKRISHNA": true, + "BSE:BALKRISIND": true, + "BSE:BALMLAWRIE": true, + "BSE:BALPHARMA": true, + "BSE:BALRAMCHIN": true, + "BSE:BANARBEADS": true, + "BSE:BANARISUG": true, + "BSE:BANCOINDIA": true, + "BSE:BANDHANBNK": true, + "BSE:BANG": true, + "BSE:BANKBARODA": true, + "BSE:BANKINDIA": true, + "BSE:BANSWRAS": true, + "BSE:BARBEQUE": true, + "BSE:BASF": true, + "BSE:BASML": true, + "BSE:BATAINDIA": true, + "BSE:BAYERCROP": true, + "BSE:BBL": true, + "BSE:BBOX": true, + "BSE:BBTC": true, + "BSE:BBTCL": true, + "BSE:BCG": true, + "BSE:BCLIND": true, + "BSE:BCONCEPTS": true, + "BSE:BDL": true, + "BSE:BEARDSELL": true, + "BSE:BECTORFOOD": true, + "BSE:BEDMUTHA": true, + "BSE:BEL": true, + "BSE:BEML": true, + "BSE:BEPL": true, + "BSE:BERGEPAINT": true, + "BSE:BFINVEST": true, + "BSE:BFUTILITIE": true, + "BSE:BGRENERGY": true, + "BSE:BHAGCHEM": true, + "BSE:BHAGERIA": true, + "BSE:BHAGYANGR": true, + "BSE:BHANDARI": true, + "BSE:BHARATFORG": true, + "BSE:BHARATGEAR": true, + "BSE:BHARATRAS": true, + "BSE:BHARATWIRE": true, + "BSE:BHARTIARTL": true, + "BSE:BHEL": true, + "BSE:BIGBLOC": true, + "BSE:BIKAJI": true, + "BSE:BIL": true, + "BSE:BINANIIND": true, + "BSE:BIOCON": true, + "BSE:BIOFILCHEM": true, + "BSE:BIRLACABLE": true, + "BSE:BIRLACORPN": true, + "BSE:BIRLAMONEY": true, + "BSE:BKMINDST": true, + "BSE:BLAL": true, + "BSE:BLBLIMITED": true, + "BSE:BLISSGVS": true, + "BSE:BLKASHYAP": true, + "BSE:BLS": true, + "BSE:BLUECHIP": true, + "BSE:BLUEDART": true, + "BSE:BLUESTARCO": true, + "BSE:BODALCHEM": true, + "BSE:BOMDYEING": true, + "BSE:BOROLTD": true, + "BSE:BORORENEW": true, + "BSE:BOSCHLTD": true, + "BSE:BPCL": true, + "BSE:BPL": true, + "BSE:BRIGADE": true, + "NSE:BRIGHT": true, + "BSE:BRITANNIA": true, + "BSE:BRNL": true, + "BSE:BROOKS": true, + "BSE:BSL": true, + "BSE:BSOFT": true, + "BSE:BTML": true, + "BSE:BURNPUR": true, + "BSE:BUTTERFLY": true, + "BSE:BVCL": true, + "BSE:BYKE": true, + "BSE:CALSOFT": true, + "BSE:CAMLINFINE": true, + "BSE:CAMPUS": true, + "BSE:CAMS": true, + "BSE:CANBK": true, + "BSE:CANFINHOME": true, + "BSE:CANTABIL": true, + "BSE:CAPACITE": true, + "BSE:CAPLIPOINT": true, + "BSE:CAPTRUST": true, + "BSE:CARBORUNIV": true, + "BSE:CAREERP": true, + "BSE:CARERATING": true, + "BSE:CARTRADE": true, + "BSE:CARYSIL": true, + "BSE:CASTROLIND": true, + "BSE:CCHHL": true, + "BSE:CCL": true, + "BSE:CEATLTD": true, + "BSE:CELEBRITY": true, + "BSE:CENTENKA": true, + "BSE:CENTEXT": true, + "BSE:CENTRALBK": true, + "BSE:CENTRUM": true, + "BSE:CENTUM": true, + "BSE:CENTURYPLY": true, + "BSE:CENTURYTEX": true, + "BSE:CERA": true, + "BSE:CEREBRAINT": true, + "BSE:CESC": true, + "BSE:CGCL": true, + "BSE:CGPOWER": true, + "BSE:CHALET": true, + "BSE:CHAMBLFERT": true, + "BSE:CHEMBOND": true, + "BSE:CHEMCON": true, + "BSE:CHEMFAB": true, + "BSE:CHEMPLASTS": true, + "BSE:CHENNPETRO": true, + "BSE:CHEVIOT": true, + "BSE:CHOICEIN": true, + "BSE:CHOLAFIN": true, + "BSE:CHOLAHLDNG": true, + "BSE:CIEINDIA": true, + "BSE:CIGNITITEC": true, + "BSE:CINELINE": true, + "BSE:CINEVISTA": true, + "BSE:CIPLA": true, + "BSE:CLEAN": true, + "BSE:CLEDUCATE": true, + "BSE:CLSEL": true, + "BSE:CMSINFO": true, + "BSE:COALINDIA": true, + "BSE:COASTCORP": true, + "BSE:COCHINSHIP": true, + "BSE:COFFEEDAY": true, + "BSE:COFORGE": true, + "BSE:COLPAL": true, + "BSE:COMPINFO": true, + "BSE:COMPUSOFT": true, + "BSE:CONCOR": true, + "BSE:CONCORDBIO": true, + "BSE:CONFIPET": true, + "BSE:CONTROLPR": true, + "BSE:CORALFINAC": true, + "BSE:CORDSCABLE": true, + "BSE:COROMANDEL": true, + "BSE:COSMOFIRST": true, + "BSE:COUNCODOS": true, + "BSE:CRAFTSMAN": true, + "NSE:CREATIVE": true, + "BSE:CREATIVEYE": true, + "BSE:CREDITACC": true, + "BSE:CREST": true, + "BSE:CRISIL": true, + "BSE:CROMPTON": true, + "BSE:CSBBANK": true, + "BSE:CSLFINANCE": true, + "BSE:CTE": true, + "BSE:CUB": true, + "BSE:CUBEXTUB": true, + "BSE:CUMMINSIND": true, + "BSE:CUPID": true, + "BSE:CYBERMEDIA": true, + "BSE:CYBERTECH": true, + "BSE:CYIENT": true, + "BSE:CYIENTDLM": true, + "BSE:DABUR": true, + "BSE:DALBHARAT": true, + "BSE:DALMIASUG": true, + "BSE:DAMODARIND": true, + "BSE:DATAMATICS": true, + "BSE:DATAPATTNS": true, + "BSE:DBCORP": true, + "BSE:DBL": true, + "BSE:DBOL": true, + "BSE:DBREALTY": true, + "BSE:DBSTOCKBRO": true, + "BSE:DCAL": true, + "BSE:DCBBANK": true, + "BSE:DCI": true, + "BSE:DCM": true, + "BSE:DCMFINSERV": true, + "BSE:DCMNVL": true, + "BSE:DCMSHRIRAM": true, + "BSE:DCMSRIND": true, + "BSE:DCW": true, + "BSE:DCXINDIA": true, + "BSE:DECCANCE": true, + "BSE:DEEPAKFERT": true, + "BSE:DEEPAKNTR": true, + "BSE:DEEPENR": true, + "BSE:DEEPINDS": true, + "BSE:DELHIVERY": true, + "BSE:DELPHIFX": true, + "BSE:DELTACORP": true, + "BSE:DELTAMAGNT": true, + "BSE:DEN": true, + "BSE:DENORA": true, + "BSE:DEVIT": true, + "BSE:DEVYANI": true, + "BSE:DGCONTENT": true, + "BSE:DHAMPURSUG": true, + "BSE:DHANBANK": true, + "BSE:DHANI": true, + "BSE:DHANUKA": true, + "BSE:DHARMAJ": true, + "BSE:DHRUV": true, + "BSE:DHUNINV": true, + "BSE:DIACABS": true, + "BSE:DIAMINESQ": true, + "BSE:DIAMONDYD": true, + "BSE:DICIND": true, + "BSE:DIGISPICE": true, + "BSE:DISHTV": true, + "BSE:DIVGIITTS": true, + "BSE:DIVISLAB": true, + "BSE:DIXON": true, + "BSE:DJML": true, + "BSE:DLF": true, + "BSE:DLINKINDIA": true, + "BSE:DOLPHIN": true, + "BSE:DPWIRES": true, + "NSE:DRL": true, + "BSE:DTIL": true, + "BSE:DYCL": true, + "BSE:EMSLIMITED": true, + "BSE:EPIGRAL": true, + "BSE:FINPIPE": true, + "BSE:FOCUS": true, + "BSE:GANGESSECU": true, + "BSE:GENSOL": true, + "BSE:GHCLTEXTIL": true, + "BSE:GMMPFAUDLR": true, + "BSE:GREENLAM": true, + "BSE:GSTL": true, + "BSE:GUJGASLTD": true, + "BSE:GUJRAFFIA": true, + "BSE:HBSL": true, + "BSE:HISARMETAL": true, + "BSE:HMAAGRO": true, + "BSE:IDEAFORGE": true, + "BSE:IKIO": true, + "BSE:INNOVATIVE": true, + "BSE:IRBINVIT": true, + "BSE:JAYSREETEA": true, + "BSE:JIOFIN": true, + "BSE:JLHL": true, + "BSE:JSWINFRA": true, + "BSE:KALAMANDIR": true, + "BSE:KDL": true, + "BSE:KEYFINSERV": true, + "NSE:KORE": true, + "BSE:KPIL": true, + "BSE:KRIDHANINF": true, + "BSE:KRITI": true, + "BSE:LINCOLN": true, + "BSE:LLOYDSME": true, + "BSE:LOTUSEYE": true, + "BSE:MAGADSUGAR": true, + "BSE:MANAKALUCO": true, + "BSE:MANAKCOAT": true, + "BSE:MANAKSTEEL": true, + "BSE:MAXIND": true, + "BSE:MAZDA": true, + "BSE:MICEL": true, + "BSE:MKPL": true, + "BSE:MOLDTECH": true, + "BSE:MSTCLTD": true, + "BSE:MUTHOOTCAP": true, + "BSE:MVGJL": true, + "BSE:NAGAFERT": true, + "BSE:NETWEB": true, + "BSE:NIITMTS": true, + "BSE:NILAINFRA": true, + "BSE:NRAIL": true, + "BSE:NUVAMA": true, + "BSE:OCCL": true, + "BSE:ORCHPHARMA": true, + "BSE:ORICONENT": true, + "BSE:ORTINLAB": true, + "BSE:PAKKA": true, + "BSE:PALASHSECU": true, + "BSE:PALREDTEC": true, + "BSE:PAVNAIND": true, + "BSE:PODDARHOUS": true, + "BSE:PODDARMENT": true, + "BSE:PRECISION": true, + "BSE:PREMEXPLN": true, + "BSE:PROZONER": true, + "BSE:PTCIL": true, + "BSE:PYRAMID": true, + "BSE:RAJRATAN": true, + "BSE:RAJRILTD": true, + "BSE:RATNAVEER": true, + "BSE:REDTAPE": true, + "BSE:RHFL": true, + "BSE:RISHABH": true, + "BSE:ROML": true, + "BSE:RRKABEL": true, + "BSE:RSYSTEMS": true, + "BSE:SAFARI": true, + "BSE:SALONA": true, + "BSE:SALZERELEC": true, + "BSE:SAMHI": true, + "BSE:SAMPANN": true, + "BSE:SANDUMA": true, + "BSE:SBCL": true, + "BSE:SBFC": true, + "BSE:SEJALLTD": true, + "NSE:SEL": true, + "BSE:SELMC": true, + "BSE:SENCO": true, + "NSE:SENSEXETF": true, + "BSE:SHARDAMOTR": true, + "NSE:SHEETAL": true, + "BSE:SHIVATEX": true, + "BSE:SHYAMCENT": true, + "BSE:SIGIND": true, + "BSE:SIGMA": true, + "BSE:SIGNATURE": true, + "BSE:SINDHUTRAD": true, + "BSE:SOMICONVEY": true, + "BSE:SOUTHWEST": true, + "BSE:SPENCERS": true, + "BSE:SRGHFL": true, + "BSE:SUBEXLTD": true, + "BSE:TASTYBITE": true, + "BSE:TECILCHEM": true, + "BSE:TITAGARH": true, + "BSE:TPLPLASTEH": true, + "BSE:TREL": true, + "BSE:TTKHLTCARE": true, + "BSE:TVSSCS": true, + "BSE:UCAL": true, + "BSE:UDS": true, + "BSE:UMANGDAIRY": true, + "BSE:UNIENTER": true, + "BSE:URAVI": true, + "BSE:URJA": true, + "BSE:UTKARSHBNK": true, + "BSE:VALIANTLAB": true, + "BSE:VENKEYS": true, + "BSE:VIJIFIN": true, + "BSE:VIPULLTD": true, + "BSE:VLEGOV": true, + "BSE:VPRPL": true, + "NSE:WORTH": true, + "BSE:WSI": true, + "BSE:YASHO": true, + "BSE:YATHARTH": true, + "BSE:YATRA": true, + "NSE:ZEAL": true, + "BSE:BLUECOAST": true, + "BSE:DIGJAMLMTD": true, + "BSE:GATECH": true, + "NSE:SECMARK": true, + "BSE:KEL": true, + "BSE:PLAZACABLE": true, + "BSE:SICALLOG": true, + "BSE:LLOYDSENGG": true, + "NSE:MAL": true, + "NSE:TCLCONS": true, + "BSE:TPHQ": true, + "BSE:SURAJEST": true, + "BSE:NCC": true, +} + +func GetIndiaStock() { + for { + filter := bson.M{"Country": "India"} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + continue + } + //start := time.Now() // 获取当前时间 + validStockCodeMutex.Lock() + if len(dateList.([]primitive.M)) > 0 { + validStockCode = map[string]bool{} + } + for _, value := range dateList.([]primitive.M) { + if value["Exchange"] == nil || value["Code"] == nil { + continue + } + code := value["Code"].(string) + exchange := value["Exchange"].(string) + validStockCode[fmt.Sprintf("%s:%s", exchange, code)] = true + } + validStockCodeMutex.Unlock() + //fmt.Println("Run time: ", time.Since(start)) + applogger.Info("india stock number :%v", len(validStockCode)) + time.Sleep(1 * time.Hour) + } +} + +func GetIndiaStockBool(key, country string) bool { + if country != "India" { + return true + } + validStockCodeMutex.RLock() + defer validStockCodeMutex.RUnlock() + return validStockCode[key] +} + +func ReadTest(rename string) { + data.Mgo_init(config.Config.Mongodb) + file, err := os.Open(rename) + if err != nil { + // 错误处理 + fmt.Println("Error opening file:", err) + return + } + defer file.Close() // 确保在函数结束时关闭文件 + + scanner := bufio.NewScanner(file) + stockList := make([]string, 0) + // i := 0 + for scanner.Scan() { + //i++ + // scanner.Text() 返回当前行的内容 + // fmt.Println(strings.TrimSpace(scanner.Text()),i) + filter := bson.M{"Country": "India", "Code": strings.TrimSpace(scanner.Text())} + dateList, _ := data.MgoFind(data.StockList, filter) + res := dateList.([]primitive.M) + if len(res) <= 0 { + fmt.Println(res) + continue + } else if len(res) >= 2 { + fmt.Println(res) + break + } + str, ok := res[0]["Exchange"].(string) + if !ok { + fmt.Println(res) + continue + } + exchange := "BSE" + if str == "BSE" { + exchange = "NSE" + } + stockList = append(stockList, fmt.Sprintf("%s:%s", exchange, strings.TrimSpace(scanner.Text()))) + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading file:", err) + } + write(stockList) +} + +func write(param []string) { + fmt.Println(len(param)) + filename := "example.txt" + // 打开文件以追加数据,如果文件不存在则创建它 + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Println("Error opening file:", err) + return + } + defer file.Close() + // 要追加的内容 + for _, value := range param { + content := fmt.Sprintf(`"%s":true, %s`, value, "\n") + // 写入数据 + _, err = file.WriteString(content) + if err != nil { + fmt.Println("Error writing to file:", err) + return + } + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..7541aec --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "flag" + "wss-pool/cmd/servicemanager" + "wss-pool/cmd/websocketcollect/forex" + "wss-pool/cmd/websocketcollect/us" + "wss-pool/config" + "wss-pool/internal/data/business" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var ( + BuildTime string + configName = flag.String("config", "./config/config.yaml", "choose service") + // service selection + checkInt = flag.String("check", "gin", "choose service") + // Service Method Selection + checkStr = flag.String("model", "spots", "choose service") + + // Service IP and Port Configuration [10.148.0.7,10.148.0.5] + ipServer = flag.String("hostS", "0.0.0.0", "Server distribution IP") + addrServer = flag.String("addrS", ":8861", "Server distribution Post") + contractCode = flag.String("contract", "", "Server distribution Post") + project = flag.String("project", "", "Server distribution project") + stockTs = flag.Int64("stockTs", 0, "") +) + +func init() { + applogger.Info("build time:", BuildTime) + flag.Parse() + config.LoadConfig(*configName) +} + +func main() { + applogger.Info("gather service start") + applogger.Info("intService---checkBool:%v,ginIp:%v,ginPost:%v,checkStr:%v", *checkInt, *ipServer, *addrServer, *checkStr) + switch *checkInt { + case model.Gin: // Http查询服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.Gather: // 源订阅-火币市场行情采集 + servicemanager.Gather(*checkStr, *ipServer, *addrServer) + case model.CurrencyWss: // 项目-行情Wss订阅 + servicemanager.Currency(*ipServer, *addrServer) + case model.CollectUs: // 美股-行情采集分发 + us.SubscribeShareUs(*ipServer, *addrServer) + case model.GatherUs: // 项目-美股市场行情采集 + servicemanager.GatherUS(*checkStr, *ipServer, *addrServer) + case model.CollectForex: // 外汇-行情采集分发 + forex.SubscribeForex(*ipServer, *addrServer) + case model.GatherForex: // 项目-外汇市场行情采集 + servicemanager.GatherForex(*checkStr, *ipServer, *addrServer) + case model.ShareWss: // 股票市场-行情Wss订阅 + servicemanager.ShareWss(*ipServer, *addrServer) + case model.PinWs: // 股票市场-插针行情Wss订阅 + servicemanager.PinWs(*ipServer, *addrServer) + case model.TickDB: // 数字币|股票(mongodb)-数据优化 + servicemanager.TickDB(*checkStr, *ipServer, *addrServer, *contractCode) + case model.SelfContract: // 合约服务 + servicemanager.SelfContract(*checkStr, *ipServer, *addrServer, *contractCode) + case model.SelfMarketSpot: // 现货服务 + servicemanager.SelfMarketSpot(*checkStr, *ipServer, *addrServer, *contractCode) + case model.StockIndex: // 指数服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.IndiaOption: // 印度期权服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.USStock: // 美股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.IndonesiaStock: // 印尼股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.ThailandStock: // 泰股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.IndiaStock: // 印度股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.MalaysiaStock: // 马来西亚股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.HongKongStock: // 港股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.SingaporeStock: // 新加坡股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.UKStock: // 英股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.GermanyStock: // 德股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.FranceStock: // 法股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.BrazilStock: // 巴西股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.JapanStock: // 日本股服务 + servicemanager.GinServer(*ipServer, *addrServer, *checkInt, *project) + case model.StockData: // mongodb-倒数据 + business.SymbolToStock(*project) + case model.StockDataUs: // mongodb-倒数据 + business.SymbolToStockList(*project) + case model.StockDataInfo: // mongodb-倒详情数据 + business.SymbolToStockInfo(*project) + case model.StockDataNews: // 股票市场数据更新 + business.SymbolNews(*project) + case model.StockCode: // 更新股票市场代码列表(老版本) + business.SymbolCode(*project) + case model.DelOptionHash: // 删除期权hash + business.DelOptionHash() + case model.SendIndiaInfo: // 发送给PHP印度股票信息 + business.SendIndiaInfo() + case model.MalaysiaStockUpdate: // 新增马来西亚数字代码 + business.MalaysiaStockUpdate() + case model.DeleteIndia: // 清理印度市场后台没有权限的股票(优化印度股票k线-mongodb压力) + business.DeleteSpotDay(*project, *stockTs) + case model.ForexToExcel: // 导出外汇股票代码 + business.TickerToExcel() + default: + applogger.Debug("Please select the startup ID......") + } +} diff --git a/cmd/marketwsscliert/marketcontract.go b/cmd/marketwsscliert/marketcontract.go new file mode 100644 index 0000000..2302dc4 --- /dev/null +++ b/cmd/marketwsscliert/marketcontract.go @@ -0,0 +1,278 @@ +package marketwsscliert + +import ( + "encoding/json" + "fmt" + "wss-pool/cmd/websocketservice" + "wss-pool/config" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/hbwssclient/marketwssclient" + "wss-pool/pkg/model/market" +) + +/*U本币合约数据 +https://huobiapi.github.io/docs/usdt_swap/v1/cn/ +*/ +// subscribeCtKline 合约kline数据 +func subscribeCtKline(symbolList map[string][]string) { + client := new(marketwssclient.ContractKLineWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, period := range symbolList { + for _, value := range period { + client.Subscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtKlineResponse) + if ok { + if &resp != nil { + if resp.Tick != nil || resp.Data != nil { + //插针 + resp = RunModify(resp) + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: resp.Channel}) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + //实时数据入库 + go business.UpdateSubscribeCtKline(resp) + applogger.Info("subscribeCtKline data,ServersId:%v,Sender:%v,Content:%v-%v", resp.Channel, Cli.Id, resp.Tick, resp.Data) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, period := range symbolList { + for _, value := range period { + client.UnSubscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeCtDepth 合约深度数据源 +func subscribeCtDepth(symbolList map[string][]string) { + //改为一对一 发送 + for symbol, period := range symbolList { + for _, value := range period { + topic := fmt.Sprintf("market.%s.depth.%s", symbol, value) + client := new(marketwssclient.ContractDepthWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + client.Subscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + }, + func(response interface{}) { + resp, ok := response.(interface{}) + if ok { + if &resp != nil { + //jsonMessage, _ := json.Marshal(websocketservice.Message{ + // ServersId: topic, + // Sender: Cli.Id, + // Content: resp, + // Symbol: topic}) + //合约深度压缩数据 + red.RedisClient.Publish(topic, resp) + applogger.Info("subscribeCtDepth data,ServersId:%v,Sender:%v,Content:%v", topic, Cli.Id) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + client.Connect(true) + defer client.Close() + } + } + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + client := new(marketwssclient.ContractDepthWebSocketClient).Init(config.Config.HbContract.HbContractHost) + for symbol, period := range symbolList { + for _, value := range period { + client.UnSubscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + client.Close() + applogger.Info("Client closed") +} + +// subscribeCtAddDepth 合约深度增量数据 +func subscribeCtAddDepth(symbolList map[string][]string) { + client := new(marketwssclient.ContractDepthSizeWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, period := range symbolList { + for _, value := range period { + client.Subscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtAddDepthResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: resp.Channel}) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + applogger.Info("subscribeCtAddDepth data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, Cli.Id, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, period := range symbolList { + for _, value := range period { + client.UnSubscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeCtBbo TODO: 合约买一卖一逐笔行情数据 +func subscribeCtBbo(symbolList map[string][]string) { + client := new(marketwssclient.ContractBBOWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, _ := range symbolList { + client.Subscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtBboResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: resp.Channel}) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + applogger.Info("subscribeCtBbo data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, Cli.Id, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, _ := range symbolList { + client.UnSubscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeCtDetail TODO: 合约详情数据 +func subscribeCtDetail(symbolList map[string][]string) { + client := new(marketwssclient.ContractDetailWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, _ := range symbolList { + client.Subscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtDetailResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: resp.Channel}) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + applogger.Info("subscribeCtDetail data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, Cli.Id, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, _ := range symbolList { + client.UnSubscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeCtTradeDetail 合约贸易详情数据 +func subscribeCtTradeDetail(symbolList map[string][]string) { + client := new(marketwssclient.ContractTradeDetailWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, _ := range symbolList { + client.Subscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtTradeDetailResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: resp.Channel}) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + applogger.Info("subscribeCtTradeDetail data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, Cli.Id, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, _ := range symbolList { + client.UnSubscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + + client.Close() + applogger.Info("Client closed") +} diff --git a/cmd/marketwsscliert/marketforex.go b/cmd/marketwsscliert/marketforex.go new file mode 100644 index 0000000..4468d80 --- /dev/null +++ b/cmd/marketwsscliert/marketforex.go @@ -0,0 +1,430 @@ +package marketwsscliert + +import ( + "encoding/json" + "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" + "go.mongodb.org/mongo-driver/mongo" + "strings" + "time" + "wss-pool/internal" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + models "wss-pool/pkg/model" +) + +var messageStr chan []byte +var messageDayStr chan []byte + +func init() { + messageStr = make(chan []byte) + messageDayStr = make(chan []byte) +} + +// 外汇交易对实时报价 +func subscribeMarketForexBakNew(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + applogger.Error("err info:%v", err) + time.Sleep(50 * time.Second) + break + } + if strings.Contains(string(msg), "ping") || + strings.Contains(string(msg), "subscribe success") || + strings.Contains(string(msg), "\"ev\":\"C\"") || + strings.Contains(string(msg), "\"ev\":\"T\"") || + strings.Contains(string(msg), "pong") { + continue + } + var subModel model.FinnhubMessageNew + if err = json.Unmarshal(msg, &subModel); err != nil { + applogger.Error("subModel Unmarshal info111 err: %v", err) + continue + } + var modelCl []model.ForexJsonData + if err = json.Unmarshal([]byte(subModel.Content), &modelCl); err != nil { + applogger.Error("subModel Unmarshal info222 err: %v", err) + continue + } + for _, value := range modelCl { + //if value.Pair != "XAUUSD" { + // continue + //} + // TODO: 插针处理 + forexJson, checkBool := RunModifyForexNew(value) + msgStr, err := json.Marshal(forexJson) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + continue + } + applogger.Debug("message info:%v", string(msgStr)) + if checkBool { + // TODO: 启动插针数据推送(延缓数据推送) + messageStr <- msgStr + } else { + for k, db := range red.RedisClientMap { + err = db.Publish(fmt.Sprintf("%s.Forex", value.Pair), string(msgStr)).Err() + if err != nil { + applogger.Error("db", k, "存储失败:", err) + } + } + } + } + } +} +func pinInsertionRun() { + go func() { + for msg := range messageStr { + var forexModel models.ForexJsonData + if err := json.Unmarshal(msg, &forexModel); err != nil { + applogger.Error("Run Unmarshal info err:", err) + continue + } + // 推送到redis + for k, db := range red.RedisClientMap { + err := db.Publish(fmt.Sprintf("%s.Forex", forexModel.Pair), string(msg)).Err() + if err != nil { + applogger.Error("db", k, "存储失败:", err) + } + } + // 写入Mongodb数据格式 + t := time.Unix(forexModel.Timestamp, 0) + formatted := t.Format("2006-01-02 15:04:05") + dataStr := bson.D{ + {"code", forexModel.Pair}, + {"timestamp", formatted}, + {"open_price", decimal.NewFromFloat(forexModel.Open).String()}, + {"close_price", decimal.NewFromFloat(forexModel.Close).String()}, + {"high_price", decimal.NewFromFloat(forexModel.High).String()}, + {"low_price", decimal.NewFromFloat(forexModel.Low).String()}, + {"volume", decimal.NewFromInt(int64(forexModel.Volume)).String()}, + {"turnover", "0.00"}, + } + if err := data.MgoInsertOne(data.ForexKLine, dataStr); err != nil { + applogger.Error("Insert info err:%v", err) + return + } + } + }() +} + +// 外汇交易对实时天报价 +func subscribeMarketDayForexBakNew(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + applogger.Error("err info:%v", err) + time.Sleep(50 * time.Second) + break + } + if strings.Contains(string(msg), "ping") || + strings.Contains(string(msg), "subscribe success") || + strings.Contains(string(msg), "\"ev\":\"C\"") || + strings.Contains(string(msg), "\"ev\":\"T\"") || + strings.Contains(string(msg), "pong") { + continue + } + var subModel model.FinnhubMessageNew + if err = json.Unmarshal(msg, &subModel); err != nil { + applogger.Error("subModel Unmarshal info111 err: %v", err) + continue + } + var modelCl []model.ForexJsonData + if err = json.Unmarshal([]byte(subModel.Content), &modelCl); err != nil { + applogger.Error("subModel Unmarshal info222 err: %v", err) + continue + } + for _, value := range modelCl { + // TODO: 插针处理 + forexJson, checkBool := RunModifyForex(value) + msgStr, err := json.Marshal(forexJson) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + continue + } + applogger.Debug("message info:%v", string(msgStr)) + if checkBool { + // TODO: 启动插针数据推送(延缓数据推送) + messageDayStr <- msgStr + } else { + for k, db := range red.RedisClientMap { + err = db.Publish(fmt.Sprintf("%s.DayForex", value.Pair), string(msgStr)).Err() + if err != nil { + applogger.Error("db", k, "存储失败:", err) + } + } + } + } + } +} +func pinInsertionDayRun() { + go func() { + for msg := range messageDayStr { + var forexModel models.ForexJsonData + if err := json.Unmarshal(msg, &forexModel); err != nil { + applogger.Error("Run Unmarshal info err:", err) + continue + } + // 数据处理 + for k, db := range red.RedisClientMap { + err := db.Publish(fmt.Sprintf("%s.Forex", forexModel.Pair), string(msg)).Err() + if err != nil { + applogger.Error("db", k, "存储失败:", err) + } + } + } + }() +} + +// 外汇交易对买一卖一报价 +func subscribeMarketForexQuoteBakNew(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + applogger.Error("err info:%v", err) + time.Sleep(50 * time.Second) + break + } + msgStrOne := string(msg) + //applogger.Debug("ReadMessage data info:%v", msgStrOne) + + if strings.Contains(msgStrOne, "ping") || + strings.Contains(msgStrOne, "subscribe success") || + strings.Contains(msgStrOne, "\"ev\":\"CAS\"") || + strings.Contains(msgStrOne, "\"ev\":\"T\"") || + strings.Contains(msgStrOne, "pong") { + continue + } + + var subModel model.FinnhubMessageNew + if err = json.Unmarshal(msg, &subModel); err != nil { + applogger.Error("subModel Unmarshal info111 err: %v", err) + continue + } + + var modelCl []model.ForexLastQuote + if err = json.Unmarshal([]byte(subModel.Content), &modelCl); err != nil { + applogger.Error("subModel Unmarshal info222 err: %v", err) + continue + } + for _, value := range modelCl { + msgStr, err := json.Marshal(value) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + continue + } + applogger.Debug("message info:%v", string(msgStr)) + // Write to Redis for broadcasting + if len(value.P) == 0 { + continue + } + for k, db := range red.RedisClientMap { + err = db.Publish(fmt.Sprintf("%s.LastForex", value.P), string(msgStr)).Err() + if err != nil { + applogger.Error("db", k, "存储失败:", err) + } + } + } + } +} + +// 外汇交易对成交报价 +func subscribeMarketForexTradeBakNew(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + applogger.Error("err info:%v", err) + time.Sleep(50 * time.Second) + break + } + msgStrOne := string(msg) + //applogger.Debug("ReadMessage data info:%v", msgStrOne) + + if strings.Contains(msgStrOne, "ping") || + strings.Contains(msgStrOne, "subscribe success") || + strings.Contains(msgStrOne, "\"ev\":\"CAS\"") || + strings.Contains(msgStrOne, "\"ev\":\"C\"") || + strings.Contains(msgStrOne, "pong") { + continue + } + + var subModel model.FinnhubMessageNew + if err = json.Unmarshal(msg, &subModel); err != nil { + applogger.Error("subModel Unmarshal info111 err: %v", err) + continue + } + var modelCl []model.ForexTrade + if err = json.Unmarshal([]byte(subModel.Content), &modelCl); err != nil { + applogger.Error("subModel Unmarshal info222 err: %v", err) + continue + } + for _, value := range modelCl { + msgStr, err := json.Marshal(value) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + continue + } + applogger.Debug("message info:%v", string(msgStr)) + for k, db := range red.RedisClientMap { + err = db.Publish(fmt.Sprintf("%s.TradeForex", value.Code), string(msgStr)).Err() + if err != nil { + applogger.Error("db", k, "存储失败:", err) + } + } + } + } +} + +// 外汇交易对成交报价存储 +func subscribeMarketForexTradeBak2New(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + applogger.Error("err info:%v", err) + time.Sleep(50 * time.Second) + break + } + msgStrOne := string(msg) + //applogger.Debug("ReadMessage data info:%v", msgStrOne) + + if strings.Contains(msgStrOne, "ping") || + strings.Contains(msgStrOne, "subscribe success") || + strings.Contains(msgStrOne, "\"ev\":\"CAS\"") || + strings.Contains(msgStrOne, "\"ev\":\"C\"") || + strings.Contains(msgStrOne, "pong") { + continue + } + + var subModel model.FinnhubMessageNew + if err = json.Unmarshal(msg, &subModel); err != nil { + applogger.Error("subModel Unmarshal info111 err: %v", err) + continue + } + var modelCl []model.ForexTrade + if err = json.Unmarshal([]byte(subModel.Content), &modelCl); err != nil { + applogger.Error("subModel Unmarshal info222 err: %v", err) + continue + } + + var dataList []mongo.WriteModel + for _, v := range modelCl { + filter := bson.M{"tick_time": bson.M{"$eq": decimal.RequireFromString(v.TickTime).IntPart()}, "code": bson.M{"$eq": v.Code}} + update := bson.D{{"$set", + bson.D{ + {"ev", v.Ev}, + {"code", v.Code}, + {"seq", v.Seq}, + {"tick_time", decimal.RequireFromString(v.TickTime).IntPart()}, + {"price", v.Price}, + {"volume", v.Volume}, + {"turnover", v.Turnover}, + {"trade_direction", v.TradeDirection}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err = data.MgoBulkWrite(data.ForexTradeList, dataList); err != nil { + applogger.Error("forexTrade MgoBulkWrite err:%v", err) + } + } + } +} + +// 清理外汇成交报价数据信息 +func DeleteForexTrade() { + dateList, err := data.MgoFind(data.ForexListBak, bson.M{}) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + //applogger.Info("MgoFind info len: %v", dateList) + for _, value := range dateList.([]primitive.M) { + code := value["symbol"].(string) + filter := bson.M{"code": code} + //applogger.Debug("清理数据:%v", filter) + if err = data.MgoDeleteMany100List(data.ForexTradeList, filter, code); err != nil { + applogger.Error("DeleteForexTrade MgoDeleteMany err :", err.Error()) + } + } +} + +// 每周一到周五0点0分0秒更新外汇交易对闭盘价 +func ForexUpdateCode() { + dateList, err := data.MgoFind(data.ForexListBak, bson.M{}) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + var codeList []string + for _, value := range dateList.([]primitive.M) { + code := value["code"].(string) + codeList = append(codeList, code) + } + + UrlBatchKline := "https://quote.tradeswitcher.com/quote-b-api/batch-kline?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" + + var dataPost model.ConstructParametersPost + for _, value := range codeList { + dataPost.Trace = uuid.New().String() + dataPost.Data.DataList = append(dataPost.Data.DataList, model.DataParameters{ + Code: value, + KlineType: 1, + KlineTimestampEnd: 0, + QueryKlineNum: 1, + AdjustType: 0, + }) + } + queryStr, err := json.Marshal(&dataPost) + if err != nil { + applogger.Error("解析json错误:%v", err) + return + } + + bodyStr, err := internal.HttpPost(UrlBatchKline, string(queryStr)) + if err != nil { + applogger.Error("读取响应失败:%v", err) + return + } + + //applogger.Debug("响应内容:%v", bodyStr) + var klineNew model.KlinePostReturnStruct + if err = json.Unmarshal([]byte(bodyStr), &klineNew); err != nil { + applogger.Error("解析失败:%v", err) + return + } + + //applogger.Debug("响应内容:%v", klineNew) + var dataList []mongo.WriteModel + for _, v := range klineNew.Data.KlineList { + filter := bson.M{ + "code": v.Code, + } + updateData := bson.M{} + for _, value := range v.KlineData { + updateData["openPrice"] = value.OpenPrice + updateData["highPrice"] = value.HighPrice + updateData["lowPrice"] = value.LowPrice + updateData["closePrice"] = value.ClosePrice + updateData["timestamp"] = value.Timestamp + break + } + update := bson.M{"$set": updateData} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err = data.MgoBulkWrite(data.ForexListBak, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + return + } + } + +} diff --git a/cmd/marketwsscliert/marketshare.go b/cmd/marketwsscliert/marketshare.go new file mode 100644 index 0000000..ff3658d --- /dev/null +++ b/cmd/marketwsscliert/marketshare.go @@ -0,0 +1,216 @@ +package marketwsscliert + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/gorilla/websocket" + "go.mongodb.org/mongo-driver/bson" + "strings" + "time" + "wss-pool/cmd/common" + "wss-pool/config" + "wss-pool/internal/data" + "wss-pool/internal/data/business" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +var UsNewCode = make(map[string]bool) + +// 订阅美股股票行情数据 +func subscribeFinnhub() *websocket.Conn { + url := fmt.Sprintf("wss://%v?token=%v", config.Config.FinnhubUs.FinnhubWss, config.Config.FinnhubUs.FinnhubKey) + conn, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("Failed to link to wss server:%v", err) + //defer conn.Close() + return nil + } + + return conn +} +func subscribeMarketUsBak(conn *websocket.Conn) { + _ = sendUsCode(conn, false) // 获取订阅全部股票 + go singleSendUsCode(conn) // 单线程 订阅后续新增的股票 + // TODO: Verify if key permissions are passed + //subAuh := fmt.Sprintf("{\"action\":\"auth\",\"params\":\"%v\"}", config.Config.ShareGather.PolygonKey) + //Send(conn, subAuh) + //// Initiate event subscription + //subData := fmt.Sprintf("{\"action\":\"subscribe\",\"params\":\"A.*\"}") + //Send(conn, subData) + //usMessage := make([]model.ClientMessage, 0) + for { + _, msg, err := conn.ReadMessage() + if err != nil { + applogger.Error("err info:%v", err) + return + } + applogger.Debug("ReadMessage data info:%v", string(msg)) + var subModel model.FinnhubMessage + if err := json.Unmarshal(msg, &subModel); err != nil { + applogger.Error("subModel Unmarshal info err: %v", err) + continue + } + if !common.IsFinnhubOpeningUS() { + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), "us shareMarket it's not opening time -----------------------------end") + continue + } + for _, value := range subModel.Data { + message := model.ClientMessage{ + S: value.S, // 股票代码 + C: value.C, // 条件,有关更多信息,请参阅贸易条件术语表 + V: value.V, // 交易量,代表在相应时间戳处交易的股票数量 -- 报价交易量 + Dp: true, // 暗池真/假 + T: value.T, // 以毫秒为单位的时间戳 -- 此聚合窗口的结束时钟周期的时间戳(以 Unix 毫秒为单位) + Cl: value.P, + Op: value.P, + H: value.P, + L: value.P, + } + msgStr, err := json.Marshal(message) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return + } + applogger.Debug("message info:%v", string(msgStr)) + // Write to Redis for broadcasting + if len(message.S) == 0 { + return + } + business.JudgePublishMap("US", message.S, fmt.Sprintf("%s.US", message.S), string(msgStr)) + business.JudgeHsetMap("US", business.StockClosingPrice["USNew"], message.S, message.Cl.String()) + } + } +} +func sendUsCode(conn *websocket.Conn, isSingle bool) error { + filter := bson.M{"Country": "US", "YesterdayClose": bson.M{"$ne": ""}} + projection := bson.M{"Code": 1} + stockRes := make([]stock.StockPolygon, 0) + data.MgoPagingFindStructProjection(data.StockList, filter, projection, 20000, 1, -1, &stockRes) + old := len(UsNewCode) + for _, v := range stockRes { + if !common.IsExistStock("US", v.Code) { + continue + } + sendPing := fmt.Sprintf("{\"type\":\"subscribe\",\"symbol\":\"%v\"}", v.Code) + if !isSingle { //订阅全部美股 + fmt.Println(sendPing) + Send(conn, sendPing) + UsNewCode[v.Code] = true + continue + } + if !UsNewCode[v.Code] { //追加订阅新股票 + applogger.Debug("new us stock:%v", sendPing) + Send(conn, sendPing) + UsNewCode[v.Code] = true + } + } + applogger.Debug("old us stock num", old, len(UsNewCode)) + return nil +} +func singleSendUsCode(conn *websocket.Conn) { + for { + time.Sleep(1 * time.Hour) + //发送测试数据检查该连接是否有效 + if err := Send(conn, fmt.Sprintf("{\"type\":\"subscribe\",\"symbol\":\"%v\"}", "testcode.ping")); err != nil { + applogger.Error("ws:%v", err) + return + } + applogger.Debug("send new us stock start") + if err := sendUsCode(conn, true); err != nil { + applogger.Debug("send new us stock end") + return + } + } +} + +// 接收分发股票实时行情 +func subscribeDispense(roue string, post int) *websocket.Conn { + url := fmt.Sprintf("ws://%v:%v/%v", config.Config.FinnhubUs.DispenseWss, post, roue) + applogger.Debug("dispense wss url:", url) + conn, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("Failed to link to wss server:%v", err) + return nil + } + applogger.Debug("dispense wss connect success......") + + return conn +} + +// 美股订阅 +func subscribeMarketUsBakNew(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + applogger.Error("err info:%v", err) + time.Sleep(50 * time.Second) + break + } + msgStrOne := string(msg) + applogger.Debug("ReadMessage data info:%v", msgStrOne) + + if strings.Contains(msgStrOne, "ping") || strings.Contains(msgStrOne, "subscribe success") { + continue + } + + var subModel model.FinnhubMessageNew + if err = json.Unmarshal(msg, &subModel); err != nil { + applogger.Error("subModel Unmarshal info111 err: %v", err) + continue + } + var modelCl model.FinnhubMessage + if err = json.Unmarshal([]byte(subModel.Content), &modelCl); err != nil { + applogger.Error("subModel Unmarshal info222 err: %v", err) + continue + } + + // 美股开盘时间判定 + //if !common.IsFinnhubOpeningUS() { + // applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), "us shareMarket it's not opening time -----------------------------end") + // continue + //} + + for _, value := range modelCl.Data { + message := model.ClientMessage{ + S: value.S, // 股票代码 + C: value.C, // 条件,有关更多信息,请参阅贸易条件术语表 + V: value.V, // 交易量,代表在相应时间戳处交易的股票数量 -- 报价交易量 + Dp: true, // 暗池真/假 + T: value.T, // 以毫秒为单位的时间戳 -- 此聚合窗口的结束时钟周期的时间戳(以 Unix 毫秒为单位) + Cl: value.P, + Op: value.P, + H: value.P, + L: value.P, + } + msgStr, err := json.Marshal(message) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + continue + } + applogger.Debug("message info:%v", string(msgStr)) + // Write to Redis for broadcasting + if len(message.S) == 0 { + continue + } + business.JudgePublishMap("US", message.S, fmt.Sprintf("%s.US", message.S), string(msgStr)) + business.JudgeHsetMap("US", business.StockClosingPrice["USNew"], message.S, message.Cl.String()) + } + } +} + +// 发送消息 +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") + } + err := conn.WriteMessage(websocket.TextMessage, []byte(data)) + if err != nil { + applogger.Error("WebSocket sent error: data=%s, error=%s", data, err) + return err + } + return nil +} diff --git a/cmd/marketwsscliert/marketspots.go b/cmd/marketwsscliert/marketspots.go new file mode 100644 index 0000000..0959f61 --- /dev/null +++ b/cmd/marketwsscliert/marketspots.go @@ -0,0 +1,401 @@ +package marketwsscliert + +import ( + "encoding/json" + "fmt" + "wss-pool/cmd/websocketservice" + "wss-pool/config" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/hbwssclient/marketwssclient" + "wss-pool/pkg/model/market" +) + +/* 火币现货数据来源 +https://huobiapi.github.io/docs/spot/v1/cn/#k-2 +*/ +// subscribeDepth 市场深度行情数据 +func subscribeDepth(symbolList map[string][]string) { + client := new(marketwssclient.DepthWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, value := range symbolList { + for _, vue := range value { + client.Request(key, vue, config.Config.HbGather.HbSubUids) + client.Subscribe(key, vue, config.Config.HbGather.HbSubUids) + } + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeDepthResponse) + if ok { + if &depthResponse != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: depthResponse.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: depthResponse.Channel}) + red.RedisClient.Publish(depthResponse.Channel, string(jsonMessage)) + applogger.Info("subscribeDepth data,ServersId:%v,Sender:%v,Content:%v-%v", depthResponse.Channel, Cli.Id, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, value := range symbolList { + for _, vue := range value { + client.UnSubscribe(key, vue, config.Config.HbGather.HbSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeLevelMbp 市场深度MBP行情数据(增量推送)(150挡) +func subscribeLevelMbp(symbolList map[string][]string) { + client := new(marketwssclient.MarketByPriceWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, _ := range symbolList { + client.Request(key, config.Config.HbGather.HbSubUids) + client.Subscribe(key, config.Config.HbGather.HbSubUids) + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeMarketByPriceResponse) + if ok { + if &depthResponse != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: depthResponse.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: depthResponse.Channel}) + red.RedisClient.Publish(depthResponse.Channel, string(jsonMessage)) + applogger.Info("subscribeLevelMbp data,ServersId:%v,Sender:%v,Content:%v-%v", depthResponse.Channel, Cli.Id, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, _ := range symbolList { + client.UnSubscribe(key, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeFullMbp 市场深度MBP行情数据(全量推送) +func subscribeFullMbp(symbolList map[string][]int) { + client := new(marketwssclient.MarketByPriceWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, value := range symbolList { + for _, vue := range value { + client.SubscribeFull(key, vue, config.Config.HbGather.HbSubUids) + } + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeMarketByPriceResponse) + if ok { + if &depthResponse != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: depthResponse.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: depthResponse.Channel}) + red.RedisClient.Publish(depthResponse.Channel, string(jsonMessage)) + applogger.Info("subscribeFullMbp data,ServersId:%v,Sender:%v,Content:%v-%v", depthResponse.Channel, Cli.Id, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, value := range symbolList { + for _, vue := range value { + client.UnSubscribeFull(key, vue, config.Config.HbGather.HbSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeSubMbp 市场深度MBP行情数据(增量推送) +func subscribeSubMbp(symbolList map[string][]int) { + client := new(marketwssclient.MarketByPriceTickWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, value := range symbolList { + for _, vue := range value { + client.Request(key, vue, config.Config.HbGather.HbSubUids) + client.Subscribe(key, vue, config.Config.HbGather.HbSubUids) + } + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeMarketByPriceResponse) + if ok { + if &depthResponse != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: depthResponse.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: depthResponse.Channel}) + red.RedisClient.Publish(depthResponse.Channel, string(jsonMessage)) + applogger.Info("subscribeSubMbp data,ServersId:%v,Sender:%v,Content:%v-%v", depthResponse.Channel, Cli.Id, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, value := range symbolList { + for _, vue := range value { + client.UnSubscribe(key, vue, config.Config.HbGather.HbSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeBbo 买一卖一逐笔行情 +func subscribeBbo(symbolList map[string][]string) { + client := new(marketwssclient.BestBidOfferWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, _ := range symbolList { + client.Subscribe(key, config.Config.HbGather.HbSubUids) + } + }, + func(resp interface{}) { + bboResponse, ok := resp.(market.SubscribeBestBidOfferResponse) + if ok { + if bboResponse.Tick != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: bboResponse.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: bboResponse.Channel}) + red.RedisClient.Publish(bboResponse.Channel, string(jsonMessage)) + } + applogger.Info("subscribeBbo data,ServersId:%v,Sender:%v,Content:%v-%v", bboResponse.Channel, Cli.Id, bboResponse.Tick, nil) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, _ := range symbolList { + client.UnSubscribe(key, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Connection closed") +} + +// subscribeKLine K线数据 +func subscribeKLine(symbolList map[string][]string) { + client := new(marketwssclient.CandlestickWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for symbol, period := range symbolList { + for _, value := range period { + client.Subscribe(symbol, value, config.Config.HbGather.HbSubUids) + } + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCandlestickResponse) + if ok { + if &resp != nil { + if resp.Tick != nil || resp.Data != nil { + //插针 + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: resp.Channel, + }) + //applogger.Info("k line JSON --- %s",string(jsonMessage)) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + //实时数据入库 + applogger.Info("k line resp %v", resp.Data) + go business.UpdateWsMgo(resp) + } + applogger.Info("subscribeKLine data,ServersId:%v,Sender:%v,Content:%v-%v", resp.Channel, Cli.Id, resp.Tick, resp.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, period := range symbolList { + for _, value := range period { + client.UnSubscribe(symbol, value, config.Config.HbGather.HbSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeTrade 成交明细 +func subscribeTrade(symbolList map[string][]string) { + client := new(marketwssclient.TradeWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, _ := range symbolList { + client.Request(key, config.Config.HbGather.HbSubUids) + client.Subscribe(key, config.Config.HbGather.HbSubUids) + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeTradeResponse) + if ok { + if &depthResponse != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: depthResponse.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: depthResponse.Channel}) + red.RedisClient.Publish(depthResponse.Channel, string(jsonMessage)) + applogger.Info("subscribeTrade data,ServersId:%v,Sender:%v,Content:%v-%v", depthResponse.Channel, Cli.Id, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, _ := range symbolList { + client.UnSubscribe(key, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeLast24h 市场概要 +func subscribeLast24h(symbolList map[string][]string) { + // Initialize a new instance + client := new(marketwssclient.Last24hCandlestickWebSocketClient).Init(config.Config.HbGather.HbHost) + // Set the callback handlers + client.SetHandler( + // Connected handler + func() { + for key, _ := range symbolList { + client.Request(key, config.Config.HbGather.HbSubUids) + client.Subscribe(key, config.Config.HbGather.HbSubUids) + } + }, + // Response handler + func(resp interface{}) { + candlestickResponse, ok := resp.(market.SubscribeLast24hCandlestickResponse) + if ok { + if &candlestickResponse != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: candlestickResponse.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: candlestickResponse.Channel}) + red.RedisClient.Publish(candlestickResponse.Channel, string(jsonMessage)) + applogger.Info("subscribeLast24h data,ServersId:%v,Sender:%v,Content:%v-%v", candlestickResponse.Channel, Cli.Id, candlestickResponse.Tick, candlestickResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + // Connect to the wss and wait for the handler to handle the response + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, _ := range symbolList { + client.UnSubscribe(key, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// subscribeTicker 市场聚合行情(ticker) +func subscribeTicker(symbolList map[string][]string) { + client := new(marketwssclient.TickerWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for symbol, _ := range symbolList { + client.Subscribe(symbol, config.Config.HbGather.HbSubUids) + } + }, + func(response interface{}) { + resp, ok := response.(market.TickerWebsocketResponse) + if ok { + if &resp != nil { + if resp.Tick != nil || resp.Data != nil { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Sender: Cli.Id, + Content: resp, + Symbol: resp.Channel}) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + applogger.Info("subscribeTicker data,ServersId:%v,Sender:%v,Content:%v-%v", resp.Channel, Cli.Id, resp.Tick, resp.Data) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, _ := range symbolList { + client.UnSubscribe(symbol, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Client closed") +} diff --git a/cmd/marketwsscliert/marketwssclient.go b/cmd/marketwsscliert/marketwssclient.go new file mode 100644 index 0000000..82cb470 --- /dev/null +++ b/cmd/marketwsscliert/marketwssclient.go @@ -0,0 +1,270 @@ +package marketwsscliert + +import ( + "encoding/json" + "github.com/gorilla/websocket" + "github.com/robfig/cron" + "time" + "wss-pool/cmd/common" + "wss-pool/cmd/websocketcollect/us" + "wss-pool/cmd/websocketservice" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal/data/business" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var Cli websocketservice.User + +// 数字币行情采集 +func RunHBDataRedis(checkData string) { + HbMarketSpots(checkData) + HbContract(checkData) +} + +// 火币现货行情数据 +func HbMarketSpots(checkData string) { + switch checkData { + case "subscribeDepth": // 市场深度行情数据 8863 + subscribeDepth(model.SymbolListString(dictionary.Depth)) + case "subscribeLevelMbp": // 市场深度MBP行情数据(增量推送)(150挡) 8864 + subscribeLevelMbp(model.SymbolListString([]string{})) + case "subscribeFullMbp": // 市场深度MBP行情数据(全量推送) 8865 + subscribeFullMbp(model.SymbolListInt(dictionary.LevelsRefresh)) + case "subscribeSubMbp": // 市场深度MBP行情数据(增量推送) 8866 + subscribeSubMbp(model.SymbolListInt(dictionary.LevelsMbp)) + case "subscribeBbo": // 买一卖一逐笔行情 8867 + subscribeBbo(model.SymbolListString([]string{})) + case "subscribeKLine": // K线数据 8868 + subscribeKLine(model.SymbolListString(dictionary.TimeCycle)) + case "subscribeTrade": // 成交明细 8869 + subscribeTrade(model.SymbolListString([]string{})) + case "subscribeLast24h": // 市场概要 8847 + subscribeLast24h(model.SymbolListString([]string{})) + case "subscribeTicker": // 聚合行情(Ticker) 8848 + subscribeTicker(model.SymbolListString([]string{})) + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 币本位永续合约行情数据 +func HbContract(checkData string) { + switch checkData { + case "subscribeCtKline": // k线数据 8841 + //加载配置数据 + go GetModifyContract() + subscribeCtKline(model.SymbolCtListString(dictionary.ContractTime)) + case "subscribeCtDepth": // 深度信息 8842 + subscribeCtDepth(model.SymbolCtListString(dictionary.ContractDepth)) + case "subscribeCtAddDepth": // 新增深度信息 8843 + subscribeCtAddDepth(model.SymbolCtListString(dictionary.ContractAddDepth)) + case "subscribeCtBbo": // 买一卖一行情数据 8844 + subscribeCtBbo(model.SymbolCtListString([]string{})) + case "subscribeCtDetail": // 合约详情数据 8845 + subscribeCtDetail(model.SymbolCtListString([]string{})) + case "subscribeCtTradeDetail": // 合约贸易详情数据 8846 + subscribeCtTradeDetail(model.SymbolCtListString([]string{})) + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 美股行情采集 +func ShareMarket(checkData string) { + // TODO: 不需要推送插针数据 + go business.NewPinStock(common.GetRedisNoPin(config.Config.Redis.NoPinAss)) + switch checkData { + case "usShare": // US + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + applogger.Info("Execute automatic subscription........") + if conn := subscribeFinnhub(); conn != nil { + subscribeMarketUsBak(conn) + } + } + } + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 美股行情采集分发 +func ShareMarketBak(checkData string) { + // TODO: 不需要推送插针数据 + go business.NewPinStock(common.GetRedisNoPin(config.Config.Redis.NoPinAss)) + switch checkData { + case "usShare": + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if conn := subscribeDispense("ws", 7777); conn != nil { + webSocketsWrite(conn, "us", "subscribe") + subscribeMarketUsBakNew(conn) + } + } + } + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 外汇行情采集分发 +func ForexMarketBak(checkData string) { + switch checkData { + case "forex": + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if conn := subscribeDispense("forexWs", 7778); conn != nil { + webSocketsWrite(conn, "forex", "subscribe") + pinInsertionRun() + subscribeMarketForexBakNew(conn) + } + } + } + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 外汇行情天采集分发 +func ForexMarketDayBak(checkData string) { + switch checkData { + case "forex": + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if conn := subscribeDispense("forexWs", 7778); conn != nil { + webSocketsWrite(conn, "forexDay", "subscribe") + pinInsertionDayRun() + subscribeMarketDayForexBakNew(conn) + } + } + } + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 外汇买一卖一报价采集分发 +func ForexMarketQuoteBak(checkData string) { + switch checkData { + case "forex": + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if conn := subscribeDispense("forexWs", 7778); conn != nil { + webSocketsWrite(conn, "quotes", "subscribe") + subscribeMarketForexQuoteBakNew(conn) + } + } + } + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 外汇成交报价采集分发 +func ForexMarketTradeBak(checkData string) { + switch checkData { + case "forex": + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if conn := subscribeDispense("forexWs", 7778); conn != nil { + webSocketsWrite(conn, "trade", "subscribe") + subscribeMarketForexTradeBakNew(conn) + } + } + } + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 外汇成交报价存储 +func ForexMarketTradeBak2(checkData string) { + switch checkData { + case "forex": + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if conn := subscribeDispense("forexWs", 7778); conn != nil { + webSocketsWrite(conn, "tradeStorage", "subscribe") + subscribeMarketForexTradeBak2New(conn) + } + } + } + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 外汇成交报价数据清理 +func ForexMarketClearTradeBak2(checkData string) { + switch checkData { + case "forex": + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + for { + select { + case <-ticker.C: + DeleteForexTrade() + } + } + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 股票分发推送 +func webSocketsWrite(conn *websocket.Conn, topic, content string) { + message := us.Message{ + Topic: topic, + Content: content, + } + msg, err := json.Marshal(message) + if err != nil { + applogger.Error("marshal:%v", err) + return + } + err = conn.WriteMessage(websocket.TextMessage, msg) + if err != nil { + applogger.Error("write:%v", err) + return + } +} + +// 外汇交易对更新闭盘价 +func ForexUpdateClosePrice() { + // 创建一个cron调度器 + c := cron.New() + // 添加任务,每天0点执行 + err := c.AddFunc("0 0 0 * * 1-5", func() { + ForexUpdateCode() + }) + if err != nil { + applogger.Error("", err) + } + // 启动cron调度器 + c.Start() + // 阻塞主线程,防止程序退出 + select {} +} diff --git a/cmd/marketwsscliert/marketwssclient_test.go b/cmd/marketwsscliert/marketwssclient_test.go new file mode 100644 index 0000000..e99dda3 --- /dev/null +++ b/cmd/marketwsscliert/marketwssclient_test.go @@ -0,0 +1,589 @@ +package marketwsscliert + +import ( + "encoding/json" + "fmt" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/shopspring/decimal" + "io/ioutil" + "log" + "math/rand" + "net/http" + "testing" + "time" + "wss-pool/cmd/common" + "wss-pool/internal" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +type ConstructParameters struct { + Trace string `json:"trace"` + Data struct { + Code string `json:"code"` + KlineType int `json:"kline_type"` + KlineTimestampEnd int `json:"kline_timestamp_end"` + QueryKlineNum int `json:"query_kline_num"` + AdjustType int `json:"adjust_type"` + } `json:"data"` +} +type KlinePostReturnStruct struct { + Ret int `json:"ret"` + Msg string `json:"msg"` + Trace string `json:"trace"` + Data struct { + KlineList []struct { + Code string `json:"code"` + KlineType int `json:"kline_type"` + KlineData []struct { + Timestamp string `json:"timestamp"` + OpenPrice string `json:"open_price"` + ClosePrice string `json:"close_price"` + HighPrice string `json:"high_price"` + LowPrice string `json:"low_price"` + Volume string `json:"volume"` + Turnover string `json:"turnover"` + } `json:"kline_data"` + } `json:"kline_list"` + } `json:"data"` +} +type KlineGetReturnStruct struct { + Ret int `json:"ret"` + Msg string `json:"msg"` + Trace string `json:"trace"` + Data struct { + Code string `json:"code"` + KlineType int `json:"kline_type"` + KlineList []struct { + Timestamp string `json:"timestamp"` + OpenPrice string `json:"open_price"` + ClosePrice string `json:"close_price"` + HighPrice string `json:"high_price"` + LowPrice string `json:"low_price"` + Volume string `json:"volume"` + Turnover string `json:"turnover"` + } `json:"kline_list"` + } `json:"data"` +} +type Symbol struct { + Code string `json:"code"` + DepthLevel int `json:"depth_level"` +} +type DataList struct { + SymbolList []Symbol `json:"symbol_list"` +} +type Request struct { + CmdID int `json:"cmd_id"` + SeqID int `json:"seq_id"` + Trace string `json:"trace"` + Data DataList `json:"data"` +} +type OrderBookOrTradeTick struct { + Trace string `json:"trace"` + Data struct { + SymbolList []SymbolList `json:"symbol_list"` + } `json:"data"` +} +type SymbolList struct { + Code string `json:"code"` +} +type DepthTradeReturnStruct struct { + Msg string `json:"msg"` + Trace string `json:"trace"` + Data struct { + TickList []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:"tick_list"` + } `json:"data"` +} +type TradeReturnStruct struct { + Msg string `json:"msg"` + Trace string `json:"trace"` + Data struct { + TickList []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"` + } `json:"tick_list"` + } `json:"data"` +} +type Results 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"` +} + +func GenerateParameters(trace, code string, kline_type, query_kline_num int) string { + queryStr := fmt.Sprintf("{'trace':'%v','data':{'data_list':[{'code':'%v','kline_type':%v,'kline_timestamp_end':0,'query_kline_num':%v,'adjust_type':0}]}}", trace, code, kline_type, query_kline_num) + return queryStr +} + +// GenerateRandomFloat64 在min和max之间生成一个随机浮点数 +func GenerateRandomFloat64(min, max float64) float64 { + return min + rand.Float64()*(max-min) +} + +// 将两个时间区间划分成指定数量的等分区间 +func TestTime(t *testing.T) { + // 假设这是从MongoDB获取的时间戳(以秒为单位) + timestamp := int64(1735064100000) + + // 将时间戳转换为time.Time类型 + t1 := time.Unix(timestamp, 0) + + // 格式化时间 + formattedTime := t1.Format(time.RFC3339) + + fmt.Println("时间戳:", timestamp) + fmt.Println("转换后的时间:", formattedTime) + return + layoutStart := "2024-12-17 17:00:00" + layoutEnd := "2024-12-17 17:09:00" + startT := common.TimeStringToTime(layoutStart) + endT := common.TimeStringToTime(layoutEnd) + applogger.Debug("start:%v,end:%v", startT, endT) + + startF := 2658.33000 + endF := 2652.33000 + numIntervals := 5 + msgTime := DivideTimeInterval(startT, endT, numIntervals) + msgFloat := DivideFloatInterval(startF, endF, numIntervals) + var msgTF = make(map[int]string) + // 打印每个区间的时间 + for i, mt := range msgTime { + mf, ok := msgFloat[i] + if ok { + msgTF[i] = fmt.Sprintf("%v,%v", mt, mf) + applogger.Debug("msgTF:%v,%v", i, msgTF[i]) + } + } +} + +func TestGetKline(t *testing.T) { + percentage := decimal.RequireFromString("1").Add(decimal.RequireFromString("1").Div(decimal.RequireFromString("10000"))) + startValue := 2652.33000 // 起点值 2652.33000 938.20000 0.58464 + endValue := 2658.33000 // 结束值 2658.33000 958.20000 0.68464 + maxValue := decimal.NewFromFloat(endValue).Mul(percentage).InexactFloat64() + + var currentValue = startValue + var currentValueS float64 + for { + if currentValue <= endValue { + currentValue *= percentage.InexactFloat64() + } else { + min := currentValue + currentValueS = RandomBetween(min, maxValue) + } + applogger.Debug("currentValue:%v,endSub:%v,Max:%v", currentValue, currentValueS, maxValue) + time.Sleep(1 * time.Second) + } + + //for currentValue := startValue; currentValue <= endValue; currentValue *= percentage.InexactFloat64() { + // applogger.Debug("currentValue:", currentValue) + //} +} +func TestGetKlineS(t *testing.T) { + percentage := decimal.RequireFromString("1").Add(decimal.RequireFromString("1").Div(decimal.RequireFromString("10000"))) + startValue := 2658.33000 // 起点值 + endValue := 2652.33000 // 结束值 + maxValue := decimal.NewFromFloat(endValue).Mul(percentage).InexactFloat64() + + var currentValue = startValue + var currentValueS float64 + for { + if currentValue <= startValue { + currentValue /= percentage.InexactFloat64() + // 变化值小于结束值 + if currentValue <= endValue { + currentValueS = RandomBetween(endValue, maxValue) + } + } + applogger.Debug("currentValue:%v,endSub:%v,Max:%v", currentValue, currentValueS, maxValue) + time.Sleep(1 * time.Second) + } +} +func TestMainList(t *testing.T) { + url := "wss://quote.tradeswitcher.com/quote-b-ws-api?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" + c, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + fmt.Println("dial:", err) + } + defer c.Close() + // Send heartbeat every 10 seconds + go func() { + for range time.NewTicker(10 * time.Second).C { + req := Request{ + CmdID: 22000, + SeqID: 123, + Trace: "3380a7a-3e1f-c3a5-5ee3-9e5be0ec8c241692805462", + Data: DataList{}, + } + messageBytes, err := json.Marshal(req) + if err != nil { + fmt.Println("json.Marshal error:", err) + return + } + applogger.Debug("req data:", string(messageBytes)) + + err = c.WriteMessage(websocket.TextMessage, messageBytes) + if err != nil { + fmt.Println("write:", err) + } + } + }() + req := Request{ + CmdID: 22002, + SeqID: 123, + Trace: uuid.New().String(), + Data: DataList{SymbolList: []Symbol{ + //{"GOLD", 5}, + //{"AAPL.US", 5}, + //{"700.HK", 5}, + //{"GOLD", 1}, + //{"Silver", 1}, + {"GOLD", 1}, + //{"Silver", 1}, + }}, + //Data: dataList, + } + messageBytes, err := json.Marshal(req) + if err != nil { + applogger.Debug("json.Marshal error:", err) + return + } + applogger.Debug("req data:", string(messageBytes)) + + err = c.WriteMessage(websocket.TextMessage, messageBytes) + if err != nil { + fmt.Println("write:", err) + } + + rece_count := 0 + for { + _, message, err := c.ReadMessage() + if err != nil { + applogger.Debug("read:", err) + break + } else { + var messageToJson Results + if err = json.Unmarshal(message, &messageToJson); err != nil { + applogger.Error("SendAllClientTradeSwitcher err:%v", err) + } + switch messageToJson.CmdID { + case 22001: // 心跳 + // {"ret":200,"msg":"ok","cmd_id":22001,"seq_id":123,"trace":"3380a7a-3e1f-c3a5-5ee3-9e5be0ec8c241692805462"} + applogger.Info("Heartbeat results:%v", string(message)) + 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"}]}} + applogger.Info("Received message:%v", string(message)) + default: + applogger.Info("Received message err:%v", string(message)) + } + } + rece_count++ + if rece_count%10000 == 0 { + fmt.Println("count:", rece_count, " Received message:", string(message)) + } + } +} +func TestMainLists(t *testing.T) { + url := "wss://quote.tradeswitcher.com/quote-b-ws-api?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" + c, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + fmt.Println("dial:", err) + } + defer c.Close() + // Send heartbeat every 10 seconds + go func() { + for range time.NewTicker(10 * time.Second).C { + req := Request{ + CmdID: 22000, + SeqID: 123456, + Trace: "3380a7a-3e1f-c3a5-5ee3-9e5be0ec8c241692805462787878", + Data: DataList{}, + } + messageBytes, err := json.Marshal(req) + if err != nil { + fmt.Println("json.Marshal error:", err) + return + } + applogger.Debug("req data:", string(messageBytes)) + + err = c.WriteMessage(websocket.TextMessage, messageBytes) + if err != nil { + fmt.Println("write:", err) + } + } + }() + req := Request{ + CmdID: 22004, + SeqID: 123456, + Trace: uuid.New().String(), + Data: DataList{SymbolList: []Symbol{ + //{"GOLD", 5}, + //{"AAPL.US", 5}, + //{"700.HK", 5}, + //{"GOLD", 1}, + //{"Silver", 1}, + {"GOLD", 1}, + //{"Silver", 1}, + }}, + //Data: dataList, + } + messageBytes, err := json.Marshal(req) + if err != nil { + fmt.Println("json.Marshal error:", err) + return + } + err = c.WriteMessage(websocket.TextMessage, messageBytes) + if err != nil { + fmt.Println("write:", err) + } + + rece_count := 0 + for { + _, msg, err := c.ReadMessage() + if err != nil { + fmt.Println("read:", err) + break + } else { + var messageToJson ResultsTrade + if err = json.Unmarshal(msg, &messageToJson); err != nil { + applogger.Error("json.Unmarshal error:%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 error:%v", err) + time.Sleep(5 * time.Second) + continue + } + applogger.Info("Message processing result:%v", string(msgStr)) + default: + applogger.Debug("ReadMessage data info:%v", string(msg)) + } + } + rece_count++ + if rece_count%10000 == 0 { + fmt.Println("count:", rece_count, " Received message:", string(msg)) + } + } +} +func TestGet(t *testing.T) { + urlStr := "https://quote.tradeswitcher.com/quote-b-api/kline" + log.Println("请求内容:", urlStr) + + req, err := http.NewRequest("GET", urlStr, nil) + if err != nil { + fmt.Println("Error creating request:", err) + return + } + q := req.URL.Query() + q.Add("token", "bf8f33c446c4494286eccaa57a2e6fac-c-app") + var query ConstructParameters + query.Trace = uuid.New().String() + query.Data.Code = "AUDUSD" + query.Data.KlineType = 8 + query.Data.KlineTimestampEnd = 0 + query.Data.QueryKlineNum = 3 + query.Data.AdjustType = 0 + byteStr, err := json.Marshal(&query) + if err != nil { + return + } + q.Add("query", string(byteStr)) + req.URL.RawQuery = q.Encode() + // 发送请求 + resp, err := http.DefaultClient.Do(req) + if err != nil { + fmt.Println("Error sending request:", err) + return + } + defer resp.Body.Close() + + bodyStr, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println("读取响应失败:", err) + return + } + + var klineNew KlineGetReturnStruct + if err = json.Unmarshal(bodyStr, &klineNew); err != nil { + log.Println("解析失败:", err) + return + } + log.Println("响应内容:", klineNew) +} +func TestPost(t *testing.T) { + for { + UrlBatchKline := "https://quote.tradeswitcher.com/quote-b-api/batch-kline?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" + bodyStr, err := internal.HttpPost(UrlBatchKline, GenerateParameters(uuid.New().String(), "GOLD", 1, 1)) + if err != nil { + log.Println("读取响应失败:", err) + return + } + var klineNew KlinePostReturnStruct + if err = json.Unmarshal([]byte(bodyStr), &klineNew); err != nil { + log.Println("解析失败:", err) + return + } + //applogger.Info("数据信息:%v", klineNew) + ClosePrice := klineNew.Data.KlineList[0].KlineData[0].ClosePrice + OpenPrice := klineNew.Data.KlineList[0].KlineData[0].OpenPrice + HighPrice := klineNew.Data.KlineList[0].KlineData[0].HighPrice + LowPrice := klineNew.Data.KlineList[0].KlineData[0].LowPrice + Volume := klineNew.Data.KlineList[0].KlineData[0].Volume + Timestamp := klineNew.Data.KlineList[0].KlineData[0].Timestamp + Turnover := klineNew.Data.KlineList[0].KlineData[0].Turnover + applogger.Debug("闭盘价:%v,开盘价:%v,最高价:%v,最低价:%v,交易量:%v,交易时间:%v,交易金额:%v", ClosePrice, OpenPrice, HighPrice, LowPrice, Volume, Timestamp, Turnover) + + time.Sleep(time.Second * 1) + } +} +func TestGetOrderDepth(t *testing.T) { + urlStr := "https://quote.tradeswitcher.com/quote-b-api/depth-tick" + log.Println("请求内容:", urlStr) + + req, err := http.NewRequest("GET", urlStr, nil) + if err != nil { + fmt.Println("Error creating request:", err) + return + } + q := req.URL.Query() + q.Add("token", "bf8f33c446c4494286eccaa57a2e6fac-c-app") + var query OrderBookOrTradeTick + query.Trace = uuid.New().String() + query.Data.SymbolList = []SymbolList{ + {Code: "GOLD"}, + } + + byteStr, err := json.Marshal(&query) + if err != nil { + return + } + q.Add("query", string(byteStr)) + req.URL.RawQuery = q.Encode() + // 发送请求 + resp, err := http.DefaultClient.Do(req) + if err != nil { + fmt.Println("Error sending request:", err) + return + } + defer resp.Body.Close() + + bodyStr, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println("读取响应失败:", err) + return + } + + var klineNew DepthTradeReturnStruct + if err = json.Unmarshal(bodyStr, &klineNew); err != nil { + log.Println("解析失败:", err) + return + } + log.Println("响应内容:", klineNew) +} +func TestGetOrderTrade(t *testing.T) { + urlStr := "https://quote.tradeswitcher.com/quote-b-api/trade-tick" + log.Println("请求内容:", urlStr) + + req, err := http.NewRequest("GET", urlStr, nil) + if err != nil { + fmt.Println("Error creating request:", err) + return + } + q := req.URL.Query() + q.Add("token", "bf8f33c446c4494286eccaa57a2e6fac-c-app") + var query OrderBookOrTradeTick + query.Trace = uuid.New().String() + query.Data.SymbolList = []SymbolList{ + {Code: "GOLD"}, + } + + byteStr, err := json.Marshal(&query) + if err != nil { + return + } + q.Add("query", string(byteStr)) + req.URL.RawQuery = q.Encode() + // 发送请求 + resp, err := http.DefaultClient.Do(req) + if err != nil { + fmt.Println("Error sending request:", err) + return + } + defer resp.Body.Close() + + bodyStr, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println("读取响应失败:", err) + return + } + + var klineNew TradeReturnStruct + if err = json.Unmarshal(bodyStr, &klineNew); err != nil { + log.Println("解析失败:", err) + return + } + log.Println("响应内容:", klineNew) +} diff --git a/cmd/marketwsscliert/modifycontract.go b/cmd/marketwsscliert/modifycontract.go new file mode 100644 index 0000000..14ac00a --- /dev/null +++ b/cmd/marketwsscliert/modifycontract.go @@ -0,0 +1,217 @@ +package marketwsscliert + +import ( + "fmt" + "github.com/shopspring/decimal" + "math/rand" + "strings" + "sync" + "time" + "wss-pool/cmd/common" + "wss-pool/cmd/selfContract" + "wss-pool/config" + "wss-pool/internal/data" + "wss-pool/internal/model" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/market" +) + +const ( + proportion int32 = 10000 //调价原价比例 + numPrices int = 60 * 5 //生成随机价格数量 +) + +var ( + ModifyContractMap = make(map[string]ModifyContract) + mu = sync.Mutex{} + UpdatePrice = make(map[string]decimal.Decimal) +) + +type ModifyContract struct { + ContractCode string `json:"ContractCode"` //合约 + BeginTime string `json:"BeginTime"` //开始时间 + EndTime string `json:"EndTime"` //结束时间 + Price decimal.Decimal `json:"Prices"` //价格 + Prices string `json:"Price"` //价格 + Digits int `json:"Digits"` //保留几位小数 + Step int `json:"Step"` // 浮点数 + BeginUnix int64 + EndUnix int64 +} + +func SetValue(key string, value ModifyContract) { + mu.Lock() + defer mu.Unlock() + ModifyContractMap[key] = value +} +func GetValue(key string) (ModifyContract, bool) { + mu.Lock() + defer mu.Unlock() + value, ok := ModifyContractMap[key] + return value, ok +} +func getData() []model.ContractMarket { + contract := model.NewContractMarket() + result := contract.ListModifyContract() + return result +} + +// 加载配置数据 +func GetModifyContract() { + data.InitGorm(config.Config.Bourse) + for { + t := time.NewTimer(10 * time.Second) + <-t.C + result := getData() + for _, v := range result { + end, _ := common.TimeStrToTimes(v.EndTime) + if end.Unix() < time.Now().Unix() { + applogger.Error("该调价已过期") + continue + } + price, err := decimal.NewFromString(v.MaxPrice) + if price.IsZero() { + applogger.Error("价格有误", price) + continue + } + begin, err := common.TimeStrToTimestamp(v.BeginTime) + if err != nil { + applogger.Error("begin err", err) + continue + } + ends, err := common.TimeStrToTimestamp(v.EndTime) + if err != nil { + applogger.Info("end err", err) + continue + } + //判断当前合约 是否有任务还在执行 + if mapVal, ok := GetValue(v.TradeName); ok { + if mapVal.EndUnix >= time.Now().Unix() { + applogger.Info(v.TradeName, " is run") + continue + } + } + SetValue(v.TradeName, ModifyContract{ + ContractCode: v.TradeName, + BeginTime: v.BeginTime, + EndTime: v.EndTime, + Price: price, + Digits: v.KeepDecimal, + Step: v.Step, + BeginUnix: begin, + EndUnix: ends, + }) + contract := model.NewContractMarket() + contract.ID = v.ID + contract.UpdateIsGetOne() + } + } +} + +// 合约插针 +func RunModify(param market.SubscribeCtKlineResponse) market.SubscribeCtKlineResponse { + countSplit := strings.Split(param.Channel, ".") + if len(countSplit) < 2 { + return param + } + val, ok := GetValue(countSplit[1]) + if !ok { + return param + } + //不在设置的时间区间范围 过滤 ,因k线间隔 不同 id误差大,使用ts + if (val.BeginUnix)*1000 > param.Timestamp || (val.EndUnix*1000) < param.Timestamp { + return param + } + key := fmt.Sprintf("%s-%s", param.Tick.Close.String(), countSplit[1]) + var price decimal.Decimal + applogger.Info("old", param.Tick.Close) + tick := param.Tick + //保持各种K线 落盘价一致 问题 + price, ok = UpdatePrice[key] + if !ok { + fmt.Println("new data") + price = calculateContractPrice(val.Price, int32(val.Step), int32(val.Digits)) + UpdatePrice[key] = price + } + go clearPrice(key) + tick.Close = price + if price.GreaterThan(tick.High) { + tick.High = price + } + if price.LessThan(tick.Low) { + tick.Low = price + } + applogger.Info("new", tick.Close, "channel", param.Channel) + var timestamp int64 + var open decimal.Decimal + expot := countSplit[len(countSplit)-1] + switch expot { + case "1min": + timestamp = common.GenerateSingaporeMinuteTimestamp() + open = GetContractOpen(timestamp, expot, countSplit[1]) + case "5min": + timestamp = common.GenerateSingaporeFiveMinTimestamp() + open = GetContractOpen(timestamp, expot, countSplit[1]) + case "15min": + timestamp = common.GenerateSingaporeFifteenMinTimestamp() + open = GetContractOpen(timestamp, expot, countSplit[1]) + case "30min": + timestamp = common.GenerateSingaporeThirtyMinTimestamp() + open = GetContractOpen(timestamp, expot, countSplit[1]) + case "60min": + timestamp = common.GenerateSingaporeHourTimestamp() + open = GetContractOpen(timestamp, expot, countSplit[1]) + case "4hour": + timestamp = common.GenerateSingaporeFourHourTimestamp() - (4 * 60 * 60) + open = GetContractOpen(timestamp, expot, countSplit[1]) + case "1day": + timestamp = common.GenerateSingaporeDayTimestamp("") + open = GetContractOpen(timestamp, expot, countSplit[1]) + case "1week": + timestamp = common.GetWeekTimestamp() + open = GetContractOpen(timestamp, expot, countSplit[1]) + case "1mon": + timestamp = common.GenerateSingaporeMonTimestamp() + open = GetContractOpen(timestamp, expot, countSplit[1]) + } + tick.Open = open + param.Tick = tick + return param +} + +func GetContractOpen(timestamp int64, period, contract string) decimal.Decimal { + var open decimal.Decimal + tick := selfContract.GetNewPriceAll(contract, period) + if len(tick) > 0 { + switch tick[0].Code { + case timestamp: + open, _ = decimal.NewFromString(tick[0].Open) + default: + open, _ = decimal.NewFromString(tick[0].Close) + } + } + return open +} + +func clearPrice(close string) { + if len(UpdatePrice) >= 10 { + for key := range UpdatePrice { + if key != close { + delete(UpdatePrice, key) + break + } + } + } +} + +// 计算虚拟合约价格 +func calculateContractPrice(basePrice decimal.Decimal, step int32, digits int32) decimal.Decimal { + rand.New(rand.NewSource(time.Now().UnixNano())) + max := basePrice.Mul(decimal.NewFromFloat(float64(step) / float64(proportion))).Round(digits) + min := max.Neg() + return basePrice.Add(generateRandomStep(max, min)).Round(digits) +} + +func generateRandomStep(min, max decimal.Decimal) decimal.Decimal { + return min.Add(decimal.NewFromFloat(rand.Float64()).Mul(max.Sub(min))) +} diff --git a/cmd/marketwsscliert/modifyforex.go b/cmd/marketwsscliert/modifyforex.go new file mode 100644 index 0000000..61b6e24 --- /dev/null +++ b/cmd/marketwsscliert/modifyforex.go @@ -0,0 +1,421 @@ +package marketwsscliert + +import ( + "fmt" + "github.com/shopspring/decimal" + "math/rand" + "strconv" + "strings" + "sync" + "time" + "wss-pool/cmd/common" + "wss-pool/config" + "wss-pool/internal/data" + "wss-pool/internal/model" + "wss-pool/logging/applogger" + + models "wss-pool/pkg/model" +) + +var ( + conversion_forex = decimal.RequireFromString("1") + proportion_forex = decimal.RequireFromString("1000000") +) + +var ( + muForex = sync.Mutex{} + ModifyForexMap = make(map[string]ModifyForex) +) + +type ModifyForex struct { + ForexCode string `json:"ForexCode"` // 外汇 + BeginTime string `json:"BeginTime"` // 开始时间-date + EndTime string `json:"EndTime"` // 结束时间-date + Price decimal.Decimal `json:"Prices"` // 设置价格 + ChangePrice decimal.Decimal `json:"ChangePrice"` // 插针变化价格(开盘价,初始浮点变化区间起始价) - (初始浮点变化区间结束价-动态直至不在变化) - (浮点变化区间结束后的起始价格) + EndPrice decimal.Decimal `json:"EndPrice"` // 浮点变化区间结束后的结束价格 + Proportion decimal.Decimal `json:"Proportion"` // 浮动率 + CheckBool bool `json:"CheckBool"` // 判断是负增长还是正增长 + Digits int `json:"Digits"` // 保留几位小数 + Step int `json:"Step"` // 浮点数 + BeginUnix int64 `json:"BeginUnix"` // 开始时间-unix + EndUnix int64 `json:"EndUnix"` // 结算时间-unix + RestoreChangePrice decimal.Decimal `json:"RestorePrice"` // 插针恢复变化价格(开盘价,初始浮点变化区间起始价) - (初始浮点变化区间结束价-动态直至不在变化) - (浮点变化区间结束后的起始价格) + RestoreBeginTime string `json:"RestoreBeginTime"` // 插针恢复开始时间-date + RestoreEndTime string `json:"RestoreEndTime"` // 插针恢复结束时间-date + RestoreBeginUnix int64 `json:"RestoreBeginUnix"` // 插针恢复开始时间-unix + RestoreEndUnix int64 `json:"RestoreEndUnix"` // 插针恢复结束时间-unix + RestoreBool bool `json:"RestoreBool"` // 插针恢复 + RestoreSetChangePrice bool `json:"RestoreSetChangePrice"` // 插针恢复更新 + DataHandling map[int]string `json:"DataHandling"` // 数据处理-需要初始化 + NumIntervals int `json:"NumIntervals"` // 数据处理-间隔次数 +} + +func SetValue_Forex(key string, value ModifyForex) { + muForex.Lock() + defer muForex.Unlock() + ModifyForexMap[key] = value +} +func GetValue_Forex(key string) (ModifyForex, bool) { + muForex.Lock() + defer muForex.Unlock() + value, ok := ModifyForexMap[key] + return value, ok +} +func getData_Forex() []model.ForexMarket { + forex := model.NewForexMarket() + result := forex.ListModifyForex() + return result +} + +// 外汇加载交易对插针列表 +func GetModifyForex() { + data.InitGorm(config.Config.Bourse) + for { + t := time.NewTimer(10 * time.Second) + <-t.C + result := getData_Forex() + applogger.Debug("加载需要插针的交易对设置:%v", result) + for _, v := range result { + end, _ := common.TimeStrToTimes(v.EndTime) + if end.Unix() < time.Now().Unix() { + applogger.Error("该调价已过期......") + delete(ModifyForexMap, v.TradeName) + forex := model.NewForexMarket() + forex.ID = v.ID + forex.UpdateIsGetOne() + continue + } + price, err := decimal.NewFromString(v.MaxPrice) + if price.IsZero() { + applogger.Error("price err:%v", price) + continue + } + // 插针设置 + begin, err := common.TimeStrToTimestamp(v.BeginTime) + if err != nil { + applogger.Error("begin err:%v", err) + continue + } + ends, err := common.TimeStrToTimestamp(v.EndTime) + if err != nil { + applogger.Info("end err:%v", err) + continue + } + // 插针恢复 + restoreBegin, err := common.TimeStrAddFiveTime(v.BeginTime) + if err != nil { + applogger.Error("restoreBegin err:%v", err) + continue + } + restoreEnds, err := common.TimeStrAddFiveTime(v.EndTime) + if err != nil { + applogger.Info("restoreEnds err:%v", err) + continue + } + restoreBeginUnix, err := common.TimeStrToTimestamp(restoreBegin) + if err != nil { + applogger.Error("restoreBeginUnix err:%v", err) + continue + } + restoreEndsUnix, err := common.TimeStrToTimestamp(restoreEnds) + if err != nil { + applogger.Error("restoreEndsUnix err:%v", err) + continue + } + + //判断当前外汇,是否有任务还在执行 + if mapVal, ok := GetValue_Forex(v.TradeName); ok { + if mapVal.EndUnix >= time.Now().Unix() { + applogger.Info(v.TradeName, " is run") + continue + } + } + // 第二次浮点随机数值的端点值(结束浮点) + proportion_value := conversion_forex.Add(decimal.NewFromInt32(int32(v.Step)).Div(proportion_forex)) + // 写入缓存中 + SetValue_Forex(v.TradeName, ModifyForex{ + ForexCode: v.TradeName, + BeginTime: v.BeginTime, // 设置开始时间 + EndTime: v.EndTime, // 设置结束时间 + Price: price, // 设置价格 + ChangePrice: decimal.Zero, // 插针变化价格(开盘价,初始浮点变化区间起始价) - (初始浮点变化区间结束价-动态直至不在变化) - (浮点变化区间结束后的起始价格) + EndPrice: decimal.Zero, // 浮点变化区间结束后的结束价格 + Proportion: proportion_value, // 区间浮动率 + CheckBool: false, + Digits: v.KeepDecimal, + Step: v.Step, + BeginUnix: begin, // 插针开始时间 + EndUnix: ends, // 插针结束时间 + RestoreBeginTime: restoreBegin, // 插针恢复开始时间-date + RestoreEndTime: restoreEnds, // 插针恢复结束时间-date + RestoreBeginUnix: restoreBeginUnix, // 插针恢复开始时间-unix + RestoreEndUnix: restoreEndsUnix, // 插针恢复结束时间-unix + RestoreBool: false, // 插针恢复标识 + RestoreSetChangePrice: false, // 插针恢复更新 + RestoreChangePrice: decimal.Zero, // 插针恢复变化价格(开盘价,初始浮点变化区间起始价) - (初始浮点变化区间结束价-动态直至不在变化) - (浮点变化区间结束后的起始价格) + DataHandling: make(map[int]string), + NumIntervals: 5, + }) + forex := model.NewForexMarket() + forex.ID = v.ID + forex.UpdateIsGetOne() + } + } +} + +// 外汇插针 +func RunModifyForex(param models.ForexJsonData) (models.ForexJsonData, bool) { + // 缓存中插针的交易对信息 + val, ok := GetValue_Forex(param.Pair) + if !ok { + return param, false + } + // 不在设置的时间区间范围过滤 + if val.BeginUnix > param.Timestamp || val.EndUnix < param.Timestamp { + return param, false + } + applogger.Debug("交易对%v,原始数据:%v", param.Pair, param) + + // 数据赋值 + changePrice_New := val.ChangePrice + price_new := val.Price + endPrice_new := val.EndPrice + checkBool := val.CheckBool + + // 第一次浮点区间的起始价 + if val.ChangePrice.IsZero() { + changePrice_New = decimal.NewFromFloat(param.Close) // k线闭盘价(即浮点起始价) + endPrice_new = price_new.Mul(val.Proportion) // 第二次浮点结束价 + if changePrice_New.Cmp(price_new) >= 1 { + checkBool = true + } + } + // 平滑价格生成(价格趋向结束价) + var price decimal.Decimal + if checkBool { + // 负增长 + if changePrice_New.Cmp(price_new) >= 0 { + changePrice_New = changePrice_New.Div(val.Proportion) + price = changePrice_New + // 更新缓存变量字段 + val.ChangePrice = changePrice_New + val.Price = price_new + val.EndPrice = endPrice_new + val.CheckBool = checkBool + SetValue_Forex(val.ForexCode, val) + } else { + randomPrice := RandomBetween(price_new.Div(val.Proportion).InexactFloat64(), endPrice_new.InexactFloat64()) + price = decimal.NewFromFloat(randomPrice) + } + } else { + // 正增长 + if changePrice_New.Cmp(price_new) <= 0 { + changePrice_New = changePrice_New.Mul(val.Proportion) + price = changePrice_New + // 更新缓存变量字段 + val.ChangePrice = changePrice_New + val.Price = price_new + val.EndPrice = endPrice_new + val.CheckBool = checkBool + SetValue_Forex(val.ForexCode, val) + } else { + randomPrice := RandomBetween(changePrice_New.Div(val.Proportion).InexactFloat64(), endPrice_new.InexactFloat64()) + price = decimal.NewFromFloat(randomPrice) + } + } + + param.Open = changePrice_New.InexactFloat64() + param.Close = price.InexactFloat64() + if price.GreaterThan(decimal.NewFromFloat(param.High)) { + param.High = price.InexactFloat64() + } + if price.LessThan(decimal.NewFromFloat(param.Low)) { + param.Low = price.InexactFloat64() + } + + forexJson := models.ForexJsonData{ + Event: "CAS", + Pair: param.Pair, + Open: param.Open, + Close: param.Close, + High: param.High, + Low: param.Low, + Volume: param.Volume, + Timestamp: param.Timestamp, + } + applogger.Debug("交易对%v,插针数据:%v", param.Pair, forexJson) + + return forexJson, true +} +func RunModifyForexNew(param models.ForexJsonData) (models.ForexJsonData, bool) { + // 缓存中插针的交易对信息 + val, ok := GetValue_Forex(param.Pair) + if !ok { + return param, false + } + // 不在设置的时间区间范围过滤 + if val.BeginUnix > param.Timestamp || val.EndUnix < param.Timestamp { + if val.RestoreBeginUnix > param.Timestamp || val.RestoreEndUnix < param.Timestamp || val.RestoreBool { + return param, false + } else { + applogger.Debug("恢复交易对%v,原始数据:%v", param.Pair, param) + if val.RestoreChangePrice.IsZero() { + var startT, endT time.Time + var startF, endF float64 + startT = common.TimeStringToTime(val.RestoreBeginTime) // 设置插针开始时间 + endT = common.TimeStringToTime(val.RestoreEndTime) // 设置插针结束时间 + msgTime := DivideTimeInterval(startT, endT, val.NumIntervals) // 时间区间划分 + // 判定插针结束价格和当前即时价格 + if param.Close == val.ChangePrice.InexactFloat64() { + val.RestoreBool = true + SetValue_Forex(val.ForexCode, val) + return param, false + } else { + startF = val.ChangePrice.InexactFloat64() // 插针结束价格 + endF = param.Close // 恢复即时价格 + } + applogger.Debug("开始价格:%v,结束价格:%v", startF, endF) + + msgFloat := DivideFloatInterval(startF, endF, val.NumIntervals) // 浮点区间划分 + var msgTF = make(map[int]string) + for i, mt := range msgTime { + mf, okM := msgFloat[i] + if okM { + msgTF[i] = fmt.Sprintf("%v,%v", mt, mf) + applogger.Debug("msgTF:%v,%v", i, msgTF[i]) + } + } + val.DataHandling = msgTF + val.RestoreSetChangePrice = true + } + } + } else { + applogger.Debug("插针交易对%v,原始数据:%v", param.Pair, param) + // 第一次浮点区间的起始价 + if val.ChangePrice.IsZero() { + var startT, endT time.Time + var startF, endF float64 + val.ChangePrice = decimal.NewFromFloat(param.Close) // k线闭盘价(即浮点起始价) + startT = common.TimeStringToTime(val.BeginTime) // 设置插针开始时间 + endT = common.TimeStringToTime(val.EndTime) // 设置插针结束时间 + msgTime := DivideTimeInterval(startT, endT, val.NumIntervals) // 时间区间划分 + + startF = param.Close // 设置插针开始价格 + endF = val.Price.InexactFloat64() // 设置插针结束价格 + + applogger.Debug("开始价格:%v,结束价格:%v", startF, endF) + + msgFloat := DivideFloatInterval(startF, endF, val.NumIntervals) // 浮点区间划分 + var msgTF = make(map[int]string) + for i, mt := range msgTime { + mf, okM := msgFloat[i] + if okM { + msgTF[i] = fmt.Sprintf("%v,%v", mt, mf) + applogger.Debug("msgTF:%v,%v", i, msgTF[i]) + } + } + val.DataHandling = msgTF + } + } + + /* 浮点区间价格随机生成浮点值 + 1、判定当前时间戳在区间内 + 2、在区间内则生成对应的浮点值 + */ + var price decimal.Decimal + for _, msg := range val.DataHandling { + // msgTF:0,1734426000-1734426108,2652.33-2653.5299999999997 + splitMsg := strings.Split(msg, ",") + if len(splitMsg) >= 2 { + splitT := strings.Split(splitMsg[0], "-") // 开始时间~结束时间 1734426000-1734426108 + if len(splitT) >= 2 { + start, _ := strconv.Atoi(splitT[0]) // 开始时间 1734426000 + end, _ := strconv.Atoi(splitT[1]) // 结束时间 1734426108 + if param.Timestamp >= int64(start) && param.Timestamp <= int64(end) { + splitF := strings.Split(splitMsg[1], "-") // 开始浮点~结束浮点 2652.33-2653.5299999999997 + if len(splitF) >= 2 { + sf := decimal.RequireFromString(splitF[0]) // 开始浮点 2652.33 + ef := decimal.RequireFromString(splitF[1]) // 结束浮点 2653.5299999999997 + randomPrice := RandomBetween(sf.InexactFloat64(), ef.InexactFloat64()) + price = decimal.NewFromFloat(randomPrice) + val.ChangePrice = price + if val.RestoreSetChangePrice { + val.RestoreChangePrice = price + } + SetValue_Forex(val.ForexCode, val) + } + break // 跳出循环 + } + } + } + } + + param.Close = price.InexactFloat64() + param.Open = val.ChangePrice.InexactFloat64() + if price.GreaterThan(decimal.NewFromFloat(param.High)) { + param.High = price.InexactFloat64() + } + if price.LessThan(decimal.NewFromFloat(param.Low)) { + param.Low = price.InexactFloat64() + } + + forexJson := models.ForexJsonData{ + Event: "CAS", + Pair: param.Pair, + Open: param.Open, + Close: param.Close, + High: param.High, + Low: param.Low, + Volume: param.Volume, + Timestamp: param.Timestamp, + } + applogger.Debug("交易对%v,插针数据:%v", param.Pair, forexJson) + + return forexJson, true +} + +// 在a和b之间生成平滑随机值 +func RandomBetween(a, b float64) float64 { + // 确保a小于b + if a > b { + a, b = b, a + } + // 计算范围 + rangeVal := (b - a) + // 在范围内生成随机数 + return rand.Float64()*rangeVal + a +} + +// 时间区间 +func DivideTimeInterval(start, end time.Time, numIntervals int) map[int]string { + // 计算两个时间点之间的间隔 + duration := end.Sub(start) + // 计算每个区间的持续时间 + intervalDuration := duration / time.Duration(numIntervals) + // 生成区间起始时间列表 + var msg = make(map[int]string) + for i := 0; i <= numIntervals; i++ { + subStart := start.Add(time.Duration(i) * intervalDuration).Unix() + subEnd := start.Add(time.Duration(i+1) * intervalDuration).Unix() + msg[i] = fmt.Sprintf("%v-%v", subStart, subEnd) + applogger.Debug("时间数据展示:%v,%v", i, msg[i]) + } + return msg +} + +// 浮点区间 +func DivideFloatInterval(start, end float64, numIntervals int) map[int]string { + // 计算每个子区间的宽度 + width := (end - start) / float64(numIntervals) + // 生成区间起始时间列表 + var msg = make(map[int]string) + // 生成并打印子区间 + for i := 0; i <= numIntervals; i++ { + subStart := start + width*float64(i) + subEnd := start + width*float64(i+1) + msg[i] = fmt.Sprintf("%v-%v", decimal.NewFromFloat(subStart), decimal.NewFromFloat(subEnd)) + applogger.Debug("浮点数据展示:%v,%v", i, msg[i]) + } + return msg +} diff --git a/cmd/selfContract/aggregation.go b/cmd/selfContract/aggregation.go new file mode 100644 index 0000000..8d46d94 --- /dev/null +++ b/cmd/selfContract/aggregation.go @@ -0,0 +1,517 @@ +package selfContract + +import ( + "encoding/json" + "fmt" + "github.com/shopspring/decimal" + "math/rand" + "time" + "wss-pool/cmd/common" + "wss-pool/cmd/websocketservice" + "wss-pool/internal/gzip" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/market" + "wss-pool/pkg/model/stock" +) + +const ( + volRand int = 1 //成交张数 随机最大值 + maxRand float64 = 1000 //成交笔数 随机最大值 + minRand float64 = 0.1 + minLeastRand float64 = 1 + quantity int = 60 //成交币 + tradeNum int = 50 + DepthNum int = 80 //深度数据只要 + DepthMaxNum int = 300 //深度数据只要 + defaultMaxStep float64 = 0.003 + defaultMinStep float64 = 0.000001 +) + +var ( + TotalAmount decimal.Decimal // 当前总成交量 + TotalTradeTurnover decimal.Decimal //总成交额 + TotalVol decimal.Decimal //总成交两张数 + TotalCount decimal.Decimal //总成交笔数 + oldAsks int64 + oldBids int64 +) + +func ClearTotal() { + TotalAmount = decimal.NewFromFloat(0) + TotalTradeTurnover = decimal.NewFromFloat(0) + TotalVol = decimal.NewFromFloat(0) + TotalCount = decimal.NewFromFloat(0) +} + +func CreateDepth(old, closePrice decimal.Decimal) { + chStep6 := fmt.Sprintf("market-%s-depth-step6", SelfContractCode) + //redisStep6, _ := red.Get_Cache_Data(chStep6) + //随机生成单量 + rand.New(rand.NewSource(time.Now().UnixNano())) + tick := market.CtDepthTick{ + Mrid: rand.Int63n(99999999999) + int64(99999), + Id: common.TimeToNow(), + Ts: common.TimeToNow(), + } + //卖单,买单 + tick.Asks, tick.Bids = DepthStep(closePrice, old) + //applogger.Info("asks ", tick.Asks, tick.Bids) + CreateTradeDetail(tick) + //升序 + tick.Asks = insertionSort(tick.Asks) + //降序 + tick.Bids = quickSort(tick.Bids) + res := market.SubscribeCtDepthResponse{ + Channel: chStep6, + Timestamp: common.TimeToNow(), + Tick: &tick, + } + resByte, _ := json.Marshal(res) + resStep6, _ := gzip.DecompressData(resByte) + OneDayContractDepth(res) + red.RedisClient.Publish(res.Channel, resStep6) + +} + +// 卖单 买单 +func DepthStep(closePrice, old decimal.Decimal) ([][]decimal.Decimal, [][]decimal.Decimal) { + data := make([][]decimal.Decimal, 0) + datas := make([][]decimal.Decimal, 0) + if old.Equal(closePrice) { + return data, datas + } + amaxRand := float64(0) + bmaxRand := float64(0) + //跌 + if old.GreaterThan(closePrice) { + diff := old.Sub(closePrice) + amaxRand, _ = diff.Div(old).Mul(decimal.NewFromFloat(maxRand)).Round(2).Float64() + bmaxRand = amaxRand / float64(3) + } else { + diff := closePrice.Sub(old) + bmaxRand, _ = diff.Div(old).Mul(decimal.NewFromFloat(maxRand)).Round(2).Float64() + amaxRand = bmaxRand / float64(3) + } + asksMap := make(map[string]int) + bidsMap := make(map[string]int) + minRands := minRand + if closePrice.LessThan(decimal.NewFromInt(int64(1))) { + minRands = minLeastRand + } + for i := 0; i < DepthNum; i++ { + item := make([]decimal.Decimal, 0) + price := calculateMaxMinContractPrice(closePrice, false) + if _, ok := asksMap[price.String()]; ok { + continue + } + item = append(item, price) + item = append(item, decimal.NewFromFloat(common.RandFloats(minRands, amaxRand))) + data = append(data, item) + asksMap[price.String()] = 1 + if len(data) >= 20 { + break + } + } + for i := 0; i < DepthNum; i++ { + item := make([]decimal.Decimal, 0) + price := calculateMaxMinContractPrice(closePrice, true) + if _, ok := bidsMap[price.String()]; ok { + continue + } + item = append(item, price) + item = append(item, decimal.NewFromFloat(common.RandFloats(minRands, bmaxRand))) + datas = append(datas, item) + bidsMap[price.String()] = 1 + if len(datas) >= 20 { + break + } + } + return data, datas +} + +// 生成一天聚合深度 +func OneDayContractDepth(item market.SubscribeCtDepthResponse) { + result := market.SubscribeCtDepthTempResponse{} + result.Tick = &market.CtDepthTick{} + title := fmt.Sprintf("market-%s-depth-step6", SelfContractCode) + rand.New(rand.NewSource(time.Now().UnixNano())) + result.Tick.Mrid = rand.Int63n(99999999999) + int64(99999) + result.Tick.Id = common.TimeToNow() * 1000 + result.Tick.Ts = common.TimeToNow() * 1000 + result.Tick.Asks = insertionSort(item.Tick.Asks) + result.Tick.Bids = quickSort(item.Tick.Bids) + result.Channel = fmt.Sprintf("market.%s.depth.step6", SelfContractCode) + resultJsons, _ := json.Marshal(result) + red.Set_Cache_Value(title, string(resultJsons)) + return + //titleInfo := fmt.Sprintf("market-%s-depth-step6-info", SelfContractCode) + //redisRes, _ := red.Get_Cache_Data(title) + //if common.GetWeeHours() || redisRes == "" { + // itemJson, _ := json.Marshal(item) + // if err := red.Set_Cache_Value(title, string(itemJson)); err != nil { + // applogger.Error(title, err) + // } + // //if err := red.Set_Cache_Value(titleInfo, string(itemJson)); err != nil { + // // applogger.Error(title, err) + // //} + // return + //} + //result := market.SubscribeCtDepthTempResponse{} + //if err := json.Unmarshal([]byte(redisRes), &result); err != nil { + // applogger.Error("json err", err) + // return + //} + //bids := make(map[decimal.Decimal]decimal.Decimal, 0) + //asks := make(map[decimal.Decimal]decimal.Decimal, 0) + //for _, v := range result.Tick.Bids { + // bids[v[0]] = v[1] + //} + //for _, v := range result.Tick.Asks { + // asks[v[0]] = v[1] + //} + //bidsTemp := make([][]decimal.Decimal, 0) + //bidsAbandonMap := make(map[decimal.Decimal]int) + //// fmt.Println("bids map" ,bids) + //// fmt.Println("bids",result.Tick.Bids) + //for _, v := range item.Tick.Bids { + // value := make([]decimal.Decimal, 0) + // if len(v) <= 0 { + // continue + // } + // if num, ok := bids[v[0]]; !ok { + // value = append(value, v[0]) + // value = append(value, v[1].Add(num)) + // bidsTemp = append(bidsTemp, value) + // bidsAbandonMap[v[0]] = 1 + // } else { + // value = append(value, v[0]) + // value = append(value, v[1]) + // bidsTemp = append(bidsTemp, value) + // } + //} + ////fmt.Println("bidsAb",bidsAbandonMap) + //for key, val := range bids { + // if _, ok := bidsAbandonMap[key]; !ok { + // value := make([]decimal.Decimal, 0) + // value = append(value, key) + // value = append(value, val) + // bidsTemp = append(bidsTemp, value) + // } + //} + //asksTemp := make([][]decimal.Decimal, 0) + //asksAbandonMap := make(map[decimal.Decimal]int) + //for _, v := range item.Tick.Asks { + // if len(v) <= 0 { + // continue + // } + // value := make([]decimal.Decimal, 0) + // if num, ok := asks[v[0]]; ok { + // value = append(value, v[0]) + // value = append(value, v[1].Add(num)) + // asksAbandonMap[v[0]] = 1 //收集起来 + // asksTemp = append(asksTemp, value) + // } else { + // value = append(value, v[0]) + // value = append(value, v[1]) + // asksTemp = append(asksTemp, value) + // } + //} + //for key, val := range asks { + // if _, ok := asksAbandonMap[key]; !ok { + // value := make([]decimal.Decimal, 0) + // value = append(value, key) + // value = append(value, val) + // asksTemp = append(asksTemp, value) + // } + //} + //result.Channel = fmt.Sprintf("market.%s.depth.step6-info", SelfContractCode) + //result.Tick.Asks = asksTemp + //result.Tick.Bids = bidsTemp + //result.Tick.Mrid = rand.Int63n(99999999999) + int64(99999) + //result.Tick.Id = common.TimeToNow() * 1000 + //result.Tick.Ts = common.TimeToNow() * 1000 + ////resultJson, _ := json.Marshal(result) + //////所有的买卖保存 + ////if err := red.Set_Cache_Value(titleInfo, string(resultJson)); err != nil { + //// applogger.Error(title, err) + ////} + ////推给前端数据 + //if len(asksTemp) >= DepthNum { + // asksTemp = asksTemp[:DepthNum] + // //fmt.Println(len(asksTemp)) + // //os.Exit(11) + //} + //if len(bidsTemp) >= DepthNum { + // bidsTemp = bidsTemp[:DepthNum] + //} + ////result := market.SubscribeCtDepthTempResponse{} + //result.Tick.Asks = insertionSort(asksTemp) + //result.Tick.Bids = quickSort(bidsTemp) + //result.Channel = fmt.Sprintf("market.%s.depth.step6", SelfContractCode) + //resultJsons, _ := json.Marshal(result) + //red.Set_Cache_Value(title, string(resultJsons)) + //applogger.Info("OneDayContractDepth ", string(resultJsons)) +} + +// 生成一天聚合行情 +func OneDayDetailMerged(param market.SubscribeCtKlineResponse) { + title := fmt.Sprintf("market-%s-detail-merged", SelfContractCode) + chStep6 := fmt.Sprintf("market-%s-depth-step6", SelfContractCode) + redisStep6, _ := red.Get_Cache_Data(chStep6) + resultStep6 := market.SubscribeCtDepthResponse{} + if err := json.Unmarshal([]byte(redisStep6), &resultStep6); err != nil { + applogger.Error("OneDayContractDepth json err", err) + return + } + result := market.SubscribeCtDetailResponse{} + tick := &market.CtDetailTick{} + result.Channel = title + rand.Seed(time.Now().UnixNano()) + var totalPriceAsks decimal.Decimal + var totalNumAsks decimal.Decimal + for _, v := range resultStep6.Tick.Asks { + if len(v) <= 0 { + continue + } + totalPriceAsks = totalPriceAsks.Add(v[0]) + totalNumAsks = totalNumAsks.Add(v[1]) + } + if len(resultStep6.Tick.Asks) > 0 { + value := make([]decimal.Decimal, 0) + value = append(value, totalPriceAsks.Div(decimal.NewFromInt(int64(len(resultStep6.Tick.Asks)))).Round(digits)) + value = append(value, totalNumAsks) + tick.Asks = value + } + bidsTemp := make([][]decimal.Decimal, 0) + var totalPriceBids decimal.Decimal + var totalNumBids decimal.Decimal + for _, v := range resultStep6.Tick.Bids { + if len(v) <= 0 { + continue + } + totalPriceBids = totalPriceBids.Add(v[0]) + totalNumBids = totalNumBids.Add(v[1]) + } + if len(resultStep6.Tick.Bids) > 0 { + value := make([]decimal.Decimal, 0) + value = append(value, totalPriceBids.Div(decimal.NewFromInt(int64(len(resultStep6.Tick.Bids)))).Round(digits)) + value = append(value, totalNumBids) + bidsTemp = append(bidsTemp, value) + tick.Bids = value + } + tick.Mrid = rand.Int63n(99999999999) + int64(99999) + tick.Id = common.TimeToNow() + tick.TradeTurnover = param.Tick.Rrade_Turnover + tick.Count = param.Tick.Count.IntPart() + tick.High = param.Tick.High + tick.Open = param.Tick.Open + tick.Vol = param.Tick.Vol + tick.Close = param.Tick.Close + tick.Low = param.Tick.Low + tick.Amount = param.Tick.Amount + result.Tick = tick + result.Timestamp = common.TimeToNow() + result.Channel = fmt.Sprintf("market.%s.detail.merged", SelfContractCode) + resultJson, _ := json.Marshal(result) + if err := red.Set_Cache_Value(title, string(resultJson)); err != nil { + applogger.Error(title, err) + } + //applogger.Info("OneDayContractDepth ", string(resultJson)) +} + +// 生成详情 +func CreateTradeDetail(tick market.CtDepthTick) { + res := market.SubscribeCtTradeDetailResponse{} + res.Tick = &market.CtTradeDetailTick{} + res.Tick.Data = make([]market.TradeDetail, 0) + TotalCount = decimal.NewFromInt(0) + //最近交易接口 + tradeDetailAPI := make([]stock.MarketTrade, 0) + for key, v := range tick.Asks { + if key >= 1 { + break + } + trade := v[1].Mul(FaceValue).Mul(v[0]) //每一笔成交张数 * 合约面值 * 成交价格 + item := market.TradeDetail{ + Amount: v[1], + Ts: common.TimeToNow() * 1000, + Id: common.TimeToNow() * 1000, + Price: v[0], + Direction: "sell", + Quantity: trade.Div(v[0]).Round(digits), //成交币 + TradeTurnover: trade, + } + //总成交额 + //TotalTradeTurnover = TotalTradeTurnover.Add(trade) + //总成交量 + //TotalAmount = TotalAmount.Add(item.Quantity) + //成交张数 + //TotalVol = TotalVol.Add(v[1]) + res.Tick.Data = append(res.Tick.Data, item) + apiItem := stock.MarketTrade{ + ID: common.TimeToNow() * 1000, + OrderNumber: item.Amount.String(), + DealPrice: item.Price.String(), + OrderTime: common.TimeToNow() * 1000, + TradeType: 2, + TradeTurnover: trade, + } + tradeDetailAPI = append(tradeDetailAPI, apiItem) + } + res.Channel = fmt.Sprintf("market.%s.trade.detail", SelfContractCode) + res.Timestamp = common.TimeToNow() * 1000 + if len(res.Tick.Data) > 0 { + // fmt.Println(res.Tick.Data) + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: res.Channel, + Content: res, + Symbol: res.Channel}) + //applogger.Info("CreateTradeDetail sell:", string(jsonMessage)) + red.RedisClient.Publish(res.Channel, string(jsonMessage)) + } + //总成交笔数 + //TotalCount = TotalCount.Add(decimal.NewFromInt(int64(len(tick.Asks)))) + + res = market.SubscribeCtTradeDetailResponse{} + res.Tick = &market.CtTradeDetailTick{} + res.Tick.Data = make([]market.TradeDetail, 0) + res.Channel = fmt.Sprintf("market.%s.trade.detail", SelfContractCode) + for key, v := range tick.Bids { + if key >= 1 { + break + } + trade := v[1].Mul(FaceValue).Mul(v[0]) //每一笔成交张数 * 合约面值 * 成交价格 + item := market.TradeDetail{ + Amount: v[1], //张数 + Ts: common.TimeToNow() * 1000, + Id: common.TimeToNow() * 1000, + Price: v[0], + Direction: "buy", + Quantity: trade.Div(v[0]).Round(digits), //成交币 + TradeTurnover: trade, + } + //总成交额 + TotalTradeTurnover = TotalTradeTurnover.Add(trade) + //总成交量 + TotalAmount = TotalAmount.Add(item.Quantity) + //成交张数 + TotalVol = TotalVol.Add(v[1]) + res.Tick.Data = append(res.Tick.Data, item) + apiItem := stock.MarketTrade{ + ID: common.TimeToNow() * 1000, + OrderNumber: item.Amount.String(), + DealPrice: item.Price.String(), + OrderTime: common.TimeToNow() * 1000, + TradeType: 1, + TradeTurnover: trade, + } + tradeDetailAPI = append(tradeDetailAPI, apiItem) + } + res.Timestamp = common.TimeToNow() * 1000 + //总成交笔数 + TotalCount = TotalCount.Add(decimal.NewFromInt(int64(len(tick.Bids)))) + if len(res.Tick.Data) > 0 { + //fmt.Println(res.Tick.Data) + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: res.Channel, + Content: res, + Symbol: res.Channel}) + //applogger.Info("CreateTradeDetail buy:", string(jsonMessage)) + go func(channel string, jsonMessage []byte) { + time.Sleep(1 * time.Second) + red.RedisClient.Publish(channel, string(jsonMessage)) + }(res.Channel, jsonMessage) + } + //缓存 + title := fmt.Sprintf("market-%s-trade-detail", SelfContractCode) + var resultJson []byte + //if len(tradeDetailAPI) < tradeNum { + // item, _ := red.Get_Cache_Data(title) + // //itemJson, _ := json.Marshal(item) + // res := make([]stock.MarketTrade, 0) + // json.Unmarshal([]byte(item), &res) + // l := len(res) + // res = append(res, tradeDetailAPI...) + // if l > 0 { + // res = res[l:] + // } + // resultJson, _ = json.Marshal(res) + //} else { + resultJson, _ = json.Marshal(tradeDetailAPI) + // } + if err := red.Set_Cache_Value(title, string(resultJson)); err != nil { + applogger.Error(title, err) + } + // os.Exit(11111) + // + // applogger.Info(string(resultJson)) +} + +func calculateMaxMinContractPrice(basePrice decimal.Decimal, isNegative bool) decimal.Decimal { + rand.New(rand.NewSource(time.Now().UnixNano())) + max := basePrice.Mul(decimal.NewFromFloat(rand.Float64()*defaultMinStep + rand.Float64()*(defaultMaxStep-defaultMinStep))).Round(digits) + // fmt.Println(max) + if isNegative { + return basePrice.Sub(max).Round(digits) + } + return basePrice.Add(max).Round(digits) +} + +// 买单不能高于当前价,卖但不能低于当前价 +func randDepth(max, min int, closePrice, old decimal.Decimal) ([][]decimal.Decimal, [][]decimal.Decimal) { + data := make([][]decimal.Decimal, 0) + datas := make([][]decimal.Decimal, 0) + //item := make([]decimal.Decimal, 0) + //item = append(item, calculateMaxMinContractPrice(closePrice, false)) + //item = append(item, decimal.NewFromInt(int64(max))) + //data = append(data, item) + //item = make([]decimal.Decimal, 0) + //item = append(item, calculateMaxMinContractPrice(closePrice, true)) + //item = append(item, decimal.NewFromInt(int64(min))) + //datas = append(datas, item) + //} + //if old.GreaterThan(closePrice) { + // //跌 + // return data, datas + //} + //dataTemp := make([][]decimal.Decimal, 0) + //datasTemp := make([][]decimal.Decimal, 0) + //item = make([]decimal.Decimal, 0) + //item = append(item, data[0][0]) + //item = append(item, datas[0][1]) + //dataTemp = append(dataTemp, item) + //item = make([]decimal.Decimal, 0) + //item = append(item, datas[0][0]) + //item = append(item, data[0][1]) + //datasTemp = append(datasTemp, item) + return data, datas +} + +// 升序 +func insertionSort(nums [][]decimal.Decimal) [][]decimal.Decimal { + n := len(nums) + for i := 0; i < n-1; i++ { + for j := 0; j < n-i-1; j++ { + if nums[j][0].GreaterThan(nums[j+1][0]) { + nums[j], nums[j+1] = nums[j+1], nums[j] + } + } + } + return nums +} + +// 降序 +func quickSort(nums [][]decimal.Decimal) [][]decimal.Decimal { + n := len(nums) + for i := 0; i < n-1; i++ { + minIdx := i + for j := i + 1; j < n; j++ { + if nums[j][0].GreaterThan(nums[minIdx][0]) { + minIdx = j + } + } + nums[i], nums[minIdx] = nums[minIdx], nums[i] + } + return nums +} diff --git a/cmd/selfContract/mongo.go b/cmd/selfContract/mongo.go new file mode 100644 index 0000000..56587b5 --- /dev/null +++ b/cmd/selfContract/mongo.go @@ -0,0 +1,57 @@ +package selfContract + +import ( + "fmt" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "wss-pool/internal/data" +) + +func GetNewPrice(contractCode string) []data.MongoTick { + filter := bson.M{"channel": fmt.Sprintf("market.%s.kline.1min", contractCode)} + tableName := data.GetContractKLineTableName("1min") + pagedData, _ := data.MgoLimitFind(tableName, filter, int64(1)) + return pagedData +} + +func GetNewPriceAll(contractCode, period string) []data.MongoTick { + filter := bson.M{"channel": fmt.Sprintf("market.%s.kline.%s", contractCode, period)} + tableName := data.GetContractKLineTableName(period) + pagedData, _ := data.MgoLimitFind(tableName, filter, int64(1)) + return pagedData +} + +func GetTimeNewPrice(contractCode string, from, to int64, period string) (decimal.Decimal, decimal.Decimal, decimal.Decimal, decimal.Decimal, decimal.Decimal, decimal.Decimal) { + filter := bson.M{"channel": fmt.Sprintf("market.%s.kline.%s", contractCode, period), "code": bson.M{"$gte": from, "$lte": to}} + tableName := data.GetContractKLineTableName(period) + pagedData, _ := data.MgoLimitFind(tableName, filter, int64(0)) + var low, high, vol, amount, count, tradeTurnover decimal.Decimal + for key, v := range pagedData { + lows, _ := decimal.NewFromString(v.Low) + highs, _ := decimal.NewFromString(v.High) + vols, _ := decimal.NewFromString(v.Vol) + amounts, _ := decimal.NewFromString(v.Amount) + counts, _ := decimal.NewFromString(v.Count.(string)) + tradeTurnovers, _ := decimal.NewFromString(v.TradeTurnover) + if key == 0 { + low = lows + high = highs + vol = vols + amount = amounts + count = counts + tradeTurnover = tradeTurnovers + continue + } + vol = vol.Add(vols) + amount = vol.Add(amounts) + count = vol.Add(counts) + tradeTurnover = tradeTurnovers.Add(tradeTurnover) + if low.GreaterThan(lows) { + low = lows + } + if high.LessThan(highs) { + high = highs + } + } + return low, high, vol, amount, count, tradeTurnover +} diff --git a/cmd/selfContract/virtualContract.go b/cmd/selfContract/virtualContract.go new file mode 100644 index 0000000..6795896 --- /dev/null +++ b/cmd/selfContract/virtualContract.go @@ -0,0 +1,723 @@ +package selfContract + +import ( + "encoding/json" + "fmt" + "github.com/shopspring/decimal" + "math/rand" + "sync" + "time" + "wss-pool/cmd/common" + "wss-pool/cmd/websocketservice" + "wss-pool/dictionary" + "wss-pool/internal/data/business" + "wss-pool/internal/model" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/market" +) + +const ( + proportion int32 = 10000 //调价原价比例 + step int32 = 20 //调价指数 + digits int32 = 4 //保留小数位数 + numPrices int = 20 //生成随机价格数量 + defaultStep float64 = 0.001 // 默认波动率 + PushFrequency int = 3 //S + TimeRemaining int64 = 20 //s + +) + +var ( + contractChan = make(chan string) + SelfContractCode string //合约代码 + FaceValue decimal.Decimal // 面值合约 + InitialPrice decimal.Decimal // 初始价格 + KLineMap = make(map[string]KlineLowHigh) + ClosePrices decimal.Decimal //当前价格 //做调价终止值使用 + OldFiveMin int64 //记录K线时间搓 + OldFifteenMin int64 + OldThirtyMin int64 + OldOneHour int64 + OldFourHour int64 + OldDay int64 + OldWeek int64 + OldMon int64 + IsRun bool + ContractMap = make(map[string]bool) //保存领取的任务 + lock sync.Mutex + endChan = make(chan string) +) + +type KlineLowHigh struct { + Low decimal.Decimal + High decimal.Decimal + ID int64 + Vol decimal.Decimal + Amount decimal.Decimal + Count decimal.Decimal + TradeTurnover decimal.Decimal + Open decimal.Decimal +} + +type ConstructorContract struct { + SelfContractCode string `json:"selfContractCode"` //虚拟合约 + BeginTime string `json:"beginTime"` //开始时间 + EndTime string `json:"endTime"` //结束时间 + MaxPrice decimal.Decimal `json:"maxPrices"` + MinPrice decimal.Decimal `json:"minPrice"` + MaxPriceStr string `json:"maxPrice"` +} + +func NewSelfContract() { + //首次启动 + go func() { + this := new(ConstructorContract) + this.MinPrice = InitialPrice + this.SelfContractCode = SelfContractCode + this.defaultContract() + }() + for { + t := time.NewTimer(1 * time.Minute) + <-t.C + go func() { + if IsRun { + applogger.Info("已有调价在运行, 该次调价不能运行") + return + } + result := getData() + fmt.Println(result) + for _, v := range result { + if v.TradeName != SelfContractCode { + applogger.Info("parametric inequality") + continue + } + end, _ := common.TimeStrToTimes(v.EndTime) + if end.Unix() <= time.Now().Unix() { + applogger.Info("该调价已过期") + continue + } + if _, ok := ContractMap[fmt.Sprintf("%s-%d", v.TradeName, end.Unix())]; ok { + applogger.Info("该任务正在运行", fmt.Sprintf("%s-%d", v.TradeName, end.Unix())) + continue + } + ContractMap[fmt.Sprintf("%s-%d", v.TradeName, end.Unix())] = true //保存任务 + begin, _ := common.TimeStrToTimes(v.BeginTime) + if begin.Unix() < common.TimeToNow() { + applogger.Info("该调价已过期") + continue + } else if end.Unix() <= begin.Unix() { + applogger.Info("begin end 有误") + continue + } + maxPrice, _ := decimal.NewFromString(v.MaxPrice) + //if maxPrice.LessThan(InitialPrice) { + // applogger.Info("调价有误",v.MaxPrice) + // continue + //} + //等待到开始时间 + applogger.Info("等待到begin time ", v.BeginTime, v.EndTime) + IsRun = true // 调价任务已开启 + time.Sleep(begin.Sub(common.TimeToNows())) + contractChan <- "constructorStart" + this := new(ConstructorContract) + this.SelfContractCode = SelfContractCode + this.EndTime = v.EndTime + this.MinPrice = InitialPrice + this.MaxPrice = maxPrice + this.BeginTime = v.BeginTime + this.constructor(begin, end) + //更改状态 + contract := model.NewContractMarket() + contract.ID = v.ID + contract.UpdateIsGetOne() + } + if len(result) > 0 && IsRun { + IsRun = false + this := new(ConstructorContract) + this.MinPrice = InitialPrice + this.SelfContractCode = SelfContractCode + this.defaultContract() + } + }() + } +} + +func getData() []model.ContractMarket { + contract := model.NewContractMarket() + contract.IsType = model.Contract + contract.TradeName = SelfContractCode + result := contract.List() + return result +} + +// 规定启动 +func (this *ConstructorContract) constructor(begin, end time.Time) { + applogger.Info("开始调价。。。", "结束时间:", this.EndTime, "当前价格:", this.MinPrice, "最终价格", this.MaxPrice) + totalDuration := end.Sub(common.TimeToNows()) + closePrice := this.MinPrice + highPrice := this.MinPrice + lowPrice := this.MinPrice + oldPrice := this.MinPrice + var openPrice decimal.Decimal + //var temporalFrequency = time.Duration(PushFrequency) * time.Second + timeInterval := float64(totalDuration) / float64(time.Minute) + delta := this.MaxPrice.Sub(closePrice).Div(decimal.NewFromFloat(timeInterval)) + applogger.Debug("-------------------------delta", delta) + fluctuation := closePrice.Mul(decimal.NewFromInt32(step).Div(decimal.NewFromInt32(proportion))).Round(digits) + applogger.Info("timeInterval", timeInterval, "delta", delta, "fluctuation", fluctuation) + // 开始进行调价 + for !this.MaxPrice.Equal(InitialPrice) || end.Unix() > common.TimeToNows().Unix() { + // 生成随机价格波动 + fmt.Println("当前价格", InitialPrice) + prices := this.generateRandomPrices(InitialPrice, fluctuation, delta) + openPrice = InitialPrice + this.GetAllLowHigh(highPrice, lowPrice) + nonVanishing(lowPrice) + numFrequency := int(60) / PushFrequency + ClearTotal() + for i := 1; i <= numFrequency; i++ { + //go func(prices []decimal.Decimal, closePrice, openPrice, highPrice, lowPrice, oldPrice decimal.Decimal, this *ConstructorContract) { + start := time.Now() // 获取当前时间 + rand.New(rand.NewSource(time.Now().UnixNano())) + key := rand.Intn(len(prices)) + closePrice = prices[key] + // applogger.Info("实际落盘价:", closePrice) + //if (closePrice.Sub(this.MaxPrice)).Mul(delta).GreaterThan(decimal.NewFromInt32(0)){ + // closePrice = this.MaxPrice + //} + //当只剩 20 秒 + if (end.Unix() - common.TimeToNows().Unix()) < TimeRemaining { + closePrice = this.MaxPrice + } + FaceValue = common.GetFaceValue(closePrice) + //生成深度、Trade Detail 数据 + CreateDepth(oldPrice, closePrice) + //k 线 详情 + this.pullStorage(closePrice, openPrice, highPrice, lowPrice, oldPrice, prices) + oldPrice = closePrice + applogger.Debug("目标价格", this.MaxPrice, "当前价格", closePrice, "调价结束时间", this.EndTime, "i", i, time.Now().Format("2006-01-02 15:04:05")) + //更新市价 + lock.Lock() + InitialPrice = closePrice + lock.Unlock() + if end.Unix() <= common.TimeToNows().Unix() { + break + } + fmt.Println("Run time: ", time.Since(start)) + s := float64(PushFrequency) - time.Since(start).Seconds() + if s > float64(0) { + applogger.Debug("停留 秒", s) + time.Sleep(time.Duration(s) * time.Second) + } + } + } +} + +func (this *ConstructorContract) defaultContract() { + closePrice := this.MinPrice + highPrice := this.MinPrice + lowPrice := this.MinPrice + oldPrice := this.MinPrice + var openPrice decimal.Decimal + //var temporalFrequency = time.Duration(PushFrequency) * time.Second + for { + select { + case _, ok := <-contractChan: // 从管道接收值 + if ok { + applogger.Info("calculateContractPrice start,defaultContract") + return + } + default: + //fmt.Println(time.Now().Format("2006-01-02 15:04:05")) + rand.New(rand.NewSource(time.Now().UnixNano())) + openPrice = InitialPrice + Loop: + prices := calculateContractPrice(InitialPrice) + fmt.Println("开盘价", InitialPrice) + //更新落盘价、开盘价、最高价和最低价 + key := rand.Intn(numPrices) + highPrices := this.getMaxPrices(openPrice, prices) + lowPrices := this.getMinPrices(openPrice, prices) + //被2整除 涨 + if key%2 == 0 { + highPrice = this.getMaxPrice(openPrice, highPrices) + lowPrice = openPrice + prices = highPrices + } else { + highPrice = openPrice + lowPrice = this.getMinPrice(openPrice, lowPrices) + prices = lowPrices + } + if len(prices) <= 0 { + goto Loop + } + this.GetAllLowHigh(highPrice, lowPrice) + nonVanishing(lowPrice) + numFrequency := int(60) / PushFrequency + ClearTotal() + for i := 1; i <= numFrequency; i++ { + // start := time.Now() // 获取当前时间 + // fmt.Println("Run time: ", time.Since(start)) + go func(prices []decimal.Decimal, closePrice, openPrice, highPrice, lowPrice, oldPrice decimal.Decimal, this *ConstructorContract) { + //更新市价 + //fmt.Println(closePrice) + rand.New(rand.NewSource(time.Now().UnixNano())) + key := rand.Intn(len(prices)) + closePrice = prices[key] + lock.Lock() + InitialPrice = closePrice + lock.Unlock() + FaceValue = common.GetFaceValue(closePrice) + //生成深度、Trade Detail 数据 + CreateDepth(oldPrice, closePrice) + //fmt.Println("Run time: ", time.Since(start)) + this.pullStorage(closePrice, openPrice, highPrice, lowPrice, oldPrice, prices) + //fmt.Println("123123123 ", closePrice,openPrice, highPrice, lowPrice, oldPrice) + oldPrice = closePrice + }(prices, closePrice, openPrice, highPrice, lowPrice, oldPrice, this) + time.Sleep(time.Duration(int64(PushFrequency)) * time.Second) + } + } + } +} + +func generateRandomStep(min, max decimal.Decimal) decimal.Decimal { + rand.New(rand.NewSource(time.Now().UnixNano())) + return min.Add(decimal.NewFromFloat(rand.Float64()).Mul(max.Sub(min))) +} + +// 计算虚拟合约价格 +func calculateContractPrice(basePrice decimal.Decimal) []decimal.Decimal { + prices := make([]decimal.Decimal, 0) + max := basePrice.Mul(decimal.NewFromFloat(defaultStep)).Round(digits) + min := max.Neg() + for i := 0; i < numPrices; i++ { + price := basePrice.Add(generateRandomStep(max, min)).Round(digits) + prices = append(prices, price) + } + return prices +} + +func getOpen(timestamp int64, period string) decimal.Decimal { + tick := GetNewPriceAll(SelfContractCode, period) + open := InitialPrice + if len(tick) > 0 { + switch tick[0].Code { + case timestamp: + open, _ = decimal.NewFromString(tick[0].Open) + default: + open, _ = decimal.NewFromString(tick[0].Close) + } + } + return open +} + +func nonVanishing(low decimal.Decimal) { + for k, v := range KLineMap { + if v.Low.IsZero() { + v.Low = low + } + KLineMap[k] = v + } +} + +func (this *ConstructorContract) GetAllLowHigh(high, low decimal.Decimal) { + //初始化 + if len(KLineMap) == 0 { + KLineMap["1min"] = KlineLowHigh{ + ID: common.GenerateSingaporeMinuteTimestamp(), + Low: low, + High: high, + Vol: decimal.NewFromInt(0), + Amount: decimal.NewFromInt(0), + Count: decimal.NewFromInt(0), + TradeTurnover: decimal.NewFromInt(0), + Open: InitialPrice, + } + + to := common.GenerateSingaporeFiveMinTimestamp() + from := to - int64(5*60) + low, high, vol, amount, count, tradeTurnover := GetTimeNewPrice(this.SelfContractCode, from, to, "1min") + KLineMap["5min"] = KlineLowHigh{ + ID: to, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + TradeTurnover: tradeTurnover, + Open: getOpen(to, "5min"), + } + OldFiveMin = to + + to = common.GenerateSingaporeFifteenMinTimestamp() + from = to - int64(15*60) + low, high, vol, amount, count, tradeTurnover = GetTimeNewPrice(this.SelfContractCode, from, to, "5min") + KLineMap["15min"] = KlineLowHigh{ + ID: to, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + TradeTurnover: tradeTurnover, + Open: getOpen(to, "15min"), + } + OldFifteenMin = to + + to = common.GenerateSingaporeThirtyMinTimestamp() + from = to - int64(30*60) + low, high, vol, amount, count, tradeTurnover = GetTimeNewPrice(this.SelfContractCode, from, to, "15min") + KLineMap["30min"] = KlineLowHigh{ + ID: to, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + TradeTurnover: tradeTurnover, + Open: getOpen(to, "30min"), + } + OldThirtyMin = to + + from = common.GenerateSingaporeHourTimestamp() + to = from + int64(60*59) + low, high, vol, amount, count, tradeTurnover = GetTimeNewPrice(this.SelfContractCode, from, to, "30min") + KLineMap["60min"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + TradeTurnover: tradeTurnover, + Open: getOpen(from, "60min"), + } + OldOneHour = from + + to = common.GenerateSingaporeFourHourTimestamp() + from = to - (4 * 60 * 60) + low, high, vol, amount, count, tradeTurnover = GetTimeNewPrice(this.SelfContractCode, from, to, "60min") + KLineMap["4hour"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + TradeTurnover: tradeTurnover, + Open: getOpen(from, "4hour"), + } + OldFourHour = from + + from = common.GenerateSingaporeDayTimestamp("") + to = from + int64(60*60*24-1) + low, high, vol, amount, count, tradeTurnover = GetTimeNewPrice(this.SelfContractCode, from, to, "4hour") + KLineMap["1day"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + TradeTurnover: tradeTurnover, + Open: getOpen(from, "1day"), + } + OldDay = from + + from = common.GenerateSingaporeMonTimestamp() + to = common.TimeToNow() + low, high, vol, amount, count, tradeTurnover = GetTimeNewPrice(this.SelfContractCode, from, to, "1week") + KLineMap["1mon"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + TradeTurnover: tradeTurnover, + Open: getOpen(from, "1mon"), + } + OldMon = from + + from = common.GetWeekTimestamp() + to = common.TimeToNow() + low, high, vol, amount, count, tradeTurnover = GetTimeNewPrice(this.SelfContractCode, from, to, "1day") + KLineMap["1week"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + TradeTurnover: tradeTurnover, + Open: getOpen(from, "1week"), + } + OldWeek = from + } + + for k, v := range KLineMap { + switch k { + case "1min": + v.ID = common.GenerateSingaporeMinuteTimestamp() + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.High = high + v.Low = low + v.Open = InitialPrice + case "5min": + v.ID = common.GenerateSingaporeFiveMinTimestamp() + if v.ID > OldFiveMin { + //新时间节点更新 + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldFiveMin = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "15min": + v.ID = common.GenerateSingaporeFifteenMinTimestamp() + if v.ID > OldFifteenMin { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldFifteenMin = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "30min": + v.ID = common.GenerateSingaporeThirtyMinTimestamp() + if v.ID > OldThirtyMin { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldThirtyMin = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "60min": + v.ID = common.GenerateSingaporeHourTimestamp() + if v.ID > OldOneHour { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldOneHour = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "4hour": + v.ID = common.GenerateSingaporeFourHourTimestamp() - (4 * 60 * 60) + if v.ID > OldFourHour { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldFourHour = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "1day": + v.ID = common.GenerateSingaporeDayTimestamp("") + if v.ID > OldDay { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldDay = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "1week": + v.ID = common.GetWeekTimestamp() + if v.ID > OldWeek { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldWeek = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "1mon": + v.ID = common.GenerateSingaporeMonTimestamp() + if v.ID > OldMon { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.TradeTurnover = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldMon = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + } + KLineMap[k] = v + } +} + +func (this *ConstructorContract) pullStorage(close, open, high, low, oldPrice decimal.Decimal, prices []decimal.Decimal) { + for _, v := range dictionary.ContractPriceTime { + resp := market.SubscribeCtKlineResponse{ + Channel: fmt.Sprintf("market.%s.kline.%s", this.SelfContractCode, v), + Timestamp: KLineMap[v].ID, + Tick: &market.CtKlineTick{ + Open: KLineMap[v].Open, + High: KLineMap[v].High, + Low: KLineMap[v].Low, + Close: close, + Id: KLineMap[v].ID, + Vol: TotalVol.Add(KLineMap[v].Vol), + Count: TotalCount.Add(KLineMap[v].Count), + Amount: TotalAmount.Add(KLineMap[v].Amount), + Rrade_Turnover: TotalTradeTurnover.Add(KLineMap[v].TradeTurnover), + Mrid: int64(rand.Intn(99999999999999) + 99999), + }, + } + + if v == "1day" { + go OneDayDetailMerged(resp) + } + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Content: resp, + Symbol: resp.Channel}) + //applogger.Info("SubscribeCtKline %s:", string(jsonMessage), v) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + go business.UpdateSubscribeCtKline(resp) + } + //合约详情 + detail := market.SubscribeCtDetailResponse{ + Channel: fmt.Sprintf("market.%s.detail", this.SelfContractCode), + Timestamp: time.Now().Unix(), + Tick: &market.CtDetailTick{ + Open: open, + High: high, + Low: low, + Close: close, + Id: time.Now().Unix(), + Vol: TotalVol, + Count: TotalCount.IntPart(), + Amount: TotalAmount, + TradeTurnover: TotalTradeTurnover, + }, + } + jsonMessages, _ := json.Marshal(websocketservice.Message{ + ServersId: detail.Channel, + Content: detail, + Symbol: detail.Channel}) + //applogger.Info("detail :", string(jsonMessages)) + red.RedisClient.Publish(detail.Channel, string(jsonMessages)) +} + +// 生成随机价格波动 +func (this *ConstructorContract) generateRandomPrices(currentPrice, fluctuation, delta decimal.Decimal) []decimal.Decimal { + prices := make([]decimal.Decimal, 0) + rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < numPrices; i++ { + price := currentPrice.Add(delta.Add(fluctuation.Mul(decimal.NewFromFloat(2*rand.Float64() - 1)))).Round(digits) + prices = append(prices, price) + } + return prices +} + +// 获取最高价 +func (this *ConstructorContract) getMaxPrices(highPrice decimal.Decimal, prices []decimal.Decimal) []decimal.Decimal { + res := make([]decimal.Decimal, 0) + for _, price := range prices { + if price.GreaterThan(highPrice) { + res = append(res, price) + } + } + return res +} +func (this *ConstructorContract) getMaxPrice(highPrice decimal.Decimal, prices []decimal.Decimal) decimal.Decimal { + for _, price := range prices { + if price.GreaterThan(highPrice) { + highPrice = price + } + } + return highPrice +} + +// 获取最低价 +func (this *ConstructorContract) getMinPrices(lowPrice decimal.Decimal, prices []decimal.Decimal) []decimal.Decimal { + res := make([]decimal.Decimal, 0) + for _, price := range prices { + if price.LessThan(lowPrice) { + res = append(res, price) + } + } + return res +} + +func (this *ConstructorContract) getMinPrice(lowPrice decimal.Decimal, prices []decimal.Decimal) decimal.Decimal { + for _, price := range prices { + if price.LessThan(lowPrice) { + lowPrice = price + } + } + return lowPrice +} diff --git a/cmd/selfMarketSpot/aggregation.go b/cmd/selfMarketSpot/aggregation.go new file mode 100644 index 0000000..317747c --- /dev/null +++ b/cmd/selfMarketSpot/aggregation.go @@ -0,0 +1,300 @@ +package selfMarketSpot + +import ( + "encoding/json" + "fmt" + "github.com/shopspring/decimal" + "math/rand" + "time" + "wss-pool/cmd/common" + "wss-pool/cmd/websocketservice" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/market" + "wss-pool/pkg/model/stock" +) + +const ( + maxRand float64 = 2000 //成交笔数 随机最大值 + minRand float64 = 0.1 + minLeastRand float64 = 1 + DepthNum int = 80 //深度数据只要 + DepthMaxNum int = 300 //深度数据只要 + defaultMaxStep float64 = 0.003 + defaultMinStep float64 = 0.000001 +) + +var ( + TotalAmount decimal.Decimal // 当前总成交量 + TotalCount decimal.Decimal //总成交笔数 + oldAsks int64 + oldBids int64 +) + +func ClearTotal() { + TotalAmount = decimal.NewFromFloat(0) + TotalCount = decimal.NewFromFloat(0) +} + +func CreateDepth(old, closePrice decimal.Decimal) { + chStep0 := fmt.Sprintf("market-%s-depth-step0", SelfSymbol) + rand.New(rand.NewSource(time.Now().UnixNano())) + tick := market.CtDepthTick{ + Mrid: rand.Int63n(99999999999) + int64(99999), + Id: common.TimeToNow(), + Ts: common.TimeToNow(), + } + //卖单,买单 + tick.Asks, tick.Bids = DepthStep(closePrice, old) + //applogger.Info("asks ", tick.Asks, tick.Bids) + //成交记录 + CreateTradeDetail(tick) + //升序 + tick.Asks = insertionSort(tick.Asks) + //降序 + tick.Bids = quickSort(tick.Bids) + res := market.SubscribeCtDepthResponse{ + Channel: chStep0, + Timestamp: common.TimeToNow(), + Tick: &tick, + } + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: res.Channel, + Content: res, + Symbol: res.Channel}) + OneDayContractDepth(res) + red.RedisClient.Publish(res.Channel, string(jsonMessage)) + +} + +// 卖单 买单 +func DepthStep(closePrice, old decimal.Decimal) ([][]decimal.Decimal, [][]decimal.Decimal) { + data := make([][]decimal.Decimal, 0) + datas := make([][]decimal.Decimal, 0) + if old.Equal(closePrice) { + return data, datas + } + amaxRand := float64(0) + bmaxRand := float64(0) + //跌 + if old.GreaterThan(closePrice) { + diff := old.Sub(closePrice) + amaxRand, _ = diff.Div(old).Mul(decimal.NewFromFloat(maxRand)).Round(2).Float64() + bmaxRand = amaxRand / float64(3) + } else { + diff := closePrice.Sub(old) + bmaxRand, _ = diff.Div(old).Mul(decimal.NewFromFloat(maxRand)).Round(2).Float64() + amaxRand = bmaxRand / float64(3) + } + asksMap := make(map[string]int) + bidsMap := make(map[string]int) + minRands := minRand + if closePrice.LessThan(decimal.NewFromInt(int64(1))) { + minRands = minLeastRand + } + for i := 0; i < DepthNum; i++ { + item := make([]decimal.Decimal, 0) + price := calculateMaxMinContractPrice(closePrice, false) + if _, ok := asksMap[price.String()]; ok { + continue + } + item = append(item, price) + item = append(item, decimal.NewFromFloat(common.RandFloats(minRands, amaxRand))) + data = append(data, item) + asksMap[price.String()] = 1 + if len(data) >= 20 { + break + } + } + for i := 0; i < DepthNum; i++ { + item := make([]decimal.Decimal, 0) + price := calculateMaxMinContractPrice(closePrice, true) + if _, ok := bidsMap[price.String()]; ok { + continue + } + item = append(item, price) + item = append(item, decimal.NewFromFloat(common.RandFloats(minRands, bmaxRand))) + datas = append(datas, item) + bidsMap[price.String()] = 1 + if len(datas) >= 20 { + break + } + } + return data, datas +} + +// 生成一天聚合深度 +func OneDayContractDepth(item market.SubscribeCtDepthResponse) { + result := market.SubscribeCtDepthTempResponse{} + result.Tick = &market.CtDepthTick{} + title := fmt.Sprintf("market-%s-depth-step0", SelfSymbol) + rand.New(rand.NewSource(time.Now().UnixNano())) + result.Tick.Mrid = rand.Int63n(99999999999) + int64(99999) + result.Tick.Id = common.TimeToNow() * 1000 + result.Tick.Ts = common.TimeToNow() * 1000 + result.Tick.Asks = insertionSort(item.Tick.Asks) + result.Tick.Bids = quickSort(item.Tick.Bids) + result.Channel = fmt.Sprintf("market.%s.depth.step0", SelfSymbol) + resultJsons, _ := json.Marshal(result) + red.Set_Cache_Value(title, string(resultJsons)) +} + +// TODO: 生成 聚合行情/市场概要 +func OneDayDetailMerged(param market.SubscribeCandlestickResponse) { + result := market.TickerWebsocketResponse{} + tick := &market.TickR{} + tick.Count = int(param.Tick.Count) + tick.High = param.Tick.High + tick.Open = param.Tick.Open + tick.Vol = param.Tick.Vol + tick.Close = param.Tick.Close + tick.Low = param.Tick.Low + tick.Amount = param.Tick.Amount + result.Tick = tick + result.Timestamp = common.TimeToNow() + result.Channel = fmt.Sprintf("market.%s.ticker", SelfSymbol) + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: result.Channel, + Content: result, + Symbol: result.Channel}) + red.RedisClient.Publish(result.Channel, string(jsonMessage)) + res := market.TickerWebsocketResponses{} + title := fmt.Sprintf("market-%s-detail-merged", SelfSymbol) + res.Tick = tick + res.Channel = title + jsonMessages, _ := json.Marshal(res) + if err := red.Set_Cache_Value(title, string(jsonMessages)); err != nil { + applogger.Error(title, err) + } + //applogger.Info("subscribeTicker data,ServersId:%v,Sender:%v,Content:%v-%v", result.Channel, result.Tick, result.Data) + //applogger.Info("OneDayContractDepth ", string(resultJson)) +} + +// 生成详情 +func CreateTradeDetail(tick market.CtDepthTick) { + res := market.SubscribeTradeResponse{} + res.Tick = &market.TickTrade{} + res.Tick.Data = make([]market.Trade, 0) + TotalCount = decimal.NewFromInt(0) + //最近交易接口 + tradeDetailAPI := make([]stock.MarketTrade, 0) + for key, v := range tick.Asks { + if key >= 1 { + break + } + item := market.Trade{ + Amount: v[1], + Timestamp: common.TimeToNow() * 1000, + TradeId: common.TimeToNow() * 1000, + Price: v[0], + Direction: "sell", + } + res.Tick.Data = append(res.Tick.Data, item) + apiItem := stock.MarketTrade{ + ID: common.TimeToNow() * 1000, + OrderNumber: item.Amount.String(), + DealPrice: item.Price.String(), + OrderTime: common.TimeToNow() * 1000, + TradeType: 2, + } + tradeDetailAPI = append(tradeDetailAPI, apiItem) + } + res.Channel = fmt.Sprintf("market.%s.trade.detail", SelfSymbol) + res.Timestamp = common.TimeToNow() * 1000 + if len(res.Tick.Data) > 0 { + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: res.Channel, + Content: res, + Symbol: res.Channel}) + //applogger.Info("CreateTradeDetail sell:", string(jsonMessage)) + red.RedisClient.Publish(res.Channel, string(jsonMessage)) + } + + res = market.SubscribeTradeResponse{} + res.Tick = &market.TickTrade{} + res.Tick.Data = make([]market.Trade, 0) + res.Channel = fmt.Sprintf("market.%s.trade.detail", SelfSymbol) + for key, v := range tick.Bids { + if key >= 1 { + break + } + item := market.Trade{ + Amount: v[1], //CHEN + Timestamp: common.TimeToNow() * 1000, + TradeId: common.TimeToNow() * 1000, + Price: v[0], + Direction: "buy", + } + //总成交量 + TotalAmount = TotalAmount.Add(item.Amount) + res.Tick.Data = append(res.Tick.Data, item) + apiItem := stock.MarketTrade{ + ID: common.TimeToNow() * 1000, + OrderNumber: item.Amount.String(), + DealPrice: item.Price.String(), + OrderTime: common.TimeToNow() * 1000, + TradeType: 1, + } + tradeDetailAPI = append(tradeDetailAPI, apiItem) + } + res.Timestamp = common.TimeToNow() * 1000 + //总成交笔数 + TotalCount = TotalCount.Add(decimal.NewFromInt(int64(len(tick.Bids)))) + if len(res.Tick.Data) > 0 { + //fmt.Println(res.Tick.Data) + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: res.Channel, + Content: res, + Symbol: res.Channel}) + //applogger.Info("CreateTradeDetail buy:", string(jsonMessage)) + go func(channel string, jsonMessage []byte) { + time.Sleep(1 * time.Second) + red.RedisClient.Publish(channel, string(jsonMessage)) + }(res.Channel, jsonMessage) + } + //缓存 + title := fmt.Sprintf("market-%s-trade-detail", SelfSymbol) + var resultJson []byte + resultJson, _ = json.Marshal(tradeDetailAPI) + if err := red.Set_Cache_Value(title, string(resultJson)); err != nil { + applogger.Error(title, err) + } +} + +func calculateMaxMinContractPrice(basePrice decimal.Decimal, isNegative bool) decimal.Decimal { + rand.New(rand.NewSource(time.Now().UnixNano())) + max := basePrice.Mul(decimal.NewFromFloat(rand.Float64()*defaultMinStep + rand.Float64()*(defaultMaxStep-defaultMinStep))).Round(digits) + // fmt.Println(max) + if isNegative { + return basePrice.Sub(max).Round(digits) + } + return basePrice.Add(max).Round(digits) +} + +// 升序 +func insertionSort(nums [][]decimal.Decimal) [][]decimal.Decimal { + n := len(nums) + for i := 0; i < n-1; i++ { + for j := 0; j < n-i-1; j++ { + if nums[j][0].GreaterThan(nums[j+1][0]) { + nums[j], nums[j+1] = nums[j+1], nums[j] + } + } + } + return nums +} + +// 降序 +func quickSort(nums [][]decimal.Decimal) [][]decimal.Decimal { + n := len(nums) + for i := 0; i < n-1; i++ { + minIdx := i + for j := i + 1; j < n; j++ { + if nums[j][0].GreaterThan(nums[minIdx][0]) { + minIdx = j + } + } + nums[i], nums[minIdx] = nums[minIdx], nums[i] + } + return nums +} diff --git a/cmd/selfMarketSpot/model.go b/cmd/selfMarketSpot/model.go new file mode 100644 index 0000000..86155a9 --- /dev/null +++ b/cmd/selfMarketSpot/model.go @@ -0,0 +1,59 @@ +package selfMarketSpot + +import ( + "fmt" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "wss-pool/internal/data" +) + +func GetNewPrice(contractCode string) []data.MongoTick { + filter := bson.M{"channel": fmt.Sprintf("market.%s.kline.1min", contractCode)} + tableName := data.GetStockKLineTableName("1min") + pagedData, _ := data.MgoLimitFind(tableName, filter, int64(1)) + return pagedData +} + +func GetNewPriceAll(contractCode, period string) []data.MongoTick { + filter := bson.M{"channel": fmt.Sprintf("market.%s.kline.%s", contractCode, period)} + tableName := data.GetStockKLineTableName(period) + pagedData, _ := data.MgoLimitFind(tableName, filter, int64(1)) + return pagedData +} + +func GetTimeNewPrice(contractCode string, from, to int64, period string) (decimal.Decimal, decimal.Decimal, decimal.Decimal, decimal.Decimal, decimal.Decimal) { + filter := bson.M{"channel": fmt.Sprintf("market.%s.kline.%s", contractCode, period), "code": bson.M{"$gte": from, "$lte": to}} + tableName := data.GetStockKLineTableName(period) + pagedData, _ := data.MgoLimitFind(tableName, filter, int64(0)) + var low, high, vol, amount, count decimal.Decimal + for key, v := range pagedData { + lows, _ := decimal.NewFromString(v.Low) + highs, _ := decimal.NewFromString(v.High) + vols, _ := decimal.NewFromString(v.Vol) + amounts, _ := decimal.NewFromString(v.Amount) + var counts decimal.Decimal + if countItem, ok := v.Count.(string); ok { + counts, _ = decimal.NewFromString(countItem) + } else if countItem, ok := v.Count.(int64); ok { + counts = decimal.NewFromInt(countItem) + } + if key == 0 { + low = lows + high = highs + vol = vols + amount = amounts + count = counts + continue + } + vol = vol.Add(vols) + amount = vol.Add(amounts) + count = vol.Add(counts) + if low.GreaterThan(lows) { + low = lows + } + if high.LessThan(highs) { + high = highs + } + } + return low, high, vol, amount, count +} diff --git a/cmd/selfMarketSpot/virtualContract.go b/cmd/selfMarketSpot/virtualContract.go new file mode 100644 index 0000000..e864767 --- /dev/null +++ b/cmd/selfMarketSpot/virtualContract.go @@ -0,0 +1,698 @@ +package selfMarketSpot + +import ( + "encoding/json" + "fmt" + "github.com/shopspring/decimal" + "math/rand" + "strings" + "sync" + "time" + "wss-pool/cmd/common" + "wss-pool/cmd/websocketservice" + "wss-pool/dictionary" + "wss-pool/internal/data/business" + "wss-pool/internal/model" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/market" +) + +const ( + proportion int32 = 10000 //调价原价比例 + step int32 = 20 //调价指数 + digits int32 = 4 //保留小数位数 + numPrices int = 20 //生成随机价格数量 + defaultStep float64 = 0.001 // 默认波动率 + PushFrequency int = 3 //S + TimeRemaining int64 = 20 //s +) + +var ( + contractChan = make(chan string) + SelfSymbol string //现货 + InitialPrice decimal.Decimal // 初始价格 + KLineMap = make(map[string]KlineLowHigh) + //ClosePrices decimal.Decimal //当前价格 //做调价终止值使用 + OldFiveMin int64 //记录K线时间搓 + OldFifteenMin int64 + OldThirtyMin int64 + OldOneHour int64 + OldFourHour int64 + OldDay int64 + OldWeek int64 + OldMon int64 + IsRun bool + MarketSpotMap = make(map[string]bool) //保存领取的任务 + lock sync.Mutex + EndChan = make(chan string) +) + +type KlineLowHigh struct { + Low decimal.Decimal + High decimal.Decimal + ID int64 + Vol decimal.Decimal + Amount decimal.Decimal + Count decimal.Decimal + Open decimal.Decimal +} + +type ConstructorContract struct { + SelfSymbol string `json:"selfSymbol"` //现货 + BeginTime string `json:"beginTime"` //开始时间 + EndTime string `json:"endTime"` //结束时间 + MaxPrice decimal.Decimal `json:"maxPrices"` + MinPrice decimal.Decimal `json:"minPrice"` +} + +func NewSelfMarketSpot() { + //首次启动 + go func() { + this := new(ConstructorContract) + this.MinPrice = InitialPrice + this.SelfSymbol = SelfSymbol + this.defaultContract() + }() + for { + t := time.NewTimer(1 * time.Minute) + <-t.C + go func() { + if IsRun { + applogger.Info("已有调价在运行, 该次调价不能运行") + return + } + result := getData() + fmt.Println(result) + for _, v := range result { + v.TradeName = common.ToLower(v.TradeName) + if v.TradeName != SelfSymbol { + applogger.Info("parametric inequality") + continue + } + end, _ := common.TimeStrToTimes(v.EndTime) + if end.Unix() <= common.TimeToNow() { + applogger.Info("该调价已过期") + continue + } + if _, ok := MarketSpotMap[fmt.Sprintf("%s-%d", v.TradeName, end.Unix())]; ok { + applogger.Info("该任务正在运行", fmt.Sprintf("%s-%d", v.TradeName, end.Unix())) + continue + } + MarketSpotMap[fmt.Sprintf("%s-%d", v.TradeName, end.Unix())] = true //保存任务 + begin, _ := common.TimeStrToTimes(v.BeginTime) + if begin.Unix() < common.TimeToNow() { + applogger.Info("该调价已过期") + continue + } else if end.Unix() <= begin.Unix() { + applogger.Info("begin end 有误") + continue + } + maxPrice, _ := decimal.NewFromString(v.MaxPrice) + //if maxPrice.LessThan(InitialPrice) { + // applogger.Info("调价有误",v.MaxPrice) + // continue + //} + //等待到开始时间 + applogger.Info("等待到begin time ", v.BeginTime, v.EndTime) + IsRun = true // 调价任务已开启 + time.Sleep(begin.Sub(common.TimeToNows())) + // startd := time.Now() + contractChan <- "constructorStart" + this := new(ConstructorContract) + this.SelfSymbol = SelfSymbol + this.EndTime = v.EndTime + this.MinPrice = InitialPrice + this.MaxPrice = maxPrice + this.BeginTime = v.BeginTime + // fmt.Println("断开 默认波动 时间 Run time: ", time.Since(startd)) + this.constructor(begin, end) + //更改状态 + fmt.Println("调价结束") + contract := model.NewContractMarket() + contract.ID = v.ID + contract.UpdateIsGetOne() + } + if IsRun && len(result) > 0 { + IsRun = false + applogger.Debug("开启新的默认携程") + this := new(ConstructorContract) + this.MinPrice = InitialPrice + this.SelfSymbol = SelfSymbol + this.defaultContract() + } + }() + } +} + +func getData() []model.ContractMarket { + contract := model.NewContractMarket() + contract.IsType = model.Market + symbol := strings.Split(SelfSymbol, "usdt") + contract.TradeName = strings.ToUpper(symbol[0]) + result := contract.List() + return result +} + +// 规定启动 +func (this *ConstructorContract) constructor(begin, end time.Time) { + applogger.Info("开始调价。。。", "结束时间:", this.EndTime, "当前价格:", this.MinPrice, "最终价格", this.MaxPrice) + totalDuration := end.Sub(common.TimeToNows()) + closePrice := this.MinPrice + highPrice := this.MinPrice + lowPrice := this.MinPrice + oldPrice := this.MinPrice + var openPrice decimal.Decimal + //temporalFrequency := time.Duration(PushFrequency) * time.Second + timeInterval := float64(totalDuration) / float64(time.Minute) + delta := this.MaxPrice.Sub(closePrice).Div(decimal.NewFromFloat(timeInterval)) + fluctuation := closePrice.Mul(decimal.NewFromInt32(step).Div(decimal.NewFromInt32(proportion))).Round(digits) + applogger.Info("timeInterval", timeInterval, "delta", delta, "fluctuation", fluctuation) + // 开始进行调价 + for !this.MaxPrice.Equal(InitialPrice) || end.Unix() > common.TimeToNows().Unix() { + fmt.Println("当前价格", InitialPrice) + prices := this.generateRandomPrices(InitialPrice, fluctuation, delta) + openPrice = InitialPrice + this.GetAllLowHigh(highPrice, lowPrice) + nonVanishing(lowPrice) + numFrequency := int(60) / PushFrequency + ClearTotal() + for i := 1; i <= numFrequency; i++ { + //go func(prices []decimal.Decimal, closePrice, openPrice, highPrice, lowPrice, oldPrice decimal.Decimal, this *ConstructorContract) { + start := time.Now() // 获取当前时间 + rand.New(rand.NewSource(time.Now().UnixNano())) + key := rand.Intn(len(prices)) + closePrice = prices[key] + // applogger.Info("实际落盘价:", closePrice) + //if (closePrice.Sub(this.MaxPrice)).Mul(delta).GreaterThan(decimal.NewFromInt32(0)) { + // closePrice = this.MaxPrice + //} + //当只剩 20 秒 + if (end.Unix() - common.TimeToNows().Unix()) < TimeRemaining { + closePrice = this.MaxPrice + } + ////生成深度、Trade Detail 数据 + CreateDepth(oldPrice, closePrice) + ////k 线 详情 + this.pullStorage(closePrice, openPrice, highPrice, lowPrice, oldPrice, prices) + applogger.Debug("目标价格", this.MaxPrice, "当前价格", closePrice, "调价结束时间", this.EndTime, "i", i, time.Now().Format("2006-01-02 15:04:05")) + oldPrice = closePrice + //更新市价 + lock.Lock() + InitialPrice = closePrice + lock.Unlock() + if end.Unix() <= common.TimeToNows().Unix() { + break + } + fmt.Println("Run time: ", time.Since(start)) + s := float64(PushFrequency) - time.Since(start).Seconds() + if s > float64(0) { + applogger.Debug("停留 秒", s) + time.Sleep(time.Duration(s) * time.Second) + } + } + } +} + +func nonVanishing(low decimal.Decimal) { + for k, v := range KLineMap { + if v.Low.IsZero() { + v.Low = low + } + KLineMap[k] = v + } +} + +func (this *ConstructorContract) defaultContract() { + closePrice := this.MinPrice + highPrice := this.MinPrice + lowPrice := this.MinPrice + oldPrice := this.MinPrice + var openPrice decimal.Decimal + //var temporalFrequency = time.Duration(PushFrequency) * time.Second + for { + select { + case _, ok := <-contractChan: // 从管道接收值 + if ok { + applogger.Info("calculateContractPrice start,defaultContract") + return + } + default: + //fmt.Println(time.Now().Format("2006-01-02 15:04:05")) + rand.New(rand.NewSource(time.Now().UnixNano())) + openPrice = InitialPrice + Loop: + prices := calculateContractPrice(InitialPrice) + fmt.Println("开盘价", InitialPrice) + // 更新落盘价、开盘价、最高价和最低价 + key := rand.Intn(numPrices) + highPrices := this.getMaxPrices(openPrice, prices) + lowPrices := this.getMinPrices(openPrice, prices) + //被2整除 涨 + if key%2 == 0 { + highPrice = this.getMaxPrice(openPrice, highPrices) + lowPrice = openPrice + prices = highPrices + } else { + highPrice = openPrice + lowPrice = this.getMinPrice(openPrice, lowPrices) + prices = lowPrices + } + if len(prices) <= 0 { + goto Loop + } + this.GetAllLowHigh(highPrice, lowPrice) + nonVanishing(lowPrice) + numFrequency := int(60) / PushFrequency + ClearTotal() + for i := 1; i <= numFrequency; i++ { + // start := time.Now() // 获取当前时间 + // fmt.Println("Run time: ", time.Since(start)) + go func(prices []decimal.Decimal, closePrice, openPrice, highPrice, lowPrice, oldPrice decimal.Decimal, this *ConstructorContract) { + ////更新市价 + //fmt.Println(closePrice) + rand.New(rand.NewSource(time.Now().UnixNano())) + key := rand.Intn(len(prices)) + closePrice = prices[key] + lock.Lock() + InitialPrice = closePrice + lock.Unlock() + //// 生成深度、Trade Detail 数据 + CreateDepth(oldPrice, closePrice) + //fmt.Println("Run time: ", time.Since(start)) + this.pullStorage(closePrice, openPrice, highPrice, lowPrice, oldPrice, prices) + //fmt.Println("123123123 ", closePrice,openPrice, highPrice, lowPrice, oldPrice) + oldPrice = closePrice + }(prices, closePrice, openPrice, highPrice, lowPrice, oldPrice, this) + time.Sleep(time.Duration(int64(PushFrequency)) * time.Second) + } + } + } +} + +func generateRandomStep(min, max decimal.Decimal) decimal.Decimal { + rand.New(rand.NewSource(time.Now().UnixNano())) + return min.Add(decimal.NewFromFloat(rand.Float64()).Mul(max.Sub(min))) +} + +// 计算现货价格 +func calculateContractPrice(basePrice decimal.Decimal) []decimal.Decimal { + prices := make([]decimal.Decimal, 0) + max := basePrice.Mul(decimal.NewFromFloat(defaultStep)).Round(digits) + min := max.Neg() + for i := 0; i < numPrices; i++ { + price := basePrice.Add(generateRandomStep(max, min)).Round(digits) + prices = append(prices, price) + } + return prices +} + +func getOpen(timestamp int64, period string) decimal.Decimal { + tick := GetNewPriceAll(SelfSymbol, period) + open := InitialPrice + if len(tick) > 0 { + switch tick[0].Code { + case timestamp: + open, _ = decimal.NewFromString(tick[0].Open) + default: + open, _ = decimal.NewFromString(tick[0].Close) + } + } + return open +} + +func (this *ConstructorContract) GetAllLowHigh(high, low decimal.Decimal) { + //初始化 + if len(KLineMap) == 0 { + KLineMap["1min"] = KlineLowHigh{ + ID: common.GenerateSingaporeMinuteTimestamp(), + Low: low, + High: high, + Vol: decimal.NewFromInt(0), + Amount: decimal.NewFromInt(0), + Count: decimal.NewFromInt(0), + Open: InitialPrice, + } + + to := common.GenerateSingaporeFiveMinTimestamp() + from := to - int64(5*60) + low, high, vol, amount, count := GetTimeNewPrice(SelfSymbol, from, to, "1min") + KLineMap["5min"] = KlineLowHigh{ + ID: to, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + Open: getOpen(to, "5min"), + } + OldFiveMin = to + + to = common.GenerateSingaporeFifteenMinTimestamp() + from = to - int64(15*60) + low, high, vol, amount, count = GetTimeNewPrice(SelfSymbol, from, to, "5min") + KLineMap["15min"] = KlineLowHigh{ + ID: to, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + Open: getOpen(to, "15min"), + } + OldFifteenMin = to + + to = common.GenerateSingaporeThirtyMinTimestamp() + from = to - int64(30*60) + low, high, vol, amount, count = GetTimeNewPrice(SelfSymbol, from, to, "15min") + KLineMap["30min"] = KlineLowHigh{ + ID: to, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + Open: getOpen(to, "30min"), + } + OldThirtyMin = to + + from = common.GenerateSingaporeHourTimestamp() + to = from + int64(60*59) + low, high, vol, amount, count = GetTimeNewPrice(SelfSymbol, from, to, "30min") + KLineMap["60min"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + Open: getOpen(from, "60min"), + } + OldOneHour = from + + to = common.GenerateSingaporeFourHourTimestamp() + from = to - (4 * 60 * 60) + low, high, vol, amount, count = GetTimeNewPrice(SelfSymbol, from, to, "60min") + KLineMap["4hour"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + Open: getOpen(from, "4hour"), + } + OldFourHour = from + + from = common.GenerateSingaporeDayTimestamp("") + to = from + int64(60*60*24-1) + low, high, vol, amount, count = GetTimeNewPrice(SelfSymbol, from, to, "4hour") + KLineMap["1day"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + Open: getOpen(from, "1day"), + } + OldDay = from + + from = common.GenerateSingaporeMonTimestamp() + to = common.TimeToNow() + low, high, vol, amount, count = GetTimeNewPrice(SelfSymbol, from, to, "1week") + KLineMap["1mon"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + Open: getOpen(from, "1mon"), + } + OldMon = from + + from = common.GetWeekTimestamp() + to = common.TimeToNow() + low, high, vol, amount, count = GetTimeNewPrice(SelfSymbol, from, to, "1day") + KLineMap["1week"] = KlineLowHigh{ + ID: from, + Low: low, + High: high, + Vol: vol, + Amount: amount, + Count: count, + Open: getOpen(from, "1week"), + } + OldWeek = from + } + + for k, v := range KLineMap { + switch k { + case "1min": + v.ID = common.GenerateSingaporeMinuteTimestamp() + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.High = high + v.Low = low + v.Open = InitialPrice + case "5min": + v.ID = common.GenerateSingaporeFiveMinTimestamp() + if v.ID > OldFiveMin { + //新时间节点更新 + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldFiveMin = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "15min": + v.ID = common.GenerateSingaporeFifteenMinTimestamp() + if v.ID > OldFifteenMin { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldFifteenMin = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "30min": + v.ID = common.GenerateSingaporeThirtyMinTimestamp() + if v.ID > OldThirtyMin { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldThirtyMin = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "60min": + v.ID = common.GenerateSingaporeHourTimestamp() + if v.ID > OldOneHour { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldOneHour = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "4hour": + v.ID = common.GenerateSingaporeFourHourTimestamp() - (4 * 60 * 60) + if v.ID > OldFourHour { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldFourHour = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "1day": + v.ID = common.GenerateSingaporeDayTimestamp("") + if v.ID > OldDay { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldDay = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "1week": + v.ID = common.GetWeekTimestamp() + if v.ID > OldWeek { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldWeek = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + case "1mon": + v.ID = common.GenerateSingaporeMonTimestamp() + if v.ID > OldMon { + v.Vol = decimal.NewFromFloat(0) + v.Amount = decimal.NewFromFloat(0) + v.Count = decimal.NewFromFloat(0) + v.Low = low + v.High = high + v.Open = getOpen(v.ID, k) + OldMon = v.ID + } + if v.Low.GreaterThan(low) { + v.Low = low + } + if v.High.LessThan(high) { + v.High = high + } + } + KLineMap[k] = v + } +} + +func (this *ConstructorContract) pullStorage(close, open, high, low, oldPrice decimal.Decimal, prices []decimal.Decimal) { + for _, v := range dictionary.ContractPriceTime { + resp := market.SubscribeCandlestickResponse{ + Channel: fmt.Sprintf("market.%s.kline.%s", this.SelfSymbol, v), + Timestamp: KLineMap[v].ID, + Tick: &market.Tick{ + Open: KLineMap[v].Open, + High: KLineMap[v].High, + Low: KLineMap[v].Low, + Close: close, + Id: KLineMap[v].ID, + Count: int(TotalCount.Add(KLineMap[v].Count).IntPart()), + Amount: TotalAmount.Add(KLineMap[v].Amount), + }, + } + if v == "1day" { + go OneDayDetailMerged(resp) + } + jsonMessage, _ := json.Marshal(websocketservice.Message{ + ServersId: resp.Channel, + Content: resp, + Symbol: resp.Channel}) + //applogger.Info("SubscribeKline %s:", string(jsonMessage), v) + red.RedisClient.Publish(resp.Channel, string(jsonMessage)) + go business.UpdateWsMgo(resp) + } + //详情 + detail := market.SubscribeCtDetailResponse{ + Channel: fmt.Sprintf("market.%s.detail", this.SelfSymbol), + Timestamp: time.Now().Unix(), + Tick: &market.CtDetailTick{ + Open: open, + High: high, + Low: low, + Close: close, + Id: time.Now().Unix(), + Count: TotalCount.IntPart(), + Amount: TotalAmount, + }, + } + jsonMessages, _ := json.Marshal(websocketservice.Message{ + ServersId: detail.Channel, + Content: detail, + Symbol: detail.Channel}) + //applogger.Info("detail :", string(jsonMessages)) + red.RedisClient.Publish(detail.Channel, string(jsonMessages)) +} + +// 生成随机价格波动 +func (this *ConstructorContract) generateRandomPrices(currentPrice, fluctuation, delta decimal.Decimal) []decimal.Decimal { + prices := make([]decimal.Decimal, 0) + rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < numPrices; i++ { + price := currentPrice.Add(delta.Add(fluctuation.Mul(decimal.NewFromFloat(2*rand.Float64() - 1)))).Round(digits) + prices = append(prices, price) + } + return prices +} + +// 获取最高价 +func (this *ConstructorContract) getMaxPrices(highPrice decimal.Decimal, prices []decimal.Decimal) []decimal.Decimal { + res := make([]decimal.Decimal, 0) + for _, price := range prices { + if price.GreaterThan(highPrice) { + res = append(res, price) + } + } + return res +} +func (this *ConstructorContract) getMaxPrice(highPrice decimal.Decimal, prices []decimal.Decimal) decimal.Decimal { + for _, price := range prices { + if price.GreaterThan(highPrice) { + highPrice = price + } + } + return highPrice +} + +// 获取最低价 +func (this *ConstructorContract) getMinPrices(lowPrice decimal.Decimal, prices []decimal.Decimal) []decimal.Decimal { + res := make([]decimal.Decimal, 0) + for _, price := range prices { + if price.LessThan(lowPrice) { + res = append(res, price) + } + } + return res +} + +func (this *ConstructorContract) getMinPrice(lowPrice decimal.Decimal, prices []decimal.Decimal) decimal.Decimal { + for _, price := range prices { + if price.LessThan(lowPrice) { + lowPrice = price + } + } + return lowPrice +} diff --git a/cmd/servicemanager/currencyWss.go b/cmd/servicemanager/currencyWss.go new file mode 100644 index 0000000..fe87d33 --- /dev/null +++ b/cmd/servicemanager/currencyWss.go @@ -0,0 +1,13 @@ +package servicemanager + +import ( + "wss-pool/cmd/websocketservice" + "wss-pool/config" + red "wss-pool/internal/redis" +) + +// Currency TODO: 优化需要多个广播通道 +func Currency(ipServer, addrServer string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + websocketservice.Connect(ipServer, addrServer) +} diff --git a/cmd/servicemanager/gather.go b/cmd/servicemanager/gather.go new file mode 100644 index 0000000..ad14a42 --- /dev/null +++ b/cmd/servicemanager/gather.go @@ -0,0 +1,31 @@ +package servicemanager + +import ( + "fmt" + "github.com/gin-gonic/gin" + "wss-pool/cmd/marketwsscliert" + "wss-pool/config" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" +) + +// Gather +func Gather(checkStr, ipServer, addrServer string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + // Enable collaborative data collection + data.Mgo_init(config.Config.Mongodb) + //binance + //go bamarketwsscliert.RunBaDataRedis(checkStr) + go marketwsscliert.RunHBDataRedis(checkStr) + + // Register Route + router := gin.Default() + addr := fmt.Sprintf("%v%v", ipServer, addrServer) + applogger.Info("intService---addr:%v", addr) + + // Start Service + if err := router.Run(addr); err != nil { + applogger.Error("Failed to start Gin data collection service:%v", err) + } +} diff --git a/cmd/servicemanager/gin.go b/cmd/servicemanager/gin.go new file mode 100644 index 0000000..5c47aa0 --- /dev/null +++ b/cmd/servicemanager/gin.go @@ -0,0 +1,68 @@ +package servicemanager + +import ( + "fmt" + "github.com/gin-gonic/gin" + "wss-pool/api" + "wss-pool/cmd/common" + "wss-pool/cmd/websocketservice" + "wss-pool/config" + "wss-pool/internal/data" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" +) + +// GinServer +func GinServer(ipServer, addrServer, serverName, project string) { + data.Mgo_init(config.Config.Mongodb) + var server = &gin.Engine{} + if serverName != "gin" { + red.RedisInitMap(common.GetRedisDBMore(config.Config.Redis.DbMore)) + // TODO: 插针(股票市场名称),目前p6,p7不要插针 + go business.NewPinStock(common.GetRedisNoPin(config.Config.Redis.NoPinAss)) + } + switch serverName { + case "gin": // 数字币市场 + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + data.InitMsqlDB(config.Config.Bourse) + server = api.RouterApiServer(project) + case "stockIndex": // 股票指数 + server = api.RouterStockIndexApiServer() + case "indiaOption": // 印度期权市场 + server = api.RouterIndiaOptionApiServer() + case "usStock": // 美股市场 + server = api.RouterUSApiServer() + case "indonesiaStock": // 印尼市场 + server = api.RouterIndonesiaApiServer() + case "thailandStock": // 泰国市场 + server = api.RouterThailandApiServer() + case "indiaStock": // 印度市场 + server = api.RouterIndiaApiServer() + case "malaysiaStock": // 马股市场 + server = api.RouterMalaysiaApiServer() + case "singaporeStock": // 新加坡市场 + server = api.RouterSingaporeApiServer() + case "hongkongStock": // 港股市场 + server = api.RouterHongKongApiServer() + case "ukStock": // 英股市场 + server = api.RouterUKApiServer() + case "franceStock": // 法股市场 + server = api.RouterFranceApiServer() + case "germanyStock": // 德股市场 + server = api.RouterGermanyApiServer() + case "brazilStock": // 巴西市场 + server = api.RouterBrazilApiServer() + case "japanStock": // 日本市场 + server = api.RouterJapanApiServer() + } + addr := fmt.Sprintf("%v%v", ipServer, addrServer) + applogger.Info("intService---addr:%v", addr) + // TODO: 缓存交易对数据 + if project == common.CoinProject || config.Config.ServerLevel == "test" { + websocketservice.SubscriptionCache() + } + if err := server.Run(addr); err != nil { + applogger.Error("Failed to start Gin data collection service:%v", err) + } +} diff --git a/cmd/servicemanager/selfContract.go b/cmd/servicemanager/selfContract.go new file mode 100644 index 0000000..092a1ab --- /dev/null +++ b/cmd/servicemanager/selfContract.go @@ -0,0 +1,38 @@ +package servicemanager + +import ( + "github.com/shopspring/decimal" + "os" + "wss-pool/cmd/selfContract" + "wss-pool/config" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" +) + +// Gather +func SelfContract(checkStr, ipServer, addrServer, contractCode string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + data.Mgo_init(config.Config.Mongodb) + data.InitGorm(config.Config.Bourse) + if contractCode == "" { + applogger.Error("Lack of contract code") + os.Exit(400) + } + //初始价格优先数据库 + res := selfContract.GetNewPrice(contractCode) + if len(res) > 0 { + selfContract.InitialPrice = decimal.RequireFromString(res[0].Close) + } + if selfContract.InitialPrice.IsZero() { + selfContract.InitialPrice, _ = decimal.NewFromString(checkStr) + } + if selfContract.InitialPrice.IsZero() { + applogger.Error("Lack of initial price") + os.Exit(400) + } + selfContract.SelfContractCode = contractCode + applogger.Info("合约", selfContract.SelfContractCode, "初始价格", selfContract.InitialPrice) + // Enable collaborative data collection + selfContract.NewSelfContract() +} diff --git a/cmd/servicemanager/selfMarketSpot.go b/cmd/servicemanager/selfMarketSpot.go new file mode 100644 index 0000000..2de0aba --- /dev/null +++ b/cmd/servicemanager/selfMarketSpot.go @@ -0,0 +1,38 @@ +package servicemanager + +import ( + "github.com/shopspring/decimal" + "os" + "wss-pool/cmd/selfMarketSpot" + "wss-pool/config" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" +) + +// Gather +func SelfMarketSpot(checkStr, ipServer, addrServer, symbol string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + data.Mgo_init(config.Config.Mongodb) + data.InitGorm(config.Config.Bourse) + if symbol == "" { + applogger.Error("Lack of symbol") + os.Exit(400) + } + //初始价格优先数据库 + res := selfMarketSpot.GetNewPrice(symbol) + if len(res) > 0 { + selfMarketSpot.InitialPrice = decimal.RequireFromString(res[0].Close) + } + if selfMarketSpot.InitialPrice.IsZero() { + selfMarketSpot.InitialPrice, _ = decimal.NewFromString(checkStr) + } + if selfMarketSpot.InitialPrice.IsZero() { + applogger.Error("Lack of initial price") + os.Exit(400) + } + selfMarketSpot.SelfSymbol = symbol + applogger.Info("现货", selfMarketSpot.SelfSymbol, "初始价格", selfMarketSpot.InitialPrice) + // Enable collaborative data collection + selfMarketSpot.NewSelfMarketSpot() +} diff --git a/cmd/servicemanager/shareWss.go b/cmd/servicemanager/shareWss.go new file mode 100644 index 0000000..fc665f0 --- /dev/null +++ b/cmd/servicemanager/shareWss.go @@ -0,0 +1,79 @@ +package servicemanager + +import ( + "fmt" + "github.com/gin-gonic/gin" + "wss-pool/cmd/common" + "wss-pool/cmd/marketwsscliert" + "wss-pool/config" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" +) + +// GatherUS +// +// @Description: 美股采集 +// @param checkStr +// @param ipServer +// @param addrServer +func GatherUS(checkStr, ipServer, addrServer string) { + // 初始化相同项目mongodb和redis的ip对应关系 + //data.MgoDbToRedisMap = common.GetMgoDbToRedisMap(config.Config.Mongodb.RedisToMongodb) + // 初始化所有项目MongoDB + //data.Mgo_initMap(config.Config.Mongodb, common.GetMongodbAddrList(config.Config.Mongodb.AddrList)) + // 初始化所有项目Redis + red.RedisInitMapList(common.GetRedisAddrList(config.Config.Redis.AddrList)) + + // Enable collaborative data collection + go marketwsscliert.ShareMarketBak(checkStr) + + // Register Route + router := gin.Default() + addr := fmt.Sprintf("%v%v", ipServer, addrServer) + applogger.Info("intService---addr:%v", addr) + + // Start Service + if err := router.Run(addr); err != nil { + applogger.Error("Failed to start Gin data collection service:%v", err) + } +} + +// GatherForex +// +// @Description: 外汇采集 +// @param checkStr +// @param ipServer +// @param addrServer +func GatherForex(checkStr, ipServer, addrServer string) { + data.Mgo_init(config.Config.Mongodb) + // init Redis + red.RedisInitMapList(common.GetRedisAddrList(config.Config.Redis.AddrList)) + + // 外汇插针配置加载 + go marketwsscliert.GetModifyForex() + // 外汇实时报价 + go marketwsscliert.ForexMarketBak(checkStr) + // 外汇实时天报价 + go marketwsscliert.ForexMarketDayBak(checkStr) + // 外汇买一卖一报价 + go marketwsscliert.ForexMarketQuoteBak(checkStr) + // 外汇成交报价 + go marketwsscliert.ForexMarketTradeBak(checkStr) + // 外汇成交报价存储 + go marketwsscliert.ForexMarketTradeBak2(checkStr) + // 外汇成交报价清理 + go marketwsscliert.ForexMarketClearTradeBak2(checkStr) + // 外汇交易对每天0点0分0秒更新数据 + go marketwsscliert.ForexUpdateClosePrice() + + // Register Route + router := gin.Default() + addr := fmt.Sprintf("%v%v", ipServer, addrServer) + applogger.Info("intService---addr:%v", addr) + + // Start Service + if err := router.Run(addr); err != nil { + applogger.Error("Failed to start Gin data collection service:%v", err) + } +} diff --git a/cmd/servicemanager/shareus.go b/cmd/servicemanager/shareus.go new file mode 100644 index 0000000..ee815d4 --- /dev/null +++ b/cmd/servicemanager/shareus.go @@ -0,0 +1,20 @@ +package servicemanager + +import ( + "wss-pool/cmd/closingMarket" + "wss-pool/cmd/websocketservice" + "wss-pool/config" + red "wss-pool/internal/redis" +) + +// shareWs +func ShareWss(ipServer, addrServer string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbUser) + websocketservice.ShareConnect(ipServer, addrServer) +} + +// TODO: 针对插针 +func PinWs(ipServer, addrServer string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbUser) + closingMarket.ShareConnect(ipServer, addrServer) +} diff --git a/cmd/servicemanager/tickdb.go b/cmd/servicemanager/tickdb.go new file mode 100644 index 0000000..913537e --- /dev/null +++ b/cmd/servicemanager/tickdb.go @@ -0,0 +1,55 @@ +package servicemanager + +import ( + "wss-pool/config" + "wss-pool/internal/data" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" +) + +// TickDB +func TickDB(checkStr, ipServer, addrServer, contractCode string) { + data.Mgo_init(config.Config.Mongodb) + switch checkStr { + case "spotKline": // 现货历史数据 + business.TickUpdateSpotKline() + case "all": // 合约全部历史数据 + business.TickUpdateContractKline(true) + case "allUs": // 美股全部历史数据 + business.TickUpdateStockUs(true) + case "Us": // 实时数据 + business.TickUpdateStockUs(false) + case "previousClose": // TODO: 更新美股上次比盘价 + business.UpdateStockUS() + case "UsNewPrice": + business.UsNewPrice(contractCode) + case "usOpenPrice": + business.UpdateOpenPrice() + case "southAsiaStock": // TODO: 自动聚合东南亚国家数据 + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + business.TickSouthAsiaSpotKline(contractCode) + case "stockIndex": // TODO: 自动聚合指数 + business.TickSpotIndexKline() + case "deleteSpot": // TODO: 自动数据清理:contractCode(true:清理数据,false:清理并优化多余数据) + business.DeleteSpot(contractCode) + case "stockCloseData": // TODO: 自动检测并推送插针数据 + business.StockClosedData() + case "checkStock": // TODO: 自动机器人行情检查 + business.CheckStock() + case "updateStockCode": // 更新股票code和交易所 + business.UpdateStockCode() + case "updateStockUsCode": // 更新美股股票code和交易所 + business.UpdateStockUsCode() + case "updateStockExchange": // 更新印度股票(Exchange字段) + business.UpdateStockExchange() + case "forexClosePrice": // TODO: 更新外汇闭盘价 + business.ForexUpdateCode() + case "deleteForexTrade": // TODO: 清理外汇成交报价 + business.DeleteForexTrade() + default: + //抓取美股 + //business.InitStockList() + //查询美股交易所 + //business.UpdateStockUSTape() + } +} diff --git a/cmd/websocketcollect/cache/leveldb.go b/cmd/websocketcollect/cache/leveldb.go new file mode 100644 index 0000000..5bf8aef --- /dev/null +++ b/cmd/websocketcollect/cache/leveldb.go @@ -0,0 +1,161 @@ +package cache + +import ( + "encoding/json" + "fmt" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/filter" + "github.com/syndtr/goleveldb/leveldb/opt" + "go.mongodb.org/mongo-driver/bson" + "strings" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +var db *leveldb.DB + +// initDB +// +// @Description: +func InitDB(name string) { + o := &opt.Options{ + Filter: filter.NewBloomFilter(10), + } + + var tableNamePath string + switch config.Config.ServerLevel { + case "test": + tableNamePath = fmt.Sprintf("./cmd/websocketcollect/db/%v", name) + default: + tableNamePath = fmt.Sprintf("/home/ubuntu/wss-server/db/%v", name) + } + + dba, err := leveldb.OpenFile(tableNamePath, o) + if err != nil { + applogger.Error("OpenFile err:%v", err) + return + } + + db = dba +} + +// WriteDB +// +// @Description: +// @param key +// @param value +// @return error +func WriteDB(key, value string) error { + batch := new(leveldb.Batch) + batch.Put([]byte(key), []byte(value)) + if err := db.Write(batch, nil); err != nil { + fmt.Printf("Write err:%v\n", err) + return err + } + + return nil +} + +// ReadDB +// +// @Description: +// @param key +// @return []byte +func ReadDB(key string) []byte { + data, err := db.Get([]byte(key), nil) + if err != nil { + fmt.Printf("OpenFile err:%v\n", err) + return []byte{} + } + + return data +} + +// ReadListDB +// +// @Description: +// @return []string +func ReadListDB() []string { + var privateKey []string + iter := db.NewIterator(nil, nil) + for iter.Next() { + value := iter.Value() + privateKey = append(privateKey, string(value)) + } + iter.Release() + if err := iter.Error(); err != nil { + fmt.Printf("iter.Error:%v\n", err) + return privateKey + } + + return privateKey +} + +// DeleteByKey +// +// @Description: +// @param key +// @return error +func DeleteByKey(key string) error { + if err := db.Delete([]byte(key), nil); err != nil { + return err + } + + return nil +} + +// InitShareUsCode +// +// @Description: 初始化美股股票代码列表 +func InitShareUsCode() { + filter := bson.M{"Country": "US", "YesterdayClose": bson.M{"$ne": ""}} + projection := bson.M{"Code": 1} + + for _, mongoClient := range data.MgoDbClientMap { + stockRes := make([]stock.StockPolygon, 0) + data.MgoPagingFindStructProjectionNew(mongoClient, data.StockList, filter, projection, 20000, 1, -1, &stockRes) + for _, v := range stockRes { + if err := WriteDB(v.Code, v.Code); err != nil { + applogger.Error("sendUsCodeNew err:%v", err) + continue + } + } + applogger.Info("写入美股股票代码列表:%v", ReadListDB()) + applogger.Debug("写入美股股票代码列表总数:%v", len(ReadListDB())) + } +} + +// InitForexCode +// +// @Description: 初始化外汇代码列表 +func InitForexCode() { + // https://finnhub.io/api/v1/forex/symbol?exchange=oanda&token=cqt0409r01qvdch2gnpgcqt0409r01qvdch2gnq0 + url := fmt.Sprintf("https://%vforex/symbol?exchange=oanda&token=%v", config.Config.FinnhubUs.FinnhubHost, config.Config.FinnhubUs.FinnhubKey) + applogger.Debug("url data info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("WriteShareUs err info:%v", err) + return + } + applogger.Debug("new add shareUs code info:%v", bodyStr) + + var dataForex []model.ForexCodeList + if err = json.Unmarshal([]byte(bodyStr), &dataForex); err != nil { + applogger.Error("json.Unmarshal err info:%v", err) + return + } + for _, v := range dataForex { + code := strings.Replace(v.DisplaySymbol, "/", "", -1) + if err := WriteDB(code, code); err != nil { + applogger.Error("sendUsCodeNew err:%v", err) + continue + } + } + applogger.Info("写入外汇代码列表:%v", ReadListDB()) + applogger.Debug("写入外汇代码列表总数:%v", len(ReadListDB())) +} diff --git a/cmd/websocketcollect/db/forex/CURRENT b/cmd/websocketcollect/db/forex/CURRENT new file mode 100644 index 0000000..4fb1dad --- /dev/null +++ b/cmd/websocketcollect/db/forex/CURRENT @@ -0,0 +1 @@ +MANIFEST-000003 diff --git a/cmd/websocketcollect/db/forex/CURRENT.bak b/cmd/websocketcollect/db/forex/CURRENT.bak new file mode 100644 index 0000000..feda7d6 --- /dev/null +++ b/cmd/websocketcollect/db/forex/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000000 diff --git a/cmd/websocketcollect/db/forex/LOCK b/cmd/websocketcollect/db/forex/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/cmd/websocketcollect/db/forex/LOG b/cmd/websocketcollect/db/forex/LOG new file mode 100644 index 0000000..b959276 --- /dev/null +++ b/cmd/websocketcollect/db/forex/LOG @@ -0,0 +1,15 @@ +=============== Oct 23, 2024 (CST) =============== +16:49:09.927634 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +16:49:09.929200 db@open opening +16:49:09.929718 version@stat F·[] S·0B[] Sc·[] +16:49:09.930859 db@janitor F·2 G·0 +16:49:09.930859 db@open done T·1.6589ms +=============== Oct 23, 2024 (CST) =============== +16:50:25.834513 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +16:50:25.835027 version@stat F·[] S·0B[] Sc·[] +16:50:25.835027 db@open opening +16:50:25.835027 journal@recovery F·1 +16:50:25.835565 journal@recovery recovering @1 +16:50:25.836251 version@stat F·[] S·0B[] Sc·[] +16:50:25.839923 db@janitor F·2 G·0 +16:50:25.839923 db@open done T·4.8966ms diff --git a/cmd/websocketcollect/db/forex/MANIFEST-000003 b/cmd/websocketcollect/db/forex/MANIFEST-000003 new file mode 100644 index 0000000000000000000000000000000000000000..0a36c9e7a9fc8206d8d3d0e6d09c8f6314ac2a43 GIT binary patch literal 41 wcmXTkt0SVsz{n_-lUkOVlai$8R9TW*o>`pgoS$2eSd>_jU&O@3%)-I|01ML$E&u=k literal 0 HcmV?d00001 diff --git a/cmd/websocketcollect/db/us/000016.ldb b/cmd/websocketcollect/db/us/000016.ldb new file mode 100644 index 0000000000000000000000000000000000000000..47a042f2c46c624112393090e351990da2205bce GIT binary patch literal 120276 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 Oe}O-Qd$l$>J^DWb&VL{P literal 0 HcmV?d00001 diff --git a/cmd/websocketcollect/db/us/000033.ldb b/cmd/websocketcollect/db/us/000033.ldb new file mode 100644 index 0000000000000000000000000000000000000000..9f4f14dd75a1a5fd2cb5d9d92dd6f9a4992e7cf3 GIT binary patch literal 185 zcmb2uVc=!;a`N`|WxS%vz{qF-W-|Z*BM8r#`6h#dL4bvkp+SKSB+3cI+zb{IGW+zl zFeoymW#*Km7U|`rmZj#TB= 0 { + conn = append(conn[:index], conn[index+1:]...) + mutex.Lock() + wsConMap[symbol] = conn + mutex.Unlock() + } + return nil +} + +func deleteWsconn(symbol string) { + mutex.Lock() + defer mutex.Unlock() + delete(wsConMap, symbol) +} + +// 检查是否订阅过 +func checkClient(conns []*websocket.Conn, conn *websocket.Conn) []*websocket.Conn { + for _, v := range conns { + if v == conn { + return conns + } + } + conns = append(conns, conn) + return conns +} + +// 广播 +func broadcastWebSocket(msg []byte, symbol string) { + mutex.RLock() + conns, ok := wsConMap[symbol] + mutex.RUnlock() + if !ok { + applogger.Error("Parsing data information:%v") + return + } + for _, val := range conns { + if err := aloneSend(val, msg); err != nil { + removeWsconn(conns, val, symbol) + } + } +} + +// 断开连接 清理订阅 +func offLine(userConn *websocket.Conn) { + for key, v := range wsConMap { + removeWsconn(v, userConn, key) + } +} + +// send message +func aloneSend(conn *websocket.Conn, message []byte) error { + mutexSpotConn.Lock() + defer mutexSpotConn.Unlock() + conn.SetWriteDeadline(time.Now().Add(writeWait)) + w, err := conn.NextWriter(websocket.TextMessage) // Write data in the form of io, with parameters of data type + if err != nil { + //发送失败 + applogger.Error("Failed to conn.NextWriter :%v", err) + return err + } + if _, err := w.Write(message); err != nil { // Write data, this function is truly used to transmit data to the foreground + applogger.Error("Failed Write message :%v", err) + return err + } + if err := w.Close(); err != nil { // Close write stream + applogger.Error("Failed to close write stream:%v", err) + return nil + } + return nil +} + +//func hearBeat(user *User) { +// defer func() { +// user.conn.Close() +// }() +// ticker := time.NewTicker(writeWait) // Set timing +// for { +// select { +// case <-ticker.C: +// TODO: Need to add a server to ping the client +// //发送心跳 +// if err := aloneSend(user.conn, []byte(model.ReturnValue("ping"))); err != nil { +// applogger.Error("Failed to send heartbeat message: ", err) +// //ping 不通 清理客户端 +// offLine(user.conn) +// return +// } +// } +// } +//} + +// Producer sends messages +func write() { + for message := range msgChan { + // TODO: 延迟200毫秒 + time.Sleep(20 * time.Millisecond) + var subMsg Message + json.Unmarshal(message, &subMsg) + //广播 + broadcastWebSocket(message, subMsg.Symbol) + applogger.Info("broadcast WebSocket Sub message info:%v", subMsg.Symbol) + } +} + +// User subscription and cancellation +func (u *User) userSubscribe(symbol string) { + applogger.Info("新交易对", symbol) + pubSub := red.RedisClient.Subscribe(symbol) + defer func() { + pubSub.Close() + }() + _, err := pubSub.Receive() + if err != nil { + applogger.Error("failed to receive from control PubSub,%v", zap.Error(err)) + return + } + //fmt.Println(pubSub) + ch := pubSub.Channel() + for msg := range ch { + var subMsg Message + //合约深度解压包 + isSubscribeCtDepth := strings.Contains(symbol, "USDT.depth.step") + switch isSubscribeCtDepth { + case true: + content, err := gzip.GZipDecompress([]byte(msg.Payload)) + if err != nil { + applogger.Error("UnGZip data error: %s", err) + return + } + result := market.SubscribeCtDepthResponse{} + if err := json.Unmarshal([]byte(content), &result); err != nil { + applogger.Error("Depth Unmarshal ", err) + close(u.msg) + return + } + //fmt.Println(result) + subMsg.Symbol = result.Channel + subMsg.Content = result + subMsg.ServersId = result.Channel + message, _ := json.Marshal(subMsg) + msg.Payload = string(message) + default: + //applogger.Info("Subscribe date:%v", msg.Payload) + if err := json.Unmarshal([]byte(msg.Payload), &subMsg); err != nil { + applogger.Error("Parsing data information:%v", err) + close(u.msg) + return + } + } + //fmt.Println("返回数据 : ", msg.Payload) + //交易全被被取消订阅 + mutex.RLock() + conns := wsConMap[subMsg.Symbol] + mutex.RUnlock() + if len(conns) > 0 { + msgChan <- []byte(msg.Payload) + } else { + applogger.Debug("Starting unsubscribe.....", symbol) + deleteWsconn(symbol) + pubSub.Unsubscribe(symbol) + return + } + } +} + +func HashValue(hashListName string) []stock.PHPData { + keys := red.Scan(hashListName) + result := make([]stock.PHPData, 0) + for _, v := range keys { + res, _ := red.HGetAll(v) + fmt.Println(res) + item := stock.PHPData{} + for field, value := range res { + switch field { + case "name": + item.Name = value + case "code": + item.Code = value + case "keep_decimal": + item.KeepDecimal = value + case "face_value": + item.FaceValue, _ = strconv.Atoi(value) + case "max_pry": + item.MaxPry, _ = strconv.Atoi(value) + case "min_pry": + item.MinPry, _ = strconv.Atoi(value) + case "logo_link": + item.LogoLink = value + case "status": + item.Status, _ = strconv.Atoi(value) + } + } + //if item.Status == StatusPass { + result = append(result, item) + //} + } + return result +} + +func HashValueOnce(key string) (stock.PHPData, error) { + res, _ := red.HGetAll(key) + item := stock.PHPData{} + for field, value := range res { + switch field { + case "name": + item.Name = value + case "code": + item.Code = value + case "keep_decimal": + item.KeepDecimal = value + case "face_value": + item.FaceValue, _ = strconv.Atoi(value) + case "max_pry": + item.MaxPry, _ = strconv.Atoi(value) + case "min_pry": + item.MinPry, _ = strconv.Atoi(value) + case "logo_link": + item.LogoLink = value + case "status": + item.Status, _ = strconv.Atoi(value) + } + } + if item.Status == StatusPass { + return item, nil + } + return stock.PHPData{}, errors.New("data is null") +} + +// market_type //1现货,2合约,3美股 +func PHPResutl(market_type int) stock.PHPRes { + var eodModel stock.PHPRes + url := fmt.Sprintf("https://%v/bs/market", + config.Config.HbApi.PHPHost) + bodyStr, err := internal.HttpPost(url, fmt.Sprintf(`{"market_type":"%d","trade_name":"","page":1,"page_size":1000}`, market_type)) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return eodModel + } + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return eodModel + } + return eodModel +} + +// market_type 1现货,2合约,3美股 +func PHPMarketTrade(market_type, num int, trade_name string) stock.PHPMarketTradeList { + var eodModel stock.PHPMarketTradeList + url := fmt.Sprintf("https://%v/bs/market_trade", + config.Config.HbApi.PHPHost) + fmt.Println(url) + bodyStr, err := internal.HttpPost(url, fmt.Sprintf(`{"market_type":%d,"trade_name":"%s","num":%d}`, market_type, trade_name, num)) + fmt.Println(bodyStr) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return eodModel + } + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return eodModel + } + return eodModel +} + +func SubscriptionCache() { + spot := HashValue(RedisDIGITAL) + contract := HashValue(RedisCONTRACT) + applogger.Info("spot :", spot, "contract :", contract) + for _, v := range spot { + symbol := fmt.Sprintf("market.%susdt.kline.1day", strings.ToLower(v.Name)) + go UserMainSubscribe(symbol, SpotsStatus, v.Name) + } + for _, v := range contract { + symbol := fmt.Sprintf("market.%s.kline.1day", v.Name) + go UserMainSubscribe(symbol, ContractStatus, v.Name) + } +} + +// 1 合约 2 现货 +func UserMainSubscribe(symbol string, market_type int, name string) { + applogger.Info("新交易对", symbol) + var subMsg Message + pubSub := red.RedisClient.Subscribe(symbol) + defer func() { + pubSub.Close() + }() + _, err := pubSub.Receive() + if err != nil { + applogger.Error("failed to receive from control PubSub,%v", zap.Error(err)) + return + } + ch := pubSub.Channel() + for msg := range ch { + //applogger.Info("Subscribe date:%v", msg.Payload) + if err := json.Unmarshal([]byte(msg.Payload), &subMsg); err != nil { + applogger.Error("Parsing data information:%v", err) + return + } + if subMsg.Symbol == "" { + applogger.Error("symbol not data :%v", err) + continue + } + if market_type == SpotsStatus { + val, err := HashValueOnce(fmt.Sprintf("%s:%s", RedisDIGITAL, name)) + if err != nil { + //applogger.Error(name, err.Error()) + SpotMarketCache.Delete(strings.ToLower(name)) + continue + } + subMsg.Logo = val.LogoLink + subMsg.Symbol = strings.ToLower(val.Name) + subMsg.KeepDecimal = val.KeepDecimal + SpotMarketCache.Store(strings.ToLower(name), subMsg) + } else { + val, err := HashValueOnce(fmt.Sprintf("%s:%s", RedisCONTRACT, name)) + if err != nil { + //applogger.Error(name, err.Error()) + ContractCache.Delete(name) + continue + } + subMsg.Logo = val.LogoLink + subMsg.Symbol = val.Name + subMsg.KeepDecimal = val.KeepDecimal + ContractCache.Store(name, subMsg) + } + } +} + +// Send data to websocket websocketServer +func (u *User) send(data string) (err error) { + if u.conn == nil { + applogger.Error("WebSocket sent error: no connection available") + return err + } + + u.mux.Lock() + err = u.conn.WriteMessage(websocket.TextMessage, []byte(data)) + u.mux.Unlock() + + return err +} diff --git a/cmd/websocketservice/websocketusclient.go b/cmd/websocketservice/websocketusclient.go new file mode 100644 index 0000000..c86c496 --- /dev/null +++ b/cmd/websocketservice/websocketusclient.go @@ -0,0 +1,412 @@ +package websocketservice + +import ( + "encoding/json" + "fmt" + "github.com/gorilla/websocket" + "github.com/satori/go.uuid" + "go.uber.org/zap" + "log" + "math" + "net/http" + "strings" + "sync" + "time" + "wss-pool/cmd/common" + "wss-pool/dictionary" + "wss-pool/internal/model" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + mol "wss-pool/pkg/model" +) + +// Define a websocket connection object that contains information for each connection +type Client struct { + Id string // Client ID + conn *websocket.Conn // Define websocket link objects + msg chan []byte // Define messages received and distributed + symbol sync.Map // Concurrent Security - Manage User Subscription Types + mux sync.Mutex +} + +type StockMessage struct { + S string `json:"s,omitempty"` // 股票代码 + Country string `json:"country"` //国家 + StockCode string `json:"stock_code" bson:"stock_code"` // 股票代码 + Symbol string `json:"symbol"` + Stock string `json:"stock"` // 期权代码 + IsStockIndex bool `json:"is_stock_index"` + IsOptionList bool `json:"is_option_list"` + IsOptionInfo bool `json:"is_option_info"` +} + +var ( + wsStockConMap = map[string][]*websocket.Conn{} + mutexStock = sync.RWMutex{} + msgStockChan = make(chan []byte) + mutexConn = sync.RWMutex{} + TotalNum int + mutexTotal = sync.RWMutex{} + countryMap = make(map[string][]string) + mutexCountry = sync.RWMutex{} + clearClientChan = make(chan *websocket.Conn) +) + +const ( + stockConnNum int = 20 +) + +// Define an UpGrader to upgrade a regular HTTP connection to a websocket connection +var upServer = &websocket.Upgrader{ + // Define read/write buffer size + WriteBufferSize: 1024, + ReadBufferSize: 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 != "/quotes-share-wss" { + fmt.Println("Request path error") + return false + } + token := r.URL.Query().Get("token") + if !common.CheckToken(token) { + applogger.Debug("token expired") + return false + } + // Verification rules can also be customized according to other needs + return true + }, +} + +// ShareConnect +func ShareConnect(host, addr string) { + go writeShare() + go offLineStock() + http.HandleFunc("/quotes-share-wss", wsHandleShare) + url := fmt.Sprintf("%v%v", host, addr) + + err := http.ListenAndServe(url, nil) + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} + +func wsHandleShare(w http.ResponseWriter, r *http.Request) { + // Obtain a link through the upgraded upgrade tool + conn, err := upServer.Upgrade(w, r, nil) + if err != nil { + applogger.Info("Failed to obtain connection:%v", err) + return + } + // Register users after successful connection + client := &Client{ + Id: uuid.NewV4().String(), + conn: conn, + msg: make(chan []byte), + symbol: sync.Map{}, + mux: sync.Mutex{}, + } + readShare(client) +} + +func setTotalNum(num int) { + mutexTotal.Lock() + defer mutexTotal.Unlock() + TotalNum += num +} + +func getTotalNum() { + mutexTotal.RLock() + defer mutexTotal.RUnlock() + applogger.Debug("number of colleagues online :%v", TotalNum) +} + +// Read the message sent by the client and process the return response +func readShare(cl *Client) { + defer cl.conn.Close() + setTotalNum(1) + getTotalNum() + for { + _, msg, err := cl.conn.ReadMessage() + if err != nil { + clearClientChan <- cl.conn + applogger.Debug("user exit:%v", cl.conn.RemoteAddr().String()) + return + } + // Process business logic + psgMsg := model.SubMessage(string(msg)) + if psgMsg != nil { + switch psgMsg.Type { + case "ping": // Receiving ping + aloneSendStock(cl.conn, []byte(model.ReturnValue("pong"))) + case "subscribe": // Receive subscription + country, stock := getCountry(psgMsg.Symbol) + if !dictionary.StockCountryMap[country] { + applogger.Error(country, "incorrect subscription information 不属于合规的股票市场") + aloneSendStock(cl.conn, []byte(model.ReturnValue("incorrect subscription information"))) + clearClientChan <- cl.conn + return + } + aloneSendStock(cl.conn, []byte(model.ReturnValue("subscribe success"))) + //获取 服务是否 订阅该数据 + mutexStock.RLock() + conns, ok := wsStockConMap[psgMsg.Symbol] + mutexStock.RUnlock() + if ok { + //查询client是否订阅 + conns = checkClient(conns, cl.conn) + } else { + //添加订阅 + conns = make([]*websocket.Conn, 0) + conns = append(conns, cl.conn) + } + mutexStock.Lock() + wsStockConMap[psgMsg.Symbol] = conns + mutexStock.Unlock() + applogger.Info("psgMsg.Symbol:%v,wsStockClient:%v", psgMsg.Symbol, wsStockConMap) + if !ok { + go cl.userPSubscribeUs(country, stock) + } + case "unSubscribe": // Receive unsubscribe + applogger.Info("Received unsubscribe message body:", string(msg)) + mutexStock.RLock() + conns, ok := wsStockConMap[psgMsg.Symbol] + mutexStock.RUnlock() + if ok { + //取消订阅 + removeWsconnStock(conns, cl.conn, psgMsg.Symbol) + } + aloneSendStock(cl.conn, []byte(model.ReturnValue("unSubscribe success"))) + applogger.Debug("Subscription type after current user deletion:%v", ok) + default: + // TODO: Handling other situations transmitted by customers + applogger.Debug("Please provide accurate instructions......") + } + } + } +} + +// 废弃 客户端 +func offLineStock() { + for userConn := range clearClientChan { + setTotalNum(-1) + getTotalNum() + for key, v := range wsStockConMap { + removeWsconnStock(v, userConn, key) + } + } +} + +// send message +func aloneSendStock(conn *websocket.Conn, message []byte) error { + mutexConn.Lock() + defer mutexConn.Unlock() + //applogger.Debug("aloneSendStock :%v",conn,message) + conn.SetWriteDeadline(time.Now().Add(writeWait)) + w, err := conn.NextWriter(websocket.TextMessage) // Write data in the form of io, with parameters of data type + if err != nil { + //发送失败 + applogger.Error("Failed to conn.NextWriter :%v", err) + return err + } + if _, err := w.Write(message); err != nil { // Write data, this function is truly used to transmit data to the foreground + applogger.Error("Failed Write message :%v", err) + return err + } + if err := w.Close(); err != nil { // Close write stream + applogger.Error("Failed to close write stream:%v", err) + return nil + } + return nil +} + +// 清理客户端 +func removeWsconnStock(conn []*websocket.Conn, userConn *websocket.Conn, symbol string) error { + index := -1 + for i, v := range conn { + if v == userConn { + index = i + break + } + } + if index >= 0 { + conn = append(conn[:index], conn[index+1:]...) + mutexStock.Lock() + wsStockConMap[symbol] = conn + mutexStock.Unlock() + } + return nil +} + +func deleteWsconnStock(symbol string) { + mutexStock.Lock() + defer mutexStock.Unlock() + delete(wsStockConMap, symbol) +} + +// 广播 +func broadcastWebSocketStock(msg []byte, symbol string) { + //applogger.Debug("订阅客户端:%v---%v", wsStockConMap, symbol) + mutexStock.RLock() + conns, ok := wsStockConMap[symbol] + mutexStock.RUnlock() + if !ok { + applogger.Error("Parsing data information:%v") + return + } + total := len(conns) + if total <= 0 { + return + } + start := time.Now() + connDiv := int(math.Ceil(float64(total) / float64(stockConnNum))) + //分批并发推送 + for i := 0; i < connDiv; i++ { + startIndex := i * stockConnNum + endIndex := (i + 1) * stockConnNum + if endIndex > total { + endIndex = total + } + wg := sync.WaitGroup{} + for _, val := range conns[startIndex:endIndex] { + wg.Add(1) + go func(val *websocket.Conn, msg []byte) { + defer wg.Done() + aloneSendStock(val, msg) + }(val, msg) + } + wg.Wait() + } + applogger.Info("broadcast WebSocket info : %v ;total:%v;time-consuming %v ", symbol, total, time.Since(start)) +} + +func writeShare() { + for message := range msgStockChan { + if strings.Contains(string(message), "\"ev\":\"CAS\"") { // 外汇行情订阅 + var subMsg mol.ForexJsonData + if err := json.Unmarshal(message, &subMsg); err != nil { + applogger.Error(err.Error()) + } + broadcastWebSocketStock(message, fmt.Sprintf("%s.Forex", subMsg.Pair)) + } else if strings.Contains(string(message), "\"ev\":\"CAS-D\"") { // 外汇行情天订阅 + var subMsg mol.ForexJsonData + if err := json.Unmarshal(message, &subMsg); err != nil { + applogger.Error(err.Error()) + } + broadcastWebSocketStock(message, fmt.Sprintf("%s.DayForex", subMsg.Pair)) + } else if strings.Contains(string(message), "\"ev\":\"C\"") { // 外汇买一卖一报价 + var subMsg mol.ForexLastQuote + if err := json.Unmarshal(message, &subMsg); err != nil { + applogger.Error(err.Error()) + } + broadcastWebSocketStock(message, fmt.Sprintf("%s.LastForex", subMsg.P)) + } else if strings.Contains(string(message), "\"ev\":\"T\"") { + var subMsg mol.ForexTrade + if err := json.Unmarshal(message, &subMsg); err != nil { + applogger.Error(err.Error()) + } + broadcastWebSocketStock(message, fmt.Sprintf("%s.TradeForex", subMsg.Code)) + } else { + var subMsg StockMessage + if err := json.Unmarshal(message, &subMsg); err != nil { + applogger.Error(err.Error()) + } + if subMsg.S == "" { + if subMsg.IsStockIndex { // 指数行情订阅 + broadcastWebSocketStock(message, fmt.Sprintf("%s.%s", subMsg.StockCode, common.StockIndexPrefix)) + } else if subMsg.IsOptionList { // 期权列表行情订阅 + broadcastWebSocketStock(message, fmt.Sprintf("%s.%s", subMsg.Stock, fmt.Sprintf("%s%s%s", common.StockOption, common.CapitalizeFirstLetter(subMsg.Country), common.StockOptionList))) + } else if subMsg.IsOptionInfo { // 期权详情订阅 + broadcastWebSocketStock(message, fmt.Sprintf("%s.%s", subMsg.Stock, fmt.Sprintf("%s%s%s", common.StockOption, common.CapitalizeFirstLetter(subMsg.Country), common.StockOptionInfo))) + } else { // 东南亚股票市场行情订阅(tradingView) + broadcastWebSocketStock(message, fmt.Sprintf("%s.%s", subMsg.Symbol, common.CapitalizeFirstLetter(subMsg.Country))) + } + continue + } + // 美股行情订阅 + broadcastWebSocketStock(message, fmt.Sprintf("%s.US", subMsg.S)) + } + } +} + +func getCountry(symbol string) (string, string) { + symbolArr := strings.Split(symbol, ".") + if len(symbolArr) < 2 { + applogger.Error("symbol 有误") + return "", "" + } + county := symbolArr[len(symbolArr)-1] + return county, symbol[0 : strings.Index(symbol, county)-1] +} + +// 按市场订阅 +func (cl *Client) userPSubscribeUs(country, symbol string) { + mutexCountry.RLock() + symbols, ok := countryMap[country] + mutexCountry.RUnlock() + //提加 + mutexCountry.Lock() + countryMap[country] = append(symbols, symbol) + mutexCountry.Unlock() + if ok { + //applogger.Error(country, "已订阅") + return + } + applogger.Debug(country, "start a stock subscription") + pubSub := red.RedisClient.PSubscribe(fmt.Sprintf("*.%s", country)) + defer func() { + pubSub.Close() + }() + + _, err := pubSub.Receive() + if err != nil { + applogger.Error("failed to receive from control PubSub,%v", zap.Error(err)) + return + } + ch := pubSub.Channel() + for msg := range ch { + mutexStock.RLock() + conns, ok := wsStockConMap[msg.Channel] + mutexStock.RUnlock() + //未订阅股票跳出 + if !ok { + continue + } + if len(conns) > 0 { + msgStockChan <- []byte(msg.Payload) + } else { + deleteWsconnStock(msg.Channel) + mutexCountry.RLock() + symbols := countryMap[country] + mutexCountry.RUnlock() + index := -1 + msgChannel := strings.Split(msg.Channel, ".") + for i, v := range symbols { + if v == msgChannel[0] { + index = i + break + } + } + if index >= 0 { + symbols = append(symbols[:index], symbols[index+1:]...) + mutexCountry.Lock() + countryMap[country] = symbols + mutexCountry.Unlock() + } + // 退订 + if len(symbols) <= 0 { + mutexCountry.Lock() + delete(countryMap, country) + mutexCountry.Unlock() + applogger.Debug("Starting unsubscribe.....", country) + pubSub.PUnsubscribe(fmt.Sprintf("*.%s", country)) + return + } + } + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..b327176 --- /dev/null +++ b/config/config.go @@ -0,0 +1,25 @@ +package config + +import ( + "gopkg.in/yaml.v3" + "os" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var Config model.Config + +// LoadConfig Load config +func LoadConfig(urlPath string) { + dataBytes, err := os.ReadFile(urlPath) + if err != nil { + applogger.Info("fail to read file:%v", err) + return + } + err = yaml.Unmarshal(dataBytes, &Config) + if err != nil { + applogger.Info("Failed to parse yaml file:%v", string(dataBytes), err.Error()) + return + } + applogger.Info("config---->:%v", Config) +} diff --git a/dictionary/publickey.go b/dictionary/publickey.go new file mode 100644 index 0000000..0be8004 --- /dev/null +++ b/dictionary/publickey.go @@ -0,0 +1,84 @@ +package dictionary + +// usdt wbtc dai busd +// 计量单位 +var TypeUnit = "usdt" + +// TODO: 系统数字火币-后期需要做个字典管理功能 +var Symbol = []string{"btc", "eth", "bnb", "usdc", "xrp", "eos", "ada", "doge", "sol", "trx", "ltc", "dot", "matic", "bch", "ton", "avax", "shib", "invu", "osel", "fmd", "dten", "dten", "xnsl", "kools"} + +// 火币K线 +var TimeCycle = []string{"1min", "5min", "15min", "30min", "60min", "4hour", "1day", "1mon", "1week", "1year"} + +// 币安K线 +var BaTimeCycle = []string{"1m", "5m", "15m", "30m", "1h", "4h", "1d", "1M", "1w"} +var BaToBaMap = map[string]string{ + "1m": "1min", + "5m": "5min", + "15m": "15min", + "30m": "30min", + "1h": "60min", + "4h": "4hour", + "1d": "1day", + "1M": "1mon", + "1w": "1week", +} + +// 币安深度 +var BaDepth = []string{"5", "10", "20"} + +// 深度 +var Depth = []string{"step0", "step1", "step2", "step3", "step4", "step5"} + +// 市场深度MBP行情数据 +var LevelsRefresh = []int{5, 10, 20} +var LevelsMbp = []int{5, 20, 400} + +/* + 合约订阅接口 + +1、永续:BTC-USDT(永续合约代码) +2、交割:BTC-USDT-210625(交割合约代码) +3、BTC-USDT-CW(当周合约标识) +4、BTC-USDT-NW(次周合约标识) +5、BTC-USDT-CQ(当季合约标识) +6、BTC-USDT-NQ(次季合约标识) +*/ +var ContractCodeList = []string{"BTC-USDT", "ETH-USDT", "BCH-USDT", "XRP-USDT", "EOS-USDT", "LTC-USDT", "TRX-USDT", "ETC-USDT", "LINK-USDT", "BNB-USDT", "ADA-USDT", "DOGE-USDT", "SOL-USDT", "DOT-USDT", "MATIC-USDT", "AVAX-USDT", "SHIB-USDT", "BNBS-USDT", "INVU-USDT", "OSEL-USDT", "FMD-USDT", "DTEN-USDT", "XNSL-USDT", "KOOLS-USDT"} +var ContractCode = []string{"-USDT", "-USDT-CW", "-USDT-NW", "-USDT-CQ", "-USDT-NQ"} +var ContractTime = []string{"1min", "5min", "15min", "30min", "60min", "4hour", "1day", "1week", "1mon"} +var ContractPriceTime = []string{"1min", "5min", "15min", "30min", "60min", "4hour", "1day", "1week", "1mon"} +var ContractDepth = []string{"step0", "step1", "step2", "step3", "step4", "step5", "step6", "step7", "step8", "step9", "step10", "step11", "step12", "step13", "step14", "step15", "step16", "step17", "step18", "step19"} +var ContractAddDepth = []string{"20", "150"} + +// 美股市场 +var StockUsListTime = []string{"5min", "15min", "30min", "1hour", "1day", "1week", "1mon"} +var StockUsListDayTime = []string{"1day", "1week", "1mon"} +var StockUsListTimeMap = map[string]string{"5min": "5", "15min": "15", "30min": "30", "1hour": "hour", "1day": "day", "1week": "week", "1mon": "month"} + +// 股票(马股|泰股|印尼股|印度股|新加坡股|港股|英股|法股|德股|巴西股|日本股)市场 +var StockCodeList = []string{"Malaysia", "Thailand", "Indonesia", "India", "Singapore", "HongKong", "UK", "France", "Germany", "Brazil", "Japan"} +var StockSouthAsiaListTime = []string{"5min", "15min", "30min", "1hour", "1day", "1week", "1mon"} +var StockSouthAsiaListTimes = []string{"1hour", "1day", "1week", "1mon"} +var StockCountryMap = map[string]bool{ + "Thailand": true, + "Indonesia": true, + "India": true, + "US": true, + "StockIndex": true, + "Malaysia": true, + "Singapore": true, + "HongKong": true, + "OptionIndiaList": true, + "OptionIndiaInfo": true, + "UK": true, + "France": true, + "Germany": true, + "Brazil": true, + "Japan": true, + "Forex": true, + "DayForex": true, + "LastForex": true, + "TradeForex": true, +} +var OptionCodeList = []string{"US", "India"} diff --git a/internal/Publicmethods.go b/internal/Publicmethods.go new file mode 100644 index 0000000..a6c8b68 --- /dev/null +++ b/internal/Publicmethods.go @@ -0,0 +1,106 @@ +package internal + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "math/rand" + "strings" + "time" + "wss-pool/config" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var ( + pr = fmt.Sprintf + topIckLine = "market.kline" +) + +const ( + OneMin = "1min" + FiveTime = "5min" + FifteenTime = "15min" + ThirtyTime = "30min" + SixtyTime = "60min" + fourTime = "4hour" + OneDay = "1day" + OneMon = "1mon" + OneWeek = "1week" + OneYear = "1year" +) + +// CheckKLineTable +func CheckKLineTable(suffix string) string { + // "1min", "5min", "15min", "30min", "60min", "4hour", "1day", "1mon", "1week", "1year" + if strings.Contains(suffix, OneMin) { + return pr("%v.%v", topIckLine, OneMin) + } + if strings.Contains(suffix, FiveTime) { + return pr("%v.%v", topIckLine, FiveTime) + } + if strings.Contains(suffix, FifteenTime) { + return pr("%v.%v", topIckLine, FifteenTime) + } + if strings.Contains(suffix, ThirtyTime) { + return pr("%v.%v", topIckLine, ThirtyTime) + } + if strings.Contains(suffix, SixtyTime) { + return pr("%v.%v", topIckLine, SixtyTime) + } + if strings.Contains(suffix, fourTime) { + return pr("%v.%v", topIckLine, fourTime) + } + if strings.Contains(suffix, OneDay) { + return pr("%v.%v", topIckLine, OneDay) + } + if strings.Contains(suffix, OneMon) { + return pr("%v.%v", topIckLine, OneMon) + } + if strings.Contains(suffix, OneWeek) { + return pr("%v.%v", topIckLine, OneWeek) + } + if strings.Contains(suffix, OneYear) { + return pr("%v.%v", topIckLine, OneYear) + } + + return "" +} + +// Md5 token +func Md5(s string) string { + h := md5.New() + h.Write([]byte(s)) + return hex.EncodeToString(h.Sum(nil)) +} + +// Captcha +func Captcha(n int) string { + code := "" + rand.Seed(time.Now().Unix()) + for i := 0; i < n; i++ { + code = fmt.Sprintf("%s%d", code, rand.Intn(10)) + } + return code +} + +// GetToken +func GetToken() (string, error) { + var tm model.Token + url := fmt.Sprintf("http://%v:9999/v1/getcomm/get-comm/token.html", config.Config.DomainName) + jsonP, err := HttpPost(url, "") + if err != nil { + applogger.Error("HttpPost err: %v", err) + return "", err + } + + if err := json.Unmarshal([]byte(jsonP), &tm); err != nil { + applogger.Error("json Unmarshal err: %v", err) + return "", err + } + + token := tm.Data + + return token, nil +} diff --git a/internal/common.go b/internal/common.go new file mode 100644 index 0000000..f52a971 --- /dev/null +++ b/internal/common.go @@ -0,0 +1,58 @@ +package internal + +import ( + "strings" + "time" +) + +// GetBetweenDates 根据开始日期和结束日期计算出时间段内所有日期 +// 参数为日期格式,如:2020-01-01 +func GetBetweenDates(sdate, edate string) []string { + d := []string{} + timeFormatTpl := "2006-01-02 15:04:05" + if len(timeFormatTpl) != len(sdate) { + timeFormatTpl = timeFormatTpl[0:len(sdate)] + } + date, err := time.Parse(timeFormatTpl, sdate) + if err != nil { + // 时间解析,异常 + return d + } + date2, err := time.Parse(timeFormatTpl, edate) + if err != nil { + // 时间解析,异常 + return d + } + if date2.Before(date) { + // 如果结束时间小于开始时间,异常 + return d + } + // 输出日期格式固定 + timeFormatTpl = "2006-01-02" + date2Str := date2.Format(timeFormatTpl) + d = append(d, date.Format(timeFormatTpl)) + for { + date = date.AddDate(0, 0, 1) + dateStr := date.Format(timeFormatTpl) + d = append(d, dateStr) + if dateStr == date2Str { + break + } + } + return d +} + +func StringReplace(str string) string { + return strings.Replace(str, " ", "", -1) +} + +func ConversionTime(beforeTime string) int64 { + afterTime, err := time.ParseInLocation("2006-01-02", beforeTime, time.Local) + if err != nil { + afterTime, err = time.ParseInLocation("2006-01-02", beforeTime, time.Local) + if err != nil { + afterTime, err = time.ParseInLocation("2006-01-02 15:04:05", beforeTime, time.Local) + } + } + return afterTime.Unix() +} diff --git a/internal/data/business/forextoexcel.go b/internal/data/business/forextoexcel.go new file mode 100644 index 0000000..5cf54a6 --- /dev/null +++ b/internal/data/business/forextoexcel.go @@ -0,0 +1,48 @@ +package business + +import ( + "fmt" + "github.com/360EntSecGroup-Skylar/excelize" + "go.mongodb.org/mongo-driver/bson" + "strconv" + "wss-pool/config" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/stock" +) + +// TickerToExcel +// +// @Description: 导出excel +func TickerToExcel() { + data.Mgo_init(config.Config.Mongodb) + filter := bson.M{"ticker": bson.M{"$regex": "USD$"}} + res := make([]stock.ForexData, 0) + data.MgoFindRes(data.ForexList, filter, &res) + applogger.Debug("TickerToExcel to info:", len(res)) + // 创建Excel文件 + file := excelize.NewFile() + sheetName := "Sheet1" + // 写入表头 + file.SetCellValue(sheetName, "A1", "code") + file.SetCellValue(sheetName, "B1", "Name") + file.SetCellValue(sheetName, "C1", "Country") + file.SetCellValue(sheetName, "D1", "PrimaryExchange") + file.SetCellValue(sheetName, "E1", "Symbol") + file.SetCellValue(sheetName, "F1", "NumericCode") + // 写入数据 + row := 2 // 从第二行开始写入数据 + for _, val := range res { + file.SetCellValue(sheetName, "A"+strconv.Itoa(row), val.Ticker) + file.SetCellValue(sheetName, "B"+strconv.Itoa(row), "") + file.SetCellValue(sheetName, "C"+strconv.Itoa(row), "") + file.SetCellValue(sheetName, "D"+strconv.Itoa(row), "") + file.SetCellValue(sheetName, "E"+strconv.Itoa(row), "") + file.SetCellValue(sheetName, "F"+strconv.Itoa(row), "") + row++ + } + err := file.SaveAs(fmt.Sprintf("./cmd/%s.xlsx", "forex")) + if err != nil { + applogger.Error("TickerToExecl to info err:", err) + } +} diff --git a/internal/data/business/mgocontract.go b/internal/data/business/mgocontract.go new file mode 100644 index 0000000..bb63f09 --- /dev/null +++ b/internal/data/business/mgocontract.go @@ -0,0 +1,245 @@ +package business + +import ( + "fmt" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/logging/applogger" + "wss-pool/pkg/hbwssclient/marketwssclient" + "wss-pool/pkg/model" + "wss-pool/pkg/model/market" +) + +// k线数据 +func MgoSubscribeCtKline() { + symbolList := model.SymbolCtListString(dictionary.ContractTime) + client := new(marketwssclient.ContractKLineWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, period := range symbolList { + for _, value := range period { + client.Subscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtKlineResponse) + if ok { + if &resp != nil { + if resp.Tick != nil || resp.Data != nil { + + applogger.Info("subscribeCtKline data,ServersId:%v,Sender:%v,Content:%v-%v", resp.Channel, resp.Tick, resp.Data) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, period := range symbolList { + for _, value := range period { + client.UnSubscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// 深度信息 +func MgoSubscribeCtDepth() { + symbolList := model.SymbolCtListString(dictionary.ContractDepth) + client := new(marketwssclient.ContractDepthWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, period := range symbolList { + for _, value := range period { + client.Subscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtDepthResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + + applogger.Info("subscribeCtDepth data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, period := range symbolList { + for _, value := range period { + client.UnSubscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// 新增深度信息 +func MgoSubscribeCtAddDepth() { + symbolList := model.SymbolCtListString(dictionary.ContractAddDepth) + client := new(marketwssclient.ContractDepthSizeWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, period := range symbolList { + for _, value := range period { + client.Subscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtAddDepthResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + + applogger.Info("subscribeCtAddDepth data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, period := range symbolList { + for _, value := range period { + client.UnSubscribe(symbol, value, config.Config.HbContract.HbContractSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// 买一卖一行情数据 +func MgoSubscribeCtBbo() { + symbolList := model.SymbolCtListString([]string{}) + client := new(marketwssclient.ContractBBOWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, _ := range symbolList { + client.Subscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtBboResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + + applogger.Info("subscribeCtBbo data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, _ := range symbolList { + client.UnSubscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// 合约详情数据 +func MgoSubscribeCtDetail() { + symbolList := model.SymbolCtListString([]string{}) + client := new(marketwssclient.ContractDetailWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, _ := range symbolList { + client.Subscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtDetailResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + + applogger.Info("subscribeCtDetail data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, _ := range symbolList { + client.UnSubscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// 合约贸易详情数据 +func MgoSubscribeCtTradeDetail() { + symbolList := model.SymbolCtListString([]string{}) + client := new(marketwssclient.ContractTradeDetailWebSocketClient).Init(config.Config.HbContract.HbContractHost) + client.SetHandler( + func() { + for symbol, _ := range symbolList { + client.Subscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCtTradeDetailResponse) + if ok { + if &resp != nil { + if resp.Tick != nil { + + applogger.Info("subscribeCtTradeDetail data,ServersId:%v,Sender:%v,Content:%v", resp.Channel, resp.Tick) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, _ := range symbolList { + client.UnSubscribe(symbol, config.Config.HbContract.HbContractSubUids) + } + + client.Close() + applogger.Info("Client closed") +} diff --git a/internal/data/business/mgohistoricalus.go b/internal/data/business/mgohistoricalus.go new file mode 100644 index 0000000..a87eb61 --- /dev/null +++ b/internal/data/business/mgohistoricalus.go @@ -0,0 +1,119 @@ +package business + +import ( + "encoding/json" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "strconv" + "strings" + "sync" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/stock" +) + +/* +采集美股历史数据 +*/ +const ( + UsPageSize int64 = 500 +) + +func UpdateStockUs(start, end interface{}, state string) { + fmt.Println("updateStockUs", start, end, state) + stocks, _, pageTotal := GetStockAll("US", 1, UsPageSize) + wg := sync.WaitGroup{} + for _, value := range stocks { + wg.Add(1) + go func(start, end interface{}, state, code string) { + defer wg.Done() + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + multiplier, _ := strconv.Atoi(dictionary.StockUsListTimeMap[state]) + timespan := "minute" + if !strings.Contains(state, "min") { + multiplier = 1 + timespan = dictionary.StockUsListTimeMap[state] + } + url := fmt.Sprintf("https://%v/v2/aggs/ticker/%v/range/%d/%v/%v/%v?adjusted=true&sort=asc&%v", + config.Config.ShareGather.PolygonHost, code, multiplier, timespan, start, end, param) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + var eodModel stock.PreviousCloseResponse + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return + } + applogger.Info("info", eodModel) + UpdateUs(eodModel, state) + }(start, end, state, value.Code) + } + wg.Wait() + for i := int64(2); i <= pageTotal; i++ { + stocks, _, _ := GetStockAll("US", i, UsPageSize) + wg := sync.WaitGroup{} + for _, value := range stocks { + wg.Add(1) + go func(start, end interface{}, state, code string) { + defer wg.Done() + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + multiplier, _ := strconv.Atoi(dictionary.StockUsListTimeMap[state]) + timespan := "minute" + if !strings.Contains(state, "min") { + multiplier = 1 + timespan = dictionary.StockUsListTimeMap[state] + } + url := fmt.Sprintf("https://%v/v2/aggs/ticker/%v/range/%d/%v/%v/%v?adjusted=true&sort=asc&%v", + config.Config.ShareGather.PolygonHost, code, multiplier, timespan, start, end, param) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + var eodModel stock.PreviousCloseResponse + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return + } + UpdateUs(eodModel, state) + applogger.Info("info", eodModel) + }(start, end, state, value.Code) + } + wg.Wait() + } +} + +func UpdateUs(result stock.PreviousCloseResponse, period string) { + if len(result.Results) <= 0 { + applogger.Error("us data is null %v", result) + return + } + var dataList []mongo.WriteModel + for _, val := range result.Results { + filter := bson.M{"code": bson.M{"$eq": result.Ticker}, "timestamp": bson.M{"$eq": val.T}} + update := bson.D{{"$set", bson.D{ + {"code", result.Ticker}, + {"timestamp", val.T}, + {"vw", val.VW.String()}, + {"o", val.O.String()}, + {"c", val.C.String()}, + {"h", val.H.String()}, + {"n", val.N}, + }}} + applogger.Info("UpdateUs info:%v", update) + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + tableName := data.GetStockUsTableName(period) + if len(dataList) > 0 { + if err := data.MgoBulkWrite(tableName, dataList); err != nil { + applogger.Error("UpdateUs MgoInsertMany err:%v", err) + } + } +} diff --git a/internal/data/business/mgoinitdata.go b/internal/data/business/mgoinitdata.go new file mode 100644 index 0000000..02cf1df --- /dev/null +++ b/internal/data/business/mgoinitdata.go @@ -0,0 +1,993 @@ +package business + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "math" + "strconv" + "strings" + "sync" + "time" + "wss-pool/cmd/common" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +/* + TODO: 股票列表更新规则(注意:每种股票的开盘时间进行更新) + +1、股票列表数据采集-定时任务(新增和更新) +2、美股列表数据-定时任务(更新数据) +3、马股列表数据-定时任务(更新数据) +*/ +const ( + ContracttTime int = 1 //数字币数据年限 + StockTime int = 5 //美股数据年限 + PageSize int64 = 120 //聚合股票条数 + StockStatusOn int = 1 + StockStatusOff int = 2 +) + +var ( + OldNextUrl string + StockClosedDataList = map[string]int{ + "US": 3, // 美股 + "Malaysia": 5, // 马股 + "Thailand": 6, // 泰股 + "Indonesia": 4, // 印尼股 + "India": 7, // 印度股 + "Singapore": 9, // 新加坡股 + "HongKong": 12, // 港股 + "UK": 14, // 英股 + "France": 15, // 法股 + "Germany": 16, // 德股 + "Brazil": 17, // 巴西股 + "Japan": 18, // 日股 + } +) + +var CountryStartTime = map[string]int64{ + "India": 42300000, + "Thailand": 39000000, + "Indonesia": 36000000, + "Malaysia": 32400000, + "Singapore": 32400000, + "UK": 54000000, + "France": 54000000, + "Germany": 54000000, + "Brazil": 75600000, + "Japan": 42300000, +} + +var pinStock = map[string]map[string]map[string]bool{} +var pinStockMutex = sync.RWMutex{} + +// InitStockList Stock List Collection +func InitStockList() { + url := fmt.Sprintf("https://%s/v3/reference/tickers?market=stocks&active=true&limit=1000&sort=ticker&apiKey=%s", + config.Config.ShareGather.PolygonHost, config.Config.ShareGather.PolygonKey) + for url != "" { + applogger.Debug("url info:%v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + var shareModel stock.StockPolygonParam + if err := json.Unmarshal([]byte(bodyStr), &shareModel); err != nil { + applogger.Error("Failed to parse stock list information:%v", err) + return + } + //调用失败 + if shareModel.Status == "ERROR" { + fmt.Printf("%+v", shareModel) + time.Sleep(10 * time.Second) + continue + } + url = "" + if shareModel.NextUrl != "" && shareModel.NextUrl != OldNextUrl { + OldNextUrl = shareModel.NextUrl + url = fmt.Sprintf("%s&apiKey=%s&market=stocks", shareModel.NextUrl, config.Config.ShareGather.PolygonKey) + } + var dataList []mongo.WriteModel + for _, value := range shareModel.Results { + // TODO: 更新本地股票列表查 + value.YesterdayClose = "" + value.BeforeClose = "" + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}, {"Country", bson.M{ + "$eq": "US", + }}} + update := bson.D{{"$set", bson.D{ + {"Code", value.Code}, + {"Name", value.Name}, + {"Country", strings.ToUpper(value.Locale)}, + {"Exchange", value.PrimaryExchange}, + {"Currency", value.Currency}, + {"Type", value.Type}, + {"Cik", value.CIK}, + {"CompositeFigi", value.CompositeFigi}, + {"ShareClassFigi", value.ShareClassFigi}, + {"YesterdayClose", value.YesterdayClose}, + {"BeforeClose", value.BeforeClose}}}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if err := data.MgoBulkWrite(data.StockList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + return + } + } +} + +// TickUpdateStockUS US Stock List Data - Scheduled Tasks (Update Data) +func TickUpdateStockUS() { + for { + now := time.Now() + next := now.Add(time.Hour * 24) + next = time.Date(next.Year(), next.Month(), next.Day(), 6, 0, 0, 0, next.Location()) + t := time.NewTimer(next.Sub(now)) + <-t.C + + UpdateStockUS() + } +} + +// TickUpdateStockKLSE Equities List Data - Scheduled Tasks (Update Data) +func TickUpdateStockKLSE() { + for { + now := time.Now() + next := now.Add(time.Hour * 24) + next = time.Date(next.Year(), next.Month(), next.Day(), 6, 0, 0, 0, next.Location()) + t := time.NewTimer(next.Sub(now)) + <-t.C + + UpdateStockKLSE() + } +} + +// 火币现货只允许拉取当前2000条数据 +func TickUpdateSpotKline() { + for _, value := range dictionary.TimeCycle { + UpdateSpotKline(value) + } + return + for { + t := time.NewTimer(1 * time.Hour) + <-t.C + for _, value := range dictionary.TimeCycle { + UpdateSpotKline(value) + } + } +} + +// 火币K线价格只允许拉取当前2000条数据 +func TickUpdateContractPriceKline() { + for _, value := range dictionary.ContractPriceTime { + UpdatePriceKline(value) + } + for { + t := time.NewTimer(1 * time.Hour) + <-t.C + for _, value := range dictionary.ContractPriceTime { + UpdatePriceKline(value) + } + } +} + +// 合约 +func TickUpdateContractKline(isAll bool) { + if isAll { + applogger.Info("start TickUpdateContractKline") + //endTime := time.Now().Unix() + //startTime := time.Now().AddDate(-0, -6, 0).Unix() + for _, value := range dictionary.ContractTime { + UpdateContractKline(value) + } + return + } + //测试专用 + //for _, value := range dictionary.ContractTime { + // start := time.Now().Add(-2 * time.Hour).Unix() + // end := time.Now().Unix() + // UpdateContractKline(value, start, end) + //} + + //for { + // t := time.NewTimer(2 * time.Hour) + // <-t.C + // for _, value := range dictionary.ContractTime { + // start := time.Now().Add(-2 * time.Hour).Unix() + // end := time.Now().Unix() + // UpdateContractKline(value, start, end) + // } + // + //} +} + +// 美股 +func TickUpdateStockUs(isAll bool) { + if isAll { + start := common.TimeToNows().AddDate(0, 0, -20).Format("2006-01-02") + end := common.TimeToNows().AddDate(0, 0, 0).Format("2006-01-02") + for _, value := range dictionary.StockUsListDayTime { + UpdateStockUs(start, end, value) + } + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), "run us stock -----------------------------end") + return + } + //nextFiveMinute := common.GenerateSingaporeFifteenMinTimestampOrigins() + //waitDuration := nextFiveMinute.Sub(common.TimeToNows()) + //time.Sleep(waitDuration) // 60 min + runTime := time.Now() // 获取当前时间 + if !common.IsOpeningUS() { + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), "us it's not opening time -----------------------------end") + return + } + end := common.GenerateSingaporeFifteenMinTimestampOrigin() * 1000 + start := end - int64(60*60*1000) + for _, value := range dictionary.StockUsListTime { + UpdateStockUs(start, end, value) + } + fmt.Println("Run time: ", time.Since(runTime)) +} + +func GetTimeNewPrice(symbol string, from, to int64, country, period string) error { + //fmt.Println("country", country, "period", period, "from", common.ConvertToTimeStr(from/1000), from, "to", common.ConvertToTimeStr(to/1000), to) + filter := bson.M{"symbol": symbol, "timestamp": bson.M{"$gte": from, "$lte": to}} + tableName := data.GetStockTableName(common.CapitalizeFirstLetter(country)) + highRes := make([]model.StockMogoParam, 0) + projection := bson.M{"price": 1} + sort := bson.M{"price": -1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &highRes, 1) + if len(highRes) <= 0 { + applogger.Error(symbol+" no data", period) + return errors.New(symbol + " no data") + } + high := highRes[0].Price + + lowRes := make([]model.StockMogoParam, 0) + sort = bson.M{"price": 1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &lowRes, 1) + low := lowRes[0].Price + + openRes := make([]model.StockMogoParam, 0) + sort = bson.M{"timestamp": 1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &openRes, 1) + open := openRes[0].Price + + closeRes := make([]model.StockMogoParam, 0) + projection = bson.M{} + sort = bson.M{"timestamp": -1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &closeRes, 1) + + var dataList []mongo.WriteModel + filter = bson.M{"timestamp": bson.M{"$eq": from}, "symbol": bson.M{"$eq": symbol}} + update := bson.D{{"$set", bson.D{ + {"symbol", closeRes[0].Symbol}, + {"stock_code", closeRes[0].StockCode}, + {"stock_name", closeRes[0].StockName}, + {"open_price", fmt.Sprintf("%f", open)}, + {"high_price", fmt.Sprintf("%f", high)}, + {"low_price", fmt.Sprintf("%f", low)}, + {"close_price", fmt.Sprintf("%f", closeRes[0].Price)}, + {"up_down_rate", closeRes[0].UpDownRate}, + {"up_down", closeRes[0].UpDown}, + {"trade_v", closeRes[0].TradeV}, + {"trade_k", closeRes[0].TradeK}, + {"vol", closeRes[0].Vol}, + {"turnover_price_total", closeRes[0].TurnoverPriceTotal}, + {"price_total", closeRes[0].PriceTotal}, + //{"p_e", res[l].PE}, + //{"eps", res[l].Eps}, + //{"employees_number", res[l].EmployeesNumber}, + //{"plate", res[l].Plate}, + //{"desc", res[l].Desc}, + {"price_code", closeRes[0].PriceCode}, + {"country", closeRes[0].Country}, + {"timestamp", from}, + }}} + //applogger.Info("GetTimeNewPrice info: %v", update) + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + if err := data.MgoBulkWrite(data.GetStockSouthAsiaTableName(country, period), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + return err + } + return nil +} + +func GetTimeNewPriceAll(symbol string, from, to int64, country, period, periodPre string) { + //fmt.Println("country", country, "period", period, "from", common.ConvertToTimeStr(from/1000), "to", common.ConvertToTimeStr(to/1000)) + filter := bson.M{"symbol": symbol, "timestamp": bson.M{"$gte": from, "$lte": to}} + tableName := data.GetStockSouthAsiaTableName(country, periodPre) + projection := bson.M{"stringVal": 1} + sort := bson.M{"stringVal": -1} + highRes := data.MgoFindProjectionAggregate(tableName, "high_price", filter, projection, sort, 1) + if len(highRes) <= 0 { + applogger.Error(symbol+" no data", period) + return + } + if highRes[0].StringVal <= 0 { + applogger.Error(symbol+" no data", period) + return + } + high := highRes[0].StringVal + sort = bson.M{"stringVal": 1} + lowRes := data.MgoFindProjectionAggregate(tableName, "low_price", filter, projection, sort, 1) + low := lowRes[0].StringVal + + sort = bson.M{"timestamp": 1} + openRes := data.MgoFindProjectionAggregate(tableName, "open_price", filter, projection, sort, 1) + open := openRes[0].StringVal + + sort = bson.M{"timestamp": -1} + closeRes := data.MgoFindProjectionAggregate(tableName, "close_price", filter, projection, sort, 1) + var dataList []mongo.WriteModel + ts := from + if period == "1day" || period == "1week" || period == "1mon" { + ts = from + CountryStartTime[country] + if country == "Brazil" && (period == "1day" || period == "1mon") { + ts = from + } + } + filter = bson.M{"timestamp": bson.M{"$eq": ts}, "symbol": bson.M{"$eq": symbol}} + update := bson.D{{"$set", bson.D{ + {"symbol", closeRes[0].Symbol}, + {"stock_code", closeRes[0].StockCode}, + {"stock_name", closeRes[0].StockName}, + {"open_price", fmt.Sprintf("%f", open)}, + {"high_price", fmt.Sprintf("%f", high)}, + {"low_price", fmt.Sprintf("%f", low)}, + {"close_price", fmt.Sprintf("%f", closeRes[0].StringVal)}, + //{"up_down_rate", res[l].UpDownRate}, + //{"up_down", res[l].UpDown}, + //{"trade_v", res[l].TradeV}, + //{"trade_k", res[l].TradeK}, + {"vol", closeRes[0].Vol}, + {"turnover_price_total", closeRes[0].TurnoverPriceTotal}, + {"price_total", closeRes[0].PriceTotal}, + //{"p_e", res[l].PE}, + //{"eps", res[l].Eps}, + //{"employees_number", res[l].EmployeesNumber}, + //{"plate", res[l].Plate}, + // {"desc", res[l].Desc}, + {"price_code", closeRes[0].PriceCode}, + {"country", closeRes[0].Country}, + {"timestamp", ts}, + }}} + //applogger.Info("GetTimeNewPriceAll info: %v", update) + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + if err := data.MgoBulkWrite(data.GetStockSouthAsiaTableName(country, period), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + } +} + +func GetStockAll(country string, pageNum, pageSize int64) ([]stock.StockPolygon, int64, int64) { + filter := bson.M{"Country": country, "YesterdayClose": bson.M{"$ne": ""}} + projection := bson.M{"Code": 1, "Country": 1} + res := make([]stock.StockPolygon, 0) + total, _ := data.MgoFindTotal(data.StockList, filter) + data.MgoPagingFindStructProjection(data.StockList, filter, projection, pageSize, pageNum, -1, &res) + //印度股票过多 现在只画后台有权限得股票 + //if country == "India" { + // data := make([]stock.StockPolygon, 0) + // for k, v := range res { + // if common.IsExistStock(v.Locale, v.Code) { + // data = append(data, res[k]) + // } + // } + // return data, total, int64(math.Ceil(float64(total) / float64(pageSize))) + //} + return res, total, int64(math.Ceil(float64(total) / float64(pageSize))) +} + +func getOpen(timestamp int64, country, period, symbol string, price string) string { + filter := bson.M{"stock_code": symbol} + projection := bson.M{"timestamp": 1, "open_price": 1, "close_price": 1} + sort := bson.M{"timestamp": -1} + res, _ := data.MgoFindProjection(data.GetStockSouthAsiaTableName(country, period), filter, projection, sort, int64(1)) + open := price + if len(res) > 0 { + timestamps, _ := res[0]["timestamp"].(int64) + switch timestamps { + case timestamp: + open = res[0]["open_price"].(string) + default: + open = res[0]["close_price"].(string) + } + } + return open +} + +func DeleteUs() { + data.Mgo_init(config.Config.Mongodb) + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + filter := bson.M{"Country": "US", "YesterdayClose": ""} + projection := bson.M{"Code": 1, "Country": 1} + sort := bson.M{} + result, _ := data.MgoFindProjection(data.StockList, filter, projection, sort, 0) + fmt.Println(len(result)) + country := "US" + for _, v := range result { + code := v["Code"].(string) + red.Hset(StockClosingPrice[country], code, "0") + red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", country)], code, "0") + red.Hset(StockClosingPrice[fmt.Sprintf("%sBeforeClose", country)], code, "0") + } +} + +// 只保留前半天数据 +func DeleteSpot(param string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + times := common.TimeToNows().Add(-10 * time.Minute).UnixMilli() + filter := bson.M{"timestamp": bson.M{"$lte": times}} + for _, country := range dictionary.StockCodeList { + tableName := data.GetStockTableName(common.CapitalizeFirstLetter(country)) + if err := data.MgoDeleteMany(tableName, filter); err != nil { + applogger.Error(country, "err :", err.Error()) + } + //只清理数据 + if param == "true" { + continue + } + for _, v := range dictionary.StockSouthAsiaListTime { + tableNames := data.GetStockSouthAsiaTableName(country, v) + applogger.Debug(tableNames, "start") + timeHour := common.TimeToNows().Add(-48 * time.Hour).UnixMilli() + switch v { + case "15min": + timeHour = common.TimeToNows().Add(-75 * time.Hour).UnixMilli() + case "30min": + timeHour = common.TimeToNows().Add(-75 * time.Hour * 2).UnixMilli() + case "1hour": + timeHour = common.TimeToNows().Add(-7 * time.Hour * 24 * 2).UnixMilli() + case "1day": + timeHour = common.TimeToNows().Add(-365 * time.Hour * 24).UnixMilli() + case "1week": + timeHour = common.TimeToNows().Add(-365 * time.Hour * 24).UnixMilli() + case "1mon": + timeHour = common.TimeToNows().Add(-365 * time.Hour * 24 * 2).UnixMilli() + } + fmt.Println(timeHour) + filter = bson.M{"timestamp": bson.M{"$lte": timeHour}} + if err := data.MgoDeleteMany(tableNames, filter); err != nil { + applogger.Error(country, "err :", err.Error()) + } + } + + } + applogger.Debug("delete run end") +} + +func DeleteSpotDay(times string, ts int64) { + data.Mgo_init(config.Config.Mongodb) + // var stockTime = []string{ "1day", "1week", "1mon"} + stocks, _, _ := GetStockAll("India", 1, 7001) + for _, stock := range stocks { + // for _,times:= range stockTime { + tableNames := data.GetStockSouthAsiaTableName("India", times) + filter := bson.M{"symbol": stock.Code, "timestamp": ts} + projection := bson.M{"stringVal": 1} + sort := bson.M{"stringVal": -1} + highRes := data.MgoFindProjectionAggregate(tableNames, "high_price", filter, projection, sort, 1) + fmt.Printf("%+v", highRes) + if len(highRes) <= 0 { + applogger.Info(times, ts, stock.Code, "id empty ") + continue + } + if highRes[0].ID.String() == "" { + applogger.Info(times, ts, stock.Code, "id kong ") + continue + } + filter = bson.M{"symbol": stock.Code, "timestamp": ts, "_id": bson.M{"$ne": highRes[0].ID}} + if err := data.MgoDeleteMany(tableNames, filter); err != nil { + applogger.Error(times, ts, stock.Code, "err :", err.Error()) + } + applogger.Info(times, ts, stock.Code) + // os.Exit(111) + } +} + +// 清理外汇成交报价数据信息 +func DeleteForexTrade() { + data.Mgo_init(config.Config.Mongodb) + // 当前时间 + now := time.Now() + // 时间戳格式化函数 + timestamp := func(t time.Time) int64 { + return t.UnixNano() / int64(time.Millisecond) + } + // 计算5分钟前的时间戳 + fiveMinutesAgo := now.Add(time.Duration(-3) * time.Minute) + fiveMinutesAgoMillis := timestamp(fiveMinutesAgo) + filter := bson.M{"tick_time": bson.M{"$lte": fiveMinutesAgoMillis}} + if err := data.MgoDeleteMany(data.ForexTradeList, filter); err != nil { + applogger.Error("DeleteForexTrade MgoDeleteMany err :", err.Error()) + } +} + +// 推送插针数据:[3:美股 4:印尼 5:马股 6:泰股 9:新加坡 11:期权印度 12:港股 14:英国 15:法国 16:德国 17:巴西 18:日本] +func StockClosedData() { + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + for k, v := range StockClosedDataList { + // TODO: 盘前数据功能 实行全天更改数据 + //if k == "US" && common.IsOpeningUS() { + // applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), k, " it's opening time -----------------------------end") + // continue + //} else if (k == "Thailand" || k == "Indonesia" || k == "India" || k == "Singapore" || k == "Malaysia" || k == "HongKong" || k == "UK" || k == "France" || k == "Germany" || k == "Brazil") && common.IsOpening(k) { + // applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), k, " it's opening time -----------------------------end") + // continue + //} + // TODO: 插针 + hashListName := fmt.Sprintf("STOCK_PRICES:%d", v) + keys := red.Scan(hashListName) + stockCodes := make(map[string]bool) + for _, key := range keys { + res, _ := red.HGetAll(key) + status, _ := strconv.Atoi(res["status"]) + code := res["stock_code"] + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), res) + if status != StockStatusOn { + continue + } + if k == "US" { + UsStock(code, res["price"], k, true) + } else { + stockCode := common.GetOldCode(code) + SouthAsiaSpot(code, stockCode, res["price"], k, true) + } + stockCodes[code] = true + } + FullPush(stockCodes, k, v) + } +} + +// 只全量推送旧数据,对数据不做修改 +func FullPush(stockCodes map[string]bool, country string, countryNum int) { + if config.Config.Redis.FullPush != 1 { + return + } + res, _ := red.HGetAll(fmt.Sprintf("STOCK_MARKET:LIST:%d", countryNum)) + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), country, res) + status, _ := strconv.Atoi(res["status"]) + if status != StockStatusOn { + return + } + if !common.IsPullOpen(country, res["am_open_time"], res["am_close_time"], res["pm_open_time"], res["pm_close_time"]) { + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), country, " it's not pull push time -----------------------------end") + return + } + filter := bson.M{"Country": country, "YesterdayClose": bson.M{"$ne": ""}} + projection := bson.M{"Code": 1, "YesterdayClose": 1} + stockRes := make([]stock.StockPolygon, 0) + data.MgoPagingFindStructProjection(data.StockList, filter, projection, 11000, 1, -1, &stockRes) + for _, v := range stockRes { + if !common.IsExistStock(country, v.Code) || stockCodes[v.Code] { + applogger.Debug(country, v.Code, "not pin code") + continue + } + key := StockClosingPrice[fmt.Sprintf("%sNew", country)] + closePrice, _ := red.Hget(key, v.Code) + if closePrice == "0" || closePrice == "" { + closePrice = v.YesterdayClose + } + if country == "US" { + UsStock(v.Code, closePrice, country, false) + } else { + stockCode := common.GetOldCode(v.Code) + SouthAsiaSpot(v.Code, stockCode, closePrice, country, false) + } + } +} + +func CheckNewPrice(code, country, price string) { + key := StockClosingPrice[fmt.Sprintf("%sNew", country)] + value, _ := red.Hget(key, code) + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), code, country, price, value, "CheckNewPrice") + if value != "" && value != "0" && value == price { //value 不是 0 并且 跟盘前价格一致 + red.Hset(key, code, "0") + } +} + +// 推送目标美股插针数据行情 +func UsStock(code, price, country string, isUpdate bool) { + message := &model.ClientMessage{ + S: code, // 股票代码 + C: []decimal.Decimal{decimal.NewFromInt(0), decimal.NewFromFloat(1)}, // 条件,有关更多信息,请参阅贸易条件术语表 + V: common.CalculateContractPrices(decimal.NewFromInt(int64(100)), float64(0.02), 0, 1)[0].IntPart(), // 交易量,代表在相应时间戳处交易的股票数量 -- 报价交易量 + Dp: true, // 暗池真/假 + Ms: "open", // 市场状态,指示股票市场的当前状态(“开盘”、“收盘”、“延长交易时间”) + T: time.Now().UnixMilli(), // 以毫秒为单位的时间戳 -- 此聚合窗口的结束时钟周期的时间戳(以 Unix 毫秒为单位) + Cl: decimal.RequireFromString(price), // 此聚合窗口的收盘价 + A: decimal.NewFromInt(11), // 今天的成交量加权平均价格 + Se: time.Now().UnixMilli(), + H: decimal.RequireFromString(price), // 此聚合窗口的最高逐笔报价 + L: decimal.RequireFromString(price), // 此聚合窗口的最低价格变动价格 + Op: decimal.RequireFromString(price), // 今天正式开盘价格 + // P: decimal.RequireFromString(price), + ClosingMarket: true, + } + msgStr, err := json.Marshal(message) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return + } + if isUpdate { + red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", country)], code, price) + } + applogger.Info("last date info: %v", string(msgStr)) + red.RedisClient.Publish(fmt.Sprintf("%s.US", message.S), string(msgStr)) +} + +// 推送目标市场股票插针数据行情 +func SouthAsiaSpot(symbol, stockCode, price, country string, isUpdate bool) { + prices, _ := strconv.ParseFloat(price, 64) + param := model.StockParam{ + Symbol: symbol, + StockCode: stockCode, + StockName: "", + Price: prices, + UpDownRate: decimal.NewFromInt(0), + UpDown: decimal.NewFromInt(0), + TradeV: decimal.NewFromInt(0), + TradeK: "买入", + Country: strings.ToLower(country), + Ts: time.Now().UnixMilli(), + ClosingMarket: true, + } + param.Token = "" + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return + } + applogger.Info("last date info: %v", string(msgStr)) + // Write to Redis for broadcasting + red.RedisClient.Publish(fmt.Sprintf("%s.%s", param.Symbol, country), string(msgStr)) + if isUpdate { + red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", country)], symbol, price) + } +} + +// 东南亚聚合行情 +func TickSouthAsiaSpotKline(stockName string) { + if !common.IsOpening(stockName) { + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), "it's not opening time -----------------------------end") + return + } + start := time.Now() // 获取当前时间 + applogger.Info(common.TimeToNows().Format("2006-01-02 15:04:05"), "start TickSouthAsiaSpotKline") + // 五分钟聚合 + FiveMinTo := common.GenerateSingaporeFiveMinTimestampOrigin() * 1000 + FiveMinFrom := FiveMinTo - int64(5*60*1000) + // 十五分钟聚合 + FifteenMinTo := common.GenerateSingaporeFifteenMinTimestampOrigin() * 1000 + FifteenMinFrom := FifteenMinTo - int64(15*60*1000) + // 三十分钟聚合 + ThirtyMinTo := common.GenerateSingaporeThirtyMinTimestampOrigin() * 1000 + ThirtyMinToForm := ThirtyMinTo - int64(30*60*1000) + // 小时聚合 + HourFrom := common.GenerateSingaporeHourTimestampOrigin() * 1000 + HourTo := HourFrom + int64(60*60*1000) + // 天聚合 + DayFrom := common.GenerateSingaporeDayTimestamp(stockName) * 1000 + DayTo := common.TimeToNow() * 1000 + // 周聚合 + WeekFrom := common.GetWeekTimestamp() * 1000 + WeekTo := common.TimeToNow() * 1000 + // 月聚合 + MonFrom := common.GenerateSingaporeMonTimestampStock(stockName) * 1000 + MonTo := common.TimeToNow() * 1000 + //并发过高 拆分 + stocks, _, pageTotal := GetStockAll(stockName, 1, PageSize) + wg := sync.WaitGroup{} + for _, value := range stocks { + wg.Add(1) + go func(FiveMinTo, FiveMinFrom, FifteenMinTo, FifteenMinFrom, ThirtyMinTo, ThirtyMinToForm, HourFrom, HourTo, DayFrom, DayTo, WeekFrom, WeekTo, MonFrom, MonTo int64, value stock.StockPolygon) { + defer wg.Done() + applogger.Info(common.TimeToNows().Format("2006-01-02 15:04:05"), "start ", value.Code, value.Locale) + if err := GetTimeNewPrice(value.Code, FiveMinFrom, FiveMinTo, value.Locale, "5min"); err != nil { + applogger.Error(err.Error(), "run end") + return + } + GetTimeNewPriceAll(value.Code, FifteenMinFrom, FifteenMinTo, value.Locale, "15min", "5min") + GetTimeNewPriceAll(value.Code, ThirtyMinToForm, ThirtyMinTo, value.Locale, "30min", "15min") + GetTimeNewPriceAll(value.Code, HourFrom, HourTo, value.Locale, "1hour", "30min") + + GetTimeNewPriceAll(value.Code, DayFrom, DayTo, value.Locale, "1day", "1hour") + GetTimeNewPriceAll(value.Code, WeekFrom, WeekTo, value.Locale, "1week", "1day") + GetTimeNewPriceAll(value.Code, MonFrom, MonTo, value.Locale, "1mon", "1week") + }(FiveMinTo, FiveMinFrom, FifteenMinTo, FifteenMinFrom, ThirtyMinTo, ThirtyMinToForm, HourFrom, HourTo, DayFrom, DayTo, WeekFrom, WeekTo, MonFrom, MonTo, value) + } + wg.Wait() + for i := int64(2); i <= pageTotal; i++ { + stocks, _, _ := GetStockAll(stockName, i, PageSize) + wg := sync.WaitGroup{} + for _, value := range stocks { + wg.Add(1) + go func(FiveMinTo, FiveMinFrom, FifteenMinTo, FifteenMinFrom, ThirtyMinTo, ThirtyMinToForm, HourFrom, HourTo, DayFrom, DayTo, WeekFrom, WeekTo, MonFrom, MonTo int64, value stock.StockPolygon) { + defer wg.Done() + applogger.Info(common.TimeToNows().Format("2006-01-02 15:04:05"), "start ", value.Code, value.Locale) + if err := GetTimeNewPrice(value.Code, FiveMinFrom, FiveMinTo, value.Locale, "5min"); err != nil { + applogger.Error(err.Error(), "run end") + return + } + GetTimeNewPriceAll(value.Code, FifteenMinFrom, FifteenMinTo, value.Locale, "15min", "5min") + GetTimeNewPriceAll(value.Code, ThirtyMinToForm, ThirtyMinTo, value.Locale, "30min", "15min") + GetTimeNewPriceAll(value.Code, HourFrom, HourTo, value.Locale, "1hour", "30min") + GetTimeNewPriceAll(value.Code, DayFrom, DayTo, value.Locale, "1day", "1hour") + GetTimeNewPriceAll(value.Code, WeekFrom, WeekTo, value.Locale, "1week", "1day") + GetTimeNewPriceAll(value.Code, MonFrom, MonTo, value.Locale, "1mon", "1week") + }(FiveMinTo, FiveMinFrom, FifteenMinTo, FifteenMinFrom, ThirtyMinTo, ThirtyMinToForm, HourFrom, HourTo, DayFrom, DayTo, WeekFrom, WeekTo, MonFrom, MonTo, value) + } + wg.Wait() + } + fmt.Println("Run time: ", time.Since(start)) + // TODO: 删除已画好的数据,提高查询速度 + filter := bson.M{"timestamp": bson.M{"$lte": FiveMinTo}} + tableName := data.GetStockTableName(stockName) + + data.MgoDeleteMany(tableName, filter) + applogger.Info(tableName, FiveMinTo, "DELETE INFO") +} + +// 指数数据聚合 +func TickSpotIndexKline() { + start := time.Now() // 获取当前时间 + applogger.Info(common.TimeToNows().Format("2006-01-02 15:04:05"), "start TickSpotIndexKline") + // 五分钟聚合 + FiveMinTo := common.GenerateSingaporeFiveMinTimestampOrigin() * 1000 + FiveMinFrom := FiveMinTo - int64(5*60*1000) + // 十五分钟聚合 + FifteenMinTo := common.GenerateSingaporeFifteenMinTimestampOrigin() * 1000 + FifteenMinFrom := FifteenMinTo - int64(15*60*1000) + // 三十分钟聚合 + ThirtyMinTo := common.GenerateSingaporeThirtyMinTimestampOrigin() * 1000 + ThirtyMinToForm := ThirtyMinTo - int64(30*60*1000) + // 小时聚合 + HourFrom := common.GenerateSingaporeHourTimestampOrigin() * 1000 + HourTo := HourFrom + int64(60*60*1000) + // 天聚合 + DayFrom := common.GenerateSingaporeDayTimestamp("") * 1000 + DayTo := common.TimeToNow() * 1000 + // 周聚合 + WeekFrom := common.GetWeekTimestamp() * 1000 + WeekTo := common.TimeToNow() * 1000 + // 月聚合 + MonFrom := common.GenerateSingaporeMonTimestampStock("") * 1000 + MonTo := common.TimeToNow() * 1000 + //并发过高 拆分 + filter := bson.M{"State": common.StockIndexOn} + res := make([]stock.StockIndexPolygon, 0) + data.MgoFindStockRes(data.StockIndexList, filter, &res) + wg := sync.WaitGroup{} + for _, value := range res { + wg.Add(1) + go func(FiveMinTo, FiveMinFrom, FifteenMinTo, FifteenMinFrom, ThirtyMinTo, ThirtyMinToForm, HourFrom, HourTo, DayFrom, DayTo, WeekFrom, WeekTo, MonFrom, MonTo int64, value stock.StockIndexPolygon) { + defer wg.Done() + applogger.Info(common.TimeToNows().Format("2006-01-02 15:04:05"), "start ", value.Code, value.Locale) + if err := GetTimeNewIndexPrice(value.Code, FiveMinFrom, FiveMinTo, value.Locale, "5min"); err != nil { + applogger.Error(err.Error(), "run end") + return + } + GetTimeNewIndexPriceAll(value.Code, FifteenMinFrom, FifteenMinTo, value.Locale, "15min", "5min") + GetTimeNewIndexPriceAll(value.Code, ThirtyMinToForm, ThirtyMinTo, value.Locale, "30min", "15min") + GetTimeNewIndexPriceAll(value.Code, HourFrom, HourTo, value.Locale, "1hour", "30min") + + GetTimeNewIndexPriceAll(value.Code, DayFrom, DayTo, value.Locale, "1day", "1hour") + GetTimeNewIndexPriceAll(value.Code, WeekFrom, WeekTo, value.Locale, "1week", "1day") + GetTimeNewIndexPriceAll(value.Code, MonFrom, MonTo, value.Locale, "1mon", "1week") + }(FiveMinTo, FiveMinFrom, FifteenMinTo, FifteenMinFrom, ThirtyMinTo, ThirtyMinToForm, HourFrom, HourTo, DayFrom, DayTo, WeekFrom, WeekTo, MonFrom, MonTo, value) + } + wg.Wait() + fmt.Println("Run time: ", time.Since(start)) +} + +func GetTimeNewIndexPrice(symbol string, from, to int64, country, period string) error { + //fmt.Println("country", country, "period", period, "from", common.ConvertToTimeStr(from/1000), from, "to", common.ConvertToTimeStr(to/1000), to) + filter := bson.M{"stock_code": symbol, "timestamp": bson.M{"$gte": from, "$lte": to}} + tableName := data.GetStockIndexTableName() + highRes := make([]model.StockIndexParam, 0) + projection := bson.M{"price": 1} + sort := bson.M{"price": -1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &highRes, 1) + if len(highRes) <= 0 { + applogger.Error(symbol+" no data", period) + return errors.New(symbol + " no data") + } + high := highRes[0].Price + + lowRes := make([]model.StockIndexParam, 0) + sort = bson.M{"price": 1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &lowRes, 1) + low := lowRes[0].Price + + openRes := make([]model.StockIndexParam, 0) + sort = bson.M{"timestamp": 1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &openRes, 1) + open := openRes[0].Price + closeRes := make([]model.StockIndexParam, 0) + projection = bson.M{} + sort = bson.M{"timestamp": -1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &closeRes, 1) + + var dataList []mongo.WriteModel + filter = bson.M{"timestamp": bson.M{"$eq": from}, "stock_code": bson.M{"$eq": symbol}} + update := bson.D{{"$set", bson.D{ + {"stock_code", closeRes[0].StockCode}, + {"stock_name", closeRes[0].StockName}, + {"open_price", fmt.Sprintf("%f", open)}, + {"high_price", fmt.Sprintf("%f", high)}, + {"low_price", fmt.Sprintf("%f", low)}, + {"close_price", fmt.Sprintf("%f", closeRes[0].Price)}, + {"up_down_rate", closeRes[0].UpDownRate}, + {"up_down", closeRes[0].UpDown}, + {"vol", closeRes[0].Vol}, + {"country", closeRes[0].Country}, + {"timestamp", from}, + }}} + applogger.Info("GetTimeNewPrice info: %v", update) + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + if err := data.MgoBulkWrite(data.GetStockIndixKlineTableName(period), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + return err + } + return nil +} + +func GetTimeNewIndexPriceAll(symbol string, from, to int64, country, period, periodPre string) { + filter := bson.M{"stock_code": symbol, "timestamp": bson.M{"$gte": from, "$lte": to}} + tableName := data.GetStockIndixKlineTableName(periodPre) + res := make([]model.StockMogoParam, 0) + projection := bson.M{"symbol": 1, "stock_code": 1, "stock_name": 1, "open_price": 1, "high_price": 1, "low_price": 1, "close_price": 1, "vol": 1, "country": 1, "timestamp": 1} + sort := bson.M{"timestamp": 1} + data.MgoFindProjectionRes(tableName, filter, projection, sort, &res, 0) + if len(res) <= 0 { + applogger.Error(symbol+" no data", period) + return + } + var low, high, vol decimal.Decimal + for key, v := range res { + lows, _ := decimal.NewFromString(v.LowPrice) + highs, _ := decimal.NewFromString(v.HighPrice) + var vols decimal.Decimal + if key == 0 { + low = lows + high = highs + vol = vols + continue + } + vol = vol.Add(vols) + if low.GreaterThan(lows) { + low = lows + } + if high.LessThan(highs) { + high = highs + } + } + l := len(res) - 1 + var dataList []mongo.WriteModel + ts := from + open := res[0].OpenPrice + opens, _ := decimal.NewFromString(open) + if low.GreaterThan(opens) && !opens.Equal(decimal.NewFromInt(0)) { + low = opens + } + if high.LessThan(opens) { + high = opens + } + filter = bson.M{"timestamp": bson.M{"$eq": ts}, "stock_code": bson.M{"$eq": symbol}} + update := bson.D{{"$set", bson.D{ + {"symbol", res[l].Symbol}, + {"stock_code", res[l].StockCode}, + {"stock_name", res[l].StockName}, + {"open_price", open}, + {"high_price", high.String()}, + {"low_price", low.String()}, + {"close_price", res[l].ClosePrice}, + {"vol", res[l].Vol}, + {"country", res[l].Country}, + {"timestamp", ts}, + }}} + applogger.Info("GetTimeNewPriceAll info: %v", update) + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + if err := data.MgoBulkWrite(data.GetStockIndixKlineTableName(period), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + } +} + +// 控制插针数据不推送 +func NewPinStock(noPin map[string]bool) { + for { + fmt.Println(noPin) + pinStockMutex.Lock() + for k, v := range StockClosedDataList { + hashListName := fmt.Sprintf("STOCK_PRICES:%d", v) + dbs := red.ScanMap(hashListName) + dbData := make(map[string]map[string]bool) + for db, keys := range dbs { + //不用插针服务 + if noPin[db] { + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), db, "no pin 不用插针") + continue + } + stockCode := make(map[string]bool) + for _, key := range keys { + res, _ := red.HGetAllMap(db, key) + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), db, res) + status, _ := strconv.Atoi(res["status"]) + code := res["stock_code"] + if status == StockStatusOn { + stockCode[code] = true + } + } + dbData[db] = stockCode + } + pinStock[k] = dbData + } + pinStockMutex.Unlock() + applogger.Info("pin stock :%v", pinStock) + time.Sleep(1 * time.Minute) + } +} + +func getPinStock(db, country string) map[string]bool { + pinStockMutex.RLock() + defer pinStockMutex.RUnlock() + return pinStock[country][db] +} + +func JudgePublishMap(country, code, channel string, message interface{}) { + for k, db := range red.RedisClientMap { + if getPinStock(k, country)[code] { + applogger.Debug(k, code, "pin stock", message) + continue + } + applogger.Info("channel", channel, "DB", k) + err := db.Publish(channel, message).Err() + if err != nil { + fmt.Println("db", k, "存储失败:", err) + } + } +} + +func JudgeHsetMap(country, key, field string, value interface{}) { + for k, db := range red.RedisClientMap { + if getPinStock(k, country)[field] { + applogger.Debug(k, field, "pin stock", value) + continue + } + applogger.Info("key", key, "field", field, "value", value, "DB", k) + err := db.HSet(key, field, value).Err() + if err != nil { + fmt.Println("db", k, "存储失败:", err) + } + } +} + +func StockWs(param model.StockParam, country string) { + param.Token = "" + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return + } + //applogger.Info("last date info: %v", string(msgStr)) + // Write to Redis for broadcasting + JudgePublishMap(country, param.Symbol, fmt.Sprintf("%s.%s", param.Symbol, country), string(msgStr)) +} diff --git a/internal/data/business/mgolistdataus.go b/internal/data/business/mgolistdataus.go new file mode 100644 index 0000000..8db4d85 --- /dev/null +++ b/internal/data/business/mgolistdataus.go @@ -0,0 +1,1175 @@ +package business + +import ( + "encoding/json" + "fmt" + "github.com/360EntSecGroup-Skylar/excelize" + "github.com/google/uuid" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" + "math" + "strconv" + "strings" + "sync" + "time" + "wss-pool/cmd/common" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +var TapsMapMu = map[int32]string{ + 1: "NYSE", + 2: "NYSEARCA", + 3: "NASDAQ", +} + +var TapsMapMuNew = map[string]string{ + "XNYS": "NYSE", + "BATS": "NYSE", + "XASE": "NYSE", + "ARCX": "NYSE", + "XNAS": "NASDAQ", +} + +var CountryEx = map[string]string{ + "Thailand": "SET", + "Indonesia": "IDX", + "Malaysia": "MYX", + "Singapore": "SGX", + "HongKong": "HKEX", + "Japan": "TSE", +} + +var StockClosingPrice = map[string]string{ + "US": "Stock:US:ClosePrice", //美股当天闭盘价 + "USNew": "Stock:US:CloseNewPrice", //美股实时落盘价 + "USBeforeClose": "Stock:US:BeforeClose", //美股上一次闭盘价 + + "Malaysia": "Stock:Malaysia:ClosePrice", //马股当天闭盘价 + "MalaysiaNew": "Stock:Malaysia:CloseNewPrice", //马股实时落盘价 + "MalaysiaBeforeClose": "Stock:Malaysia:BeforeClose", //马股上一次闭盘价 + + "Thailand": "Stock:Thailand:ClosePrice", //泰股当天闭盘价 + "ThailandNew": "Stock:Thailand:CloseNewPrice", //泰股实时落盘价 + "ThailandBeforeClose": "Stock:Thailand:BeforeClose", //泰股上一次闭盘价 + + "Indonesia": "Stock:Indonesia:ClosePrice", //印尼股当天闭盘价 + "IndonesiaNew": "Stock:Indonesia:CloseNewPrice", //印尼股实时落盘价 + "IndonesiaBeforeClose": "Stock:Indonesia:BeforeClose", //印尼股上一次闭盘价 + + "India": "Stock:India:ClosePrice", //印度股当天闭盘价 + "IndiaNew": "Stock:India:CloseNewPrice", //印度股实时落盘价 + "IndiaBeforeClose": "Stock:India:BeforeClose", //印度股上一次闭盘价 + + "StockIndex": "Stock:Index:ClosePrice", //指数当天闭盘价 + "StockIndexNew": "Stock:Index:CloseNewPrice", //指数实时落盘价 + "StockIndexBeforeClose": "Stock:Index:BeforeClose", //指数股上一次闭盘价 + + "Singapore": "Stock:Singapore:ClosePrice", //新加坡当天闭盘价 + "SingaporeNew": "Stock:Singapore:CloseNewPrice", //新加坡股实时落盘价 + "SingaporeBeforeClose": "Stock:Singapore:BeforeClose", //新加坡股上一次闭盘价 + + "HongKong": "Stock:HongKong:ClosePrice", //港股当天闭盘价 + "HongKongNew": "Stock:HongKong:CloseNewPrice", //港股实时落盘价 + "HongKongBeforeClose": "Stock:HongKong:BeforeClose", //港股上一次闭盘价 + + "OptionUS": "Stock:OptionUS:ClosePrice", //期权美股当天闭盘价 + "OptionUSNew": "Stock:OptionUS:CloseNewPrice", //期权美股实时落盘价 + "OptionUSBeforeClose": "Stock:OptionUS:BeforeClose", //期权美股上一次闭盘价 + + "OptionIndia": "Option:India:ClosePrice", //期权印度股当天闭盘价 + "OptionIndiaNew": "Option:India:CloseNewPrice", //期权印度股实时落盘价 + "OptionIndiaBeforeClose": "Option:India:BeforeClose", //期权印度股上一次闭盘价 + "OptionIndiaPrice": "Option:India:List", + + "UK": "Stock:UK:ClosePrice", //英股当天闭盘价 + "UKNew": "Stock:UK:CloseNewPrice", //英股实时落盘价 + "UKBeforeClose": "Stock:UK:BeforeClose", //英股上一次闭盘价 + + "Germany": "Stock:Germany:ClosePrice", //德股当天闭盘价 + "GermanyNew": "Stock:Germany:CloseNewPrice", //德股实时落盘价 + "GermanyBeforeClose": "Stock:Germany:BeforeClose", //德股上一次闭盘价 + + "France": "Stock:France:ClosePrice", //法股当天闭盘价 + "FranceNew": "Stock:France:CloseNewPrice", //法股实时落盘价 + "FranceBeforeClose": "Stock:France:BeforeClose", //法股上一次闭盘价 + + "Brazil": "Stock:Brazil:ClosePrice", //巴西当天闭盘价 + "BrazilNew": "Stock:Brazil:CloseNewPrice", //巴西实时落盘价 + "BrazilBeforeClose": "Stock:Brazil:BeforeClose", //巴西上一次闭盘价 + + "Japan": "Stock:Japan:ClosePrice", //日本当天闭盘价 + "JapanNew": "Stock:Japan:CloseNewPrice", //日本实时落盘价 + "JapanBeforeClose": "Stock:Japan:BeforeClose", //日本上一次闭盘价 +} + +var ( + mutexMap = sync.RWMutex{} + NewPriceMap = make(map[string]string) + IsRealFlase int = 2 + StockCountryMap = map[string]string{ + "US": "TSLA.US", + "Thailand": "SET:AAV.Thailand", + "Indonesia": "IDX:GOTO.Indonesia", + "India": "NSE:SUZLON.India", + "Malaysia": "MYX:MAYBANK.Malaysia", + "Singapore": "SGX:D05.Singapore", + "HongKong": "HKEX:9888.HongKong", + "UK": "LSE:BRBY.UK", + "France": "EURONEXT:RMS.France", + "Germany": "FWB:TMV.Germany", + "Brazil": "BMFBOVESPA:BBAS3.Brazil", + "Japan": "TSE:1380.Japan", + } + StockCountryIsValidMap = map[string]bool{ + "US": false, + "Thailand": false, + "Indonesia": false, + "India": false, + "Malaysia": false, + "Singapore": false, + "HongKong": false, + "UK": false, + "France": false, + "Germany": false, + "Brazil": false, + "Japan": false, + } + StockIsValidMutex = sync.RWMutex{} +) + +// UpdateStockUS Daily update of US stock list data【ws.eodhistoricaldata.com】 +func UpdateStockUS() { + red.RedisInitMap(common.GetRedisDBMore(config.Config.Redis.DbMore)) + filter := bson.M{"Country": "US", "IsReal": bson.M{"$ne": IsRealFlase}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + day := common.TimeToNows().Weekday() + if day == time.Sunday { + applogger.Debug("周日 无数据") + return + } + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + //定时任务 最长休假 算 10 天 + // today := common.TimeToNows().AddDate(0, 0, -10).Unix() + // eodModel, _ := PreviousClose(code) + // if len(eodModel.Results) <= 0 { + // applogger.Error("close price null") + // continue + // } + // dateStrs := common.ConvertToTimezones(eodModel.Results[0].T) + // if dateStrs.Unix() < today { + // applogger.Error(code, dateStrs, "超出时间范围") + // //updateYesterdayClose(code, "US") + // continue + // } + // yesterday := dateStrs + //Loop: + // yesterday = yesterday.AddDate(0, 0, -1) + // yesterdayClose, _ := UsData(code, yesterday.Format("2006-01-02")) + // if yesterdayClose == "" { + // goto Loop + // } + Loop: + res := GetFinnhubBeforClose(code) + if strings.Contains(res.Error, "API limit reached") { //防止被限制 + time.Sleep(6 * time.Second) + goto Loop + } + if !res.C.GreaterThan(decimal.Zero) { + applogger.Error(code, "close price null") + continue + } + filterS := bson.D{{"Code", bson.M{ + "$eq": code, + }}, {"Country", bson.M{ + "$eq": "US", + }}} + dp, _ := res.DP.Float64() //股价百分比变化 + updateData := bson.M{ + "$set": bson.M{ + "BeforeClose": res.PC.String(), + "DateStr": common.ConvertToTimezone(res.T * 1000), + "DP": dp, + "YesterdayClose": res.C.String()}} + red.HsetMap(StockClosingPrice["US"], code, res.C.String()) + red.HsetMap(StockClosingPrice["USBeforeClose"], code, res.PC.String()) + red.HsetMap(StockClosingPrice["USNew"], code, "0") + applogger.Debug("update data info:%v", updateData) + if err := data.MgoUpdateOne(data.StockList, filterS, updateData); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + return + } + // TODO: 更改副表 没有公用数据的服务不需要 暂用redis 参数 + dbs := common.GetRedisDBMore(config.Config.Redis.DbMore) + if len(dbs) > 1 { + applogger.Info("StockListAdd update table db", dbs[1]) + data.MgoUpdateOne(fmt.Sprintf("%s%s", data.StockList, dbs[1]), filterS, updateData) + } + } +} + +func UpdateOpenPrice() { + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + stocks, _, pageTotal := GetStockAll("US", 1, UsPageSize) + wg := sync.WaitGroup{} + end := common.TimeToNows().Format("2006-01-02") + for _, value := range stocks { + wg.Add(1) + go func(end, code string) { + defer wg.Done() + res := GetBeforClose(code, "1", "day", end, end) + if len(res.Results) > 0 { + applogger.Debug("US open price %v ,%v", code, res.Results[0].O.String()) + red.Hset(StockClosingPrice["US"], code, res.Results[0].O.String()) + } + }(end, value.Code) + } + wg.Wait() + for i := int64(2); i <= pageTotal; i++ { + stocks, _, _ := GetStockAll("US", i, UsPageSize) + wg := sync.WaitGroup{} + for _, value := range stocks { + wg.Add(1) + go func(end, code string) { + defer wg.Done() + res := GetBeforClose(code, "1", "day", end, end) + if len(res.Results) > 0 { + applogger.Debug("US open price %v ,%v", code, res.Results[0].O.String()) + red.Hset(StockClosingPrice["US"], code, res.Results[0].O.String()) + } + }(end, value.Code) + } + wg.Wait() + } +} + +func updateYesterdayClose(code, country string) { + filterS := bson.D{{"Code", bson.M{ + "$eq": code, + }}, {"Country", bson.M{ + "$eq": country, + }}} + updateData := bson.M{ + "$set": bson.M{ + "YesterdayClose": ""}} + red.Hset(StockClosingPrice[country], code, "0") + red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", country)], code, "0") + red.Hset(StockClosingPrice[fmt.Sprintf("%sBeforeClose", country)], code, "0") + if err := data.MgoUpdateOne(data.StockList, filterS, updateData); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + return + } +} + +func UpdateYesterdayCloseIs(code, country, yesterdayClose, beforeClose string) { + filterS := bson.D{{"Code", bson.M{ + "$eq": code, + }}, {"Country", bson.M{ + "$eq": country, + }}} + updateData := bson.M{ + "$set": bson.M{ + "YesterdayClose": yesterdayClose}} + red.Hset(StockClosingPrice[country], code, yesterdayClose) + red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", country)], code, 0) + red.Hset(StockClosingPrice[fmt.Sprintf("%sBeforeClose", country)], code, beforeClose) + if err := data.MgoUpdateOne(data.StockList, filterS, updateData); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + return + } +} + +func GetBeforClose(code string, multiplier, timespan, start, end string) stock.PreviousCloseResponse { + var eodModel stock.PreviousCloseResponse + url := fmt.Sprintf("https://%v/v2/aggs/ticker/%v/range/%v/%v/%v/%v?adjusted=true&sort=desc&%v", + config.Config.ShareGather.PolygonHost, code, multiplier, timespan, start, end, fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey)) + applogger.Debug("UrlHttp getBeforClose info: %v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return eodModel + } + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return eodModel + } + return eodModel +} + +func GetFinnhubBeforClose(code string) stock.PreviousCloseRes { + var eodModel stock.PreviousCloseRes + url := fmt.Sprintf("https://%vquote?symbol=%s&token=%s", + config.Config.FinnhubUs.FinnhubHost, code, config.Config.FinnhubUs.FinnhubKey) + applogger.Debug("UrlHttp getBeforClose info: %v", url) + bodyStr, err := internal.HttpGet(url) + fmt.Println(bodyStr) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return eodModel + } + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return eodModel + } + return eodModel +} + +// 更新交易所 +func UpdateStockUSTape() { + filter := bson.M{"Country": "US"} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + eodModel, _ := TradesTape(code) + filterS := bson.D{{"Code", bson.M{ + "$eq": code, + }}, {"Country", bson.M{ + "$eq": "US", + }}} + if len(eodModel.Results) <= 0 { + continue + } + updateData := bson.M{ + "$set": bson.M{ + "Code": code, + "Tape": eodModel.Results[0].Tape}} + applogger.Debug("update data info:%v", updateData) + if err := data.MgoUpdateOne(data.StockList, filterS, updateData); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + return + } + } +} + +// 推送 +func StockPyWs(param model.StockParam, country string) { + param.Token = "" + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return + } + //applogger.Info("last date info: %v", string(msgStr)) + // Write to Redis for broadcasting + red.PublishMap(fmt.Sprintf("%s.%s", param.Symbol, country), string(msgStr)) +} + +func IsPriceTime(symbol, price, country string) bool { + //fmt.Println(symbol, price, country) + mutexMap.RLock() + val := NewPriceMap[fmt.Sprintf("%s-%s", country, symbol)] + mutexMap.RUnlock() + if val != "" { + strs := strings.Split(val, "-") + if len(strs) > 0 { + timeInt, _ := strconv.ParseInt(strs[1], 10, 64) + if strs[0] == price && common.TimeToNow() < timeInt { + return false + } + } + } + mutexMap.Lock() + NewPriceMap[fmt.Sprintf("%s-%s", country, symbol)] = fmt.Sprintf("%s-%d", price, common.TimeToNow()+30) + mutexMap.Unlock() + return true +} + +// 推送 +func StockPyWsStockIndex(param model.StockIndexParam, country string) { + param.Token = "" + param.IsStockIndex = true + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return + } + //applogger.Info("last date info: %v", string(msgStr)) + // Write to Redis for broadcasting + red.PublishMap(fmt.Sprintf("%s.%s", param.StockCode, country), string(msgStr)) +} + +// 期权列表推送 +func StockPyWsOptionList(param model.OptionPolygon, country string) string { + param.IsOptionList = true + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return "" + } + //applogger.Info("last date info: %v", string(msgStr)) + // Write to Redis for broadcasting + red.PublishMap(fmt.Sprintf("%s.%s", param.Stock, country), string(msgStr)) + return string(msgStr) +} + +// 期权详情推送 +func StockPyWsOptionInfo(param model.OptionInfoParam, country string) string { + param.IsOptionInfo = true + param.Token = "" + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return "" + } + //applogger.Info("StockPyWsOptionInfo info: %v", string(msgStr)) + // Write to Redis for broadcasting + red.PublishMap(fmt.Sprintf("%s.%s", param.Stock, country), string(msgStr)) + return string(msgStr) +} + +// 期权详情Exchange推送 +func StockPyWsOptionInfoExchange(param model.OptionInfoExchange, country string) string { + param.IsOptionInfo = true + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return "" + } + applogger.Info("StockPyWsOptionInfoExchange info: %v", string(msgStr)) + red.PublishMap(fmt.Sprintf("%s.%s", param.Stock, country), string(msgStr)) + return string(msgStr) +} + +func OptionResPrice(param model.StrikePrice, key, code string, isClose bool, optionDate, expiration string) string { + result := model.StrikePrice{} + resStr, _ := red.Hget(code, key) + param.DueDate = common.OptionTime(expiration) + if resStr != "" { + json.Unmarshal([]byte(resStr), &result) + param.BeforeClose = result.BeforeClose + param.YesterdayClose = result.YesterdayClose + param.CloseDate = result.CloseDate + if isClose && optionDate != result.CloseDate { //多次传闭盘价 + param.BeforeClose = result.YesterdayClose + param.YesterdayClose = param.Price + param.Price = "0" + param.CloseDate = optionDate + } else if isClose && optionDate == result.CloseDate { + param.BeforeClose = result.BeforeClose + param.YesterdayClose = param.Price + param.Price = "0" + } + } + msgStr, err := json.Marshal(param) + if err != nil { + applogger.Error("json.Marshal err: %v", err) + return "" + } + red.HsetMap(code, key, string(msgStr)) + return string(msgStr) +} + +func DelOptionHash() { + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + keys := red.Scan("Option:India:List") + for _, key := range keys { + res, _ := red.HGetAll(key) + for k, val := range res { + result := model.StrikePrice{} + json.Unmarshal([]byte(val), &result) + if common.TimeToNow() >= (result.DueDate + 24*60*60) { + applogger.Info("DelOptionHash : expireTime %v", result.DueDate, key, k) + red.HDel(key, k) + } + } + + } +} + +func UpdateStockBeforeClose(symbol, price, country string) { + //if val, ok := NewPriceMap[fmt.Sprintf("%s-%s", country, symbol)]; ok && val == price { + // applogger.Info("new price", fmt.Sprintf("%s-%s", country, symbol), price) + // return + //} + red.HsetMap(StockClosingPrice[fmt.Sprintf("%sNew", country)], symbol, price) + //NewPriceMap[fmt.Sprintf("%s-%s", country, symbol)] = price +} + +// UpdateStockUSBak api.polygon.io Update US stock list data +func UpdateStockUSBak() { + yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") + before := time.Now().AddDate(0, 0, -2).Format("2006-01-02") + + // 从mongodb中获取股票代码 + filter := bson.M{"Country": "USA", "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + yesterdayClose, err := UsData(code, yesterday) + if err != nil || len(yesterdayClose) == 0 { + applogger.Error("yesterdayClose ConstructorData info err: %v", err) + yesterdayClose = TypeCheck(value["YesterdayClose"]) + } + beforeClose, err := UsData(code, before) + if err != nil || len(beforeClose) == 0 { + applogger.Error("beforeClose ConstructorData info err: %v", err) + beforeClose = TypeCheck(value["BeforeClose"]) + } + + applogger.Debug("data info:%v-----%v-----%v", code, yesterdayClose, beforeClose) + + filterS := bson.D{{"Code", bson.M{ + "$eq": code, + }}} + updateData := bson.M{ + "$set": bson.M{ + "Code": code, + "YesterdayClose": yesterdayClose, + "BeforeClose": beforeClose}} + + applogger.Debug("update data info:%v", updateData) + if err := data.MgoUpdateOne(data.StockList, filterS, updateData); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + return + } + } +} + +// UsData Obtaining the closing price of US stocks through time【api.polygon.io】 +func UsData(code, date string) (string, error) { + url := fmt.Sprintf("https://%v/v1/open-close/%v/%v?apiKey=%v&adjusted=true", config.Config.ShareGather.PolygonHost, code, date, config.Config.ShareGather.PolygonKey) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return "", err + } + applogger.Debug("url info:%v", url) + + if strings.Contains(bodyStr, ",\"message\":") { + return "", err + } + + var eodModel stock.UsDateClose + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("Unmarshal err: %v---%v", eodModel.Symbol, err) + return "", err + } + return eodModel.Close.String(), err +} + +func UsNewPrice(code string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + //定时任务 最长休假 算 10 天 + today := common.TimeToNows().AddDate(0, 0, -10).Unix() + eodModel, _ := PreviousClose(code) + if len(eodModel.Results) <= 0 { + applogger.Error("close price null") + return + } + fmt.Println(eodModel) + dateStrs := common.ConvertToTimezones(eodModel.Results[0].T) + if dateStrs.Unix() < today { + applogger.Error(code, dateStrs, "超出时间范围") + //updateYesterdayClose(code, "US") + return + } + yesterday := dateStrs +Loop: + yesterday = yesterday.AddDate(0, 0, -1) + yesterdayClose, _ := UsData(code, yesterday.Format("2006-01-02")) + if yesterdayClose == "" { + goto Loop + } + filterS := bson.D{{"Code", bson.M{ + "$eq": code, + }}, {"Country", bson.M{ + "$eq": "US", + }}} + updateData := bson.M{ + "$set": bson.M{ + "Code": code, + "BeforeClose": yesterdayClose, + "DateStr": dateStrs.Format("2006-01-02"), + "Vol": eodModel.Results[0].V.IntPart(), + "YesterdayClose": eodModel.Results[0].C.String()}} + red.Hset(StockClosingPrice["US"], code, eodModel.Results[0].C.String()) + red.Hset(StockClosingPrice["USBeforeClose"], code, yesterdayClose) + red.Hset(StockClosingPrice["USNew"], code, "0") + applogger.Debug("update data info:%v", updateData) + if err := data.MgoUpdateOne(data.StockList, filterS, updateData); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + return + } +} + +// 倒数据 +func SymbolToStock(country string) { + if country == "" { + return + } + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + config.Config.Mongodb.DbHost = "10.154.0.5" + data.Mgo_init(config.Config.Mongodb) + filter := bson.M{"Country": country} + if country == "StockIndex" { + filter = bson.M{} + } + res := make([]stock.StockPolygonS, 0) + tableName := data.StockList + if country == "StockIndex" { + tableName = data.StockIndexList + } + data.MgoFindRes(tableName, filter, &res) + config.Config.Mongodb.DbHost = "10.154.0.10" + data.Mgo_init(config.Config.Mongodb) + var dataList []mongo.WriteModel + fmt.Println(len(res)) + for _, value := range res { + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}, {"Country", bson.M{ + "$eq": value.Locale, + }}} + update := bson.D{{"$set", bson.D{ + {"Code", value.Code}, + {"Name", value.Name}, + {"Country", value.Locale}, + {"Exchange", value.PrimaryExchange}, + {"Currency", value.Currency}, + {"Intro", value.Intro}, + {"Type", value.Type}, + {"Cik", value.CIK}, + {"ShareClassFigi", value.ShareClassFigi}, + {"YesterdayClose", value.YesterdayClose}, + {"BeforeClose", value.BeforeClose}, + {"Tape", value.Tape}, + {"State", value.State}, + {"NumericCode", value.NumericCode}, + {"DateStr", value.DateStr}, + {"Sort", value.Sort}, + {"LogoUrl", value.LogoUrl}}}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + //red.Hset(StockClosingPrice[country], value.Code, value.YesterdayClose) + //red.Hset(StockClosingPrice[fmt.Sprintf("%sBeforeClose", country)], value.Code, value.BeforeClose) + //red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", country)], value.Code, "0") + } + // fmt.Println(2222) + if err := data.MgoBulkWrite(tableName, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + return + } +} + +// SymbolToStockList +// +// @Description: 将旧的美股数据导入新的项目 +// @param country +func SymbolToStockList(country string) { + config.Config.Mongodb.DbHost = "35.189.116.242" + data.Mgo_init(config.Config.Mongodb) + res := make([]stock.StockListBak, 0) + data.MgoFindRes(data.StockList, bson.M{"Country": country}, &res) + applogger.Debug("查询股票列表:%v", len(res)) + + config.Config.Mongodb.DbHost = "104.198.117.66" + data.Mgo_init(config.Config.Mongodb) + var dataList []mongo.WriteModel + for _, value := range res { + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}, {"Country", bson.M{ + "$eq": value.Country, + }}} + update := bson.D{{"$set", bson.D{ + {"Country", value.Country}, + {"Code", value.Code}, + {"BeforeClose", value.BeforeClose}, + {"Cik", value.Cik}, + {"CompositeFigi", value.CompositeFigi}, + {"Currency", value.Currency}, + {"Exchange", value.Exchange}, + {"Name", value.Name}, + {"ShareClassFigi", value.ShareClassFigi}, + {"Type", value.Type}, + {"YesterdayClose", value.YesterdayClose}, + {"DP", value.DP}, + {"DateStr", value.DateStr}}}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + + applogger.Debug("倒入数据:%v", len(dataList)) + + if err := data.MgoBulkWrite(data.StockList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + return + } +} + +// 倒数据 +func SymbolToStockInfo(country string) { + if country == "" { + return + } + config.Config.Mongodb.DbHost = "34.93.29.102" + data.Mgo_init(config.Config.Mongodb) + config.Config.Mongodb.DbHost = "47.236.120.73" + client := data.Mgo_inits(config.Config.Mongodb) + fmt.Println(country) + for _, period := range dictionary.StockSouthAsiaListTimes { + tableName := data.GetStockSouthAsiaTableName(country, period) + if country == "StockIndex" { + tableName = data.GetStockIndixKlineTableName(period) + } + fmt.Println(tableName) + res, _, pageTotal := GetStockIndoAll(tableName, 1, 100, period) + var dataList []mongo.WriteModel + for _, v := range res { + filter := bson.M{"timestamp": bson.M{"$eq": v.Ts}, "symbol": bson.M{"$eq": v.Symbol}} + update := bson.D{{"$set", bson.D{ + {"symbol", v.Symbol}, + {"stock_code", v.StockCode}, + {"stock_name", v.StockName}, + {"open_price", v.OpenPrice}, + {"high_price", v.HighPrice}, + {"low_price", v.LowPrice}, + {"close_price", v.ClosePrice}, + {"vol", v.Vol}, + {"turnover_price_total", v.TurnoverPriceTotal}, + {"price_total", v.PriceTotal}, + {"price_code", v.PriceCode}, + {"country", v.Country}, + {"timestamp", v.Ts}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if err := data.MgoBulkWrites(client, tableName, dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + } + fmt.Println(pageTotal) + //for i := int64(2); i <= pageTotal; i++ { + // res, _, _ := GetStockIndoAll(tableName, i, 100, period) + // fmt.Println(period, i) + // for _, v := range res { + // filter := bson.M{"timestamp": bson.M{"$eq": v.Ts}, "symbol": bson.M{"$eq": v.Symbol}} + // update := bson.D{{"$set", bson.D{ + // {"symbol", v.Symbol}, + // {"stock_code", v.StockCode}, + // {"stock_name", v.StockName}, + // {"open_price", v.OpenPrice}, + // {"high_price", v.HighPrice}, + // {"low_price", v.LowPrice}, + // {"close_price", v.ClosePrice}, + // {"vol", v.Vol}, + // {"turnover_price_total", v.TurnoverPriceTotal}, + // {"price_total", v.PriceTotal}, + // {"price_code", v.PriceCode}, + // {"country", v.Country}, + // {"timestamp", v.Ts}, + // }}} + // models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + // dataList = append(dataList, models) + // } + // if err := data.MgoBulkWrites(client, tableName, dataList); err != nil { + // applogger.Error("stock MgoInsertMany err:%v", err) + // } + //} + } +} + +func GetStockIndoAll(tableName string, pageNum, pageSize int64, period string) ([]model.StockMogoParams, int64, int64) { + filter := bson.M{} + if period == "1hour" { + filter = bson.M{"timestamp": bson.M{"$gte": time.Now().AddDate(0, 0, -20).UnixMilli()}} + } else if period == "1day" { + filter = bson.M{"timestamp": bson.M{"$gte": time.Now().AddDate(0, -6, 0).UnixMilli()}} + } + projection := bson.M{} + res := make([]model.StockMogoParams, 0) + total, _ := data.MgoFindTotal(tableName, filter) + data.MgoPagingFindStructProjection(tableName, filter, projection, pageSize, pageNum, -1, &res) + return res, total, int64(math.Ceil(float64(total) / float64(pageSize))) +} + +func SymbolNews(country string) { + config.Config.Mongodb.DbHost = "104.198.117.66" + data.Mgo_init(config.Config.Mongodb) + filter := bson.M{"country": country} + res := make([]model.StockNews, 0) + data.MgoFindRes(data.StockNews, filter, &res) + config.Config.Mongodb.DbHost = "35.186.148.111" + data.Mgo_init(config.Config.Mongodb) + var bsonEod []interface{} + for _, v := range res { + bsonEod = append(bsonEod, bson.D{ + {"country", v.Country}, + {"pubdate", v.Pubdate}, + {"title", v.Title}, + {"link", v.Link}, + {"source", v.Source}, + {"code", v.Code}, + }) + } + if err := data.MgoInsertMany(data.StockNews, bsonEod); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + return + } + //} + fmt.Println(1111) +} + +// 更改股票code结构(美股不需要更改) +func UpdateStockCode() { + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + filter := bson.M{} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + for _, value := range dateList.([]primitive.M) { + if value["Code"] == nil || value["Country"] == nil || (value["Tape"] == nil && value["Exchange"] == nil) { + continue + } + code := value["Code"].(string) + var exchange, symbol string + country := value["Country"].(string) + if country == "US" { + exchange = TapsMapMu[value["Tape"].(int32)] + symbol = code + } else { + exchange = value["Exchange"].(string) + if exchange == "" && country != "India" { + exchange = CountryEx[country] + } + if exchange == "" { + applogger.Error("没有值", country, symbol, exchange) + continue + } + symbol = fmt.Sprintf("%s:%s", exchange, code) + red.HDel(StockClosingPrice[country], code) + red.Hset(StockClosingPrice[country], symbol, value["YesterdayClose"]) + red.HDel(StockClosingPrice[fmt.Sprintf("%sBeforeClose", country)], code) + red.Hset(StockClosingPrice[fmt.Sprintf("%sBeforeClose", country)], symbol, value["BeforeClose"]) + red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", country)], code, "0") + red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", country)], symbol, "0") + } + filterList := bson.M{"Code": code, "Country": country} + if err = data.MgoUpdateOne( + data.StockList, + filterList, + bson.D{{"$set", bson.D{ + {"Code", symbol}, + {"Exchange", exchange}, + {"Symbol", code}}}}); err != nil { + applogger.Error(symbol, err) + } + applogger.Info(symbol, exchange, code, country) + } +} +func UpdateStockUsCode() { + red.RedisClient = red.RedisInit(config.Config.Redis.DbEleven) + filter := bson.M{"Country": "US"} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + for _, value := range dateList.([]primitive.M) { + if value["Code"] == nil || value["Country"] == nil || value["Exchange"] == nil { + continue + } + codeList := strings.Split(value["Code"].(string), ".") + if len(codeList) == 0 { + continue + } + code := codeList[0] + var exchange, symbol string + country := value["Country"].(string) + if country == "US" { + exchange = TapsMapMuNew[value["Exchange"].(string)] + symbol = code + } + if err = data.MgoUpdateOne( + data.StockList, + bson.M{"Code": value["Code"].(string), "Country": country}, + bson.D{{"$set", bson.D{ + {"Code", symbol}, + //{"Exchange", exchange}, + {"Symbol", code}}}}); err != nil { + applogger.Error(symbol, err) + } + applogger.Info(symbol, exchange, code, country) + } +} +func UpdateStockExchange() { + filter := bson.M{ + "Country": "India", + } + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + applogger.Info("total ", len(dateList.([]primitive.M))) + var i int + for _, value := range dateList.([]primitive.M) { + if value["Exchange"] == nil || value["Exchange"].(string) == "" { + i++ + fmt.Println(i) + code := value["Code"].(string) + filter = bson.M{"stock_code": code} + res, _ := data.MgoFinds(data.GetStockSouthAsiaTableName("India", "1day"), filter, int64(1)) + if len(res) > 0 { + exchange := strings.Split(res[0]["symbol"].(string), ":")[0] + applogger.Info(exchange, code) + filterList := bson.M{"Code": code, + "Country": "India", + } + //applogger.Info(exchange,code) + if err := data.MgoUpdateOne(data.StockList, filterList, bson.D{{"$set", bson.D{ + {"Exchange", exchange}}}}); err != nil { + } + } else { + applogger.Error(code, "no data") + } + } + } +} + +// 外汇代码数据信息更新 +func ForexUpdateCode() { + dateList, err := data.MgoFind(data.ForexListBak, bson.M{}) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + var codeList []string + for _, value := range dateList.([]primitive.M) { + code := value["code"].(string) + codeList = append(codeList, code) + } + + UrlBatchKline := "https://quote.tradeswitcher.com/quote-b-api/batch-kline?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" + + var dataPost model.ConstructParametersPost + for _, value := range codeList { + dataPost.Trace = uuid.New().String() + dataPost.Data.DataList = append(dataPost.Data.DataList, model.DataParameters{ + Code: value, + KlineType: 1, + KlineTimestampEnd: 0, + QueryKlineNum: 1, + AdjustType: 0, + }) + } + queryStr, err := json.Marshal(&dataPost) + if err != nil { + applogger.Error("解析json错误:%v", err) + return + } + + bodyStr, err := internal.HttpPost(UrlBatchKline, string(queryStr)) + if err != nil { + applogger.Error("读取响应失败:%v", err) + return + } + + applogger.Debug("响应内容:%v", bodyStr) + + var klineNew model.KlinePostReturnStruct + if err = json.Unmarshal([]byte(bodyStr), &klineNew); err != nil { + applogger.Error("解析失败:%v", err) + return + } + + applogger.Debug("响应内容:%v", klineNew) + + var dataList []mongo.WriteModel + for _, v := range klineNew.Data.KlineList { + filter := bson.M{ + "code": v.Code, + } + updateData := bson.M{} + for _, value := range v.KlineData { + updateData["openPrice"] = value.OpenPrice + updateData["highPrice"] = value.HighPrice + updateData["lowPrice"] = value.LowPrice + updateData["closePrice"] = value.ClosePrice + updateData["timestamp"] = value.Timestamp + break + } + update := bson.M{"$set": updateData} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err = data.MgoBulkWrite(data.ForexListBak, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + return + } + } + +} + +func MalaysiaStockUpdate() { + data.Mgo_init(config.Config.Mongodb) + f, err := excelize.OpenFile("final.xlsx") + if err != nil { + fmt.Println(err) + return + } + // 获取 Sheet1 上所有单元格 + rows := f.GetRows("Sheet1") + for k, row := range rows { + if k == 0 { + continue + } + fmt.Println(k, row) + symbol := strings.TrimSpace(row[0]) + numericCode := strings.TrimSpace(row[1]) + filter := bson.M{"Code": common.GetNewCode("MYX", symbol, "Malaysia"), + "Country": "Malaysia", + } + updateData := bson.D{{"$set", bson.D{ + {"NumericCode", numericCode}}}} + if err := data.MgoUpdateOne(data.StockList, filter, updateData); err != nil { + applogger.Error(err.Error()) + } + } +} + +func SendIndiaInfo() { + for { + if !common.IsOpening("India") { + applogger.Debug(time.Now().Format("2006-01-02 15:04:05"), " it's not opening time -----------------------------end") + continue + } + url := fmt.Sprintf("%s/india/spots/new/add", config.Config.SendIn.URL) + param := fmt.Sprintf(`[{"stock_code":"%s","symbol":"%s","country":"india","price":%v,"vol":%d,"ts":%d,"token":"asdfsnl123jlknl3nksdf32345ln98sdfsfs8891232nsdfsdfsdfsdxcfvbhnfgh"}]`, + common.GetOldCode(config.Config.SendIn.Symbol), config.Config.SendIn.Symbol, config.Config.SendIn.Price, config.Config.SendIn.Vol, time.Now().UnixMilli()) + + applogger.Info(param) + + bodyStr, err := internal.HttpPost(url, param) + if err != nil { + applogger.Error("Failed to query data:%v", err) + } + + applogger.Info(bodyStr) + time.Sleep(2 * time.Minute) + } +} + +func CheckStock() { + time.Sleep(1 * time.Minute) + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + for k, _ := range StockClosedDataList { + go func(k string) { + if strings.Contains(config.Config.TgBot.NoWarn, k) { + fmt.Println(k, "no warn") + return + } else if k == "US" && !common.IsOpeningUS() { + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), k, " it's not opening time -----------------------------end") + return + } else if (k == "Thailand" || k == "Indonesia" || k == "India" || k == "Malaysia" || k == "Singapore" || k == "HongKong" || k == "UK" || k == "France" || k == "Germany" || k == "Japan") && !common.IsOpening(k) { + applogger.Debug(common.TimeToNows().Format("2006-01-02 15:04:05"), k, " it's not opening time -----------------------------end") + return + } + fmt.Println(k) + fmt.Println(StockCountryMap[k]) + pubSub := red.RedisClient.Subscribe(StockCountryMap[k]) + defer func() { + pubSub.Close() + }() + _, err := pubSub.Receive() + if err != nil { + applogger.Error("failed to receive from control PubSub,%v", zap.Error(err)) + return + } + go func(k string) { + time.Sleep(10 * time.Minute) + if !StockCountryIsValidMap[k] { + common.TgBotSendMsg(fmt.Sprintf("%s %s %s 市场 %s 股票 行情异常,请注意!", common.TimeToNows().Format("2006-01-02 15:04:05"), config.Config.TgBot.Server, k, StockCountryMap[k])) + } + }(k) + ch := pubSub.Channel() + for msg := range ch { + applogger.Info("Subscribe date:%v", msg.Payload) + StockIsValidMutex.Lock() + StockCountryIsValidMap[k] = true + StockIsValidMutex.Unlock() + return + } + }(k) + } + time.Sleep(13 * time.Minute) +} + +func SymbolCode(country string) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + data.Mgo_init(config.Config.Mongodb) + filter := bson.M{"Country": country} + res := make([]stock.StockPolygonS, 0) + tableName := data.StockList + data.MgoFindRes(tableName, filter, &res) + var dataList []mongo.WriteModel + //fmt.Println(len(res)) + for _, value := range res { + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}, {"Country", bson.M{ + "$eq": value.Locale, + }}} + exchange := value.PrimaryExchange + if value.Locale == "Thailand" { + exchange = "SET" + + } else if value.Locale == "Indonesia" { + exchange = "IDX" + + } else if value.Locale == "US" { + switch value.Tape { + case 1: + exchange = "NYSE" + case 2: + exchange = "NYSE-ARCA/NYSE-American" + case 3: + exchange = "NASDAQ" + } + } + code := fmt.Sprintf("%s.%s", value.Code, exchange) + //if exchange == "" { + // fmt.Println(value.Code) + //} + //continue + update := bson.D{{"$set", bson.D{ + {"Code", code}, + {"Exchange", exchange}, + {"Symbol", value.Code}}}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + red.Hset(StockClosingPrice[value.Locale], value.Code, value.YesterdayClose) + red.Hset(StockClosingPrice[fmt.Sprintf("%sBeforeClose", value.Locale)], value.Code, value.BeforeClose) + red.Hset(StockClosingPrice[fmt.Sprintf("%sNew", value.Locale)], value.Code, "0") + } + if err := data.MgoBulkWrite(tableName, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + return + } +} diff --git a/internal/data/business/mgolistdatemys.go b/internal/data/business/mgolistdatemys.go new file mode 100644 index 0000000..5d78937 --- /dev/null +++ b/internal/data/business/mgolistdatemys.go @@ -0,0 +1,638 @@ +package business + +import ( + "encoding/json" + "fmt" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go/types" + "strings" + "time" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/market" + "wss-pool/pkg/model/stock" +) + +const ( + MaxLimit int = 100 +) + +type SpotKlineResInfo struct { + ID int64 `json:"id"` + Open decimal.Decimal `json:"open"` + Close decimal.Decimal `json:"close"` + Low decimal.Decimal `json:"low"` + High decimal.Decimal `json:"high"` + Amount decimal.Decimal `json:"amount"` + Vol decimal.Decimal `json:"vol"` + Count decimal.Decimal `json:"count"` + TradeTurnover string `json:"trade_turnover"` +} + +type LinearlineResInfo struct { + ID int64 `json:"id"` + Open decimal.Decimal `json:"open"` + Close decimal.Decimal `json:"close"` + Low decimal.Decimal `json:"low"` + High decimal.Decimal `json:"high"` + Amount decimal.Decimal `json:"amount"` + Vol decimal.Decimal `json:"vol"` + Count decimal.Decimal `json:"count"` + TradeTurnover decimal.Decimal `json:"trade_turnover"` +} + +type LinearKlineRes struct { + Ch string `json:"ch"` + Status string `json:"status"` + Ts int64 `json:"ts"` + Data []LinearlineResInfo `json:"data"` +} + +type SpotKlineWsRes struct { + Ch string `json:"ch"` + Status string `json:"status"` + Ts int64 `json:"ts"` + Tick SpotKlineResInfo `json:"tick"` +} + +type SpotKlineRes struct { + Ch string `json:"ch"` + Status string `json:"status"` + Ts int64 `json:"ts"` + Data []SpotKlineResInfo `json:"data"` +} +type ContractKlineRes struct { + Ch string `json:"ch"` + Status string `json:"status"` + Ts int64 `json:"ts"` + Data []LinearlineResInfo `json:"data"` +} + +type MarketTradeIDInfo struct { + ID int64 `json:"id"` + Ts int64 `json:"ts"` + TradeId int64 `json:"trade-id"` + Amount float64 `json:"amount"` + Price float64 `json:"price"` + Direction string `json:"direction"` + TradeTurnover decimal.Decimal `json:"trade_turnover"` +} + +type MarketTradeID struct { + ID int64 `json:"id"` + Ts int64 `json:"ts"` + Data []MarketTradeIDInfo `json:"data"` +} + +type MarketTrade struct { + Ch string `json:"ch"` + Status string `json:"status"` + Ts int64 `json:"ts"` + Data []MarketTradeID `json:"data"` +} + +// UpdateStockKLSE Daily update of stock market list data【ws.eodhistoricaldata.com】 +func UpdateStockKLSE() { + var dataList []mongo.WriteModel + yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") + before := time.Now().AddDate(0, 0, -2).Format("2006-01-02") + + filter := bson.M{"Country": "Malaysia", "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + yesterdayClose := TypeCheck(value["YesterdayClose"]) + beforeClose := TypeCheck(value["BeforeClose"]) + + eodModel, _ := ShareData(code, "KLSE", yesterday, before) + for _, eo := range eodModel { + switch eo.Date { + case yesterday: + yesterdayClose = eo.Close.String() + case before: + beforeClose = eo.Close.String() + default: + } + } + applogger.Debug("data info:%v-----%v-----%v", code, yesterdayClose, beforeClose) + + filterD := bson.D{{"Code", bson.M{ + "$eq": code, + }}} + updateData := bson.M{ + "$set": bson.M{ + "Code": code, + "YesterdayClose": yesterdayClose, + "BeforeClose": beforeClose}} + + models := mongo.NewUpdateOneModel().SetFilter(filterD).SetUpdate(updateData).SetUpsert(true) + dataList = append(dataList, models) + } + + applogger.Debug("update data info:%v", dataList) + + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.StockList, dataList); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + return + } + } +} + +// U本位数据 +func UpdateContractKline(period string) { + for _, val := range dictionary.ContractCodeList { + applogger.Info("UpdateContractKline") + result := LinearKlineRes{} + bodyStr, err := internal.HttpGet(fmt.Sprintf("https://api.hbdm.com/linear-swap-ex/market/history/kline?contract_code=%s&period=%s&size=2000", val, period)) + applogger.Info(bodyStr) + if err != nil { + applogger.Error("Failed to query data:%v", err) + continue + } + if err = json.Unmarshal([]byte(bodyStr), &result); err != nil { + applogger.Error("Unmarshal err: %v---%v", err) + continue + } + var dataList []mongo.WriteModel + for _, eodValue := range result.Data { + open := eodValue.Open.String() + high := eodValue.High.String() + low := eodValue.Low.String() + close := eodValue.Close.String() + vol := eodValue.Vol.String() + amount := eodValue.Amount.String() + count := eodValue.Count.String() + tradeTurnover := eodValue.TradeTurnover.String() + //applogger.Info("data: ", eodValue) + filter := bson.M{"code": bson.M{"$eq": eodValue.ID}, "channel": bson.M{"$eq": result.Ch}} + update := bson.D{{"$set", bson.D{ + {"channel", result.Ch}, + {"timestamp", eodValue.ID}, + {"code", eodValue.ID}, + {"open", open}, + {"high", high}, + {"low", low}, + {"close", close}, + {"vol", vol}, + {"amount", amount}, + {"count", count}, + {"trade_turnover", tradeTurnover}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.GetContractKLineTableName(period), dataList); err != nil { + applogger.Error("ContractKline MgoInsertMany err:%v", err) + } + } + } +} + +// 现货 +func UpdateSpotKline(period string) { + for _, val := range dictionary.Symbol { + result := SpotKlineRes{} + bodyStr, err := internal.HttpGet(fmt.Sprintf("https://api.huobi.pro/market/history/kline?period=%s&size=2000&symbol=%susdt", period, val)) + if err != nil { + applogger.Error("Failed to query data:%v", err) + time.Sleep(2 * time.Second) + continue + } + if err = json.Unmarshal([]byte(bodyStr), &result); err != nil { + applogger.Error("Unmarshal err: %v---%v", err) + continue + } + var dataList []mongo.WriteModel + for _, eodValue := range result.Data { + open := eodValue.Open.String() + high := eodValue.High.String() + low := eodValue.Low.String() + close := eodValue.Close.String() + vol := eodValue.Vol.String() + amount := eodValue.Amount.String() + count := eodValue.Count.String() + applogger.Info("data: ", eodValue) + filter := bson.M{"code": bson.M{"$eq": eodValue.ID}, "channel": bson.M{"$eq": result.Ch}} + update := bson.D{{"$set", bson.D{ + {"channel", result.Ch}, + {"timestamp", eodValue.ID}, + {"code", eodValue.ID}, + {"open", open}, + {"high", high}, + {"low", low}, + {"close", close}, + {"vol", vol}, + {"amount", amount}, + {"count", count}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.GetStockKLineTableName(period), dataList); err != nil { + applogger.Error(" SpotKline MgoInsertMany err:%v", err) + } + } + } +} + +func UpdatePriceKline(period string) { + for _, val := range dictionary.ContractCodeList { + result := SpotKlineRes{} + bodyStr, err := internal.HttpGet(fmt.Sprintf("https://api.hbdm.com/index/market/history/linear_swap_mark_price_kline?contract_code=%s&period=%s&size=2000", val, period)) + if err != nil { + applogger.Error("Failed to query data:%v", err) + time.Sleep(2 * time.Second) + continue + } + if err = json.Unmarshal([]byte(bodyStr), &result); err != nil { + applogger.Error("Unmarshal err: %v---%v", err) + continue + } + var dataList []mongo.WriteModel + for _, eodValue := range result.Data { + open := eodValue.Open.String() + high := eodValue.High.String() + low := eodValue.Low.String() + close := eodValue.Close.String() + vol := eodValue.Vol.String() + amount := eodValue.Amount.String() + count := eodValue.Count.String() + applogger.Info("data: ", eodValue) + filter := bson.M{"code": bson.M{"$eq": eodValue.ID}, "channel": bson.M{"$eq": result.Ch}} + update := bson.D{{"$set", bson.D{ + {"channel", result.Ch}, + {"timestamp", eodValue.ID}, + {"code", eodValue.ID}, + {"open", open}, + {"high", high}, + {"low", low}, + {"close", close}, + {"vol", vol}, + {"amount", amount}, + {"count", count}, + {"trade_turnover", eodValue.TradeTurnover}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.GetContractPriceKLineTableName(period), dataList); err != nil { + applogger.Error("PriceKline info err:%v", err) + } + } + } +} + +// 现货实时入库 +func UpdateWsMgo(result market.SubscribeCandlestickResponse) { + if result.Tick.Id <= 0 { + applogger.Error("ws data is null %v", result) + return + } + var dataList []mongo.WriteModel + open := result.Tick.Open.String() + high := result.Tick.High.String() + low := result.Tick.Low.String() + close := result.Tick.Close.String() + vol := result.Tick.Vol.String() + amount := result.Tick.Amount.String() + //applogger.Info("data: ", result.Tick) + filter := bson.M{"code": bson.M{"$eq": result.Tick.Id}, "channel": bson.M{"$eq": result.Channel}} + update := bson.D{{"$set", bson.D{ + {"channel", result.Channel}, + {"timestamp", result.Tick.Id}, + {"code", result.Tick.Id}, + {"open", open}, + {"high", high}, + {"low", low}, + {"close", close}, + {"vol", vol}, + {"amount", amount}, + {"is_ba", result.Tick.IsBa}, + {"count", result.Tick.Count}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + ch := strings.Split(result.Channel, ".") + if len(ch) == 0 { + applogger.Error("ch is null") + return + } + period := ch[len(ch)-1] + tableName := data.GetStockKLineTableName(period) + if tableName == "" { + applogger.Error("table info is null") + return + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(tableName, dataList); err != nil { + applogger.Error("ContractKline MgoInsertMany err:%v", err) + } + } +} + +// 现货实时入库 测试 +func UpdateWsMgoTest(result market.SubscribeCandlestickResponse) { + if result.Tick.Id <= 0 { + applogger.Error("ws data is null %v", result) + return + } + var dataList []mongo.WriteModel + open := result.Tick.Open.String() + high := result.Tick.High.String() + low := result.Tick.Low.String() + close := result.Tick.Close.String() + vol := result.Tick.Vol.String() + amount := result.Tick.Amount.String() + applogger.Info("data: ", result.Tick) + filter := bson.M{"code": bson.M{"$eq": result.Tick.Id}, "channel": bson.M{"$eq": result.Channel}} + update := bson.D{{"$set", bson.D{ + {"channel", result.Channel}, + {"timestamp", result.Tick.Id}, + {"code", result.Tick.Id}, + {"open", open}, + {"high", high}, + {"low", low}, + {"close", close}, + {"vol", vol}, + {"amount", amount}, + {"is_ba", result.Tick.IsBa}, + {"count", result.Tick.Count}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + ch := strings.Split(result.Channel, ".") + if len(ch) == 0 { + applogger.Error("ch is null") + return + } + period := ch[len(ch)-1] + tableName := data.GetStockKLineTestTableName(period) + if tableName == "" { + applogger.Error("table info is null") + return + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(tableName, dataList); err != nil { + applogger.Error("ContractKline MgoInsertMany err:%v", err) + } + } +} + +// 合约测试 +func UpdateSubscribeCtKlineTest(result market.SubscribeCtKlineResponse) { + if result.Tick.Id <= 0 { + applogger.Error("ws data is null %v", result) + return + } + var dataList []mongo.WriteModel + open := result.Tick.Open.String() + high := result.Tick.High.String() + low := result.Tick.Low.String() + close := result.Tick.Close.String() + vol := result.Tick.Vol.String() + amount := result.Tick.Amount.String() + applogger.Info("data: ", result.Tick) + filter := bson.M{"code": bson.M{"$eq": result.Tick.Id}, "channel": bson.M{"$eq": result.Channel}} + update := bson.D{{"$set", bson.D{ + {"channel", result.Channel}, + {"timestamp", result.Tick.Id}, + {"code", result.Tick.Id}, + {"open", open}, + {"high", high}, + {"low", low}, + {"close", close}, + {"vol", vol}, + {"amount", amount}, + {"count", result.Tick.Count.String()}, + {"trade_turnover", result.Tick.Rrade_Turnover.String()}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + ch := strings.Split(result.Channel, ".") + if len(ch) == 0 { + applogger.Error("ch is null") + return + } + period := ch[len(ch)-1] + tableName := data.GetContractKLineTestTableName(period) + if tableName == "" { + applogger.Error("table info is null") + return + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(tableName, dataList); err != nil { + applogger.Error("ContractKline MgoInsertMany err:%v", err) + } + } +} + +// 合约实时入库 +func UpdateSubscribeCtKline(result market.SubscribeCtKlineResponse) { + if result.Tick.Id <= 0 { + applogger.Error("ws data is null %v", result) + return + } + //fmt.Println("kline ",result) + var dataList []mongo.WriteModel + open := result.Tick.Open.String() + high := result.Tick.High.String() + low := result.Tick.Low.String() + close := result.Tick.Close.String() + vol := result.Tick.Vol.String() + amount := result.Tick.Amount.String() + //applogger.Info("data: ", result.Tick) + filter := bson.M{"code": bson.M{"$eq": result.Tick.Id}, "channel": bson.M{"$eq": result.Channel}} + update := bson.D{{"$set", bson.D{ + {"channel", result.Channel}, + {"timestamp", result.Tick.Id}, + {"code", result.Tick.Id}, + {"open", open}, + {"high", high}, + {"low", low}, + {"close", close}, + {"vol", vol}, + {"amount", amount}, + {"count", result.Tick.Count.String()}, + {"trade_turnover", result.Tick.Rrade_Turnover.String()}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + ch := strings.Split(result.Channel, ".") + if len(ch) == 0 { + applogger.Error("ch is null") + return + } + period := ch[len(ch)-1] + tableName := data.GetContractKLineTableName(period) + if tableName == "" { + applogger.Error("table info is null") + return + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(tableName, dataList); err != nil { + applogger.Error("ContractKline MgoInsertMany err:%v", err) + } + } +} + +// ShareData Obtaining the closing price of Malaysian stocks through time【ws.eodhistoricaldata.com】 +func ShareData(code, exchange, yesterday, before string) ([]stock.EodTimeMessage, error) { + model := fmt.Sprintf("%v.%v", code, exchange) + from := before + to := yesterday + urlHttp := fmt.Sprintf("https://%v/api/eod/%v?api_token=%v&fmt=json&period=D&order=d&from=%v&to=%v", + config.Config.ShareGather.FinancialHost, model, config.Config.ShareGather.FinancialKey, from, to) + + //applogger.Debug("UrlHttp info: %v", urlHttp) + + bodyStr, err := internal.HttpGet(urlHttp) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return nil, err + } + + var eodModel []stock.EodTimeMessage + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("Unmarshal err: %v---%v", code, err) + return nil, err + } + + return eodModel, nil +} + +// 获取给定时间段内该代码的收盘价 +func PreviousClose(code string) (stock.PreviousCloseResponse, error) { + var eodModel stock.PreviousCloseResponse + url := fmt.Sprintf("https://%s/v2/aggs/ticker/%s/prev?adjusted=true&apiKey=%s", config.Config.ShareGather.PolygonHost, code, config.Config.ShareGather.PolygonKey) + applogger.Debug("UrlHttp info: %v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return eodModel, err + } + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("Unmarshal err: %v---%v", code, err) + return eodModel, err + } + return eodModel, nil +} + +// 通过交易查询股票对应的交易所 +func TradesTape(code string) (stock.TypeTradesResponse, error) { + var eodModel stock.TypeTradesResponse + url := fmt.Sprintf("https://%s/v3/trades/%s?apiKey=%s&limit=1", config.Config.ShareGather.PolygonHost, code, config.Config.ShareGather.PolygonKey) + applogger.Debug("UrlHttp info: %v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return eodModel, err + } + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("Unmarshal err: %v---%v", code, err) + return eodModel, err + } + return eodModel, nil +} + +// TypeCheck Character determination +func TypeCheck(m interface{}) string { + switch m.(type) { + case types.Nil: + return "" + case string: + return m.(string) + default: + return "" + } +} + +// UpdateStockKLSEBak 马股列表闭盘数据更新--已废弃 +func UpdateStockKLSEBak() { + var dataList []mongo.WriteModel + yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") + before := time.Now().AddDate(0, 0, -2).Format("2006-01-02") + + // TODO: 更改为从mongoDB中查数据源 + var shareModel []stock.StockShare + url := fmt.Sprintf("https://%v/api/exchange-symbol-list/KLSE?api_token=%v&fmt=json", config.Config.ShareGather.FinancialHost, config.Config.ShareGather.FinancialKey) + applogger.Debug("url info:%v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + if err := json.Unmarshal([]byte(bodyStr), &shareModel); err != nil { + applogger.Error("Failed to parse stock list information:%v", err) + return + } + // 赋值更新的数据 + for _, value := range shareModel { + applogger.Debug("share types info: %v", value) + + model := fmt.Sprintf("%v.KLSE", value.Code) + from := before + to := yesterday + urlHttp := fmt.Sprintf("https://%v/api/eod/%v?api_token=%v&fmt=json&period=D&order=d&from=%v&to=%v", + config.Config.ShareGather.FinancialHost, model, config.Config.ShareGather.FinancialKey, from, to) + + applogger.Debug("UrlHttp info: %v", urlHttp) + + bodyStr, err = internal.HttpGet(urlHttp) + if err != nil { + applogger.Error("Failed to query data:%v", err) + } + var eodModel []stock.EodTimeMessage + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("Unmarshal err: %v---%v", value.Code, err) + } + + for _, eo := range eodModel { + switch eo.Date { + case yesterday: + value.YesterdayClose = eo.Close.String() + case before: + value.BeforeClose = eo.Close.String() + default: + } + } + + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}} + update := bson.D{{"$set", bson.D{ + {"Code", value.Code}, + {"Name", value.Name}, + {"Country", value.Country}, + {"Exchange", value.Exchange}, + {"Currency", value.Currency}, + {"Type", value.Type}, + {"Isin", value.Isin}, + {"YesterdayClose", value.YesterdayClose}, + {"BeforeClose", value.BeforeClose}}}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + + applogger.Debug("update data info:%v", dataList) + + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.StockList, dataList); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + return + } + } +} diff --git a/internal/data/business/mgomanager.go b/internal/data/business/mgomanager.go new file mode 100644 index 0000000..93bf804 --- /dev/null +++ b/internal/data/business/mgomanager.go @@ -0,0 +1,67 @@ +package business + +import ( + "wss-pool/logging/applogger" +) + +func RunHBData(checkData string) { + HbMarketSpots(checkData) + HbContract(checkData) + ShareMarket(checkData) +} + +// 现货行情数据 +func HbMarketSpots(checkData string) { + switch checkData { + case "subscribeDepth": // 市场深度行情数据 8863 + MgoSubscribeDepth() + case "subscribeLevelMbp": // 市场深度MBP行情数据(增量推送)(150挡) 8864 + MgoSubscribeLevelMbp() + case "subscribeFullMbp": // 市场深度MBP行情数据(全量推送) 8865 + MgoSubscribeFullMbp() + case "subscribeSubMbp": // 市场深度MBP行情数据(增量推送) 8866 + MgoSubscribeSubMbp() + case "subscribeBbo": // 买一卖一逐笔行情 8867 + MgoSubscribeBbo() + case "subscribeKLine": // K线数据 8868 + MgoSubscribeKLine() + case "subscribeTrade": // 成交明细 8869 + MgoSubscribeTrade() + case "subscribeLast24h": // 市场概要 8847 + MgoSubscribeLast24h() + case "subscribeTicker": // 聚合行情(Ticker) 8848 + MgoSubscribeTicker() + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 合约行情数据 +func HbContract(checkData string) { + switch checkData { + case "subscribeCtKline": // k线数据 8841 + MgoSubscribeCtKline() + case "subscribeCtDepth": // 深度信息 8842 + MgoSubscribeCtDepth() + case "subscribeCtAddDepth": // 新增深度信息 8843 + MgoSubscribeCtAddDepth() + case "subscribeCtBbo": // 买一卖一行情数据 8844 + MgoSubscribeCtBbo() + case "subscribeCtDetail": // 合约详情数据 8845 + MgoSubscribeCtDetail() + case "subscribeCtTradeDetail": // 合约贸易详情数据 8846 + MgoSubscribeCtTradeDetail() + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} + +// 股票行情数据 +func ShareMarket(checkData string) { + switch checkData { + case "usShare": // US 8849 + ShareUsData() + default: + applogger.Info("Please select the data source that needs to be connected......") + } +} diff --git a/internal/data/business/mgoshareus.go b/internal/data/business/mgoshareus.go new file mode 100644 index 0000000..f42425e --- /dev/null +++ b/internal/data/business/mgoshareus.go @@ -0,0 +1,86 @@ +package business + +import ( + "encoding/json" + "fmt" + "github.com/gorilla/websocket" + "go.mongodb.org/mongo-driver/bson" + "strings" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/stock" +) + +// ShareUsData TODO: 美股数据采集 修改为分布式多节点 +func ShareUsData() { + url := fmt.Sprintf("https://%v/api/exchange-symbol-list/us?api_token=%v&fmt=json", config.Config.ShareGather.FinancialHost, config.Config.ShareGather.FinancialKey) + applogger.Debug("select info:%v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("HttpGet err:%v", err) + return + } + var shareModel []stock.StockShare + if err := json.Unmarshal([]byte(bodyStr), &shareModel); err != nil { + applogger.Error("Unmarshal err:%v", err) + return + } + + for _, value := range shareModel { + applogger.Info("select data info:%v", value) + go func() { + url := fmt.Sprintf("wss://%v/ws/%v?api_token=%v", config.Config.ShareGather.FinancialWsUs, "us", config.Config.ShareGather.FinancialKey) + conn, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("链接wss服务器失败:%v", err) + return + } + defer conn.Close() + + subscribe := fmt.Sprintf("{\"action\": \"subscribe\", \"symbols\": \"%v\"}", value.Code) + if err := conn.WriteMessage(websocket.TextMessage, []byte(subscribe)); err != nil { + applogger.Error("send connSub WriteMessage err::%v", err) + return + } + for { + _, msg, err := conn.ReadMessage() + if err != nil { + applogger.Error("err info:%v", err) + return + } + applogger.Info("Subscribe date:%v", string(msg)) + if !strings.Contains(string(msg), "{\"status_code\":200,\"message\":\"Authorized\"}") { + go func() { + // 持久化数据信息 + var msgC stock.RealTimeMessage + if err := bson.Unmarshal(msg, &msgC); err != nil { + applogger.Error("Insert info err:%v", err) + return + } + applogger.Debug("date info:%v", msgC) + + var c = []string{} + for _, vc := range msgC.C { + c = append(c, vc.String()) + } + cxt := bson.D{ + {"S", msgC.S}, + {"Dp", msgC.Dp}, + {"C", c}, + {"T", msgC.T}, + {"Ms", msgC.Ms}, + {"V", msgC.V}, + {"P", msgC.P.String()}, + } + if err := data.MgoInsertOne(data.StockUs, cxt); err != nil { + applogger.Error("Insert info err:%v", err) + return + } + }() + } + } + }() + } +} diff --git a/internal/data/business/mgospots.go b/internal/data/business/mgospots.go new file mode 100644 index 0000000..1b7de70 --- /dev/null +++ b/internal/data/business/mgospots.go @@ -0,0 +1,381 @@ +package business + +import ( + "fmt" + "go.mongodb.org/mongo-driver/bson" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/hbwssclient/marketwssclient" + "wss-pool/pkg/model" + "wss-pool/pkg/model/market" +) + +// 市场深度行情数据 +func MgoSubscribeDepth() { + symbolList := model.SymbolListString(dictionary.Depth) + client := new(marketwssclient.DepthWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, value := range symbolList { + for _, vue := range value { + client.Request(key, vue, config.Config.HbGather.HbSubUids) + client.Subscribe(key, vue, config.Config.HbGather.HbSubUids) + } + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeDepthResponse) + if ok { + if &depthResponse != nil { + + applogger.Info("subscribeDepth data,ServersId:%v,Content:%v-%v", depthResponse.Channel, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, value := range symbolList { + for _, vue := range value { + client.UnSubscribe(key, vue, config.Config.HbGather.HbSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// 市场深度MBP行情数据(增量推送)(150挡) +func MgoSubscribeLevelMbp() { + symbolList := model.SymbolListString([]string{}) + client := new(marketwssclient.MarketByPriceWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, _ := range symbolList { + client.Request(key, config.Config.HbGather.HbSubUids) + client.Subscribe(key, config.Config.HbGather.HbSubUids) + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeMarketByPriceResponse) + if ok { + if &depthResponse != nil { + + applogger.Info("subscribeLevelMbp data,ServersId:%v,Content:%v-%v", depthResponse.Channel, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, _ := range symbolList { + client.UnSubscribe(key, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// 市场深度MBP行情数据(全量推送) +func MgoSubscribeFullMbp() { + symbolList := model.SymbolListInt(dictionary.LevelsRefresh) + client := new(marketwssclient.MarketByPriceWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, value := range symbolList { + for _, vue := range value { + client.SubscribeFull(key, vue, config.Config.HbGather.HbSubUids) + } + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeMarketByPriceResponse) + if ok { + if &depthResponse != nil { + + applogger.Info("subscribeFullMbp data,ServersId:%v,Content:%v-%v", depthResponse.Channel, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, value := range symbolList { + for _, vue := range value { + client.UnSubscribeFull(key, vue, config.Config.HbGather.HbSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// 市场深度MBP行情数据(增量推送) +func MgoSubscribeSubMbp() { + symbolList := model.SymbolListInt(dictionary.LevelsMbp) + client := new(marketwssclient.MarketByPriceTickWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, value := range symbolList { + for _, vue := range value { + client.Request(key, vue, config.Config.HbGather.HbSubUids) + client.Subscribe(key, vue, config.Config.HbGather.HbSubUids) + } + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeMarketByPriceResponse) + if ok { + if &depthResponse != nil { + + applogger.Info("subscribeSubMbp data,ServersId:%v,Sender:%v,Content:%v-%v", depthResponse.Channel, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, value := range symbolList { + for _, vue := range value { + client.UnSubscribe(key, vue, config.Config.HbGather.HbSubUids) + } + } + + client.Close() + applogger.Info("Client closed") +} + +// 买一卖一逐笔行情 +func MgoSubscribeBbo() { + symbolList := model.SymbolListString([]string{}) + client := new(marketwssclient.BestBidOfferWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, _ := range symbolList { + client.Subscribe(key, config.Config.HbGather.HbSubUids) + } + }, + func(resp interface{}) { + bboResponse, ok := resp.(market.SubscribeBestBidOfferResponse) + if ok { + if bboResponse.Tick != nil { + + } + applogger.Info("subscribeBbo data,ServersId:%v,Sender:%v,Content:%v-%v", bboResponse.Channel, bboResponse.Tick, nil) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, _ := range symbolList { + client.UnSubscribe(key, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Connection closed") +} + +// K线数据 +func MgoSubscribeKLine() { + symbolList := model.SymbolListString(dictionary.TimeCycle) + client := new(marketwssclient.CandlestickWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for symbol, period := range symbolList { + for _, value := range period { + client.Subscribe(symbol, value, config.Config.HbGather.HbSubUids) + } + } + }, + func(response interface{}) { + resp, ok := response.(market.SubscribeCandlestickResponse) + if ok { + if &resp != nil { + if resp.Tick != nil || resp.Data != nil { + go func() { + Channel := resp.Channel + Tick := resp.Tick + Data := resp.Data + Timestamp := resp.Timestamp + tick := bson.D{ + {"Id", Tick.Id}, + {"Close", Tick.Close.String()}, + {"Low", Tick.Low.String()}, + {"Amount", Tick.Amount.String()}, + {"High", Tick.High.String()}, + {"Count", Tick.Count}, + {"Open", Tick.Open.String()}, + {"Vol", Tick.Vol.String()}, + } + cxt := bson.D{ + {"Channel", Channel}, + {"Tick", tick}, + {"Data", Data}, + {"Timestamp", Timestamp}, + } + table := internal.CheckKLineTable(Channel) + applogger.Debug("数据库表名称:%v", table) + if err := data.MgoInsertOne(table, cxt); err != nil { + applogger.Error("writeKLine err:%v", err) + return + } + }() + } + applogger.Info("subscribeKLine data Key:%v,time:%v,tick:%v,data:%v", resp.Channel, resp.Timestamp, resp.Tick, resp.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, period := range symbolList { + for _, value := range period { + client.UnSubscribe(symbol, value, config.Config.HbGather.HbSubUids) + } + } + + client.UnSubscribe("btcusdt", "1min", config.Config.HbGather.HbSubUids) + + client.Close() + applogger.Info("Client closed") +} + +// 成交明细 +func MgoSubscribeTrade() { + symbolList := model.SymbolListString([]string{}) + client := new(marketwssclient.TradeWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, _ := range symbolList { + client.Request(key, config.Config.HbGather.HbSubUids) + client.Subscribe(key, config.Config.HbGather.HbSubUids) + } + }, + func(resp interface{}) { + depthResponse, ok := resp.(market.SubscribeTradeResponse) + if ok { + if &depthResponse != nil { + + applogger.Info("subscribeTrade data,ServersId:%v,Sender:%v,Content:%v-%v", depthResponse.Channel, depthResponse.Tick, depthResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, _ := range symbolList { + client.UnSubscribe(key, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// 市场概要 +func MgoSubscribeLast24h() { + symbolList := model.SymbolListString([]string{}) + client := new(marketwssclient.Last24hCandlestickWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for key, _ := range symbolList { + client.Request(key, config.Config.HbGather.HbSubUids) + client.Subscribe(key, config.Config.HbGather.HbSubUids) + } + }, + func(resp interface{}) { + candlestickResponse, ok := resp.(market.SubscribeLast24hCandlestickResponse) + if ok { + if &candlestickResponse != nil { + + applogger.Info("subscribeLast24h data,ServersId:%v,Sender:%v,Content:%v-%v", candlestickResponse.Channel, candlestickResponse.Tick, candlestickResponse.Data) + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for key, _ := range symbolList { + client.UnSubscribe(key, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Client closed") +} + +// 聚合行情(Ticker) +func MgoSubscribeTicker() { + symbolList := model.SymbolListString([]string{}) + client := new(marketwssclient.TickerWebSocketClient).Init(config.Config.HbGather.HbHost) + client.SetHandler( + func() { + for symbol, _ := range symbolList { + client.Subscribe(symbol, config.Config.HbGather.HbSubUids) + } + }, + func(response interface{}) { + resp, ok := response.(market.TickerWebsocketResponse) + if ok { + if &resp != nil { + if resp.Tick != nil || resp.Data != nil { + + applogger.Info("subscribeTicker data,ServersId:%v,Sender:%v,Content:%v-%v", resp.Channel, resp.Tick, resp.Data) + } + } + } else { + applogger.Warn("Unknown response: %v", resp) + } + }) + + client.Connect(true) + + fmt.Println("Press ENTER to unsubscribe and stop...") + fmt.Scanln() + + for symbol, _ := range symbolList { + client.UnSubscribe(symbol, config.Config.HbGather.HbSubUids) + } + + client.Close() + applogger.Info("Client closed") +} diff --git a/internal/data/business/mgostockus.go b/internal/data/business/mgostockus.go new file mode 100644 index 0000000..e4cfff6 --- /dev/null +++ b/internal/data/business/mgostockus.go @@ -0,0 +1,331 @@ +package business + +import ( + "encoding/json" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/stock" +) + +/* +采集美股历史数据 +一、日终数据 +1、每日 daily +https://eodhistoricaldata.com/api/eod/0001.KLSE?api_token=647dd6744b94f4.20894198&period=d&fmt=json&from=2022-08-01&to=2023-06-15 +2、每周 weekly +https://eodhistoricaldata.com/api/eod/MCD.US?api_token=647dd6744b94f4.20894198&period=w&fmt=json&from=2022-08-01&to=2023-06-15 +3、每月 monthly +https://eodhistoricaldata.com/api/eod/MCD.US?api_token=647dd6744b94f4.20894198&period=m&fmt=json&from=2022-08-01&to=2023-06-15 + +二、日内数据 +1、1小时 hour +https://eodhistoricaldata.com/api/intraday/AAPL.US?api_token=647dd6744b94f4.20894198&interval=1h&fmt=json&from=1659283200&to=1686815102 +2、5分钟 fiveminutes +https://eodhistoricaldata.com/api/intraday/AAPL.US?api_token=647dd6744b94f4.20894198&interval=5m&fmt=json&from=1659283200&to=1686815102 +3、1分钟 oneminute +https://eodhistoricaldata.com/api/intraday/AAPL.US?api_token=647dd6744b94f4.20894198&interval=1m&fmt=json +*/ +func StockUsDaily() { + filter := bson.M{"Country": "USA", "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + applogger.Debug("code info: %v", code) + codeUS := fmt.Sprintf("%v.US", code) + // https://eodhistoricaldata.com/api/eod/0001.KLSE?api_token=647dd6744b94f4.20894198&period=d&fmt=json&from=2022-08-01&to=2023-06-15 + url := fmt.Sprintf("https://%v/api/eod/%v?api_token=%v&period=d&fmt=json&from=2022-08-01&to=2023-06-17", + config.Config.ShareGather.FinancialHost, codeUS, config.Config.ShareGather.FinancialKey) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + fmt.Println(url) + var eodModel []stock.EodData + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return + } + applogger.Info("data info: %v", eodModel) + + var bsonEod []interface{} + for _, eodValue := range eodModel { + bsonEod = append(bsonEod, bson.D{ + {"code", code}, + {"date", eodValue.Date}, + {"open", eodValue.Open}, + {"high", eodValue.High}, + {"low", eodValue.Low}, + {"close", eodValue.Close}, + {"adjusted_close", eodValue.AdjustedClose}, + {"volume", eodValue.Volume}, + }) + } + + if len(bsonEod) > 0 { + if err := data.MgoInsertMany(data.StockUsDaily, bsonEod); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + return + } + } + } +} + +func StockUsWeekly() { + filter := bson.M{"Country": "USA", "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + applogger.Debug("code info: %v", code) + codeUS := fmt.Sprintf("%v.US", code) + // https://eodhistoricaldata.com/api/eod/MCD.US?api_token=647dd6744b94f4.20894198&period=w&fmt=json&from=2022-08-01&to=2023-06-15 + url := fmt.Sprintf("https://%v/api/eod/%v?api_token=%v&period=w&fmt=json&from=2022-08-01&to=2023-06-17", + config.Config.ShareGather.FinancialHost, codeUS, config.Config.ShareGather.FinancialKey) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + + var eodModel []stock.EodData + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return + } + applogger.Info("data info: %v", eodModel) + var bsonEod []interface{} + for _, eodValue := range eodModel { + bsonEod = append(bsonEod, bson.D{ + {"code", code}, + {"date", eodValue.Date}, + {"open", eodValue.Open}, + {"high", eodValue.High}, + {"low", eodValue.Low}, + {"close", eodValue.Close}, + {"adjusted_close", eodValue.AdjustedClose}, + {"volume", eodValue.Volume}, + }) + } + + if len(bsonEod) > 0 { + if err := data.MgoInsertMany(data.StockUsWeekly, bsonEod); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + return + } + } + } +} + +func StockUsMonthly() { + filter := bson.M{"Country": "USA", "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + applogger.Debug("code info: %v", code) + codeUs := fmt.Sprintf("%v.US", code) + // https://eodhistoricaldata.com/api/eod/MCD.US?api_token=647dd6744b94f4.20894198&period=m&fmt=json&from=2022-08-01&to=2023-06-17 + url := fmt.Sprintf("https://%v/api/eod/%v?api_token=%v&period=m&fmt=json&from=2022-08-01&to=2023-06-17", + config.Config.ShareGather.FinancialHost, codeUs, config.Config.ShareGather.FinancialKey) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + + var eodModel []stock.EodData + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return + } + applogger.Info("data info: %v", eodModel) + var bsonEod []interface{} + for _, eodValue := range eodModel { + bsonEod = append(bsonEod, bson.D{ + {"code", code}, + {"date", eodValue.Date}, + {"open", eodValue.Open}, + {"high", eodValue.High}, + {"low", eodValue.Low}, + {"close", eodValue.Close}, + {"adjusted_close", eodValue.AdjustedClose}, + {"volume", eodValue.Volume}, + }) + } + if len(bsonEod) > 0 { + if err := data.MgoInsertMany(data.StockUsMonthly, bsonEod); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + return + } + } + } +} + +func StockUsHour() { + filter := bson.M{"Country": "USA", "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + applogger.Debug("code info: %v", code) + codeUs := fmt.Sprintf("%v.US", code) + // https://eodhistoricaldata.com/api/intraday/AAPL.US?api_token=647dd6744b94f4.20894198&interval=1h&fmt=json&from=1659283200&to=1686759266 + url := fmt.Sprintf("https://%v/api/intraday/%v?api_token=%v&interval=1h&fmt=json&from=1659283200&to=1686759266", + config.Config.ShareGather.FinancialHost, codeUs, config.Config.ShareGather.FinancialKey) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + + var eodModel []stock.IntraDayData + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return + } + applogger.Info("data info: %v", eodModel) + var bsonEod []interface{} + for _, eodValue := range eodModel { + bsonEod = append(bsonEod, bson.D{ + {"code", code}, + {"timestamp", eodValue.Timestamp}, + {"gmtoffset", eodValue.Gmtoffset}, + {"datetime", eodValue.Datetime}, + {"open", eodValue.Open}, + {"high", eodValue.High}, + {"low", eodValue.Low}, + {"close", eodValue.Close}, + {"volume", eodValue.Volume}, + }) + } + + if len(bsonEod) > 0 { + if err := data.MgoInsertMany(data.StockUsHour, bsonEod); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + return + } + } + } +} + +func StockUsFiveMinutes() { + filter := bson.M{"Country": "USA", "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + applogger.Debug("code info: %v", code) + codeUs := fmt.Sprintf("%v.US", code) + // https://eodhistoricaldata.com/api/intraday/AAPL.US?api_token=647dd6744b94f4.20894198&interval=5m&fmt=json&from=1659283200&to=1686815102 + url := fmt.Sprintf("https://%v/api/intraday/%v?api_token=%v&interval=5m&fmt=json&from=1659283200&to=1686759266", + config.Config.ShareGather.FinancialHost, codeUs, config.Config.ShareGather.FinancialKey) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + + var eodModel []stock.IntraDayData + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return + } + applogger.Info("data info: %v", eodModel) + var bsonEod []interface{} + for _, eodValue := range eodModel { + bsonEod = append(bsonEod, bson.D{ + {"code", code}, + {"timestamp", eodValue.Timestamp}, + {"gmtoffset", eodValue.Gmtoffset}, + {"datetime", eodValue.Datetime}, + {"open", eodValue.Open}, + {"high", eodValue.High}, + {"low", eodValue.Low}, + {"close", eodValue.Close}, + {"volume", eodValue.Volume}, + }) + } + if len(bsonEod) > 0 { + if err := data.MgoInsertMany(data.StockUsFiveMinutes, bsonEod); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + return + } + } + } +} + +func StockUsOneMinute() { + filter := bson.M{"Country": "USA", "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + dateList, err := data.MgoFind(data.StockList, filter) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return + } + + for _, value := range dateList.([]primitive.M) { + code := TypeCheck(value["Code"]) + applogger.Debug("code info: %v", code) + codeUs := fmt.Sprintf("%v.US", code) + // https://eodhistoricaldata.com/api/intraday/AAPL.US?api_token=647dd6744b94f4.20894198&interval=1m&fmt=json + url := fmt.Sprintf("https://%v/api/intraday/%v?api_token=%v&interval=1m&fmt=json", + config.Config.ShareGather.FinancialHost, codeUs, config.Config.ShareGather.FinancialKey) + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("Failed to query data:%v", err) + return + } + + var eodModel []stock.IntraDayData + if err = json.Unmarshal([]byte(bodyStr), &eodModel); err != nil { + applogger.Error("eodModel json Unmarshal err: %v", err) + return + } + applogger.Info("data info: %v", eodModel) + var bsonEod []interface{} + for _, eodValue := range eodModel { + bsonEod = append(bsonEod, bson.D{ + {"code", code}, + {"timestamp", eodValue.Timestamp}, + {"gmtoffset", eodValue.Gmtoffset}, + {"datetime", eodValue.Datetime}, + {"open", eodValue.Open}, + {"high", eodValue.High}, + {"low", eodValue.Low}, + {"close", eodValue.Close}, + {"volume", eodValue.Volume}, + }) + } + if len(bsonEod) > 0 { + if err := data.MgoInsertMany(data.StockUsOneMinute, bsonEod); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + return + } + } + } +} diff --git a/internal/data/gorm.go b/internal/data/gorm.go new file mode 100644 index 0000000..eabeba7 --- /dev/null +++ b/internal/data/gorm.go @@ -0,0 +1,24 @@ +package data + +import ( + "fmt" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "sync" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var WebGorm *gorm.DB + +type DBConnect struct { + sync.Mutex +} + +func InitGorm(config model.Bourse) { + var err error + if WebGorm, err = gorm.Open(mysql.Open(config.Datasource), &gorm.Config{Logger: logger.Default.LogMode(logger.Info)}); err != nil { + applogger.Error(fmt.Sprintf("连接%s数据库失败 err: %s", config.Datasource, err.Error())) + } +} diff --git a/internal/data/index.go b/internal/data/index.go new file mode 100644 index 0000000..328d474 --- /dev/null +++ b/internal/data/index.go @@ -0,0 +1,501 @@ +package data + +import ( + "context" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "wss-pool/dictionary" + "wss-pool/logging/applogger" +) + +const ( + StockIndexList = "stockIndexList" + StockUs = "stockListUs" // US Real time + StockUsDaily = "stockUsDaily" // Us daily data + StockUsWeekly = "stockUsWeekly" // Us Weekly data + StockUsMonthly = "stockUsMonthly" // Us Monthly data + StockUsHour = "stockUsHour" // Us Hourly data + StockUsFiveMinutes = "stockUsFiveMinutes" // Us Data every five minutes + StockUsOneMinute = "stockUsOneMinute" // Us Data every minutes + StockNews = "stockNews" + OptionList = "optionList" // list + ForexList = "forexList" // forex list + ForexListBak = "forexListBak" // forex list + ForexTradeList = "forexTradeList" // forex trade list + ForexKLine = "forexKLine" // forex kline +) + +var StockList string + +// Create_stockList_index New Stock List Index +func Create_stockList_index() { + c := MgoConnect(StockList) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"Code", -1}, + }}, + {Keys: bson.D{ + {"Symbol", -1}, + }}, + {Keys: bson.D{ + {"Exchange", -1}, + }}, + {Keys: bson.D{ + {"Vol", -1}, + }}, + {Keys: bson.D{ + {"Source", -1}, + }}, + {Keys: bson.D{ + {"Country", -1}, + }}, + {Keys: bson.D{ + {"NumericCode", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} + +func Create_stockIndixList_index() { + c := MgoConnect(StockIndexList) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"Code", -1}, + }}, + {Keys: bson.D{ + {"Exchange", -1}, + }}, + {Keys: bson.D{ + {"Sort", -1}, + }}, + {Keys: bson.D{ + {"State", -1}, + }}, + {Keys: bson.D{ + {"Country", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} + +func CreateOptionList() { + c := MgoConnect(OptionList) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"Code", -1}, + }}, + {Keys: bson.D{ + {"Percent", -1}, + }}, + {Keys: bson.D{ + {"DateTime", -1}, + }}, + {Keys: bson.D{ + {"Country", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} + +func CreateOptionindex() { + for _, value := range dictionary.OptionCodeList { + tableName := GetOptionTableName(value) + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"code", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + tableName = GetOptionExpiryTableName(value) + c = MgoConnect(tableName) + indexModel = []mongo.IndexModel{ + {Keys: bson.D{ + {"code", -1}, + }}, + {Keys: bson.D{ + {"expiry", -1}, + }}, + {Keys: bson.D{ + {"expiry_date", -1}, + }}, + } + _, err = c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } +} + +// 外汇索引 +func CreateForexList() { + c := MgoConnect(ForexList) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"ticker", -1}, + }}, + {Keys: bson.D{ + {"updated", -1}, + }}, + {Keys: bson.D{ + {"day.o", -1}, + }}, + {Keys: bson.D{ + {"day.l", -1}, + }}, + {Keys: bson.D{ + {"day.h", -1}, + }}, + {Keys: bson.D{ + {"day.c", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} +func CreateForexListNew() { + c := MgoConnect(ForexListBak) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"code", -1}, + }}, + {Keys: bson.D{ + {"name", -1}, + }}, + {Keys: bson.D{ + {"category", -1}, + }}, + {Keys: bson.D{ + {"symbol", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} +func CreateForexTradeLis() { + // {"ev":"T","code":"NZDCAD","seq":"75468393","tick_time":"1732274869856","price":"0.81696","volume":"92500.00","turnover":"0.00000","trade_direction":0} + c := MgoConnect(ForexTradeList) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"code", -1}, + }}, + {Keys: bson.D{ + {"tick_time", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} +func CreateForexKLine() { + c := MgoConnect(ForexKLine) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"code", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} + +// Create_stockUs_index 美股实时数据索引 +func Create_stockUs_index() { + c := MgoConnect(StockUs) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"s", -1}, + }}, + {Keys: bson.D{ + {"se", -1}, + }}, + {Keys: bson.D{ + {"t", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} + +// Create_stockUs_index +func Create_SpotKline_index() { + for _, value := range dictionary.TimeCycle { + tableName := GetStockKLineTableName(value) + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"channel", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, + {Keys: bson.D{ + {"code", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } +} + +func Create_ContractKline_index() { + for _, value := range dictionary.ContractTime { + tableName := GetContractKLineTableName(value) + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"channel", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, + {Keys: bson.D{ + {"code", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } +} + +func Create_ContractPriceKline_index() { + for _, value := range dictionary.ContractPriceTime { + tableName := GetContractPriceKLineTableName(value) + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"channel", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, + {Keys: bson.D{ + {"code", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } +} + +func Create_Stock_News() { + c := MgoConnect(StockNews) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"code", -1}, + }}, + {Keys: bson.D{ + {"country", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } +} + +func Create_Stock_index() { + for _, value := range dictionary.StockCodeList { + tableName := GetStockTableName(value) + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"stock_code", -1}, + }}, + {Keys: bson.D{ + {"symbol", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, { + Keys: bson.D{ + {"price", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + //DAY K WEEK + for _, v := range dictionary.StockSouthAsiaListTime { + c := MgoConnect(fmt.Sprintf("%s%s", tableName, v)) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"stock_code", -1}, + }}, + {Keys: bson.D{ + {"symbol", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, + {Keys: bson.D{ + {"high_price", -1}, + }}, + {Keys: bson.D{ + {"low_price", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } + } +} + +func Create_SpockIndexKline() { + tableName := GetStockIndexTableName() + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"stock_code", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, { + Keys: bson.D{ + {"price", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + //DAY K WEEK + for _, v := range dictionary.StockSouthAsiaListTime { + c := MgoConnect(fmt.Sprintf("%s%s", tableName, v)) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"stock_code", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, + {Keys: bson.D{ + {"high_price", -1}, + }}, + {Keys: bson.D{ + {"low_price", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } +} + +func Create_StockUsList_index() { + for _, value := range dictionary.StockUsListTime { + tableName := GetStockUsTableName(value) + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"timestamp", -1}, + }}, + {Keys: bson.D{ + {"code", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } +} + +func Create_SpotKlineTest_index() { + for _, value := range dictionary.TimeCycle { + tableName := GetStockKLineTestTableName(value) + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"channel", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, + {Keys: bson.D{ + {"code", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } +} + +func Create_ContractKlineTest_index() { + for _, value := range dictionary.ContractTime { + tableName := GetContractKLineTestTableName(value) + c := MgoConnect(tableName) + indexModel := []mongo.IndexModel{ + {Keys: bson.D{ + {"channel", -1}, + }}, + {Keys: bson.D{ + {"timestamp", -1}, + }}, + {Keys: bson.D{ + {"code", -1}, + }}, + } + _, err := c.Indexes().CreateMany(context.TODO(), indexModel) + if err != nil { + applogger.Error("Failed to create index:%v", err) + return + } + } +} diff --git a/internal/data/mongo.go b/internal/data/mongo.go new file mode 100644 index 0000000..71f42ed --- /dev/null +++ b/internal/data/mongo.go @@ -0,0 +1,767 @@ +package data + +import ( + "context" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "log" + "os" + "sort" + "time" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var DataBase string +var mgoDb *mongo.Client +var MgoDbClientMap = map[string]*mongo.Client{} +var MgoDbToRedisMap = map[string]string{} + +type MongoTick struct { + Id string `bson:"_id" json:"id"` + Channel string `json:"channel"` + Amount string `json:"amount"` // 成交量 + Count interface{} `bson:"count" json:"count"` // 成交笔数 + Open string `json:"open"` // 开盘价 + Close string `json:"close"` // 收盘价(当K线为最晚的一根时,是最新成交价) + Low string `json:"low"` // 最低价 + High string `json:"high"` // 最高价 + Vol string `json:"vol"` // 成交额, 即 sum(每一笔成交价 * 该笔的成交量) + Timestamp int64 `json:"timestamp"` + TradeTurnover string `bson:"trade_turnover" json:"trade_turnover"` + Code int64 `json:"code"` +} + +func GetStockKLineTableName(period string) string { + return fmt.Sprintf("marketKline%v", period) +} + +func GetContractKLineTableName(period string) string { + return fmt.Sprintf("contractKline%v", period) +} + +func GetContractPriceKLineTableName(period string) string { + return fmt.Sprintf("contractPriceKline%v", period) +} + +func GetStockUsTableName(period string) string { + return fmt.Sprintf("stockUs%v", period) +} + +func GetStockKLineTestTableName(period string) string { + return fmt.Sprintf("marketKline%v_test", period) +} + +func GetContractKLineTestTableName(period string) string { + return fmt.Sprintf("contractKline%v_test", period) +} + +func GetStockTableName(period string) string { + return fmt.Sprintf("stock%v", period) +} + +func GetStockIndexTableName() string { + return fmt.Sprintf("stockIndex") +} + +func GetStockIndixKlineTableName(period string) string { + return fmt.Sprintf("stockIndex%s", period) +} + +func GetStockSouthAsiaTableName(stock, period string) string { + return fmt.Sprintf("stock%s%s", stock, period) +} + +func GetOptionTableName(country string) string { + return fmt.Sprintf("option%v", country) +} +func GetOptionExpiryTableName(country string) string { + return fmt.Sprintf("optionExpiry%v", country) +} +func GetStockList(stockListTable int) { + if stockListTable == 0 { + StockList = "stockListBak" + return + } + StockList = fmt.Sprintf("stockListBak%d", stockListTable) +} + +// Mgo_init +func Mgo_init(config model.Mongodb) { + mongodb := config.DbBase + GetStockList(config.Table) + fmt.Println(StockList) + clientOptions := options.Client().ApplyURI(fmt.Sprintf("mongodb://%v:%v@%v:%v/%v?ssl=false&authSource=admin", config.DbUser, config.Password, config.DbHost, config.DbPort, mongodb)) + client, err := mongo.Connect(context.TODO(), clientOptions) + if err != nil { + log.Printf("connect err: %v", err) + return + } + // Check the connection + err = client.Ping(context.TODO(), nil) + if err != nil { + log.Printf("test connect err: %v", err) + return + } + fmt.Println("Mongodb ok") + mgoDb = client + DataBase = mongodb + + // init mongo_index + Create_stockList_index() + Create_stockUs_index() + Create_stockIndixList_index() + //Create_StockUs_Daily() + //Create_StockUs_Weekly() + //Create_StockUs_Monthly() + //Create_StockUs_Hour() + //Create_StockUs_Five_Minutes() + //Create_StockUs_One_Minute() + Create_SpotKline_index() + Create_ContractKline_index() + Create_ContractPriceKline_index() + Create_StockUsList_index() + Create_Stock_index() + Create_SpockIndexKline() + Create_Stock_News() + //测试 + //Create_SpotKlineTest_index() + //Create_ContractKlineTest_index() + //期权 + CreateOptionList() + CreateOptionindex() + // 外汇 + CreateForexList() + CreateForexListNew() + CreateForexTradeLis() + CreateForexKLine() +} +func Mgo_inits(config model.Mongodb) *mongo.Client { + mongodb := config.DbBase + clientOptions := options.Client().ApplyURI(fmt.Sprintf("mongodb://%v:%v@%v:%v/%v?ssl=false&authSource=admin", config.DbUser, config.Password, config.DbHost, config.DbPort, mongodb)) + client, err := mongo.Connect(context.TODO(), clientOptions) + if err != nil { + log.Printf("connect err: %v", err) + return client + } + // Check the connection + err = client.Ping(context.TODO(), nil) + if err != nil { + log.Printf("test connect err: %v", err) + return client + } + fmt.Println("Mongodb ok") + return client +} +func Mgo_initMap(config model.Mongodb, addrList []string) { + GetStockList(config.Table) + DataBase = config.DbBase + for _, addr := range addrList { + clientOptions := options.Client().ApplyURI(fmt.Sprintf("mongodb://%v:%v@%v:%v/%v?ssl=false&authSource=admin", config.DbUser, config.Password, addr, config.DbPort, DataBase)) + client, err := mongo.Connect(context.TODO(), clientOptions) + if err != nil { + log.Printf("connect err: %v", err) + return + } + // Check the connection + err = client.Ping(context.TODO(), nil) + if err != nil { + os.Exit(0) + } else { + applogger.Info("mongodb init success") + } + MgoDbClientMap[addr] = client + } +} + +// mgoConnect +func mgoConnect(collection string) *mongo.Collection { + return mgoDb.Database(DataBase).Collection(collection) +} + +// MgoConnect +func MgoConnect(collection string) *mongo.Collection { + return mgoConnect(collection) +} + +// MgoInsertOne +func MgoInsertOne(collection string, docs interface{}) error { + c := mgoConnect(collection) + if _, err := c.InsertOne(context.TODO(), docs); err != nil { + return err + } + return nil +} + +// MgoInsertMany +func MgoInsertMany(collection string, docs []interface{}) error { + c := mgoConnect(collection) + if _, err := c.InsertMany(context.TODO(), docs); err != nil { + return err + } + return nil +} + +// MgoUpdateID +func MgoUpdateID(collection string, id interface{}, update interface{}) error { + c := mgoConnect(collection) + if _, err := c.UpdateByID(context.TODO(), id, update); err != nil { + return err + } + return nil +} + +// MgoUpdateOne +func MgoUpdateOne(collection string, filter interface{}, update interface{}) error { + c := mgoConnect(collection) + if _, err := c.UpdateOne(context.TODO(), filter, update); err != nil { + return err + } + return nil +} + +func MgoUpdateOneTrue(collection string, filter interface{}, update interface{}) error { + c := mgoConnect(collection) + opts := options.Update().SetUpsert(true) + result, err := c.UpdateOne(context.TODO(), filter, update, opts) + if err != nil { + return err + } + if result.MatchedCount == 0 && result.UpsertedCount > 0 { + log.Printf("A new document was inserted with the id: %v", result.UpsertedID) + } else if result.MatchedCount > 0 { + log.Println("An existing document was updated") + } else { + log.Println("No operation was performed") + } + return nil +} + +// MgoUpdateMany +func MgoUpdateMany(collection string, filter interface{}, update interface{}) error { + c := mgoConnect(collection) + if _, err := c.UpdateMany(context.TODO(), filter, update); err != nil { + return err + } + return nil +} + +// MgoBulkWrite +func MgoBulkWrite(collection string, models []mongo.WriteModel) error { + c := mgoConnect(collection) + _, err := c.BulkWrite(context.TODO(), models) + if err != nil { + applogger.Error("UpdateMany err:%v", err) + } + return err +} + +func MgoBulkWrites(client *mongo.Client, collection string, models []mongo.WriteModel) error { + c := client.Database(DataBase).Collection(collection) + _, err := c.BulkWrite(context.TODO(), models) + if err != nil { + applogger.Error("UpdateMany err:%v", err) + } + return err +} + +// MgoIsExist +func MgoIsExist(collection string, filter interface{}) bool { + c := mgoConnect(collection) + cur, err := c.Find(context.TODO(), filter) + if err != nil { + return false + } + + var numDocs int + for cur.Next(context.Background()) { + numDocs++ + } + + return numDocs > 0 +} + +// MgoFind +func MgoFind(collection string, filter interface{}) (interface{}, error) { + c := mgoConnect(collection) + cursor, err := c.Find(context.TODO(), filter, options.Find().SetSort(bson.M{"Vol": -1})) + if err != nil { + applogger.Error("Find err: %v", err) + return nil, err + } + var results []bson.M + if err = cursor.All(context.TODO(), &results); err != nil { + log.Fatal(err) + } + + return results, nil +} + +func MgoFindProjectionRes(collection string, filter, projection, sort interface{}, res interface{}, limit int64) { // + c := mgoConnect(collection) + var optionStr *options.FindOptions + if limit > 0 { + optionStr = options.Find().SetProjection(projection).SetSort(sort).SetLimit(limit) + } else { + optionStr = options.Find().SetProjection(projection).SetSort(sort) + } + cursor, err := c.Find(context.TODO(), filter, optionStr) + if err != nil { + applogger.Error("Find MgoFindProjectionRes err: %v", err) + return + } + if err = cursor.All(context.TODO(), res); err != nil { + log.Fatal(err) + } + return +} + +func MgoFindProjectionAggregate(collection, field string, filter, projection, sort interface{}, limit int64) []model.StockMogoParam { // + //fmt.Println(collection, field, filter) + params := make([]model.StockMogoParam, 0) + pipeline := mongo.Pipeline{ + {{"$match", filter}}, + // Convert the string field to a double (floating-point) field + {{"$addFields", bson.D{{"stringVal", bson.D{{"$toDouble", "$" + field}}}}}}, + // Sort by the new double field + {{"$sort", sort}}, + // Project only the numeric field + // {{"$project", projection}}, + // Limit to one document to get the single sorted numeric value + {{"$limit", limit}}, + } + c := mgoConnect(collection) + aggregate, err := c.Aggregate(context.TODO(), pipeline) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return params + } + if aggregate.Next(context.TODO()) { + param := model.StockMogoParam{} + if err = aggregate.Decode(¶m); err != nil { + log.Fatal(err) + } + params = append(params, param) + } + + //fmt.Printf("%+v", params) + return params +} + +func MgoFindRes(collection string, filter interface{}, res interface{}) { + c := mgoConnect(collection) + cursor, err := c.Find(context.TODO(), filter, options.Find().SetSort(bson.M{"timestamp": 1})) + if err != nil { + applogger.Error("Find err: %v", err) + return + } + if err = cursor.All(context.TODO(), res); err != nil { + log.Fatal(err) + } + return +} + +func MgoFindStockRes(collection string, filter interface{}, res interface{}) { + c := mgoConnect(collection) + cursor, err := c.Find(context.TODO(), filter, options.Find().SetSort(bson.M{"Vol": -1})) + if err != nil { + applogger.Error("Find err: %v", err) + return + } + if err = cursor.All(context.TODO(), res); err != nil { + log.Fatal(err) + } + return +} + +// MgoFindOne +func MgoFindOne(collection string, filter interface{}) (bson.M, error) { + c := mgoConnect(collection) + cur := c.FindOne(context.TODO(), filter) + var res bson.M + if err := cur.Decode(&res); err != nil { + return nil, err + } + return res, nil +} + +// MgoFindAll +func MgoFindAll(collection string, filter interface{}) (interface{}, error) { + c := mgoConnect(collection) + cur, err := c.Find(context.TODO(), filter) + if err != nil { + return nil, err + } + var res interface{} + if err := cur.All(context.TODO(), &res); err != nil { + return nil, err + } + return res, err +} + +// MgoDeleteOne +func MgoDeleteOne(collection string, filter interface{}) error { + c := mgoConnect(collection) + _, err := c.DeleteOne(context.TODO(), filter) + if err != nil { + return err + } + return nil +} + +// MgoDeleteMany +func MgoDeleteMany(collection string, filter interface{}) error { + c := mgoConnect(collection) + _, err := c.DeleteMany(context.TODO(), filter) + if err != nil { + return err + } + return nil +} +func MillisToTime(millis int64) time.Time { + return time.Unix(0, millis*int64(time.Millisecond)) +} + +// MgoDeleteMany100List +func MgoDeleteMany100List(collection string, filter interface{}, code string) error { + c := mgoConnect(collection) + var count int64 = 100 + // 查询所有文档并排序 + findOptions := options.Find().SetSort(bson.D{{"tick_time", -1}}).SetLimit(count) + cursor, err := c.Find(context.Background(), filter, findOptions) + if err != nil { + applogger.Error("Find err:%v", err) + return err + } + + var docs []bson.M + if err = cursor.All(context.Background(), &docs); err != nil { + return err + } + + if len(docs) < 100 { + return nil + } + + //applogger.Debug("总数据:%v", len(docs)) + + // 获取最新N条记录的创建时间 + var latestTimes []time.Time + for _, doc := range docs { + createdAt := MillisToTime(doc["tick_time"].(int64)) + latestTimes = append(latestTimes, createdAt) + } + + // 按时间排序,保证时间最新的在前面 + sort.Slice(latestTimes, func(i, j int) bool { + return latestTimes[i].After(latestTimes[j]) + }) + + times := latestTimes[count-1].UnixNano() / 1e6 // 转换为毫秒 + //applogger.Debug("需要删除的时间节点:%v,时间:%v", latestTimes[count-1], times) + + // 删除旧的数据,保留最新的N条 + deleteFilter := bson.M{ + "tick_time": bson.M{ + "$lt": times, + }, + "code": code, + } + _, err = c.DeleteMany(context.Background(), deleteFilter) + if err != nil { + return err + } + + //applogger.Debug("Deleted %v documents", deleteResult.DeletedCount) + + return nil +} + +func MgoFindResM(collection string, filter interface{}) ([]mongo.WriteModel, error) { + var results []mongo.WriteModel + c := mgoConnect(collection) + cur, err := c.Find(context.TODO(), filter, options.Find().SetSort(bson.M{"timestamp": 1})) + if err != nil { + applogger.Error("Find err: %v", err) + return results, err + } + if err = cur.All(context.TODO(), &results); err != nil { + log.Fatal(err) + } + return results, nil +} + +// MgoPagingFind +func MgoPagingFind(collection string, filter interface{}, limit, page int64, sort int) ([]bson.M, error) { + c := mgoConnect(collection) + + var optionStr *options.FindOptions + if sort != 0 { + optionStr = options.Find().SetLimit(limit).SetSkip((limit * page) - limit).SetSort(bson.M{"Code": sort}) + } else { + optionStr = options.Find().SetLimit(limit).SetSkip((limit * page) - limit) + } + fmt.Println(filter) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return nil, err + } + + var results []bson.M + if err = cur.All(context.TODO(), &results); err != nil { + log.Fatal(err) + } + + return results, err +} + +func MgoPagingFindStruct(collection string, filter interface{}, limit, page int64, sortField string, sort int, res interface{}) error { + c := mgoConnect(collection) + var optionStr *options.FindOptions + optionStr = options.Find().SetLimit(limit).SetSkip((limit * page) - limit).SetSort(bson.M{sortField: sort}) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return err + } + + if err = cur.All(context.TODO(), res); err != nil { + log.Fatal(err) + return err + } + + return nil +} + +func MgoPagingFindStructList(collection string, filter interface{}, limit, page int64, sortStr string, sort int, res interface{}) error { + c := mgoConnect(collection) + var optionStr *options.FindOptions + optionStr = options.Find().SetLimit(limit).SetSkip((limit * page) - limit).SetSort(bson.M{sortStr: sort}) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return err + } + + if err = cur.All(context.TODO(), res); err != nil { + log.Fatal(err) + return err + } + + return nil +} + +func MgoPagingFindStructSort(collection string, filter interface{}, limit, page int64, sort interface{}, res interface{}) error { + c := mgoConnect(collection) + var optionStr *options.FindOptions + optionStr = options.Find().SetLimit(limit).SetSkip((limit * page) - limit).SetSort(sort) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return err + } + + if err = cur.All(context.TODO(), res); err != nil { + log.Fatal(err) + return err + } + + return nil +} + +func MgoPagingFindStructProjection(collection string, filter, projection interface{}, limit, page int64, sort int, res interface{}) error { + c := mgoConnect(collection) + var optionStr *options.FindOptions + optionStr = options.Find().SetLimit(limit).SetSkip((limit * page) - limit).SetSort(bson.M{"Vol": sort}).SetProjection(projection) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return err + } + + if err = cur.All(context.TODO(), res); err != nil { + log.Fatal(err) + return err + } + + return nil +} +func MgoPagingFindStructProjectionNew(mongoClient *mongo.Client, collection string, filter, projection interface{}, limit, page int64, sort int, res interface{}) error { + // 链接mongodb数据库 + c := mongoClient.Database(DataBase).Collection(collection) + // 查询mongodb数据 + var optionStr *options.FindOptions + optionStr = options.Find().SetLimit(limit).SetSkip((limit * page) - limit).SetSort(bson.M{"Vol": sort}).SetProjection(projection) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v,dataTable Name:%v", err, collection) + return err + } + if err = cur.All(context.TODO(), res); err != nil { + log.Fatal(err) + return err + } + return nil +} + +func MgoFindToStr(collection string, filter interface{}, limit int64, res interface{}) error { + c := mgoConnect(collection) + var optionStr *options.FindOptions + if limit > 0 { + optionStr = options.Find().SetLimit(limit).SetSort(bson.M{"timestamp": -1}) + } else { + optionStr = options.Find().SetSort(bson.M{"timestamp": -1}) + } + //fmt.Println(filter) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return err + } + if err = cur.All(context.TODO(), res); err != nil { + log.Fatal(err) + return err + } + return nil +} + +func MgoFindForexToStr(collection string, filter interface{}, limit int64, res interface{}) error { + c := mgoConnect(collection) + var optionStr *options.FindOptions + if limit > 0 { + optionStr = options.Find().SetLimit(limit).SetSort(bson.M{"tick_time": -1}) + } else { + optionStr = options.Find().SetSort(bson.M{"tick_time": -1}) + } + //fmt.Println(filter) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return err + } + if err = cur.All(context.TODO(), res); err != nil { + log.Fatal(err) + return err + } + return nil +} + +func MgoLimitFind(collection string, filter interface{}, limit int64) ([]MongoTick, error) { + res := make([]MongoTick, 0) + c := mgoConnect(collection) + var optionStr *options.FindOptions + if limit > 0 { + optionStr = options.Find().SetLimit(limit).SetSort(bson.M{"code": -1}) + } else { + optionStr = options.Find().SetSort(bson.M{"code": -1}) + } + //fmt.Println(filter) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return nil, err + } + //var results []bson.M + if err = cur.All(context.TODO(), &res); err != nil { + log.Fatal(err) + } + return res, err +} + +func MgoFinds(collection string, filter interface{}, limit int64) ([]bson.M, error) { + c := mgoConnect(collection) + var optionStr *options.FindOptions + if limit > 0 { + optionStr = options.Find().SetLimit(limit).SetSort(bson.M{"timestamp": -1}) + } else { + optionStr = options.Find().SetSort(bson.M{"timestamp": -1}) + } + //fmt.Println(filter) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return nil, err + } + var results []bson.M + if err = cur.All(context.TODO(), &results); err != nil { + log.Fatal(err) + } + return results, err +} + +func MgoFindsCode(collection string, filter interface{}, limit int64) ([]bson.M, error) { + c := mgoConnect(collection) + var optionStr *options.FindOptions + if limit > 0 { + optionStr = options.Find().SetLimit(limit).SetSort(bson.M{"code": -1}) + } else { + optionStr = options.Find().SetSort(bson.M{"code": -1}) + } + //fmt.Println(filter) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return nil, err + } + var results []bson.M + if err = cur.All(context.TODO(), &results); err != nil { + log.Fatal(err) + } + return results, err +} + +func MgoFindProjection(collection string, filter, projection, sort interface{}, limit int64) ([]bson.M, error) { + c := mgoConnect(collection) + var optionStr *options.FindOptions + if limit > 0 { + optionStr = options.Find().SetLimit(limit).SetSort(sort).SetProjection(projection) + } else { + optionStr = options.Find().SetSort(sort).SetProjection(projection) + } + //fmt.Println(filter) + cur, err := c.Find(context.Background(), filter, optionStr) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return nil, err + } + var results []bson.M + if err = cur.All(context.TODO(), &results); err != nil { + log.Fatal(err) + } + return results, err +} + +// MgoFindTotal +func MgoFindTotal(collection string, filter interface{}) (int64, error) { + c := mgoConnect(collection) + + total, err := c.CountDocuments(context.TODO(), filter) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return 0, err + } + + return total, nil +} + +// MgoAggregate +func MgoAggregate(collection string, filter mongo.Pipeline) (interface{}, error) { + c := mgoConnect(collection) + + aggregate, err := c.Aggregate(context.TODO(), filter) + if err != nil { + applogger.Error("MgoPagingFind info err:%v", err) + return 0, err + } + + var showsWithInfo []map[string]interface{} + if err = aggregate.All(context.TODO(), &showsWithInfo); err != nil { + log.Printf("collection %s", err) + panic(err) + } + + return showsWithInfo, nil +} diff --git a/internal/data/mysql.go b/internal/data/mysql.go new file mode 100644 index 0000000..653ffc3 --- /dev/null +++ b/internal/data/mysql.go @@ -0,0 +1,48 @@ +package data + +import ( + "database/sql" + _ "github.com/go-sql-driver/mysql" + "github.com/go-xorm/xorm" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var Engine *xorm.EngineGroup +var DB *sql.DB + +// InitMysql Init Mysql DB +func InitMysql(config model.Bourse) { + var err error + + driver := config.Driver + datasource := config.Datasource + + conns := []string{ + datasource, + } + Engine, err = xorm.NewEngineGroup(driver, conns, xorm.RandomPolicy()) + if err != nil { + applogger.Error("orm failed to initialized: %v", err) + panic(err) + } + + Engine.ShowExecTime(true) + Engine.ShowSQL(true) + Engine.SetMaxOpenConns(500) + err = Engine.Ping() + if err != nil { + panic(err) + } +} + +func InitMsqlDB(config model.Bourse) { + // root:IcWw%%r3pt@tcp(34.126.77.164:3307)/bourse?charset=utf8 + db, err := sql.Open("mysql", config.Datasource) + if err != nil { + applogger.Error("Datasource open err: %v", err) + return + } + DB = db + return +} diff --git a/internal/data/mysqlbusiness/addinfo.go b/internal/data/mysqlbusiness/addinfo.go new file mode 100644 index 0000000..7bcea4d --- /dev/null +++ b/internal/data/mysqlbusiness/addinfo.go @@ -0,0 +1,65 @@ +package mysqlbusiness + +import ( + "strconv" + "time" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/sqlmodel" +) + +// SaveBoUserSms +func SaveBoUserSms(userSms sqlmodel.BoUserSms) error { + boUserSms := &sqlmodel.BoUserSms{ + From: userSms.From, + To: userSms.To, + Message: userSms.Message, + TaskId: userSms.TaskId, + MessageResult: userSms.MessageResult, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + if _, err := data.Engine.Table("bo_user_sms").Insert(boUserSms); err != nil { + applogger.Error("SaveBoUserSms info err: %v", err) + return err + } + + return nil +} + +// SaveBoUsers +func SaveBoUsers(phoneNumber string, password string, InvitationCode string) (string, error) { + token, err := internal.GetToken() + if err != nil { + applogger.Error("select token err:%v", err) + return "生成token失败,请联系管理员", err + } + uid := internal.Captcha(10) + phone, err := strconv.Atoi(phoneNumber) + if err != nil { + applogger.Error("Atoi err: %v", err) + return "电话号码解析失败,请联系管理员", err + } + bom := sqlmodel.BoUsers{ + Uid: uid, + Phonenumber: int64(phone), + Loginpassword: password, + Invitecode: InvitationCode, + Accesstoken: token, + Status: 1, + Addtime: time.Now(), + Updatetime: time.Now(), + } + checkInt, err := data.Engine.Table("bo_users").Insert(&bom) + if err != nil { + applogger.Error("SaveBoUsers Insert err: %v", err) + return "", err + } + applogger.Debug("新增数据:%v", checkInt) + if checkInt == 0 { + return "注册用户失败", nil + } + + return "注册用户成功", nil +} diff --git a/internal/data/mysqlbusiness/getinfo.go b/internal/data/mysqlbusiness/getinfo.go new file mode 100644 index 0000000..468c0cc --- /dev/null +++ b/internal/data/mysqlbusiness/getinfo.go @@ -0,0 +1,112 @@ +package mysqlbusiness + +import ( + "strconv" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/sqlmodel" +) + +// GetBoUserOptionalStocksNew +func GetBoUserOptionalStocksNew(bourseType, systemBoursesId int, userId int64) ([]sqlmodel.BoUserOptionalStocks, error) { + var bom []sqlmodel.BoUserOptionalStocks + if err := data.Engine.Table("bo_user_optional_stocks"). + Where("bourseType = ?", bourseType). + Where("systemBoursesId = ?", systemBoursesId). + Where("userId = ?", userId). + Desc("id"). + Find(&bom); err != nil { + applogger.Error("GetBoUserOptionalStocksNew find info err: %v", err) + return nil, err + } + + return bom, nil +} + +// GetBoUsers +func GetBoUsers(token string) (bool, int64, error) { + var bom []sqlmodel.BoUsers + err := data.Engine.Table("bo_users"). + Where("accessToken=?", token). + Where("status=1"). + Where("deleteTime is null"). + Find(&bom) + if err != nil { + applogger.Error("GetBoUsers info err: %v", err) + return false, 0, err + } + for _, value := range bom { + return true, value.Id, nil + } + return false, 0, nil +} + +// GetBoUsersByPhoneNumber +func GetBoUsersByPhoneNumber(phoneNumber string) (string, error) { + phone, err := strconv.Atoi(phoneNumber) + if err != nil { + applogger.Error("Atoi err: %v", err) + return "电话号码解析失败,请联系管理员", err + } + var bom []sqlmodel.BoUsers + err = data.Engine.Table("bo_users"). + Where("phoneNumber=?", phone). + Where("status=1"). + Where("deleteTime is null"). + Find(&bom) + if err != nil { + applogger.Error("GetBoUsersByPhoneNumber info err: %v", err) + return "注册异常请联系管理员", err + } + + applogger.Debug("查询数据信息:", bom) + + if len(bom) > 0 { + return "用户已经存在", nil + } + + return "", nil +} + +// GetBoUsersByPhoneAndPassWord +func GetBoUsersByPhoneAndPassWord(phoneNumber string, password string) (string, error) { + token, err := internal.GetToken() + if err != nil { + applogger.Error("select token err:%v", err) + return "生成token失败,请联系管理员", err + } + phone, err := strconv.Atoi(phoneNumber) + if err != nil { + applogger.Error("Atoi err: %v", err) + return "电话号码解析失败,请联系管理员", err + } + var u sqlmodel.UsersJson + query := "select id,uid,accessToken from bo_users where phoneNumber=? and loginPassword=? and status=1 and deleteTime is null limit 1" + if err := data.DB.QueryRow(query, phone, password).Scan(&u.Id, &u.Uid, &u.AccessToken); err != nil { + applogger.Error("QueryRow err:%v", err) + if err.Error() == "sql: no rows in result set" { + return "用户不存在", err + } + return "登录异常请联系管理员", err + } + + if err = UpdateBoUsersTokenById(u.Id, token); err != nil { + applogger.Error("update info err: %v", err) + return "更新token失败,请联系管理员", err + } + + return token, nil +} + +func GetBoUserTerminalEquipments(userId int64) error { + var bom []sqlmodel.BoUserTerminalEquipments + if err := data.Engine.Table("bo_user_terminal_equipments").Where("userId = ?", userId).Find(&bom); err != nil { + applogger.Error("SaveBoUserSms info err: %v", err) + return err + } + + applogger.Debug("查询数据信息:%v", bom) + + return nil +} diff --git a/internal/data/mysqlbusiness/updateinfo.go b/internal/data/mysqlbusiness/updateinfo.go new file mode 100644 index 0000000..e3048a1 --- /dev/null +++ b/internal/data/mysqlbusiness/updateinfo.go @@ -0,0 +1,102 @@ +package mysqlbusiness + +import ( + "go.uber.org/zap" + "strconv" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/sqlmodel" +) + +// UpdateBoUserSms +func UpdateBoUserSms(userSms sqlmodel.BoUserSms) error { + _, err := data.Engine.Table("bo_user_sms"). + Where("`from` = ?", userSms.From). + Where("`to` = ?", userSms.To). + Where("task_id = ?", userSms.TaskId). + Where("message = ?", userSms.Message). + Update(&userSms) + if err != nil { + applogger.Error("UpdateBoUserSms info err:%v", err) + return err + } + + return nil +} + +// UpdateBoUsersById +func UpdateBoUsersById(phoneNumber string, password string) (string, error) { + phone, err := strconv.Atoi(phoneNumber) + if err != nil { + applogger.Error("Atoi err: %v", err) + return "电话号码解析失败,请联系管理员", err + } + sqlStr := "UPDATE bo_users set loginPassword=? where phoneNumber=? and status=1" + _, err = data.DB.Exec(sqlStr, password, phone) + if err != nil { + applogger.Error("Login User verification failed.", zap.Error(err)) + return "修改密码异常请联系管理员", err + } + + return "密码修改成功", nil +} + +// UpdateBoUsersTokenById +func UpdateBoUsersTokenById(id int64, accessToken string) error { + sqlStr := "UPDATE bo_users set accessToken=? where id=? and status=1" + _, err := data.DB.Exec(sqlStr, accessToken, id) + if err != nil { + applogger.Error("Login User verification failed.", zap.Error(err)) + return err + } + + return nil +} + +// UpdateBoUsersById +func UpdateBoUsersPassWordByPhoneNumber(phoneNumber string, password string) (string, error) { + phone, err := strconv.Atoi(phoneNumber) + if err != nil { + applogger.Error("Atoi err: %v", err) + return "电话号码解析失败,请联系管理员", err + } + //sqlStr := "UPDATE bo_users set loginPassword=? where phoneNumber=? and status=1" + //_, err = data.DB.Exec(sqlStr, password, phone) + //if err != nil { + // applogger.Error("Login User verification failed.", zap.Error(err)) + // return "修改密码异常请联系管理员", err + //} + bom := sqlmodel.BoUsers{ + Loginpassword: password, + } + checkInt, err := data.Engine.Table("bo_users"). + Where("phoneNumber=?", phone). + Where("status=1"). + Update(&bom) + if err != nil { + applogger.Error("UpdateBoUsersPassWordByPhoneNumber info err: %v", err) + return "修改密码异常请联系管理员", err + } + if checkInt == 0 { + return "修改密码失败", nil + } + + return "修改密码成功", nil +} + +// UpdateBoUsersById +func UpdateBoUsersPhoneNumberById(phoneNumber string, id int64) (string, error) { + phone, err := strconv.Atoi(phoneNumber) + if err != nil { + applogger.Error("Atoi err: %v", err) + return "电话号码解析失败,请联系管理员", err + } + sqlStr := "UPDATE bo_users set phoneNumber=? where id=? and status=1" + _, err = data.DB.Exec(sqlStr, phone, id) + if err != nil { + applogger.Error("Login User verification failed.", zap.Error(err)) + return "设置手机号异常请联系管理员", err + } + + return "设置手机号成功", err +} diff --git a/internal/gzip/gzipanalysis.go b/internal/gzip/gzipanalysis.go new file mode 100644 index 0000000..3b0c586 --- /dev/null +++ b/internal/gzip/gzipanalysis.go @@ -0,0 +1,61 @@ +package gzip + +import ( + "bytes" + "compress/gzip" + "io/ioutil" +) + +// 使用gzip解压缩数据 +func DecompressData(compressedData []byte) ([]byte, error) { + buf := bytes.NewReader(compressedData) + decompressor, err := gzip.NewReader(buf) + if err != nil { + return nil, err + } + defer decompressor.Close() + + result, err := ioutil.ReadAll(decompressor) + if err != nil { + return nil, err + } + + return result, nil +} + +func GZipDecompress(input []byte) (string, error) { + buf := bytes.NewBuffer(input) + reader, gzipErr := gzip.NewReader(buf) + if gzipErr != nil { + return "", gzipErr + } + defer reader.Close() + + result, readErr := ioutil.ReadAll(reader) + if readErr != nil { + return "", readErr + } + return string(result), nil +} + +func GZipCompress(input string) ([]byte, error) { + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + + _, err := gz.Write([]byte(input)) + if err != nil { + return nil, err + } + + err = gz.Flush() + if err != nil { + return nil, err + } + + err = gz.Close() + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/internal/httprequest.go b/internal/httprequest.go new file mode 100644 index 0000000..5c57589 --- /dev/null +++ b/internal/httprequest.go @@ -0,0 +1,195 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "strings" + "wss-pool/config" + "wss-pool/logging/applogger" + "wss-pool/logging/perflogger" +) + +var ( + ResultStr = "" + QueryError = "Query failed" + QuerySuccess = "query was successful" + QueryToken = "Token failure" + ParameterError = "参数错误" + PhoneError = "电话号码不正确" + PassWordError = "密码不正确" + TokenError = "登录成功并且生成新的token" + UserIdError = "用户Id不正确" + CodeError = "验证码不正确" +) + +func HttpGet(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + result, err := ioutil.ReadAll(resp.Body) + if err != nil { + applogger.Error("ioutil.ReadAll err: %v", err) + return "", err + } + + return string(result), err +} + +// 统一请求Get方法 +func HttpGetApi(url string) (map[string]interface{}, error) { + logger := perflogger.GetInstance() + logger.Start() + + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + //logger.StopAndLog("GET", url) + + var status map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + applogger.Error("NewDecoder err: %v", err) + return nil, err + } + + return status, err +} + +func GetWithHeader(url string, headers map[string]string) (map[string]interface{}, error) { + client := &http.Client{} + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + for key, header := range headers { + req.Header.Set(key, header) + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + result, err := ioutil.ReadAll(resp.Body) + if err != nil { + applogger.Error("select err: %v", err) + return nil, err + } + if strings.Contains(string(result), "html") { + return nil, nil + } + + var status map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + applogger.Error("json NewDecoder err: %v", err) + return nil, err + } + return status, nil +} + +// 股票静态数据接入 +func HttpGetDo(url string) (string, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + applogger.Error("http NewRequest err: %v", err) + return "", err + } + + req.Header.Add("X-RapidAPI-Key", config.Config.ShareGather.RapidApiKey) + req.Header.Add("X-RapidAPI-Host", config.Config.ShareGather.RapidApiHost) + + res, err := http.DefaultClient.Do(req) + if err != nil { + applogger.Error("http DefaultClient Do err: %v", err) + return "", err + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + applogger.Error("io ReadAll err: %v", err) + return "", err + } + + return string(body), nil +} + +func HttpGetDoNew(url string) (map[string]interface{}, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + applogger.Error("NewRequest err: %v", err) + return nil, err + } + + req.Header.Add("X-RapidAPI-Key", config.Config.ShareGather.RapidApiKey) + req.Header.Add("X-RapidAPI-Host", config.Config.ShareGather.RapidApiHost) + + res, err := http.DefaultClient.Do(req) + if err != nil { + applogger.Error("http DefaultClient Do err: %v", err) + return nil, err + } + defer res.Body.Close() + + var status map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&status); err != nil { + applogger.Error("json NewDecoder err: %v", err) + return nil, err + } + + return status, nil +} + +func HttpPost(url string, body string) (string, error) { + resp, err := http.Post(url, "application/json", strings.NewReader(body)) + if err != nil { + return "", err + } + defer resp.Body.Close() + result, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(result), err +} + +func HttpPostFrom(url string, postData map[string]string) (string, error) { + fmt.Println(url) + payload := &bytes.Buffer{} + w := multipart.NewWriter(payload) + for k, v := range postData { + w.WriteField(k, v) + } + w.Close() + client := &http.Client{} + req, _ := http.NewRequest("POST", url, payload) + req.Header.Set("Content-Type", w.FormDataContentType()) + resp, err := client.Do(req) + if err != nil { + return "", err + } + data, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + return string(data), err +} + +// Unified return format +func GinResult(code int, value interface{}, msg string) interface{} { + return gin.H{ + "code": code, + "data": value, + "message": msg, + } +} diff --git a/internal/model/contractMarket.go b/internal/model/contractMarket.go new file mode 100644 index 0000000..1f6d349 --- /dev/null +++ b/internal/model/contractMarket.go @@ -0,0 +1,194 @@ +package model + +import ( + "errors" + "gorm.io/gorm" + "time" + "wss-pool/internal/data" +) + +const ( + AlreadyReceived int = 2 //已领取 + Unclaimed int = 1 //未领取 + ModifyContract int = 0 //插针 + Market int = 2 //现货 + Contract int = 1 //合约 +) + +type ContractMarket struct { + ID int64 `gorm:"column:id;"json:"id"` + TradeName string `gorm:"column:trade_name;" json:"name"` + BeginTime string `gorm:"column:begin_time;" json:"begin_time"` + Step int `gorm:"column:step;" json:"step"` + EndTime string `gorm:"column:end_time;" json:"end_time"` + MaxPrice string `gorm:"column:max_price" json:"max_price"` + IsType int `gorm:"column:type;not null;type:tinyint(2);COMMENT:'0 插针 1 自发合约 2 自发现货 '"json:"type"` + IsGet int `gorm:"column:is_get;not null;type:tinyint(2);DEFAULT:1;COMMENT:'是否完成(1 未领取 2 已领取 ) '"json:"is_get"` + KeepDecimal int `gorm:"column:keep_decimal; '"json:"keep_decimal"` + Gorm *gorm.DB `gorm:"-" json:"-"` +} + +func (this *ContractMarket) TableName() string { + return "bot_contract_market" +} + +func NewContractMarket() *ContractMarket { + ContractMarket := &ContractMarket{} + ContractMarket.NewGorm() + return ContractMarket +} + +func (this *ContractMarket) NewGorm() *ContractMarket { + this.Gorm = data.WebGorm.Table(this.TableName()) + return this +} + +func (this *ContractMarket) WhereBegin() *ContractMarket { + + this.Gorm = this.Gorm.Table(this.TableName()).Where("begin_time >= ?", time.Now().Format("2006-01-02 15:04:05")) + return this +} + +func (this *ContractMarket) WhereID() *ContractMarket { + if this.ID == 0 { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where(&ContractMarket{ID: this.ID}) + return this +} + +func (this *ContractMarket) WhereName() *ContractMarket { + if this.TradeName == "" { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where(&ContractMarket{TradeName: this.TradeName}) + return this +} + +func (this *ContractMarket) WhereModifyContract() *ContractMarket { + this.Gorm = this.Gorm.Table(this.TableName()).Where("`bot_contract_market`.`type` = ?", ModifyContract) + return this +} + +func (this *ContractMarket) WhereInID(ids []int64) *ContractMarket { + if len(ids) <= 0 { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where("id in (?)", ids) + return this +} + +func (this *ContractMarket) WhereIsTyep() *ContractMarket { + if this.IsType <= 0 { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where(&ContractMarket{IsType: this.IsType}) + return this +} + +func (this *ContractMarket) WhereIsGet() *ContractMarket { + if this.IsGet == 0 { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where(&ContractMarket{IsGet: this.IsGet}) + return this +} + +func (this *ContractMarket) Update(record *ContractMarket) *ContractMarket { + this.Gorm = this.Gorm.Updates(record) + return this +} + +func (this *ContractMarket) Create() *ContractMarket { + this.Gorm = this.Gorm.Create(&this) + return this +} +func (this *ContractMarket) Joins(sql string) *ContractMarket { + this.Gorm = this.Gorm.Joins(sql) + return this +} + +func (this *ContractMarket) Limit(limit int) *ContractMarket { + this.Gorm = this.Gorm.Limit(limit) + return this +} +func (this *ContractMarket) Offset(Offset int) *ContractMarket { + this.Gorm = this.Gorm.Offset(Offset) + return this +} +func (this *ContractMarket) First() *ContractMarket { + Accounts := ContractMarket{} + this.Gorm = this.Gorm.First(&Accounts) + return &Accounts +} + +func (this *ContractMarket) Assign(ContractMarket *ContractMarket) *ContractMarket { + this.Gorm = this.Gorm.Assign(ContractMarket) + return this +} + +func (this *ContractMarket) FirstOrCreate() *ContractMarket { + this.Gorm = this.Gorm.FirstOrCreate(&this) + return this +} + +func (this *ContractMarket) Order(value interface{}) *ContractMarket { + this.Gorm = this.Gorm.Order(value) + return this +} + +func (this *ContractMarket) Count() int64 { + var num int64 + this.Gorm = this.Gorm.Count(&num) + return num +} +func (this *ContractMarket) Select(column string) *ContractMarket { + this.Gorm = this.Gorm.Select(column) + return this +} + +func (this *ContractMarket) WhereTime(LoginAt string) *ContractMarket { + if LoginAt == "" { + this.Gorm.Error = errors.New("date require ") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where("login_at = ?", LoginAt) + return this +} + +func (this *ContractMarket) Pluck(column string, value interface{}) *ContractMarket { + this.Gorm = this.Gorm.Pluck(column, value) + return this +} + +func (this *ContractMarket) Find(list *[]ContractMarket) *ContractMarket { + this.Gorm = this.Gorm.Find(&list) + return this +} + +func (this *ContractMarket) List() (result []ContractMarket) { + this.IsGet = Unclaimed + this.WhereIsTyep().WhereIsGet().WhereBegin().WhereName().Order("id asc").Find(&result) + return result +} + +func (this *ContractMarket) ListModifyContract() (result []ContractMarket) { + this.IsGet = Unclaimed + this.Select("l.keep_decimal,bot_contract_market.*").WhereModifyContract().Joins("inner join bot_contract_list l on l.trade_name = bot_contract_market.trade_name").WhereIsGet().Order("id asc").Find(&result) + return result +} + +func (this *ContractMarket) UpdateIsGet(ids []int64) error { + this.IsGet = Unclaimed + return this.WhereInID(ids).WhereIsGet().Update(&ContractMarket{IsGet: AlreadyReceived}).Gorm.Error +} + +func (this *ContractMarket) UpdateIsGetOne() error { + this.IsGet = Unclaimed + return this.WhereID().WhereIsGet().Update(&ContractMarket{IsGet: AlreadyReceived}).Gorm.Error +} diff --git a/internal/model/forexMarket.go b/internal/model/forexMarket.go new file mode 100644 index 0000000..a76d966 --- /dev/null +++ b/internal/model/forexMarket.go @@ -0,0 +1,193 @@ +package model + +import ( + "errors" + "gorm.io/gorm" + "time" + "wss-pool/internal/data" +) + +const ( + AlreadyReceivedForex int = 2 //已领取 + UnclaimedForex int = 1 //未领取 + ModifyForex int = 0 //插针 + Forex int = 1 //外汇 +) + +type ForexMarket struct { + ID int64 `gorm:"column:id;"json:"id"` + TradeName string `gorm:"column:trade_name;" json:"name"` + BeginTime string `gorm:"column:begin_time;" json:"begin_time"` + Step int `gorm:"column:step;" json:"step"` + EndTime string `gorm:"column:end_time;" json:"end_time"` + MaxPrice string `gorm:"column:max_price" json:"max_price"` + IsType int `gorm:"column:type;not null;type:tinyint(2);COMMENT:'0 插针 1 自发合约 2 自发现货 '"json:"type"` + IsGet int `gorm:"column:is_get;not null;type:tinyint(2);DEFAULT:1;COMMENT:'是否完成(1 未领取 2 已领取 ) '"json:"is_get"` + KeepDecimal int `gorm:"column:keep_decimal; '"json:"keep_decimal"` + Gorm *gorm.DB `gorm:"-" json:"-"` +} + +func (this *ForexMarket) TableName() string { + return "bot_forex_market" +} + +func NewForexMarket() *ForexMarket { + ForexMarket := &ForexMarket{} + ForexMarket.NewGorm() + return ForexMarket +} + +func (this *ForexMarket) NewGorm() *ForexMarket { + this.Gorm = data.WebGorm.Table(this.TableName()) + return this +} + +func (this *ForexMarket) WhereBegin() *ForexMarket { + + this.Gorm = this.Gorm.Table(this.TableName()).Where("begin_time >= ?", time.Now().Format("2006-01-02 15:04:05")) + return this +} + +func (this *ForexMarket) WhereID() *ForexMarket { + if this.ID == 0 { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where(&ForexMarket{ID: this.ID}) + return this +} + +func (this *ForexMarket) WhereName() *ForexMarket { + if this.TradeName == "" { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where(&ForexMarket{TradeName: this.TradeName}) + return this +} + +func (this *ForexMarket) WhereModifyForex() *ForexMarket { + this.Gorm = this.Gorm.Table(this.TableName()).Where("`bot_forex_market`.`type` = ?", ModifyForex) + return this +} + +func (this *ForexMarket) WhereInID(ids []int64) *ForexMarket { + if len(ids) <= 0 { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where("id in (?)", ids) + return this +} + +func (this *ForexMarket) WhereIsTyep() *ForexMarket { + if this.IsType <= 0 { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where(&ForexMarket{IsType: this.IsType}) + return this +} + +func (this *ForexMarket) WhereIsGet() *ForexMarket { + if this.IsGet == 0 { + this.Gorm.Error = errors.New("param is null") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where(&ForexMarket{IsGet: this.IsGet}) + return this +} + +func (this *ForexMarket) Update(record *ForexMarket) *ForexMarket { + this.Gorm = this.Gorm.Updates(record) + return this +} + +func (this *ForexMarket) Create() *ForexMarket { + this.Gorm = this.Gorm.Create(&this) + return this +} +func (this *ForexMarket) Joins(sql string) *ForexMarket { + this.Gorm = this.Gorm.Joins(sql) + return this +} + +func (this *ForexMarket) Limit(limit int) *ForexMarket { + this.Gorm = this.Gorm.Limit(limit) + return this +} +func (this *ForexMarket) Offset(Offset int) *ForexMarket { + this.Gorm = this.Gorm.Offset(Offset) + return this +} +func (this *ForexMarket) First() *ForexMarket { + Accounts := ForexMarket{} + this.Gorm = this.Gorm.First(&Accounts) + return &Accounts +} + +func (this *ForexMarket) Assign(ForexMarket *ForexMarket) *ForexMarket { + this.Gorm = this.Gorm.Assign(ForexMarket) + return this +} + +func (this *ForexMarket) FirstOrCreate() *ForexMarket { + this.Gorm = this.Gorm.FirstOrCreate(&this) + return this +} + +func (this *ForexMarket) Order(value interface{}) *ForexMarket { + this.Gorm = this.Gorm.Order(value) + return this +} + +func (this *ForexMarket) Count() int64 { + var num int64 + this.Gorm = this.Gorm.Count(&num) + return num +} +func (this *ForexMarket) Select(column string) *ForexMarket { + this.Gorm = this.Gorm.Select(column) + return this +} + +func (this *ForexMarket) WhereTime(LoginAt string) *ForexMarket { + if LoginAt == "" { + this.Gorm.Error = errors.New("date require ") + return this + } + this.Gorm = this.Gorm.Table(this.TableName()).Where("login_at = ?", LoginAt) + return this +} + +func (this *ForexMarket) Pluck(column string, value interface{}) *ForexMarket { + this.Gorm = this.Gorm.Pluck(column, value) + return this +} + +func (this *ForexMarket) Find(list *[]ForexMarket) *ForexMarket { + this.Gorm = this.Gorm.Find(&list) + return this +} + +func (this *ForexMarket) List() (result []ForexMarket) { + this.IsGet = UnclaimedForex + this.WhereIsTyep().WhereIsGet().WhereBegin().WhereName().Order("id asc").Find(&result) + return result +} + +func (this *ForexMarket) ListModifyForex() (result []ForexMarket) { + this.IsGet = UnclaimedForex + this.Select("l.keep_decimal,bot_forex_market.*").WhereModifyForex().Joins("inner join bot_forex_list l on l.trade_name = bot_forex_market.trade_name").WhereIsGet().Order("id asc").Find(&result) + return result +} + +func (this *ForexMarket) UpdateIsGet(ids []int64) error { + this.IsGet = UnclaimedForex + return this.WhereInID(ids).WhereIsGet().Update(&ForexMarket{IsGet: AlreadyReceivedForex}).Gorm.Error +} + +func (this *ForexMarket) UpdateIsGetOne() error { + this.IsGet = UnclaimedForex + return this.WhereID().WhereIsGet().Update(&ForexMarket{IsGet: AlreadyReceivedForex}).Gorm.Error +} diff --git a/internal/model/pingmessage.go b/internal/model/pingmessage.go new file mode 100644 index 0000000..fcb86c4 --- /dev/null +++ b/internal/model/pingmessage.go @@ -0,0 +1,44 @@ +package model + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" +) + +type PingMessage struct { + Ping int64 `json:"ping"` +} + +type SymbolMessage struct { + Type string `json:"type"` + Symbol string `json:"symbol"` +} + +func ParsePingMessage(message string) *PingMessage { + result := PingMessage{} + err := json.Unmarshal([]byte(message), &result) + if err != nil { + return nil + } + + return &result +} + +func SubMessage(message string) *SymbolMessage { + result := SymbolMessage{} + err := json.Unmarshal([]byte(message), &result) + applogger.Info("ws param %v", message) + if err != nil { + fmt.Println("subMessage", err) + return nil + } + + return &result +} + +func ReturnValue(str string) string { + pongMsg := fmt.Sprintf("{\"type\": \"%v\"}", str) + + return pongMsg +} diff --git a/internal/model/pingv1message.go b/internal/model/pingv1message.go new file mode 100644 index 0000000..9b9982b --- /dev/null +++ b/internal/model/pingv1message.go @@ -0,0 +1,22 @@ +package model + +import "encoding/json" + +type PingV1Message struct { + Op string `json:"op"` + Timestamp int64 `json:"ts"` +} + +func (p *PingV1Message) IsPing() bool { + return p != nil && p.Op == "ping" && p.Timestamp != 0 +} + +func ParsePingV1Message(message string) *PingV1Message { + result := PingV1Message{} + err := json.Unmarshal([]byte(message), &result) + if err != nil { + return nil + } + + return &result +} diff --git a/internal/model/pingv2message.go b/internal/model/pingv2message.go new file mode 100644 index 0000000..2db11d4 --- /dev/null +++ b/internal/model/pingv2message.go @@ -0,0 +1,24 @@ +package model + +import "encoding/json" + +type PingV2Message struct { + Action string `json:"action"` + Data *struct { + Timestamp int64 `json:"ts"` + } +} + +func (p *PingV2Message) IsPing() bool { + return p != nil && p.Action == "ping" && p.Data.Timestamp != 0 +} + +func ParsePingV2Message(message string) *PingV2Message { + result := PingV2Message{} + err := json.Unmarshal([]byte(message), &result) + if err != nil { + return nil + } + + return &result +} diff --git a/internal/model/websocketv1authenticationrequest.go b/internal/model/websocketv1authenticationrequest.go new file mode 100644 index 0000000..5e9ba02 --- /dev/null +++ b/internal/model/websocketv1authenticationrequest.go @@ -0,0 +1,18 @@ +package model + +type WebSocketV1AuthenticationRequest struct { + Op string `json:"op"` + AccessKeyId string + SignatureMethod string + SignatureVersion string + Timestamp string + Signature string +} + +func (p *WebSocketV1AuthenticationRequest) Init() *WebSocketV1AuthenticationRequest { + p.Op = "auth" + p.SignatureMethod = "HmacSHA256" + p.SignatureVersion = "2" + + return p +} diff --git a/internal/model/websocketv2authenticationrequest.go b/internal/model/websocketv2authenticationrequest.go new file mode 100644 index 0000000..88bd6fd --- /dev/null +++ b/internal/model/websocketv2authenticationrequest.go @@ -0,0 +1,28 @@ +package model + +type WebSocketV2AuthenticationRequest struct { + Action string `json:"action"` + Ch string `json:"ch"` + Params *Params `json:"params"` +} + +type Params struct { + AuthType string `json:"authType"` + AccessKey string `json:"accessKey"` + SignatureMethod string `json:"signatureMethod"` + SignatureVersion string `json:"signatureVersion"` + Timestamp string `json:"timestamp"` + Signature string `json:"signature"` +} + +func (p *WebSocketV2AuthenticationRequest) Init() *WebSocketV2AuthenticationRequest { + + p.Action = "req" + p.Ch = "auth" + p.Params = new(Params) + p.Params.AuthType = "api" + p.Params.SignatureMethod = "HmacSHA256" + p.Params.SignatureVersion = "2.1" + + return p +} diff --git a/internal/mq/redis.go b/internal/mq/redis.go new file mode 100644 index 0000000..69ff969 --- /dev/null +++ b/internal/mq/redis.go @@ -0,0 +1,48 @@ +package mq + +import ( + "github.com/go-redis/redis" + "wss-pool/logging/applogger" +) + +var Rdb *redis.Client + +const ( + PublishKey = "websocket" +) + +func init() { + Rdb = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, + }) +} + +// Publish 发布消息到redis +// channel是发布的目标信道 +// payload是要发布的消息内容 +func Publish(channel string, payload string) error { + var err error + applogger.Debug("[Redis] publish [%s]: %s", channel, payload) + err = Rdb.Publish(channel, payload).Err() + if err != nil { + applogger.Error("[Redis] pulish error: %s", err.Error()) + return err + } + return err +} + +// Subscribe 订阅redis消息 +// channel是订阅的目标信道 +func Subscribe(channel string) (string, error) { + applogger.Info("[Redis] subscribe [%s]", channel) + sub := Rdb.Subscribe(channel) + msg, err := sub.ReceiveMessage() + if err != nil { + applogger.Error("[Redis] subscribe [%s]", channel) + return "", err + } + applogger.Debug("[Redis] subscribe [%s]: %s", channel, msg.String()) + return msg.Payload, err +} diff --git a/internal/mq/redis_test.go b/internal/mq/redis_test.go new file mode 100644 index 0000000..ca63b78 --- /dev/null +++ b/internal/mq/redis_test.go @@ -0,0 +1,17 @@ +package mq + +import ( + "testing" + "time" + "wss-pool/logging/applogger" +) + +// TestPublish 测试发布消息到redis +func TestPublish(t *testing.T) { + msg := "当前时间: " + time.Now().Format("15:04:05") + applogger.Debug("[publish] msg: %s", msg) + err := Publish(PublishKey, msg) + if err != nil { + applogger.Error("publish error: %s", err.Error()) + } +} diff --git a/internal/paramstr.go b/internal/paramstr.go new file mode 100644 index 0000000..96f3e46 --- /dev/null +++ b/internal/paramstr.go @@ -0,0 +1,89 @@ +package internal + +import ( + "fmt" + "github.com/gin-gonic/gin" + "strconv" + "strings" + "time" + "wss-pool/pkg/model" +) + +// Analysis of spot parameters +func StrParamStr(c *gin.Context) (*model.SpotsModel, error) { + var paramModel model.SpotsModel + err := c.BindJSON(¶mModel) + if err != nil { + return ¶mModel, err + } + return ¶mModel, err +} + +// Contract parameter analysis +func ContractStr(c *gin.Context) (*model.ContractModel, error) { + var paramModel model.ContractModel + err := c.BindJSON(¶mModel) + if err != nil { + return ¶mModel, err + } + return ¶mModel, err +} + +// Empty character replacement +func ReplaceStr(value string) string { + return strings.Replace(value, " ", "", 1) +} + +// Integer conversion +func IntegerInit(value string) int { + in, err := strconv.Atoi(value) + if err != nil { + return 0 + } + return in +} + +// TimeMaoSendToString int64 - string +func TimeMaoSendToString(i int64) string { + layout := "2006-01-02 15:04:05.000" + t := time.Unix(0, i*int64(time.Microsecond)) + + return t.Format(layout) +} + +// TimeDateToMaoSend time - int64 +func TimeDateToMaoSend(t time.Time) int64 { + now := time.Now() + fmt.Println(now) + return now.UnixMicro() +} + +// TimeStringToTime string - time +func TimeStringToTime(t string) time.Time { + timeLayout := "2006-01-02 15:04:05" + loc, err := time.LoadLocation("Local") + if err != nil { + return time.Time{} + } + theTime, err := time.ParseInLocation(timeLayout, t, loc) + if err != nil { + return time.Time{} + } + + return theTime +} + +// TimeStringToIn64 string - int64 +func TimeStringToIn64(t string) int64 { + timeLayout := "2006-01-02 15:04:05" + loc, err := time.LoadLocation("Local") + if err != nil { + return int64(0) + } + theTime, err := time.ParseInLocation(timeLayout, t, loc) + if err != nil { + return int64(0) + } + + return theTime.UnixMicro() +} diff --git a/internal/pubsub/pubsub.go b/internal/pubsub/pubsub.go new file mode 100644 index 0000000..099d4e6 --- /dev/null +++ b/internal/pubsub/pubsub.go @@ -0,0 +1,85 @@ +package pubsub + +import ( + "sync" + "time" +) + +type ( + subscriber chan interface{} //订阅者,类型为管道 + topicFunc func(v interface{}) bool //主题,是一个过滤器函数 +) + +// 发布者对象 +type publisher struct { + m sync.RWMutex //读写锁 + buffer int //订阅队列缓存大小 + timeout time.Duration //发布超时时间 + subscribers map[subscriber]topicFunc //订阅者信息 +} + +// 构建一个新的发布者对象 +func NewPublisher(buffer int, publishTimeout time.Duration) *publisher { + return &publisher{ + m: sync.RWMutex{}, + buffer: buffer, + timeout: publishTimeout, + subscribers: make(map[subscriber]topicFunc), + } +} + +// 添加一个新的订阅者,订阅过滤器筛选后的主题 +func (p *publisher) SubscriberTopic(topic topicFunc) chan interface{} { + ch := make(chan interface{}, p.buffer) + p.m.Lock() + defer p.m.Unlock() + p.subscribers[ch] = topic + return ch +} + +// 添加一个订阅者,订阅所有主题 +func (p *publisher) SubscriberAllTopic() chan interface{} { + return p.SubscriberTopic(nil) +} + +// 退出订阅 +func (p *publisher) Exict(sub chan interface{}) { + p.m.Lock() + defer p.m.Unlock() + delete(p.subscribers, sub) + close(sub) +} + +// 关闭发布者对象,同时关闭所有订阅者管道 +func (p *publisher) Close() { + p.m.Lock() + defer p.m.Unlock() + + for sub := range p.subscribers { + close(sub) + delete(p.subscribers, sub) + } +} + +// 发布一个主题 +func (p *publisher) Publish(v interface{}) { + p.m.RLock() + defer p.m.RUnlock() + wg := sync.WaitGroup{} + for sub, topic := range p.subscribers { //向所有的订阅者管道发送主题 + wg.Add(1) + go p.SendTopic(sub, topic, v, &wg) + } +} + +// 向订阅者发送主题 +func (p *publisher) SendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) { + defer wg.Done() + if topic != nil && !topic(v) { //订阅者未订阅这个主题,不发送 + return + } + select { + case sub <- v: + case <-time.After(p.timeout): //超时后就不再发送 + } +} diff --git a/internal/pubsub/pubsub_test.go b/internal/pubsub/pubsub_test.go new file mode 100644 index 0000000..3d9022c --- /dev/null +++ b/internal/pubsub/pubsub_test.go @@ -0,0 +1,42 @@ +package pubsub + +import ( + "fmt" + "strings" + "testing" + "time" +) + +func Test_NewPublisher(t *testing.T) { + //初始化一个发布者对象 + publisher := NewPublisher(3, 5*time.Second) + //创建一个订阅所有主题的订阅者 + all := publisher.SubscriberAllTopic() + //创建一个订阅golang主题的订阅者 + golang := publisher.SubscriberTopic(func(v interface{}) bool { + if s, ok := v.(string); ok { + return strings.Contains(s, "golang") + } + return false + }) + + //发布2条主题 + publisher.Publish("hello world") + publisher.Publish("hello golang") + + go func() { + for i := range all { + fmt.Println("all:", i) + } + }() + + go func() { + for i := range golang { + fmt.Println("golang:", i) + } + }() + + time.Sleep(5 * time.Second) + publisher.Close() + fmt.Println(<-all, <-golang) //发布者对象关闭后读取的都是nil +} diff --git a/internal/redis/redis.go b/internal/redis/redis.go new file mode 100644 index 0000000..7208297 --- /dev/null +++ b/internal/redis/redis.go @@ -0,0 +1,233 @@ +package redis + +import ( + "fmt" + "github.com/go-redis/redis" + "os" + "strconv" + "time" + "wss-pool/config" + "wss-pool/logging/applogger" +) + +var RedisClient *redis.Client +var RedisClientMap = map[string]*redis.Client{} + +// RedisInit init Redis +func RedisInit(db int) *redis.Client { + addr := fmt.Sprintf("%v:%v", config.Config.Redis.Server, config.Config.Redis.Port) + client := redis.NewClient(&redis.Options{ + Addr: addr, + DB: db, // use default DB + Password: config.Config.Redis.PassWord, // no password set + }) + _, err := client.Ping().Result() + if err != nil { + applogger.Error("Failed to connect to Redis, terminating startup err: %v", err) + return nil + } else { + applogger.Info("redis init success") + } + + return client +} + +func RedisInitMap(dbs []string) { + RedisClientMap = make(map[string]*redis.Client) + for key, db := range dbs { + dbInt, _ := strconv.Atoi(db) // 表库 + server := config.Config.Redis.Server // Ip + password := config.Config.Redis.PassWord // 密码 + addr := fmt.Sprintf("%v:%v", server, config.Config.Redis.Port) // 0.0.0.0:6379 + client := redis.NewClient(&redis.Options{ + Addr: addr, + DB: dbInt, + Password: password, + }) + _, err := client.Ping().Result() + if err != nil { + os.Exit(dbInt) + } else { + applogger.Info("redis init success") + } + if key == 0 { + RedisClient = client + } + RedisClientMap[server] = client + } +} + +// TODO: 初始化多个Redis客户端 +func RedisInitMapList(addrList map[string]string) { + RedisClientMap = make(map[string]*redis.Client) + for addr, password := range addrList { + client := redis.NewClient(&redis.Options{ + Addr: addr, + DB: 0, + Password: password, + }) + _, err := client.Ping().Result() + if err != nil { + os.Exit(0) + } else { + applogger.Info("redis init success") + } + RedisClientMap[addr] = client + } +} + +// Get_Cache_Data Query data through key +func Get_Cache_Data(key string) (string, error) { + rge, err := RedisClient.Get(key).Result() + if err != nil { + applogger.Error("Redis Get err: %v", err) + return "", err + } + + return rge, nil +} + +// Get_Cache_Data Query data through key + +func Get_Cache_Byte(key string) ([]byte, error) { + rge, err := RedisClient.Get(key).Bytes() + if err != nil { + applogger.Error("Redis Get err: %v", err) + return rge, err + } + + return rge, nil +} + +// Set_Cache_Data Set the key lifecycle +func Set_Cache_Data(key string, value interface{}, td int) error { + if err := RedisClient.Set(key, value, time.Duration(td)*time.Minute).Err(); err != nil { + applogger.Error("Redis Set err: %v", err) + return err + } + return nil +} + +// Get_Cache_Keys Query all keys +func Get_Cache_Keys() ([]string, error) { + rge, err := RedisClient.Keys("*").Result() + if err != nil { + applogger.Error("Redis Get err: %v", err) + return []string{}, err + } + + return rge, nil +} + +// Get_Cache_Count Query the total number of Redis +func Get_Cache_Count(key string) (int64, error) { + rge, err := RedisClient.DbSize().Result() + if err != nil { + applogger.Error("RedisClient DbSize err:%v", err) + return rge, err + } + return rge, nil +} + +// Set_Cache_Value persistent data +func Set_Cache_Value(key, value string) error { + if err := RedisClient.Set(key, value, 0).Err(); err != nil { + applogger.Error("Redis Set err: %v", err) + return err + } + return nil +} + +func HsetMap(key, field string, value interface{}) { + for k, db := range RedisClientMap { + applogger.Info("key", key, "field", field, "value", value, "DB", k) + err := db.HSet(key, field, value).Err() + if err != nil { + fmt.Println("db", k, "存储失败:", err) + } + } +} + +func PublishMap(channel string, message interface{}) { + for k, db := range RedisClientMap { + applogger.Info("channel", channel, "DB", k) + err := db.Publish(channel, message).Err() + if err != nil { + fmt.Println("db", k, "存储失败:", err) + } + } +} + +func ScanMap(prefix string) map[string][]string { + scanMap := make(map[string][]string) + for k, db := range RedisClientMap { + var keys []string + var err error + keys, err = db.Keys(prefix + "*").Result() + if err != nil { + applogger.Error(err.Error()) + } + scanMap[k] = keys + } + return scanMap +} + +func HGetAllMap(db string, key string) (map[string]string, error) { + return RedisClientMap[db].HGetAll(key).Result() +} + +func Hset(key, field string, value interface{}) error { + applogger.Info("key", key, "field", field, "value", value) + err := RedisClient.HSet(key, field, value).Err() + if err != nil { + fmt.Println("存储失败:", err) + return err + } + return nil +} + +func Expire(key string, expiration time.Duration) error { + err := RedisClient.Expire(key, expiration).Err() + if err != nil { + fmt.Println("设置过期时间:", err) + return err + } + return nil +} +func Hget(key, field string) (string, error) { + value, err := RedisClient.HGet(key, field).Result() + if err != nil { + return value, err + } + return value, nil +} +func HGetNew(key, field string, red *redis.Client) (string, error) { + value, err := red.HGet(key, field).Result() + if err != nil { + return value, err + } + return value, nil +} + +func HDel(key string, fields string) error { + if err := RedisClient.HDel(key, fields).Err(); err != nil { + fmt.Println("del error:", err) + return err + } + return nil +} + +func HGetAll(key string) (map[string]string, error) { + return RedisClient.HGetAll(key).Result() +} + +func Scan(prefix string) []string { + var keys []string + var err error + keys, err = RedisClient.Keys(prefix + "*").Result() + if err != nil { + applogger.Error(err.Error()) + } + //fmt.Println(keys) + return keys +} diff --git a/internal/requestbuilder/privateurlbuilder.go b/internal/requestbuilder/privateurlbuilder.go new file mode 100644 index 0000000..5d7e289 --- /dev/null +++ b/internal/requestbuilder/privateurlbuilder.go @@ -0,0 +1,60 @@ +package requestbuilder + +import ( + "fmt" + "net/url" + "time" + "wss-pool/pkg/model" +) + +type PrivateUrlBuilder struct { + host string + akKey string + akValue string + smKey string + smValue string + svKey string + svValue string + tKey string + + signer *Signer +} + +func (p *PrivateUrlBuilder) Init(accessKey string, secretKey string, host string) *PrivateUrlBuilder { + p.akKey = "AccessKeyId" + p.akValue = accessKey + p.smKey = "SignatureMethod" + p.smValue = "HmacSHA256" + p.svKey = "SignatureVersion" + p.svValue = "2" + p.tKey = "Timestamp" + + p.host = host + p.signer = new(Signer).Init(secretKey) + + return p +} + +func (p *PrivateUrlBuilder) Build(method string, path string, request *model.GetRequest) string { + time := time.Now().UTC() + + return p.BuildWithTime(method, path, time, request) +} + +func (p *PrivateUrlBuilder) BuildWithTime(method string, path string, utcDate time.Time, request *model.GetRequest) string { + time := utcDate.Format("2006-01-02T15:04:05") + + req := new(model.GetRequest).InitFrom(request) + req.AddParam(p.akKey, p.akValue) + req.AddParam(p.smKey, p.smValue) + req.AddParam(p.svKey, p.svValue) + req.AddParam(p.tKey, time) + + parameters := req.BuildParams() + + signature := p.signer.Sign(method, p.host, path, parameters) + + url := fmt.Sprintf("https://%s%s?%s&Signature=%s", p.host, path, parameters, url.QueryEscape(signature)) + + return url +} diff --git a/internal/requestbuilder/publicurlbuilder.go b/internal/requestbuilder/publicurlbuilder.go new file mode 100644 index 0000000..f59e73b --- /dev/null +++ b/internal/requestbuilder/publicurlbuilder.go @@ -0,0 +1,25 @@ +package requestbuilder + +import ( + "fmt" + "wss-pool/pkg/model" +) + +type PublicUrlBuilder struct { + host string +} + +func (p *PublicUrlBuilder) Init(host string) *PublicUrlBuilder { + p.host = host + return p +} + +func (p *PublicUrlBuilder) Build(path string, request *model.GetRequest) string { + if request != nil { + result := fmt.Sprintf("https://%s%s?%s", p.host, path, request.BuildParams()) + return result + } else { + result := fmt.Sprintf("https://%s%s", p.host, path) + return result + } +} diff --git a/internal/requestbuilder/signer.go b/internal/requestbuilder/signer.go new file mode 100644 index 0000000..d593dd6 --- /dev/null +++ b/internal/requestbuilder/signer.go @@ -0,0 +1,41 @@ +package requestbuilder + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "strings" +) + +type Signer struct { + key []byte +} + +func (p *Signer) Init(key string) *Signer { + p.key = []byte(key) + return p +} + +func (p *Signer) Sign(method string, host string, path string, parameters string) string { + if method == "" || host == "" || path == "" || parameters == "" { + return "" + } + + var sb strings.Builder + sb.WriteString(method) + sb.WriteString("\n") + sb.WriteString(host) + sb.WriteString("\n") + sb.WriteString(path) + sb.WriteString("\n") + sb.WriteString(parameters) + + return p.sign(sb.String()) +} + +func (p *Signer) sign(payload string) string { + hash := hmac.New(sha256.New, p.key) + hash.Write([]byte(payload)) + result := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + return result +} diff --git a/internal/requestbuilder/websocketv1requestbuilder.go b/internal/requestbuilder/websocketv1requestbuilder.go new file mode 100644 index 0000000..e94886f --- /dev/null +++ b/internal/requestbuilder/websocketv1requestbuilder.go @@ -0,0 +1,65 @@ +package requestbuilder + +import ( + "time" + "wss-pool/internal/model" + model2 "wss-pool/pkg/model" +) + +type WebSocketV1RequestBuilder struct { + akKey string + akValue string + smKey string + smValue string + svKey string + svValue string + tKey string + tValue string + + host string + path string + + signer *Signer +} + +func (p *WebSocketV1RequestBuilder) Init(accessKey string, secretKey string, host string, path string) *WebSocketV1RequestBuilder { + p.akKey = "AccessKeyId" + p.akValue = accessKey + p.smKey = "SignatureMethod" + p.smValue = "HmacSHA256" + p.svKey = "SignatureVersion" + p.svValue = "2" + p.tKey = "Timestamp" + + p.host = host + p.path = path + + p.signer = new(Signer).Init(secretKey) + + return p +} + +func (p *WebSocketV1RequestBuilder) Build() (string, error) { + time := time.Now().UTC() + return p.build(time) +} + +func (p *WebSocketV1RequestBuilder) build(utcDate time.Time) (string, error) { + time := utcDate.Format("2006-01-02T15:04:05") + + req := new(model2.GetRequest).Init() + req.AddParam(p.akKey, p.akValue) + req.AddParam(p.smKey, p.smValue) + req.AddParam(p.svKey, p.svValue) + req.AddParam(p.tKey, time) + + signature := p.signer.Sign("GET", p.host, p.path, req.BuildParams()) + + auth := new(model.WebSocketV1AuthenticationRequest).Init() + auth.AccessKeyId = p.akValue + auth.Timestamp = time + auth.Signature = signature + + result, err := model2.ToJson(auth) + return result, err +} diff --git a/internal/requestbuilder/websocketv2requestbuilder.go b/internal/requestbuilder/websocketv2requestbuilder.go new file mode 100644 index 0000000..acea88d --- /dev/null +++ b/internal/requestbuilder/websocketv2requestbuilder.go @@ -0,0 +1,65 @@ +package requestbuilder + +import ( + "time" + "wss-pool/internal/model" + model2 "wss-pool/pkg/model" +) + +type WebSocketV2RequestBuilder struct { + akKey string + akValue string + smKey string + smValue string + svKey string + svValue string + tKey string + tValue string + + host string + path string + + signer *Signer +} + +func (p *WebSocketV2RequestBuilder) Init(accessKey string, secretKey string, host string, path string) *WebSocketV2RequestBuilder { + p.akKey = "accessKey" + p.akValue = accessKey + p.smKey = "signatureMethod" + p.smValue = "HmacSHA256" + p.svKey = "signatureVersion" + p.svValue = "2.1" + p.tKey = "timestamp" + + p.host = host + p.path = path + + p.signer = new(Signer).Init(secretKey) + + return p +} + +func (p *WebSocketV2RequestBuilder) Build() (string, error) { + time := time.Now().UTC() + return p.build(time) +} + +func (p *WebSocketV2RequestBuilder) build(utcDate time.Time) (string, error) { + time := utcDate.Format("2006-01-02T15:04:05") + + req := new(model2.GetRequest).Init() + req.AddParam(p.akKey, p.akValue) + req.AddParam(p.smKey, p.smValue) + req.AddParam(p.svKey, p.svValue) + req.AddParam(p.tKey, time) + + signature := p.signer.Sign("GET", p.host, p.path, req.BuildParams()) + + auth := new(model.WebSocketV2AuthenticationRequest).Init() + auth.Params.AccessKey = p.akValue + auth.Params.Timestamp = time + auth.Params.Signature = signature + + result, err := model2.ToJson(auth) + return result, err +} diff --git a/logging/applogger/applogger.go b/logging/applogger/applogger.go new file mode 100644 index 0000000..0590d08 --- /dev/null +++ b/logging/applogger/applogger.go @@ -0,0 +1,55 @@ +package applogger + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" +) + +var sugaredLogger *zap.SugaredLogger +var atomicLevel zap.AtomicLevel + +func init() { + encoderCfg := zapcore.EncoderConfig{ + TimeKey: "time", + MessageKey: "msg", + LevelKey: "level", + EncodeLevel: zapcore.CapitalColorLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + } + + // define default level as debug level + atomicLevel = zap.NewAtomicLevel() + atomicLevel.SetLevel(zapcore.DebugLevel) + + core := zapcore.NewCore(zapcore.NewConsoleEncoder(encoderCfg), os.Stdout, atomicLevel) + sugaredLogger = zap.New(core).Sugar() +} + +func SetLevel(level zapcore.Level) { + atomicLevel.SetLevel(level) +} + +func Fatal(template string, args ...interface{}) { + sugaredLogger.Fatalf(template, args...) +} + +func Error(template string, args ...interface{}) { + sugaredLogger.Errorf(template, args...) +} + +func Panic(template string, args ...interface{}) { + sugaredLogger.Panicf(template, args...) +} + +func Warn(template string, args ...interface{}) { + sugaredLogger.Warnf(template, args...) +} + +func Info(template string, args ...interface{}) { + sugaredLogger.Infof(template, args...) +} + +func Debug(template string, args ...interface{}) { + sugaredLogger.Debugf(template, args...) +} diff --git a/logging/perflogger/performancelogger.go b/logging/perflogger/performancelogger.go new file mode 100644 index 0000000..d1521d6 --- /dev/null +++ b/logging/perflogger/performancelogger.go @@ -0,0 +1,84 @@ +package perflogger + +import ( + "fmt" + "log" + "os" + "strings" + "time" +) + +// The global PerformanceLogger instance +var performanceLogger *PerformanceLogger + +// The global switch for Performance logging +var logEnabled = false + +type PerformanceLogger struct { + logger *log.Logger + enable bool + file *os.File + index int + start time.Time +} + +// Enable performance logger and initialize the global instance +// This method should be called once +func Enable(enable bool) { + logEnabled = enable + if logEnabled && performanceLogger == nil { + performanceLogger = new(PerformanceLogger).init() + } +} + +// Get the global PerformanceLogger instance +func GetInstance() *PerformanceLogger { + return performanceLogger +} + +// Initialize the instance +func (p *PerformanceLogger) init() *PerformanceLogger { + if logEnabled { + var err error + fileName := time.Now().Format("20060102_150405.txt") + p.file, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + log.Fatalln("Failed to open file: ", fileName) + } + p.logger = log.New(p.file, "", 0) + p.index = 1 + } + + return p +} + +// Start timer +func (p *PerformanceLogger) Start() { + if logEnabled { + p.start = time.Now() + } +} + +// Stop timer and output log +func (p *PerformanceLogger) StopAndLog(method string, url string) { + if logEnabled { + duration := time.Since(p.start).Milliseconds() + + // Strip parameters + i := strings.IndexByte(url, '?') + var path string + if i > 0 { + path = url[0:i] + } else { + path = url + } + + // Log the header before first record + if p.index == 1 { + p.logger.Println("Index, Duration(ms), URL") + } + p.logger.Println(fmt.Sprintf("%d, %d, %s %s", p.index, duration, method, path)) + + p.index++ + } +} diff --git a/pkg/bawssclient/candlestickwebsocketclient.go b/pkg/bawssclient/candlestickwebsocketclient.go new file mode 100644 index 0000000..f8229c6 --- /dev/null +++ b/pkg/bawssclient/candlestickwebsocketclient.go @@ -0,0 +1,137 @@ +package bawssclient + +import ( + "encoding/json" + "fmt" + "github.com/shopspring/decimal" + "strings" + "wss-pool/dictionary" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/bawebsocketclientbase" + "wss-pool/pkg/model/market" +) + +type BaKLineParam struct { + Method string `json:"method"` + Params []string `json:"params"` + ID int64 `json:"id"` +} + +type BaKLineResponseK struct { + T int64 `json:"t"` + Ts int64 `json:"T"` + S string `json:"s"` + I string `json:"i"` + F int64 `json:"f"` + L interface{} `json:"L"` + O string `json:"o"` + C string `json:"c"` + H string `json:"h"` + l string `json:"l"` + V string `json:"v"` + N int `json:"n"` + X bool `json:"x"` + Vs string `json:"V"` + Q string `json:"Q"` + B string `json:"B"` +} + +type BaKLineResponse struct { + E string `json:"e"` //事件类型 + Es int64 `json:"E"` //事件时间 + S string `json:"s"` //交易对 + K BaKLineResponseK `json:"k"` +} + +// Responsible to handle candlestick data from WebSocket +type CandlestickWebSocketClient struct { + bawebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *CandlestickWebSocketClient) Init(host string) *CandlestickWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Request the full candlestick data according to specified criteria +//func (p *CandlestickWebSocketClient) Request(symbol string) { +// topic := fmt.Sprintf("market.%s.kline.%s", symbol, period) +// req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) +// +// p.Send(req) +// +// applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +//} + +// Set callback handler +func (p *CandlestickWebSocketClient) SetHandler( + connectedHandler bawebsocketclientbase.ConnectedHandler, + responseHandler bawebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full candlestick data according to specified criteria + +// Subscribe candlestick data +func (p *CandlestickWebSocketClient) Subscribe(symbol string) { + topis := make([]string, 0) + for _, v := range dictionary.BaTimeCycle { + topis = append(topis, fmt.Sprintf("%susdt@kline_%s", symbol, v)) + } + sub := BaKLineParam{ + Method: "SUBSCRIBE", + Params: topis, + ID: bawebsocketclientbase.Randomnumber(), + } + data, _ := json.Marshal(sub) + p.Send(data) + + applogger.Info("WebSocket subscribed, clientId=%s", sub.ID) +} + +// Unsubscribe candlestick data +func (p *CandlestickWebSocketClient) UnSubscribe(symbol string) { + topis := make([]string, 0) + for _, v := range dictionary.BaTimeCycle { + topis = append(topis, fmt.Sprintf("%susdt@%s", symbol, v)) + } + sub := BaKLineParam{ + Method: "UNSUBSCRIBE", + Params: topis, + ID: bawebsocketclientbase.Randomnumber(), + } + data, _ := json.Marshal(sub) + + p.Send(data) + + applogger.Info("WebSocket unsubscribed, clientId=%s", sub.ID) +} + +func (p *CandlestickWebSocketClient) handleMessage(msg string) (interface{}, error) { + baRes := BaKLineResponse{} + err := json.Unmarshal([]byte(msg), &baRes) + //转火币数据结构 + amount, _ := decimal.NewFromString(baRes.K.V) + open, _ := decimal.NewFromString(baRes.K.O) + close, _ := decimal.NewFromString(baRes.K.C) + low, _ := decimal.NewFromString(baRes.K.l) + high, _ := decimal.NewFromString(baRes.K.H) + vol, _ := decimal.NewFromString(baRes.K.Q) + result := market.SubscribeCandlestickResponse{ + Channel: fmt.Sprintf("market.%s.kline.%s", strings.ToLower(baRes.S), dictionary.BaToBaMap[baRes.K.I]), + Timestamp: baRes.Es, + Tick: &market.Tick{ + Id: baRes.Es, + Amount: amount, + Count: baRes.K.N, + Open: open, + Close: close, + Low: low, + High: high, + Vol: vol, + IsBa: bawebsocketclientbase.BinAnce, + }, + } + return result, err +} diff --git a/pkg/bawssclient/depthwebsocketclient.go b/pkg/bawssclient/depthwebsocketclient.go new file mode 100644 index 0000000..b6302da --- /dev/null +++ b/pkg/bawssclient/depthwebsocketclient.go @@ -0,0 +1,114 @@ +package bawssclient + +import ( + "encoding/json" + "fmt" + "github.com/shopspring/decimal" + "wss-pool/dictionary" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/bawebsocketclientbase" + "wss-pool/pkg/model/market" +) + +type SubscribeDepthResponse struct { + LastUpdateId string `json:"lastUpdateId"` + Bids [][]string `json:"bids"` + Asks [][]string `json:"asks"` +} + +// Responsible to handle Depth data from WebSocket +type DepthWebSocketClient struct { + bawebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *DepthWebSocketClient) Init(host string) *DepthWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Request the full Depth data according to specified criteria +//func (p *DepthWebSocketClient) Request(symbol string) { +// topic := fmt.Sprintf("market.%s.kline.%s", symbol, period) +// req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) +// +// p.Send(req) +// +// applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +//} + +// Set callback handler +func (p *DepthWebSocketClient) SetHandler( + connectedHandler bawebsocketclientbase.ConnectedHandler, + responseHandler bawebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full Depth data according to specified criteria + +// Subscribe Depth data +func (p *DepthWebSocketClient) Subscribe(symbol string) { + topis := make([]string, 0) + sub := BaKLineParam{ + Method: "SUBSCRIBE", + Params: append(topis, fmt.Sprintf("%susdt@depth5@100ms", symbol)), + ID: bawebsocketclientbase.Randomnumber(), + } + data, _ := json.Marshal(sub) + p.Send(data) + + applogger.Info("WebSocket subscribed, clientId=%s", sub.ID) +} + +// Unsubscribe Depth data +func (p *DepthWebSocketClient) UnSubscribe(symbol string) { + topis := make([]string, 0) + for _, v := range dictionary.BaTimeCycle { + topis = append(topis, fmt.Sprintf("%susdt@%s", symbol, v)) + } + sub := BaKLineParam{ + Method: "UNSUBSCRIBE", + Params: append(topis, fmt.Sprintf("%susdt@depth5@100ms", symbol)), + ID: bawebsocketclientbase.Randomnumber(), + } + data, _ := json.Marshal(sub) + + p.Send(data) + + applogger.Info("WebSocket unsubscribed, clientId=%s", sub.ID) +} + +func (p *DepthWebSocketClient) handleMessage(msg string) (interface{}, error) { + baRes := SubscribeDepthResponse{} + err := json.Unmarshal([]byte(msg), &baRes) + //转火币数据结构 + bids := make([][]decimal.Decimal, 0) + for _, v := range baRes.Bids { + if len(v) >= 2 { + info := make([]decimal.Decimal, 0) + price, _ := decimal.NewFromString(v[0]) + info = append(info, price) + size, _ := decimal.NewFromString(v[1]) + info = append(info, size) + bids = append(bids, info) + } + } + asks := make([][]decimal.Decimal, 0) + for _, v := range baRes.Asks { + if len(v) >= 2 { + info := make([]decimal.Decimal, 0) + price, _ := decimal.NewFromString(v[0]) + info = append(info, price) + size, _ := decimal.NewFromString(v[1]) + info = append(info, size) + asks = append(asks, info) + } + } + result := market.SubscribeMarketByPriceResponse{ + Tick: &market.MarketByPrice{ + Bids: bids, + Asks: asks, + }, + } + return result, err +} diff --git a/pkg/bawssclient/tickerwebsocketclient.go b/pkg/bawssclient/tickerwebsocketclient.go new file mode 100644 index 0000000..6205467 --- /dev/null +++ b/pkg/bawssclient/tickerwebsocketclient.go @@ -0,0 +1,122 @@ +package bawssclient + +import ( + "encoding/json" + "fmt" + "github.com/shopspring/decimal" + "strings" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/bawebsocketclientbase" + "wss-pool/pkg/model/market" +) + +type BaTickerResponse struct { + E string `json:"e"` + Es int64 `json:"e"` + S string `json:"s"` + P string `json:"p"` + Ps string `json:"P"` + W string `json:"w"` + X string `json:"x"` + C string `json:"c"` + Qs string `json:"Q"` + B string `json:"b"` + Bs string `json:"B"` + A string `json:"a"` + As string `json:"A"` + O string `json:"o"` + H string `json:"h"` + L string `json:"l"` + V string `json:"v"` + Q string `json:"q"` + Os int64 `json:"O"` + Cs int64 `json:"c"` + F int64 `json:"f"` + Ls int64 `json:"L"` + N int `json:"n"` +} + +// Responsible to handle Ticker data from WebSocket +type TickerWebSocketClient struct { + bawebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *TickerWebSocketClient) Init(host string) *TickerWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *TickerWebSocketClient) SetHandler( + connectedHandler bawebsocketclientbase.ConnectedHandler, + responseHandler bawebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full Ticker data according to specified criteria + +// Subscribe Ticker data +func (p *TickerWebSocketClient) Subscribe(symbol string) { + topis := make([]string, 0) + sub := BaKLineParam{ + Method: "SUBSCRIBE", + Params: append(topis, fmt.Sprintf("%susdt@ticker", symbol)), + ID: bawebsocketclientbase.Randomnumber(), + } + data, _ := json.Marshal(sub) + p.Send(data) + + applogger.Info("WebSocket subscribed, clientId=%s", sub.ID) +} + +// Unsubscribe Ticker data +func (p *TickerWebSocketClient) UnSubscribe(symbol string) { + topis := make([]string, 0) + sub := BaKLineParam{ + Method: "UNSUBSCRIBE", + Params: append(topis, fmt.Sprintf("%susdt@ticker", symbol)), + ID: bawebsocketclientbase.Randomnumber(), + } + data, _ := json.Marshal(sub) + p.Send(data) + + applogger.Info("WebSocket unsubscribed, clientId=%s", sub.ID) +} + +func (p *TickerWebSocketClient) handleMessage(msg string) (interface{}, error) { + baRes := BaTickerResponse{} + err := json.Unmarshal([]byte(msg), &baRes) + //转火币数据结构 + amount, _ := decimal.NewFromString(baRes.V) + open, _ := decimal.NewFromString(baRes.X) // 整整24小时之前,向前数的最后一次成交价格 + close, _ := decimal.NewFromString(baRes.C) + low, _ := decimal.NewFromString(baRes.L) + high, _ := decimal.NewFromString(baRes.H) + Bid, _ := decimal.NewFromString(baRes.B) + BidSize, _ := decimal.NewFromString(baRes.Bs) + Ask, _ := decimal.NewFromString(baRes.A) + AskSize, _ := decimal.NewFromString(baRes.As) + LastPrice, _ := decimal.NewFromString(baRes.C) + LastSize, _ := decimal.NewFromString(baRes.Qs) + result := market.TickerWebsocketResponse{ + Channel: fmt.Sprintf("market.%s.ticker", strings.ToLower(baRes.S)), + Timestamp: baRes.Es, + Tick: &market.TickR{ + Open: open, + High: high, + Low: low, + Close: close, + Amount: amount, + Count: baRes.N, + Bid: Bid, + BidSize: BidSize, + Ask: Ask, + AskSize: AskSize, + LastPrice: LastPrice, + LastSize: LastSize, + IsBa: bawebsocketclientbase.BinAnce, + }, + } + return result, err +} diff --git a/pkg/client/bawebsocketclientbase/common.go b/pkg/client/bawebsocketclientbase/common.go new file mode 100644 index 0000000..16dd934 --- /dev/null +++ b/pkg/client/bawebsocketclientbase/common.go @@ -0,0 +1,13 @@ +package bawebsocketclientbase + +import ( + "math/rand" + "time" +) + +func Randomnumber() int64 { + rand.Seed(time.Now().UnixNano()) + n := int64(9999999999999) // 上限值 + randomInt := rand.Int63n(n) + return randomInt +} diff --git a/pkg/client/bawebsocketclientbase/websocketclientbase.go b/pkg/client/bawebsocketclientbase/websocketclientbase.go new file mode 100644 index 0000000..ac95c19 --- /dev/null +++ b/pkg/client/bawebsocketclientbase/websocketclientbase.go @@ -0,0 +1,245 @@ +package bawebsocketclientbase + +import ( + "fmt" + "github.com/gorilla/websocket" + "strings" + "sync" + "time" + "wss-pool/internal/model" + "wss-pool/logging/applogger" +) + +const ( + TimerIntervalSecond = 5 + ReconnectWaitSecond = 60 + BinAnce int = 2 + wsPath = "/ws" +) + +// It will be invoked after websocket connected +type ConnectedHandler func() + +// It will be invoked after valid message received +type MessageHandler func(message string) (interface{}, error) + +// It will be invoked after response is parsed +type ResponseHandler func(response interface{}) + +// The base class that responsible to get data from websocket +type WebSocketClientBase struct { + host string + path string + conn *websocket.Conn + connectedHandler ConnectedHandler + messageHandler MessageHandler + responseHandler ResponseHandler + stopReadChannel chan int + stopTickerChannel chan int + ticker *time.Ticker + lastReceivedTime time.Time + sendMutex *sync.Mutex +} + +// Initializer +func (p *WebSocketClientBase) Init(host string) *WebSocketClientBase { + p.host = host + p.path = wsPath + p.stopReadChannel = make(chan int, 1) + p.stopTickerChannel = make(chan int, 1) + p.sendMutex = &sync.Mutex{} + + return p +} + +// Initializer with path +func (p *WebSocketClientBase) InitWithFeedPath(host string) *WebSocketClientBase { + p.Init(host) + return p +} + +// Set callback handler +func (p *WebSocketClientBase) SetHandler(connHandler ConnectedHandler, msgHandler MessageHandler, repHandler ResponseHandler) { + p.connectedHandler = connHandler + p.messageHandler = msgHandler + p.responseHandler = repHandler +} + +// Connect to websocket websocketserver +// if autoConnect is true, then the connection can be re-connect if no data received after the pre-defined timeout +func (p *WebSocketClientBase) Connect(autoConnect bool) { + // initialize last received time as now + p.lastReceivedTime = time.Now() + + // connect to websocket + p.connectWebSocket() + + // start ticker to manage connection + if autoConnect { + p.startTicker() + } +} + +// Send data to websocket websocketserver +func (p *WebSocketClientBase) Send(data []byte) { + time.Sleep(600 * time.Microsecond) + if p.conn == nil { + applogger.Error("WebSocket sent error: no connection available") + return + } + applogger.Info("param", + string(data)) + p.sendMutex.Lock() + err := p.conn.WriteMessage(websocket.TextMessage, data) + p.sendMutex.Unlock() + + if err != nil { + applogger.Error("WebSocket sent error: data=%s, error=%s", string(data), err) + } +} + +// Close the connection to websocketserver +func (p *WebSocketClientBase) Close() { + p.stopTicker() + p.disconnectWebSocket() +} + +// connect to websocketserver +func (p *WebSocketClientBase) connectWebSocket() { + var err error + url := fmt.Sprintf("wss://%s%s", p.host, p.path) + applogger.Debug("WebSocket connecting...", url) + p.conn, _, err = websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("WebSocket connected error: %s", err) + return + } + applogger.Info("WebSocket connected") + + // start loop to read and handle message + p.startReadLoop() + + if p.connectedHandler != nil { + p.connectedHandler() + } +} + +// disconnect with websocketserver +func (p *WebSocketClientBase) disconnectWebSocket() { + if p.conn == nil { + return + } + + // start a new goroutine to send stop signal + go p.stopReadLoop() + + applogger.Debug("WebSocket disconnecting...") + err := p.conn.Close() + if err != nil { + applogger.Error("WebSocket disconnect error: %s", err) + return + } + + applogger.Info("WebSocket disconnected") +} + +// initialize a ticker and start a goroutine tickerLoop() +func (p *WebSocketClientBase) startTicker() { + p.ticker = time.NewTicker(TimerIntervalSecond * time.Second) + + go p.tickerLoop() +} + +// stop ticker and stop the goroutine +func (p *WebSocketClientBase) stopTicker() { + p.ticker.Stop() + p.stopTickerChannel <- 1 +} + +// defines a for loop that will run based on ticker's frequency +// It checks the last data that received from websocketserver, if it is longer than the threshold, +// it will force disconnect websocketserver and connect again. +func (p *WebSocketClientBase) tickerLoop() { + applogger.Debug("tickerLoop started") + for { + select { + // Receive data from stopChannel + case <-p.stopTickerChannel: + applogger.Debug("tickerLoop stopped") + return + + // Receive tick from tickChannel + case <-p.ticker.C: + elapsedSecond := time.Now().Sub(p.lastReceivedTime).Seconds() + applogger.Debug("WebSocket received data %f sec ago", elapsedSecond) + + if elapsedSecond > ReconnectWaitSecond { + applogger.Info("WebSocket reconnect...") + p.disconnectWebSocket() + p.connectWebSocket() + } + } + } +} + +// start a goroutine readLoop() +func (p *WebSocketClientBase) startReadLoop() { + go p.readLoop() +} + +// stop the goroutine readLoop() +func (p *WebSocketClientBase) stopReadLoop() { + p.stopReadChannel <- 1 +} + +// defines a for loop to read data from websocketserver +// it will stop once it receives the signal from stopReadChannel +func (p *WebSocketClientBase) readLoop() { + applogger.Debug("readLoop started") + for { + select { + // Receive data from stopChannel + case <-p.stopReadChannel: + applogger.Debug("readLoop stopped") + return + + default: + if p.conn == nil { + applogger.Error("Read error: no connection available") + time.Sleep(TimerIntervalSecond * time.Second) + continue + } + msgType, buf, err := p.conn.ReadMessage() + if err != nil { + applogger.Error("Read error: %s", err) + time.Sleep(TimerIntervalSecond * time.Second) + continue + } + // applogger.Info("buf",string(buf),"type",msgType) + p.lastReceivedTime = time.Now() + // decompress gzip data if it is binary message + if msgType == websocket.TextMessage { + message := string(buf) + // Try to pass as PingMessage + pingMsg := model.ParsePingMessage(message) + // If it is Ping then respond Pong + if pingMsg != nil && pingMsg.Ping != 0 { + applogger.Debug("Received Ping: %d", pingMsg.Ping) + pongMsg := fmt.Sprintf("{\"pong\": %d}", pingMsg.Ping) + p.Send([]byte(pongMsg)) + applogger.Debug("Replied Pong: %d", pingMsg.Ping) + } else if strings.Contains(message, "e") { + // If it contains expected string, then invoke message handler and response handler + result, err := p.messageHandler(message) + if err != nil { + applogger.Error("Handle message error: %s", err) + continue + } + if p.responseHandler != nil { + p.responseHandler(result) + } + } + } + } + } +} diff --git a/pkg/client/hbwebsocketclientbase/websocketclientbase.go b/pkg/client/hbwebsocketclientbase/websocketclientbase.go new file mode 100644 index 0000000..7854f8b --- /dev/null +++ b/pkg/client/hbwebsocketclientbase/websocketclientbase.go @@ -0,0 +1,282 @@ +package hbwebsocketclientbase + +import ( + "fmt" + "github.com/gorilla/websocket" + "strings" + "sync" + "time" + "wss-pool/internal/gzip" + "wss-pool/internal/model" + "wss-pool/logging/applogger" +) + +const ( + TimerIntervalSecond = 5 + ReconnectWaitSecond = 60 + + wsPath = "/ws" + feedPath = "/feed" + MaxMegByte = 100 +) + +// It will be invoked after websocket connected +type ConnectedHandler func() + +// It will be invoked after valid message received +type MessageHandler func(message string) (interface{}, error) + +// It will be invoked after response is parsed +type ResponseHandler func(response interface{}) + +// The base class that responsible to get data from websocket +type WebSocketClientBase struct { + host string + path string + conn *websocket.Conn + connectedHandler ConnectedHandler + messageHandler MessageHandler + responseHandler ResponseHandler + stopReadChannel chan int + stopTickerChannel chan int + ticker *time.Ticker + lastReceivedTime time.Time + sendMutex *sync.Mutex + Symbol string +} + +// Initializer +func (p *WebSocketClientBase) Init(host string) *WebSocketClientBase { + p.host = host + p.path = wsPath + p.stopReadChannel = make(chan int, 1) + p.stopTickerChannel = make(chan int, 1) + p.sendMutex = &sync.Mutex{} + + return p +} + +// Initializer with path +func (p *WebSocketClientBase) InitWithFeedPath(host string) *WebSocketClientBase { + p.Init(host) + p.path = feedPath + return p +} + +// Set callback handler +func (p *WebSocketClientBase) SetHandler(connHandler ConnectedHandler, msgHandler MessageHandler, repHandler ResponseHandler) { + p.connectedHandler = connHandler + p.messageHandler = msgHandler + p.responseHandler = repHandler +} + +// Connect to websocket websocketserver +// if autoConnect is true, then the connection can be re-connect if no data received after the pre-defined timeout +func (p *WebSocketClientBase) Connect(autoConnect bool) { + // initialize last received time as now + p.lastReceivedTime = time.Now() + + // connect to websocket + p.connectWebSocket() + + // start ticker to manage connection + if autoConnect { + p.startTicker() + } +} + +// Send data to websocket websocketserver +func (p *WebSocketClientBase) Send(data string) { + if p.conn == nil { + applogger.Error("WebSocket sent error: no connection available") + return + } + + p.sendMutex.Lock() + err := p.conn.WriteMessage(websocket.TextMessage, []byte(data)) + p.sendMutex.Unlock() + + if err != nil { + applogger.Error("WebSocket sent error: data=%s, error=%s", data, err) + } +} + +// Close the connection to websocketserver +func (p *WebSocketClientBase) Close() { + p.stopTicker() + p.disconnectWebSocket() +} + +// connect to websocketserver +func (p *WebSocketClientBase) connectWebSocket() { + var err error + url := fmt.Sprintf("wss://%s%s", p.host, p.path) + applogger.Debug("WebSocket connecting...", url) + p.conn, _, err = websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("WebSocket connected error: %s", err) + return + } + applogger.Info("WebSocket connected") + + // start loop to read and handle message + p.startReadLoop() + + if p.connectedHandler != nil { + p.connectedHandler() + } +} + +// disconnect with websocketserver +func (p *WebSocketClientBase) disconnectWebSocket() { + if p.conn == nil { + return + } + + // start a new goroutine to send stop signal + go p.stopReadLoop() + + applogger.Debug("WebSocket disconnecting...") + err := p.conn.Close() + if err != nil { + applogger.Error("WebSocket disconnect error: %s", err) + return + } + + applogger.Info("WebSocket disconnected") +} + +// initialize a ticker and start a goroutine tickerLoop() +func (p *WebSocketClientBase) startTicker() { + p.ticker = time.NewTicker(TimerIntervalSecond * time.Second) + + go p.tickerLoop() +} + +// stop ticker and stop the goroutine +func (p *WebSocketClientBase) stopTicker() { + p.ticker.Stop() + p.stopTickerChannel <- 1 +} + +// defines a for loop that will run based on ticker's frequency +// It checks the last data that received from websocketserver, if it is longer than the threshold, +// it will force disconnect websocketserver and connect again. +func (p *WebSocketClientBase) tickerLoop() { + applogger.Debug("tickerLoop started") + for { + select { + // Receive data from stopChannel + case <-p.stopTickerChannel: + applogger.Debug("tickerLoop stopped") + return + + // Receive tick from tickChannel + case <-p.ticker.C: + elapsedSecond := time.Now().Sub(p.lastReceivedTime).Seconds() + applogger.Debug("WebSocket received data %f sec ago", elapsedSecond) + + if elapsedSecond > ReconnectWaitSecond { + applogger.Info("WebSocket reconnect...") + p.disconnectWebSocket() + p.connectWebSocket() + } + } + } +} + +// start a goroutine readLoop() +func (p *WebSocketClientBase) startReadLoop() { + go p.readLoop() +} + +// stop the goroutine readLoop() +func (p *WebSocketClientBase) stopReadLoop() { + p.stopReadChannel <- 1 +} + +func (p *WebSocketClientBase) contractDepth(buf []byte) { + applogger.Info("message big lai", len(buf)) + _, err := p.messageHandler(string(buf)) + if err != nil { + applogger.Error("Handle message error: %s", err) + return + } + if p.responseHandler != nil { + p.responseHandler(buf) + } +} + +// defines a for loop to read data from websocketserver +// it will stop once it receives the signal from stopReadChannel +func (p *WebSocketClientBase) readLoop() { + applogger.Debug("readLoop started") + for { + select { + // Receive data from stopChannel + case <-p.stopReadChannel: + applogger.Debug("readLoop stopped") + return + + default: + if p.conn == nil { + applogger.Error("Read error: no connection available") + time.Sleep(TimerIntervalSecond * time.Second) + continue + } + + msgType, buf, err := p.conn.ReadMessage() + if err != nil { + applogger.Error("Read error: %s", err) + time.Sleep(TimerIntervalSecond * time.Second) + continue + } + + p.lastReceivedTime = time.Now() + + // decompress gzip data if it is binary message + if msgType == websocket.BinaryMessage { + //只有合约深度不解压 + fmt.Println("websocket :", p.Symbol) + if len(buf) >= MaxMegByte && strings.Contains(p.Symbol, "USDT.depth.step") { + // If it contains expected string, then invoke message handler and response handler + p.contractDepth(buf) + continue + } + + // TODO: 注释GZipDecompress + message, err := gzip.GZipDecompress(buf) + if err != nil { + applogger.Error("UnGZip data error: %s", err) + return + } + // applogger.Info(string(buf)) + //Try to pass as PingMessage + pingMsg := model.ParsePingMessage(message) + + // If it is Ping then respond Pong + if pingMsg != nil && pingMsg.Ping != 0 { + applogger.Debug("Received Ping: %d", pingMsg.Ping) + pongMsg := fmt.Sprintf("{\"pong\": %d}", pingMsg.Ping) + p.Send(pongMsg) + applogger.Debug("Replied Pong: %d", pingMsg.Ping) + } else if strings.Contains(message, "tick") || strings.Contains(message, "data") { + // If it contains expected string, then invoke message handler and response handler + //可能少量数据 低于 MaxMegByte + if strings.Contains(p.Symbol, "USDT.depth.step") { + p.contractDepth(buf) + continue + } + result, err := p.messageHandler(message) + if err != nil { + applogger.Error("Handle message error: %s", err) + continue + } + if p.responseHandler != nil { + p.responseHandler(result) + } + } + } + } + } +} diff --git a/pkg/client/hbwebsocketclientbase/websocketv2clientbase.go b/pkg/client/hbwebsocketclientbase/websocketv2clientbase.go new file mode 100644 index 0000000..0977c58 --- /dev/null +++ b/pkg/client/hbwebsocketclientbase/websocketv2clientbase.go @@ -0,0 +1,263 @@ +package hbwebsocketclientbase + +import ( + "fmt" + "github.com/gorilla/websocket" + "sync" + "time" + "wss-pool/internal/gzip" + "wss-pool/internal/model" + "wss-pool/internal/requestbuilder" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/auth" + "wss-pool/pkg/model/base" +) + +const ( + websocketV2Path = "/ws/v2" +) + +// It will be invoked after websocket v2 authentication response received +type AuthenticationV2ResponseHandler func(resp *auth.WebSocketV2AuthenticationResponse) + +// The base class that responsible to get data from websocket authentication v2 +type WebSocketV2ClientBase struct { + host string + conn *websocket.Conn + + authenticationResponseHandler AuthenticationV2ResponseHandler + messageHandler MessageHandler + responseHandler ResponseHandler + + stopReadChannel chan int + stopTickerChannel chan int + ticker *time.Ticker + lastReceivedTime time.Time + sendMutex *sync.Mutex + + requestBuilder *requestbuilder.WebSocketV2RequestBuilder +} + +// Initializer +func (p *WebSocketV2ClientBase) Init(accessKey string, secretKey string, host string) *WebSocketV2ClientBase { + p.host = host + p.stopReadChannel = make(chan int, 1) + p.stopTickerChannel = make(chan int, 1) + p.requestBuilder = new(requestbuilder.WebSocketV2RequestBuilder).Init(accessKey, secretKey, host, websocketV2Path) + p.sendMutex = &sync.Mutex{} + return p +} + +// Set callback handler +func (p *WebSocketV2ClientBase) SetHandler(authHandler AuthenticationV2ResponseHandler, msgHandler MessageHandler, repHandler ResponseHandler) { + p.authenticationResponseHandler = authHandler + p.messageHandler = msgHandler + p.responseHandler = repHandler +} + +// Connect to websocket websocketserver +// if autoConnect is true, then the connection can be re-connect if no data received after the pre-defined timeout +func (p *WebSocketV2ClientBase) Connect(autoConnect bool) { + // initialize last received time as now + p.lastReceivedTime = time.Now() + + // connect to websocket + p.connectWebSocket() + + // start ticker to manage connection + if autoConnect { + p.startTicker() + } +} + +// Send data to websocket websocketserver +func (p *WebSocketV2ClientBase) Send(data string) { + if p.conn == nil { + applogger.Error("WebSocket sent error: no connection available") + return + } + + p.sendMutex.Lock() + err := p.conn.WriteMessage(websocket.TextMessage, []byte(data)) + p.sendMutex.Unlock() + + if err != nil { + applogger.Error("WebSocket sent error: data=%s, error=%s", data, err) + } +} + +// Close the connection to websocketserver +func (p *WebSocketV2ClientBase) Close() { + p.stopTicker() + p.disconnectWebSocket() +} + +// connect to websocketserver +func (p *WebSocketV2ClientBase) connectWebSocket() { + var err error + url := fmt.Sprintf("wss://%s%s", p.host, websocketV2Path) + applogger.Debug("WebSocket connecting...") + p.conn, _, err = websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("WebSocket connected error: %s", err) + return + } + applogger.Info("WebSocket connected") + + // start loop to read and handle message + p.startReadLoop() + + // send authentication if connect to websocket successfully + auth, err := p.requestBuilder.Build() + if err != nil { + applogger.Error("Signature generated error: %s", err) + return + } + p.Send(auth) + applogger.Info("WebSocket sent authentication") +} + +// disconnect with websocketserver +func (p *WebSocketV2ClientBase) disconnectWebSocket() { + if p.conn == nil { + return + } + + // start a new goroutine to send stop signal + go p.stopReadLoop() + + applogger.Debug("WebSocket disconnecting...") + err := p.conn.Close() + if err != nil { + applogger.Error("WebSocket disconnect error: %s", err) + return + } + + applogger.Info("WebSocket disconnected") +} + +// initialize a ticker and start a goroutine tickerLoop() +func (p *WebSocketV2ClientBase) startTicker() { + p.ticker = time.NewTicker(TimerIntervalSecond * time.Second) + + go p.tickerLoop() +} + +// stop ticker and stop the goroutine +func (p *WebSocketV2ClientBase) stopTicker() { + p.ticker.Stop() + p.stopTickerChannel <- 1 +} + +// defines a for loop that will run based on ticker's frequency +// It checks the last data that received from websocketserver, if it is longer than the threshold, +// it will force disconnect websocketserver and connect again. +func (p *WebSocketV2ClientBase) tickerLoop() { + applogger.Debug("tickerLoop started") + for { + select { + // start a goroutine readLoop() + case <-p.stopTickerChannel: + applogger.Debug("tickerLoop stopped") + return + + // Receive tick from tickChannel + case <-p.ticker.C: + elapsedSecond := time.Now().Sub(p.lastReceivedTime).Seconds() + applogger.Debug("WebSocket received data %f sec ago", elapsedSecond) + + if elapsedSecond > ReconnectWaitSecond { + applogger.Info("WebSocket reconnect...") + p.disconnectWebSocket() + p.connectWebSocket() + } + } + } +} + +// start a goroutine readLoop() +func (p *WebSocketV2ClientBase) startReadLoop() { + go p.readLoop() +} + +// stop the goroutine readLoop() +func (p *WebSocketV2ClientBase) stopReadLoop() { + p.stopReadChannel <- 1 +} + +// defines a for loop to read data from websocketserver +// it will stop once it receives the signal from stopReadChannel +func (p *WebSocketV2ClientBase) readLoop() { + applogger.Debug("readLoop started") + for { + select { + // Receive data from stopChannel + case <-p.stopReadChannel: + applogger.Debug("readLoop stopped") + return + + default: + if p.conn == nil { + applogger.Error("Read error: no connection available") + time.Sleep(TimerIntervalSecond * time.Second) + continue + } + + msgType, buf, err := p.conn.ReadMessage() + if err != nil { + applogger.Error("Read error: %s", err) + time.Sleep(TimerIntervalSecond * time.Second) + continue + } + + p.lastReceivedTime = time.Now() + + // decompress gzip data if it is binary message + var message string + if msgType == websocket.BinaryMessage { + message, err = gzip.GZipDecompress(buf) + if err != nil { + applogger.Error("UnGZip data error: %s", err) + return + } + } else if msgType == websocket.TextMessage { + message = string(buf) + } + + // Try to pass as PingV2Message + // If it is Ping then respond Pong + pingV2Msg := model.ParsePingV2Message(message) + if pingV2Msg.IsPing() { + applogger.Debug("Received Ping: %d", pingV2Msg.Data.Timestamp) + pongMsg := fmt.Sprintf("{\"action\": \"pong\", \"data\": { \"ts\": %d } }", pingV2Msg.Data.Timestamp) + p.Send(pongMsg) + applogger.Debug("Respond Pong: %d", pingV2Msg.Data.Timestamp) + } else { + // Try to pass as websocket v2 authentication response + // If it is then invoke authentication handler + wsV2Resp := base.ParseWSV2Resp(message) + if wsV2Resp != nil { + switch wsV2Resp.Action { + case "req": + authResp := auth.ParseWSV2AuthResp(message) + if authResp != nil && p.authenticationResponseHandler != nil { + p.authenticationResponseHandler(authResp) + } + + case "sub", "push": + { + result, err := p.messageHandler(message) + if err != nil { + applogger.Error("Handle message error: %s", err) + continue + } + if p.responseHandler != nil { + p.responseHandler(result) + } + } + } + } + } + } + } +} diff --git a/pkg/hbwssclient/marketwssclient/bestbidofferwebsocketclient.go b/pkg/hbwssclient/marketwssclient/bestbidofferwebsocketclient.go new file mode 100644 index 0000000..633af3b --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/bestbidofferwebsocketclient.go @@ -0,0 +1,53 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle BBO data from WebSocket +type BestBidOfferWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *BestBidOfferWebSocketClient) Init(host string) *BestBidOfferWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *BestBidOfferWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Subscribe latest market by price order book in snapshot mode at 1-second interval. +func (p *BestBidOfferWebSocketClient) Subscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.bbo", symbol) + sub := fmt.Sprintf("{\"sub\": \"%s\", \"id\": \"%s\"}", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe market by price order book +func (p *BestBidOfferWebSocketClient) UnSubscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.bbo", symbol) + unsub := fmt.Sprintf("{\"unsub\": \"%s\", \"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *BestBidOfferWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeBestBidOfferResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/candlestickwebsocketclient.go b/pkg/hbwssclient/marketwssclient/candlestickwebsocketclient.go new file mode 100644 index 0000000..ce60036 --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/candlestickwebsocketclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle candlestick data from WebSocket +type CandlestickWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *CandlestickWebSocketClient) Init(host string) *CandlestickWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *CandlestickWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full candlestick data according to specified criteria +func (p *CandlestickWebSocketClient) Request(symbol string, period string, from int64, to int64, clientId string) { + topic := fmt.Sprintf("market.%s.kline.%s", symbol, period) + req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe candlestick data +func (p *CandlestickWebSocketClient) Subscribe(symbol string, period string, clientId string) { + topic := fmt.Sprintf("market.%s.kline.%s", symbol, period) + sub := fmt.Sprintf("{\"sub\": \"%s\", \"id\": \"%s\"}", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe candlestick data +func (p *CandlestickWebSocketClient) UnSubscribe(symbol string, period string, clientId string) { + topic := fmt.Sprintf("market.%s.kline.%s", symbol, period) + unsub := fmt.Sprintf("{\"unsub\": \"%s\", \"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *CandlestickWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeCandlestickResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/contractbboclient.go b/pkg/hbwssclient/marketwssclient/contractbboclient.go new file mode 100644 index 0000000..2876718 --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/contractbboclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle candlestick data from WebSocket +type ContractBBOWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *ContractBBOWebSocketClient) Init(host string) *ContractBBOWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *ContractBBOWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full candlestick data according to specified criteria +func (p *ContractBBOWebSocketClient) Request(symbol string, from int64, to int64, clientId string) { + topic := fmt.Sprintf("market.%s.bbo", symbol) + req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe candlestick data +func (p *ContractBBOWebSocketClient) Subscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.bbo", symbol) + sub := fmt.Sprintf("{\"sub\": \"%s\", \"id\": \"%s\"}", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe candlestick data +func (p *ContractBBOWebSocketClient) UnSubscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.bbo", symbol) + unsub := fmt.Sprintf("{\"unsub\": \"%s\", \"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *ContractBBOWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeCtBboResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/contractdepthclient.go b/pkg/hbwssclient/marketwssclient/contractdepthclient.go new file mode 100644 index 0000000..1cb8013 --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/contractdepthclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" +) + +// Responsible to handle candlestick data from WebSocket +type ContractDepthWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *ContractDepthWebSocketClient) Init(host string) *ContractDepthWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *ContractDepthWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full candlestick data according to specified criteria +func (p *ContractDepthWebSocketClient) Request(symbol string, period string, from int64, to int64, clientId string) { + topic := fmt.Sprintf("market.%s.depth.%s", symbol, period) + req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe candlestick data +func (p *ContractDepthWebSocketClient) Subscribe(symbol, period string, clientId string) { + topic := fmt.Sprintf("market.%s.depth.%s", symbol, period) + sub := fmt.Sprintf("{\"sub\": \"%s\", \"id\": \"%s\"}", topic, clientId) + fmt.Println("Subscribe :", p.Symbol) + p.Symbol = topic + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe candlestick data +func (p *ContractDepthWebSocketClient) UnSubscribe(symbol string, period string, clientId string) { + topic := fmt.Sprintf("market.%s.depth.%s", symbol, period) + unsub := fmt.Sprintf("{\"unsub\": \"%s\", \"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *ContractDepthWebSocketClient) handleMessage(msg string) (interface{}, error) { + // result := market.SubscribeCtDepthResponse{} + //err := json.Unmarshal([]byte(msg), &result) + //return msg,nil + + return []byte(msg), nil +} diff --git a/pkg/hbwssclient/marketwssclient/contractdepthsizeclient.go b/pkg/hbwssclient/marketwssclient/contractdepthsizeclient.go new file mode 100644 index 0000000..935b2e1 --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/contractdepthsizeclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle candlestick data from WebSocket +type ContractDepthSizeWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *ContractDepthSizeWebSocketClient) Init(host string) *ContractDepthSizeWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *ContractDepthSizeWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full candlestick data according to specified criteria +func (p *ContractDepthSizeWebSocketClient) Request(symbol string, period string, from int64, to int64, clientId string) { + topic := fmt.Sprintf("market.%s.depth.size_%s.high_freq", symbol, period) + req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe candlestick data +func (p *ContractDepthSizeWebSocketClient) Subscribe(symbol string, period string, clientId string) { + topic := fmt.Sprintf("market.%s.depth.size_%s.high_freq", symbol, period) + sub := fmt.Sprintf("{\"sub\": \"%s\", \"data_type\": \"incremental\", \"id\": \"%s\"}", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe candlestick data +func (p *ContractDepthSizeWebSocketClient) UnSubscribe(symbol string, period string, clientId string) { + topic := fmt.Sprintf("market.%s.depth.size_%s.high_freq", symbol, period) + unsub := fmt.Sprintf("{\"unsub\": \"%s\", \"data_type\": \"incremental\",\"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *ContractDepthSizeWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeCtAddDepthResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/contractdetailclient.go b/pkg/hbwssclient/marketwssclient/contractdetailclient.go new file mode 100644 index 0000000..eee18cb --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/contractdetailclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle candlestick data from WebSocket +type ContractDetailWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *ContractDetailWebSocketClient) Init(host string) *ContractDetailWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *ContractDetailWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full candlestick data according to specified criteria +func (p *ContractDetailWebSocketClient) Request(symbol string, from int64, to int64, clientId string) { + topic := fmt.Sprintf("market.%s.detail", symbol) + req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe candlestick data +func (p *ContractDetailWebSocketClient) Subscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.detail", symbol) + sub := fmt.Sprintf("{\"sub\": \"%s\", \"id\": \"%s\"}", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe candlestick data +func (p *ContractDetailWebSocketClient) UnSubscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.detail", symbol) + unsub := fmt.Sprintf("{\"unsub\": \"%s\", \"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *ContractDetailWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeCtDetailResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/contractklineclient.go b/pkg/hbwssclient/marketwssclient/contractklineclient.go new file mode 100644 index 0000000..20023f4 --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/contractklineclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle candlestick data from WebSocket +type ContractKLineWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *ContractKLineWebSocketClient) Init(host string) *ContractKLineWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *ContractKLineWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full candlestick data according to specified criteria +func (p *ContractKLineWebSocketClient) Request(symbol string, period string, from int64, to int64, clientId string) { + topic := fmt.Sprintf("market.%s.kline.%s", symbol, period) + req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe candlestick data +func (p *ContractKLineWebSocketClient) Subscribe(symbol string, period string, clientId string) { + topic := fmt.Sprintf("market.%s.kline.%s", symbol, period) + sub := fmt.Sprintf("{\"sub\": \"%s\", \"id\": \"%s\"}", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe candlestick data +func (p *ContractKLineWebSocketClient) UnSubscribe(symbol string, period string, clientId string) { + topic := fmt.Sprintf("market.%s.kline.%s", symbol, period) + unsub := fmt.Sprintf("{\"unsub\": \"%s\", \"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *ContractKLineWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeCtKlineResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/contracttradedetailclient.go b/pkg/hbwssclient/marketwssclient/contracttradedetailclient.go new file mode 100644 index 0000000..3e753ea --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/contracttradedetailclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle candlestick data from WebSocket +type ContractTradeDetailWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *ContractTradeDetailWebSocketClient) Init(host string) *ContractTradeDetailWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *ContractTradeDetailWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request the full candlestick data according to specified criteria +func (p *ContractTradeDetailWebSocketClient) Request(symbol string, from int64, to int64, clientId string) { + topic := fmt.Sprintf("market.%s.trade.detail", symbol) + req := fmt.Sprintf("{\"req\": \"%s\", \"from\":%d, \"to\":%d, \"id\": \"%s\" }", topic, from, to, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe candlestick data +func (p *ContractTradeDetailWebSocketClient) Subscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.trade.detail", symbol) + sub := fmt.Sprintf("{\"sub\": \"%s\", \"id\": \"%s\"}", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe candlestick data +func (p *ContractTradeDetailWebSocketClient) UnSubscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.trade.detail", symbol) + unsub := fmt.Sprintf("{\"unsub\": \"%s\", \"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *ContractTradeDetailWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeCtTradeDetailResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/depthwebsocketclient.go b/pkg/hbwssclient/marketwssclient/depthwebsocketclient.go new file mode 100644 index 0000000..146ad1f --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/depthwebsocketclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle Depth data from WebSocket +type DepthWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *DepthWebSocketClient) Init(host string) *DepthWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *DepthWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request full depth data +func (p *DepthWebSocketClient) Request(symbol string, step string, clientId string) { + topic := fmt.Sprintf("market.%s.depth.%s", symbol, step) + req := fmt.Sprintf("{\"req\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.WebSocketClientBase.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe latest market by price order book in snapshot mode at 1-second interval. +func (p *DepthWebSocketClient) Subscribe(symbol string, step string, clientId string) { + topic := fmt.Sprintf("market.%s.depth.%s", symbol, step) + sub := fmt.Sprintf("{\"sub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe market by price order book +func (p *DepthWebSocketClient) UnSubscribe(symbol string, step string, clientId string) { + topic := fmt.Sprintf("market.%s.depth.%s", symbol, step) + unsub := fmt.Sprintf("{\"unsub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *DepthWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeDepthResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/last24hcandlestickwebsocketclient.go b/pkg/hbwssclient/marketwssclient/last24hcandlestickwebsocketclient.go new file mode 100644 index 0000000..4c9754a --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/last24hcandlestickwebsocketclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle last 24h candlestick data from WebSocket +type Last24hCandlestickWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *Last24hCandlestickWebSocketClient) Init(host string) *Last24hCandlestickWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *Last24hCandlestickWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request full candlestick data +func (p *Last24hCandlestickWebSocketClient) Request(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.detail", symbol) + req := fmt.Sprintf("{\"req\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe latest 24h market stats +func (p *Last24hCandlestickWebSocketClient) Subscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.detail", symbol) + sub := fmt.Sprintf("{\"sub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe latest 24 market stats +func (p *Last24hCandlestickWebSocketClient) UnSubscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.detail", symbol) + unsub := fmt.Sprintf("{\"unsub\": \"%s\",\"id\": \"%s\" }", symbol, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *Last24hCandlestickWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeLast24hCandlestickResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/marketbypricetickwebsocketclient.go b/pkg/hbwssclient/marketwssclient/marketbypricetickwebsocketclient.go new file mode 100644 index 0000000..f5091fb --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/marketbypricetickwebsocketclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle MBP data from WebSocket +type MarketByPriceTickWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *MarketByPriceTickWebSocketClient) Init(host string) *MarketByPriceTickWebSocketClient { + p.WebSocketClientBase.InitWithFeedPath(host) + return p +} + +// Set callback handler +func (p *MarketByPriceTickWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request full Market By Price order book, level: 5, 20, 150 +func (p *MarketByPriceTickWebSocketClient) Request(symbol string, level int, clientId string) { + topic := fmt.Sprintf("market.%s.mbp.%d", symbol, level) + req := fmt.Sprintf("{\"req\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.WebSocketClientBase.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe incremental update of Market By Price order book, level: 5, 20, 150 +func (p *MarketByPriceTickWebSocketClient) Subscribe(symbol string, level int, clientId string) { + topic := fmt.Sprintf("market.%s.mbp.%d", symbol, level) + sub := fmt.Sprintf("{\"sub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.WebSocketClientBase.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe update of Market By Price order book +func (p *MarketByPriceTickWebSocketClient) UnSubscribe(symbol string, level int, clientId string) { + topic := fmt.Sprintf("market.%s.mbp.%d", symbol, level) + unsub := fmt.Sprintf("{\"unsub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *MarketByPriceTickWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeMarketByPriceResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/marketbypricewebsocketclient.go b/pkg/hbwssclient/marketwssclient/marketbypricewebsocketclient.go new file mode 100644 index 0000000..be70a17 --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/marketbypricewebsocketclient.go @@ -0,0 +1,83 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle MBP data from WebSocket +type MarketByPriceWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *MarketByPriceWebSocketClient) Init(host string) *MarketByPriceWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *MarketByPriceWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request full Market By Price order book +func (p *MarketByPriceWebSocketClient) Request(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.mbp.150", symbol) + req := fmt.Sprintf("{\"req\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.WebSocketClientBase.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe incremental update of Market By Price order book +func (p *MarketByPriceWebSocketClient) Subscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.mbp.150", symbol) + sub := fmt.Sprintf("{\"sub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.WebSocketClientBase.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe full Market By Price order book +func (p *MarketByPriceWebSocketClient) SubscribeFull(symbol string, level int, clientId string) { + topic := fmt.Sprintf("market.%s.mbp.refresh.%d", symbol, level) + sub := fmt.Sprintf("{\"sub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe update of Market By Price order book +func (p *MarketByPriceWebSocketClient) UnSubscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.mbp.150", symbol) + unsub := fmt.Sprintf("{\"unsub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe full Market By Price order book +func (p *MarketByPriceWebSocketClient) UnSubscribeFull(symbol string, level int, clientId string) { + topic := fmt.Sprintf("market.%s.mbp.refresh.%d", symbol, level) + unsub := fmt.Sprintf("{\"unsub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *MarketByPriceWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeMarketByPriceResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/tickerwebsocketclient.go b/pkg/hbwssclient/marketwssclient/tickerwebsocketclient.go new file mode 100644 index 0000000..b8089ea --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/tickerwebsocketclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle Trade data from WebSocket +type TickerWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *TickerWebSocketClient) Init(host string) *TickerWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *TickerWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request latest 300 trade data +func (p *TickerWebSocketClient) Request(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.ticker", symbol) + req := fmt.Sprintf("{\"req\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe latest completed trade in tick by tick mode +func (p *TickerWebSocketClient) Subscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.ticker", symbol) + sub := fmt.Sprintf("{\"sub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe trade +func (p *TickerWebSocketClient) UnSubscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.ticker", symbol) + unsub := fmt.Sprintf("{\"unsub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *TickerWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.TickerWebsocketResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/hbwssclient/marketwssclient/tradewebsocketclient.go b/pkg/hbwssclient/marketwssclient/tradewebsocketclient.go new file mode 100644 index 0000000..3480f1a --- /dev/null +++ b/pkg/hbwssclient/marketwssclient/tradewebsocketclient.go @@ -0,0 +1,63 @@ +package marketwssclient + +import ( + "encoding/json" + "fmt" + "wss-pool/logging/applogger" + "wss-pool/pkg/client/hbwebsocketclientbase" + "wss-pool/pkg/model/market" +) + +// Responsible to handle Trade data from WebSocket +type TradeWebSocketClient struct { + hbwebsocketclientbase.WebSocketClientBase +} + +// Initializer +func (p *TradeWebSocketClient) Init(host string) *TradeWebSocketClient { + p.WebSocketClientBase.Init(host) + return p +} + +// Set callback handler +func (p *TradeWebSocketClient) SetHandler( + connectedHandler hbwebsocketclientbase.ConnectedHandler, + responseHandler hbwebsocketclientbase.ResponseHandler) { + p.WebSocketClientBase.SetHandler(connectedHandler, p.handleMessage, responseHandler) +} + +// Request latest 300 trade data +func (p *TradeWebSocketClient) Request(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.trade.detail", symbol) + req := fmt.Sprintf("{\"req\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(req) + + applogger.Info("WebSocket requested, topic=%s, clientId=%s", topic, clientId) +} + +// Subscribe latest completed trade in tick by tick mode +func (p *TradeWebSocketClient) Subscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.trade.detail", symbol) + sub := fmt.Sprintf("{\"sub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(sub) + + applogger.Info("WebSocket subscribed, topic=%s, clientId=%s", topic, clientId) +} + +// Unsubscribe trade +func (p *TradeWebSocketClient) UnSubscribe(symbol string, clientId string) { + topic := fmt.Sprintf("market.%s.trade.detail", symbol) + unsub := fmt.Sprintf("{\"unsub\": \"%s\",\"id\": \"%s\" }", topic, clientId) + + p.Send(unsub) + + applogger.Info("WebSocket unsubscribed, topic=%s, clientId=%s", topic, clientId) +} + +func (p *TradeWebSocketClient) handleMessage(msg string) (interface{}, error) { + result := market.SubscribeTradeResponse{} + err := json.Unmarshal([]byte(msg), &result) + return result, err +} diff --git a/pkg/memory/cache.go b/pkg/memory/cache.go new file mode 100644 index 0000000..7db2e25 --- /dev/null +++ b/pkg/memory/cache.go @@ -0,0 +1,61 @@ +package memory + +import ( + "github.com/allegro/bigcache" + "time" +) + +var ForexCache *bigcache.BigCache // 外汇行情缓存 + +// NewBigCache +// +// @Description: 初始化交易-实时数据缓存 +// @return *bigcache.BigCache +func NewBigCache() *bigcache.BigCache { + config := bigcache.Config{ + // Set the number of partitions, which must be an integer multiple of 2 + Shards: 1024, + // After LifeWindow, cached objects are considered inactive, but they are not deleted + LifeWindow: 180 * time.Second, + // After CleanWindow, objects that are considered inactive will be deleted, with 0 representing no operation + CleanWindow: 150 * time.Second, + // Set the maximum number of storage objects, which can only be set during initialization + //MaxEntriesInWindow: 1000 * 10 * 60, + MaxEntriesInWindow: 1, + // The maximum number of bytes for cache objects, which can only be set during initialization + MaxEntrySize: 500, + // Print memory allocation information + Verbose: true, + // Set the maximum cache value (in MB), where 0 represents unlimited + HardMaxCacheSize: 0, //8192 + // When the cache expires or is deleted, a callback function can be set with parameters (key, val), and the default is nil + OnRemove: nil, + // When the cache expires or is deleted, a callback function can be set with parameters such as (key, val, reason). The default setting is nil and not set + OnRemoveWithReason: nil, + } + cache, err := bigcache.NewBigCache(config) + if err != nil { + return nil + } + + return cache +} + +func init() { + ForexCache = NewBigCache() +} + +// GetForexCache +// +// @Description: 外汇买一卖一行情 +// @param key +// @return []byte +// @return error +func GetForexCache(key string) ([]byte, error) { + conP, err := ForexCache.Get(key) + if err != nil { + return conP, nil + } + + return conP, err +} diff --git a/pkg/methods/methods.go b/pkg/methods/methods.go new file mode 100644 index 0000000..67b8855 --- /dev/null +++ b/pkg/methods/methods.go @@ -0,0 +1,23 @@ +package methods + +import ( + "wss-pool/pkg/model/stock" +) + +// Paginate 函数根据 pageNumber 和 pageSize 对原始数据进行分页 +func Paginate(data []stock.StockShare, pageNumber int, pageSize int) []stock.StockShare { + // 获取原始数据长度 + dataLen := len(data) + // 计算分页开始和结束的索引 + startIndex := (pageNumber - 1) * pageSize + endIndex := startIndex + pageSize + // 处理边界情况 + if startIndex > dataLen { + return []stock.StockShare{} + } + if endIndex > dataLen { + endIndex = dataLen + } + // 返回分页后的切片 + return data[startIndex:endIndex] +} diff --git a/pkg/model/auth/websocketv1authenticationresponse.go b/pkg/model/auth/websocketv1authenticationresponse.go new file mode 100644 index 0000000..a27a239 --- /dev/null +++ b/pkg/model/auth/websocketv1authenticationresponse.go @@ -0,0 +1,26 @@ +package auth + +import "encoding/json" + +type WebSocketV1AuthenticationResponse struct { + Op string `json:"op"` + Timestamp int64 `json:"ts"` + ErrorCode int `json:"err-code"` + Data *struct { + UserId int `json:"user-id"` + } +} + +func (p *WebSocketV1AuthenticationResponse) IsAuth() bool { + return p.Op == "auth" +} + +func ParseWSV1AuthResp(message string) *WebSocketV1AuthenticationResponse { + result := &WebSocketV1AuthenticationResponse{} + err := json.Unmarshal([]byte(message), result) + if err != nil { + return nil + } + + return result +} diff --git a/pkg/model/auth/websocketv2authenticationresponse.go b/pkg/model/auth/websocketv2authenticationresponse.go new file mode 100644 index 0000000..2397805 --- /dev/null +++ b/pkg/model/auth/websocketv2authenticationresponse.go @@ -0,0 +1,20 @@ +package auth + +import ( + "encoding/json" + "wss-pool/pkg/model/base" +) + +type WebSocketV2AuthenticationResponse struct { + base.WebSocketV2ResponseBase +} + +func ParseWSV2AuthResp(message string) *WebSocketV2AuthenticationResponse { + result := &WebSocketV2AuthenticationResponse{} + err := json.Unmarshal([]byte(message), result) + if err != nil { + return nil + } + + return result +} diff --git a/pkg/model/bamodel/klineResponse.go b/pkg/model/bamodel/klineResponse.go new file mode 100644 index 0000000..0e5eb37 --- /dev/null +++ b/pkg/model/bamodel/klineResponse.go @@ -0,0 +1,34 @@ +package bamodel + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeKLineResponse struct { + base base.BaWebSocketResponseBase + Event string `json:"e"` // 事件类型 + TimeE string `json:"E"` // 事件时间 + Symbol int64 `json:"s"` // 交易对 + Tick *K + Data []K +} +type K struct { + Tt int64 `json:"t"` // 这根K线的起始时间 + TT int64 `json:"T"` // 这根K线的结束时间 + S string `json:"s"` // 交易对 + I string `json:"i"` // K线间隔 + F int `json:"f"` // 这根K线期间第一笔成交ID + L int `json:"L"` // 这根K线期间末一笔成交ID + O decimal.Decimal `json:"o"` // 这根K线期间第一笔成交价 + C decimal.Decimal `json:"c"` // 这根K线期间末一笔成交价 + H decimal.Decimal `json:"h"` // 这根K线期间最高成交价 + Ll decimal.Decimal `json:"l"` // 这根K线期间最低成交价 + V decimal.Decimal `json:"v"` // 这根K线期间成交量 + N int `json:"n"` // 这根K线期间成交笔数 + X bool `json:"x"` // 这根K线是否完结(是否已经开始下一根K线) + Q decimal.Decimal `json:"q"` // 这根K线期间成交额 + Vv decimal.Decimal `json:"V"` // 主动买入的成交量 + Qq decimal.Decimal `json:"Q"` // 主动买入的成交额 + B decimal.Decimal `json:"B"` // 忽略此参数 +} diff --git a/pkg/model/base/bawebsocketresponsebase.go b/pkg/model/base/bawebsocketresponsebase.go new file mode 100644 index 0000000..7b0d0de --- /dev/null +++ b/pkg/model/base/bawebsocketresponsebase.go @@ -0,0 +1,7 @@ +package base + +type BaWebSocketResponseBase struct { + Event string `json:"e"` + TimeE string `json:"E"` + Symbol int64 `json:"s"` +} diff --git a/pkg/model/base/websocketresponsebase.go b/pkg/model/base/websocketresponsebase.go new file mode 100644 index 0000000..4d221c5 --- /dev/null +++ b/pkg/model/base/websocketresponsebase.go @@ -0,0 +1,7 @@ +package base + +type WebSocketResponseBase struct { + Status string `json:"status"` + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` +} diff --git a/pkg/model/base/websocketv2responsebase.go b/pkg/model/base/websocketv2responsebase.go new file mode 100644 index 0000000..c75607b --- /dev/null +++ b/pkg/model/base/websocketv2responsebase.go @@ -0,0 +1,24 @@ +package base + +import "encoding/json" + +type WebSocketV2ResponseBase struct { + Action string `json:"action"` + Code int32 `json:"code"` + Ch string `json:"ch"` + Message string `json:"message"` +} + +func (p *WebSocketV2ResponseBase) IsSuccess() bool { + return p.Code == 200 +} + +func ParseWSV2Resp(message string) *WebSocketV2ResponseBase { + result := &WebSocketV2ResponseBase{} + err := json.Unmarshal([]byte(message), result) + if err != nil { + return nil + } + + return result +} diff --git a/pkg/model/config.go b/pkg/model/config.go new file mode 100644 index 0000000..8c7b751 --- /dev/null +++ b/pkg/model/config.go @@ -0,0 +1,147 @@ +package model + +type Config struct { + LogLevel string `json:"loglevel"` + ServerLevel string `json:"serverlevel"` + DomainName string `json:"domainname"` + BaWebSocket BaWebSocket `json:"bawebsocket"` + HbApi HbApi `json:"hbapi"` + HbGather HbGather `json:"hbgather"` + HbContract HbContract `json:"hbcontract"` + BaGather BaGather `json:"bagather"` + Redis Redis `json:"redis"` + Mongodb Mongodb `json:"mongodb"` + ShareGather ShareGather `json:"sharegather"` + FinnhubUs FinnhubUs `json:"finnhubus"` + Bourse Bourse `json:"bourse"` + ALiYun ALiYun `json:"aliyun"` + TgBot TgBot `json:"tgbot"` + SendIn SendIn `json:"sendin"` + PhpHost PhpHost `json:"phphost"` +} + +type PhpHost struct { + URL string `json:"url"` +} + +type SendIn struct { + URL string `json:"url"` + Symbol string `json:"symbol"` + Price float64 `json:"price"` + Vol int64 `json:"vol"` +} + +type TgBot struct { + URL string `json:"url"` + ChatId int64 `json:"chatid"` + Server string `json:"server"` + NoWarn string `json:"nowarn"` +} + +type HbApi struct { + HbSpotsApiHost string + HbContractApiHost string + PHPHost string +} + +type BaWebSocket struct { + BaHost string + BaAccessKey string + BaAccountId string + BaSubUid int + BaSubUids string + BaSecretKey string +} + +type HbGather struct { + HbHost string + HbAccessKey string + HbAccountId string + HbSubUid int + HbSubUids string + HbSecretKey string +} + +type HbContract struct { + HbContractSecretKey string + HbContractSubUids string + HbContractSubUid int + HbContractAccountId string + HbContractAccessKey string + HbContractHost string +} + +type BaGather struct { + BaSecretKey string + BaSubUids string + BaSubUid int + BaAccountId string + BaAccessKey string + BaHost string +} + +type Redis struct { + Server string + Port string + PassWord string + DbTen int + DbEleven int + DbUser int + DbMore string + NoPinAss string + FullPush int + AddrList string +} + +type Mongodb struct { + DbHost string + DbUser string + DbPort string + Password string + DbBase string + Table int + AddrList string + RedisToMongodb string // mongodb-redis +} + +type Mysql struct { + driver string + datasource string + maxElementSize int +} + +type FinnhubUs struct { + FinnhubKey string + FinnhubHost string + FinnhubWss string + ForwardingHost string + DispenseWss string +} + +type ShareGather struct { + RapidApiKey string + RapidApiHost string + FinancialKey string + FinancialHost string + FinancialWsUs string + AlphavantageKey string + AlphavantageHost string + PolygonHost string + PolygonWss string + PolygonKey string + MysCode string + UsCode string + ListUrl string +} + +type ALiYun struct { + AccessKeyId string + AccessKeySecret string + EndPoint string +} + +type Bourse struct { + Driver string + Datasource string + Maxelementsize int +} diff --git a/pkg/model/forex.go b/pkg/model/forex.go new file mode 100644 index 0000000..fce8602 --- /dev/null +++ b/pkg/model/forex.go @@ -0,0 +1,261 @@ +package model + +// 外汇废弃 代码列表 +type ForexCodeList struct { + Description string `json:"description"` + DisplaySymbol string `json:"displaySymbol"` + Symbol string `json:"symbol"` +} + +// https://polygon.io 外汇实时行情 +type ForexJsonData struct { + Event string `json:"ev"` // 事件类型(实时数据) + Pair string `json:"pair"` // 货币对 + Open float64 `json:"o"` // 开盘价 + Close float64 `json:"c"` // 收盘价 + High float64 `json:"h"` // 最高价 + Low float64 `json:"l"` // 最低价 + Volume int `json:"v"` // 交易量 + Timestamp int64 `json:"s"` // 时间戳 +} + +// 链接是否成功:[{"ev":"status","status":"connected","message":"Connected Successfully"}] +type ReceiveForexLink struct { + Ev string `json:"ev"` + Status string `json:"status"` + Message string `json:"message"` +} + +// 链接鉴权|发起订阅:{"action":"auth","params":"vG4tCD5emAFPkS4kWtXxJntMASyN4dnv"} +type SendAuthority struct { + Action string `json:"action"` + Params string `json:"params"` +} + +// 外汇股票代码列表 +type ForexCodeMap struct { + Code string `json:"code"` // 外汇代码 +} + +// ForexDataResponse 外汇代码列表 +type ForexDataResponse struct { + Tickers []ForexData `json:"tickers"` +} + +// StockData 代表外汇股票市场数据的结构 +type ForexData struct { + Ticker string `json:"ticker"` // 股票或商品的标识符 + TodaysChange float64 `json:"todaysChange"` // 今日价格变动 + TodaysChangePerc float64 `json:"todaysChangePerc"` // 今日价格变动的百分比 + Updated int64 `json:"updated"` // 更新时间的 Unix 时间戳(纳秒) + Day DayData `json:"day"` // 今日的市场数据 + LastQuote QuoteData `json:"lastQuote"` // 最新的报价信息 + Min MinData `json:"min"` // 最小价格记录 + PrevDay DayData `json:"prevDay"` // 昨日的市场数据 +} + +// DayData 代表一天的市场数据 +type DayData struct { + O float64 `json:"o"` // 开盘价 + H float64 `json:"h"` // 最高价 + L float64 `json:"l"` // 最低价 + C float64 `json:"c"` // 当前/收盘价 + V int `json:"v"` // 成交量 + VW float64 `json:"vw"` // 加权平均价格 +} + +// QuoteData 代表最新报价信息 +type QuoteData struct { + A float64 `json:"a"` // 卖价 + B float64 `json:"b"` // 买价 + T int64 `json:"t"` // 最新报价的时间戳(毫秒) + X int `json:"x"` // 交易所标识符 +} + +// MinData 代表最小价格记录 +type MinData struct { + T int64 `json:"t"` // 时间戳 + N int `json:"n"` // 记录数 + O float64 `json:"o"` // 开盘价 + H float64 `json:"h"` // 最高价 + L float64 `json:"l"` // 最低价 + C float64 `json:"c"` // 收盘价 + V int `json:"v"` // 成交量 + VW float64 `json:"vw"` // 加权平均价格 +} + +// 货币对的最后报价 {"ev":"C","p":"NZD/NOK","a":6.5496,"b":6.5472,"x":48,"t":1730194133000} +type ForexLastQuote struct { + Ev string `json:"ev"` // 事件类型 + P string `json:"p"` // 交易对 + A float64 `json:"a"` // 买价 + X int `json:"x"` // 交易所标识符 + B float64 `json:"b"` // 卖价 + T int64 `json:"t"` // 时间 +} + +// 大宗成交数据 +type ForexTrade struct { + Ev string `json:"ev"` // 事件类型 + Code string `json:"code"` // 交易对 + Seq string `json:"seq"` // ID + TickTime string `json:"tick_time"` // 时间戳 + Price string `json:"price"` // 成交价 + Volume string `json:"volume"` // 成交量 + Turnover string `json:"turnover"` // 成交金额 + TradeDirection int `json:"trade_direction"` // 成交方向 +} +type ForexTradeList struct { + Ev string `bson:"ev"` // 事件类型 + Code string `bson:"code"` // 交易对 + Seq string `bson:"seq"` // ID + TickTime int64 `bson:"tick_time"` // 时间戳 + Price string `bson:"price"` // 成交价 + Volume string `bson:"volume"` // 成交量 + Turnover string `bson:"turnover"` // 成交金额 + TradeDirection int `bson:"trade_direction"` // 成交方向 +} + +type ConstructParametersPost struct { + Trace string `json:"trace"` + Data struct { + DataList []DataParameters `json:"data_list"` + } `json:"data"` +} +type DataParameters struct { + Code string `json:"code"` + KlineType int `json:"kline_type"` + KlineTimestampEnd int `json:"kline_timestamp_end"` + QueryKlineNum int `json:"query_kline_num"` + AdjustType int `json:"adjust_type"` +} +type KlinePostReturnStruct struct { + Ret int `json:"ret"` + Msg string `json:"msg"` + Trace string `json:"trace"` + Data struct { + KlineList []struct { + Code string `json:"code"` + KlineType int `json:"kline_type"` + KlineData []struct { + Timestamp string `json:"timestamp"` + OpenPrice string `json:"open_price"` + ClosePrice string `json:"close_price"` + HighPrice string `json:"high_price"` + LowPrice string `json:"low_price"` + Volume string `json:"volume"` + Turnover string `json:"turnover"` + } `json:"kline_data"` + } `json:"kline_list"` + } `json:"data"` +} +type ConstructParameters struct { + Trace string `json:"trace"` + Data struct { + Code string `json:"code"` + KlineType int `json:"kline_type"` + KlineTimestampEnd int `json:"kline_timestamp_end"` + QueryKlineNum int `json:"query_kline_num"` + AdjustType int `json:"adjust_type"` + } `json:"data"` +} +type DataList struct { + Code string `json:"code"` + KlineType int `json:"kline_type"` + KlineTimestampEnd int `json:"kline_timestamp_end"` + QueryKlineNum int `json:"query_kline_num"` + AdjustType int `json:"adjust_type"` +} +type KlineGetReturnStruct struct { + Ret int `json:"ret"` + Msg string `json:"msg"` + Trace string `json:"trace"` + Data struct { + Code string `json:"code"` + KlineType int `json:"kline_type"` + KlineList []KlineList `json:"kline_list"` + } `json:"data"` +} + +type KlineList struct { + Timestamp string `json:"timestamp"` + OpenPrice string `json:"open_price"` + ClosePrice string `json:"close_price"` + HighPrice string `json:"high_price"` + LowPrice string `json:"low_price"` + Volume string `json:"volume"` + Turnover string `json:"turnover"` +} + +type OrderBookOrTradeTick struct { + Trace string `json:"trace"` + Data struct { + SymbolList []SymbolList `json:"symbol_list"` + } `json:"data"` +} +type SymbolList struct { + Code string `json:"code"` +} +type DepthReturnStruct struct { + Msg string `json:"msg"` + Trace string `json:"trace"` + Data struct { + TickList []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:"tick_list"` + } `json:"data"` +} +type TradeReturnStruct struct { + Msg string `json:"msg"` + Trace string `json:"trace"` + Data struct { + TickList []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"` + } `json:"tick_list"` + } `json:"data"` +} + +// 贵金属和能源交易对 +var Check_Code = map[string]string{ + "GOLD": "XAUUSD", // 现货黄金/伦敦金(XAUUSD) + "Silver": "XAGUSD", // 现货白银/伦敦银(XAGUSD) + "Aluminum": "XALUSD", // 铝(XALUSD) + "COPPER": "XCUUSD", // 铜(XCUUSD) + "Palladium": "XPDUSD", // 钯(XPDUSD) + "Platinum": "XPTUSD", // 铂金(XPTUSD) + "Nickel": "XNIUSD", // 镍(XNIUSD) + "Lead": "XPBUSD", // 铅(XPBUSD) + "Zinc": "XZNUSD", // 锌(XZNUSD) + "UKOIL": "UKOUSD", // 英国原油(UKOUSD) + "USOIL": "USOUSD", // 美国原油(USOUSD) + "NGAS": "XNGUSD", // 天然气(XNGUSD) +} +var Check_Symbol = map[string]string{ + "XAUUSD": "GOLD", // 现货黄金/伦敦金(XAUUSD) + "XAGUSD": "Silver", // 现货白银/伦敦银(XAGUSD) + "XALUSD": "Aluminum", // 铝(XALUSD) + "XCUUSD": "COPPER", // 铜(XCUUSD) + "XPDUSD": "Palladium", // 钯(XPDUSD) + "XPTUSD": "Platinum", // 铂金(XPTUSD) + "XNIUSD": "Nickel", // 镍(XNIUSD) + "XPBUSD": "Lead", // 铅(XPBUSD) + "XZNUSD": "Zinc", // 锌(XZNUSD) + "UKOUSD": "UKOIL", // 英国原油(UKOUSD) + "USOUSD": "USOIL", // 美国原油(USOUSD) + "XNGUSD": "NGAS", // 天然气(XNGUSD) +} diff --git a/pkg/model/getrequest.go b/pkg/model/getrequest.go new file mode 100644 index 0000000..46b6ea8 --- /dev/null +++ b/pkg/model/getrequest.go @@ -0,0 +1,39 @@ +package model + +import ( + "net/url" +) + +// Manage the HTTP GET request parameters +type GetRequest struct { + urls url.Values +} + +// Initializer +func (p *GetRequest) Init() *GetRequest { + p.urls = url.Values{} + return p +} + +// Initialized from another instance +func (p *GetRequest) InitFrom(reqParams *GetRequest) *GetRequest { + if reqParams != nil { + p.urls = reqParams.urls + } else { + p.urls = url.Values{} + } + return p +} + +// Add URL escape property and value pair +func (p *GetRequest) AddParam(property string, value string) *GetRequest { + if property != "" && value != "" { + p.urls.Add(property, value) + } + return p +} + +// Concat the property and value pair +func (p *GetRequest) BuildParams() string { + return p.urls.Encode() +} diff --git a/pkg/model/global.go b/pkg/model/global.go new file mode 100644 index 0000000..3d15a16 --- /dev/null +++ b/pkg/model/global.go @@ -0,0 +1,41 @@ +package model + +const ( + Gin = "gin" // http静态服务 + Gather = "gather" // 火币行情采集 + CollectUs = "collectUs" // 美股行情采集分发 + CollectForex = "collectForex" // 外汇行情采集分发 + GatherForex = "gatherForex" // 项目-外汇行情订阅 + ForexClosePrice = "forexClosePrice" // 项目-外汇更新闭盘价 + GatherUs = "gatherUs" // 项目-美股行情订阅 + ShareWss = "shareWss" // 项目-股票行情订阅 + CurrencyWss = "currencyWss" // 项目-数字币行情订阅 + TickDB = "tickDB" // 行情数据采集(历史数据清洗) + SelfContract = "selfContract" // 合约市场 + SelfMarketSpot = "selfMarketSpot" // 现货市场 + IndonesiaStock = "indonesiaStock" // 印尼股市场 + ThailandStock = "thailandStock" // 泰股市场 + MalaysiaStock = "malaysiaStock" // 马来西亚股市场 + SingaporeStock = "singaporeStock" // 新加坡股市场 + IndiaStock = "indiaStock" // 印度股市场 + GermanyStock = "germanyStock" // 德国股市场 + FranceStock = "franceStock" // 法国股市场 + UKStock = "ukStock" // 英国股市场 + USStock = "usStock" // 美股市场 + BrazilStock = "brazilStock" // 巴西股市场 + JapanStock = "japanStock" // 日本股市场 + StockIndex = "stockIndex" + StockData = "stockData" + StockDataUs = "stockDataUs" + StockDataInfo = "stockDataInfo" + StockDataNews = "stockDataNews" + StockCode = "stockCode" + IndiaOption = "indiaOption" + HongKongStock = "hongkongStock" + DelOptionHash = "delOptionHash" + SendIndiaInfo = "sendIndiaInfo" + MalaysiaStockUpdate = "malaysiaStockUpdate" + DeleteIndia = "deleteIndia" + PinWs = "pinWs" + ForexToExcel = "forexToExcel" +) diff --git a/pkg/model/market/contractbboresponse.go b/pkg/model/market/contractbboresponse.go new file mode 100644 index 0000000..ec94fb6 --- /dev/null +++ b/pkg/model/market/contractbboresponse.go @@ -0,0 +1,23 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeCtBboResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` // 数据所属的 channel,格式: market.$contract_code.bbo + Timestamp int64 `json:"ts"` // 响应生成时间点,单位:毫秒(指接口响应时间) + Tick *CtBboTick +} + +type CtBboTick struct { + Mrid int64 `json:"mrid"` // 订单ID + Id int64 `json:"id"` // tick ID + Bids []decimal.Decimal `json:"bids"` // 买一盘,[price(挂单价), vol(此价格挂单张数)] + Asks []decimal.Decimal `json:"asks"` // 卖一盘,[price(挂单价), vol(此价格挂单张数)] + Ts int64 `json:"ts"` // 响应生成时间点,单位:毫秒(指数据生成时间) + Version int64 `json:"version"` // 版本号 + Ch string `json:"ch"` // 数据所属的 channel,格式: market.$contract_code.bbo +} diff --git a/pkg/model/market/contractdepthresponse.go b/pkg/model/market/contractdepthresponse.go new file mode 100644 index 0000000..d003eca --- /dev/null +++ b/pkg/model/market/contractdepthresponse.go @@ -0,0 +1,35 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeCtDepthResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick *CtDepthTick +} + +type SubscribeCtDepthTempResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick *CtDepthTick `json:"tick"` +} + +type SubscribeCtDepthResponseData struct { + Info interface{} `json:"info"` + Symbol string `json:"symbol"` +} + +type CtDepthTick struct { + Mrid int64 `json:"mrid"` // 订单ID + Id int64 `json:"id"` // tick ID + Bids [][]decimal.Decimal `json:"bids"` // 卖盘,[price(挂单价), vol(此价格挂单张数)], 按price升序 + Asks [][]decimal.Decimal `json:"asks"` // 买盘,[price(挂单价), vol(此价格挂单张数)], 按price降序 + Ts int64 `json:"ts"` // 深度生成时间戳,每100MS生成一次,单位:毫秒 + Version int64 `json:"version"` // 版本号 + Ch string `json:"ch"` // 数据所属的 channel,格式: market.period +} diff --git a/pkg/model/market/contractdepthsizeresponse.go b/pkg/model/market/contractdepthsizeresponse.go new file mode 100644 index 0000000..d6445c9 --- /dev/null +++ b/pkg/model/market/contractdepthsizeresponse.go @@ -0,0 +1,23 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeCtAddDepthResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick *CtDepthTick +} + +type CtAddDepthTick struct { + Mrid int64 `json:"mrid"` // 订单ID + Id int64 `json:"id"` // tick ID + Bids [][]decimal.Decimal `json:"bids"` // 卖盘,[price(挂单价), vol(此价格挂单张数)], 按price升序 + Asks [][]decimal.Decimal `json:"asks"` // 买盘,[price(挂单价), vol(此价格挂单张数)], 按price降序 + Ts int64 `json:"ts"` // 深度生成时间戳,每100MS生成一次,单位:毫秒 + Version int64 `json:"version"` // 版本号 + Ch string `json:"ch"` // 数据所属的 channel,格式: market.period +} diff --git a/pkg/model/market/contractdetailresponse.go b/pkg/model/market/contractdetailresponse.go new file mode 100644 index 0000000..391f82e --- /dev/null +++ b/pkg/model/market/contractdetailresponse.go @@ -0,0 +1,28 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeCtDetailResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` // 数据所属的 channel,格式: market.$contract_code.detail + Timestamp int64 `json:"ts"` // 响应生成时间点,单位:毫秒 + Tick *CtDetailTick +} + +type CtDetailTick struct { + Id int64 `json:"id"` // ID + Mrid int64 `json:"mrid"` // 订单ID + Open decimal.Decimal `json:"open"` // 开盘价 + Close decimal.Decimal `json:"close"` // 收盘价,当K线为最晚的一根时,是最新成交价 + High decimal.Decimal `json:"high"` // 最高价 + Low decimal.Decimal `json:"low"` // 最低价 + Amount decimal.Decimal `json:"amount"` // 成交量(币), 即 sum(每一笔成交量(张) * 单张合约面值/该笔成交价)。 值是买卖双边之和 + Vol decimal.Decimal `json:"vol"` // 成交量(张)。 值是买卖双边之和 + TradeTurnover decimal.Decimal `json:"trade_turnover"` // 成交额,即sum(每一笔成交张数 * 合约面值 * 成交价格)。 值是买卖双边之和 + Count int64 `json:"count"` // 成交笔数 + Asks []decimal.Decimal `json:"asks"` // [卖1价,卖1量(张)] + Bids []decimal.Decimal `json:"bids"` // [买1价,买1量(张)] +} diff --git a/pkg/model/market/contractklineresponse.go b/pkg/model/market/contractklineresponse.go new file mode 100644 index 0000000..713fda6 --- /dev/null +++ b/pkg/model/market/contractklineresponse.go @@ -0,0 +1,27 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeCtKlineResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick *CtKlineTick + Data []CtKlineTick +} + +type CtKlineTick struct { + Id int64 `json:"id"` // K线ID,也就是K线时间戳,K线起始时间 + Mrid int64 `json:"mrid"` // 订单ID + Vol decimal.Decimal `json:"vol"` // 成交量张数。 值是买卖双边之和 + Count decimal.Decimal `json:"count"` // 成交笔数。 值是买卖双边之和 + Open decimal.Decimal `json:"open"` // 开盘价 + Close decimal.Decimal `json:"close"` // 收盘价,当K线为最晚的一根时,是最新成交价 + Low decimal.Decimal `json:"low"` // 最低价 + High decimal.Decimal `json:"high"` // 最高价 + Amount decimal.Decimal `json:"amount"` // 成交量(币), 即 sum(每一笔成交量(张) * 单张合约面值/该笔成交价)。 值是买卖双边之和 + Rrade_Turnover decimal.Decimal `json:"trade_turnover"` // 成交额, 即sum(每一笔成交张数 * 合约面值 * 成交价格)。 值是买卖双边之和 +} diff --git a/pkg/model/market/contracttradedetailresponse.go b/pkg/model/market/contracttradedetailresponse.go new file mode 100644 index 0000000..d54806a --- /dev/null +++ b/pkg/model/market/contracttradedetailresponse.go @@ -0,0 +1,29 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeCtTradeDetailResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` // 数据所属的 channel,格式: market.$contract_code.bbo + Timestamp int64 `json:"ts"` // 响应生成时间点,单位:毫秒(指接口响应时间) + Tick *CtTradeDetailTick +} + +type CtTradeDetailTick struct { + Id int64 `json:"id"` // 订单唯一id(品种唯一) + Ts int64 `json:"ts"` // tick数据戳 + Data []TradeDetail `json:"data"` +} + +type TradeDetail struct { + Amount decimal.Decimal `json:"amount"` // 数量(张)。 值是买卖双边之和 + Ts int64 `json:"ts"` // 订单时间戳 + Id int64 `json:"id"` // 成交唯一id(品种唯一) + Price decimal.Decimal `json:"price"` // 价格 + Direction string `json:"direction"` // 买卖方向,即taker(主动成交)的方向 + Quantity decimal.Decimal `json:"quantity"` // 成交量(币) + TradeTurnover decimal.Decimal `json:"trade_turnover"` // 成交额(计价币种) +} diff --git a/pkg/model/market/getcandlestickresponse.go b/pkg/model/market/getcandlestickresponse.go new file mode 100644 index 0000000..220fbdb --- /dev/null +++ b/pkg/model/market/getcandlestickresponse.go @@ -0,0 +1,20 @@ +package market + +import "github.com/shopspring/decimal" + +type GetCandlestickResponse struct { + Status string `json:"status"` + Ch string `json:"ch"` + Ts int64 `json:"ts"` + Data []Candlestick `json:"data"` +} +type Candlestick struct { + Amount decimal.Decimal `json:"amount"` + Open decimal.Decimal `json:"open"` + Close decimal.Decimal `json:"close"` + High decimal.Decimal `json:"high"` + Id int64 `json:"id"` + Count int64 `json:"count"` + Low decimal.Decimal `json:"low"` + Vol decimal.Decimal `json:"vol"` +} diff --git a/pkg/model/market/getdepthoptionalrequest.go b/pkg/model/market/getdepthoptionalrequest.go new file mode 100644 index 0000000..d93c1ea --- /dev/null +++ b/pkg/model/market/getdepthoptionalrequest.go @@ -0,0 +1,20 @@ +package market + +const ( + DEPTH_SIZE_FIVE = 5 + DEPTH_SIZE_TEN = 10 + DEPTH_SIZE_TWENTY = 20 +) + +const ( + STEP0 = "step0" + STEP1 = "step1" + STEP2 = "step2" + STEP3 = "step3" + STEP4 = "step4" + STEP5 = "step5" +) + +type GetDepthOptionalRequest struct { + Size int +} diff --git a/pkg/model/market/getdepthresponse.go b/pkg/model/market/getdepthresponse.go new file mode 100644 index 0000000..86b83f7 --- /dev/null +++ b/pkg/model/market/getdepthresponse.go @@ -0,0 +1,17 @@ +package market + +import "github.com/shopspring/decimal" + +type GetDepthResponse struct { + Status string `json:"status"` + Ch string `json:"ch"` + Ts int64 `json:"ts"` + Tick *Depth `json:"tick"` +} + +type Depth struct { + Timestamp int64 `json:"ts"` + Version int64 `json:"version"` + Bids [][]decimal.Decimal `json:"bids"` + Asks [][]decimal.Decimal `json:"asks"` +} diff --git a/pkg/model/market/subscribebestbidofferresponse.go b/pkg/model/market/subscribebestbidofferresponse.go new file mode 100644 index 0000000..7a7c806 --- /dev/null +++ b/pkg/model/market/subscribebestbidofferresponse.go @@ -0,0 +1,18 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeBestBidOfferResponse struct { + base.WebSocketResponseBase + Tick *struct { + QuoteTime int64 `json:"quoteTime"` + Symbol string `json:"symbol"` + Bid decimal.Decimal `json:"bid"` + BidSize decimal.Decimal `json:"bidSize"` + Ask decimal.Decimal `json:"ask"` + AskSize decimal.Decimal `json:"askSize"` + } +} diff --git a/pkg/model/market/subscribecandlestickresponse.go b/pkg/model/market/subscribecandlestickresponse.go new file mode 100644 index 0000000..95df72b --- /dev/null +++ b/pkg/model/market/subscribecandlestickresponse.go @@ -0,0 +1,25 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeCandlestickResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick *Tick + Data []Tick +} +type Tick struct { + Id int64 `json:"id"` // unix时间,同时作为K线ID + Amount decimal.Decimal `json:"amount"` // 成交量 + Count int `json:"count"` // 成交笔数 + Open decimal.Decimal `json:"open"` // 开盘价 + Close decimal.Decimal `json:"close"` // 收盘价(当K线为最晚的一根时,是最新成交价) + Low decimal.Decimal `json:"low"` // 最低价 + High decimal.Decimal `json:"high"` // 最高价 + Vol decimal.Decimal `json:"vol"` // 成交额, 即 sum(每一笔成交价 * 该笔的成交量) + IsBa int `json:"is_ba"` //是否 币安 2 +} diff --git a/pkg/model/market/subscribedepthresponse.go b/pkg/model/market/subscribedepthresponse.go new file mode 100644 index 0000000..f38d30d --- /dev/null +++ b/pkg/model/market/subscribedepthresponse.go @@ -0,0 +1,11 @@ +package market + +import ( + "wss-pool/pkg/model/base" +) + +type SubscribeDepthResponse struct { + base.WebSocketResponseBase + Data *Depth + Tick *Depth +} diff --git a/pkg/model/market/subscribelast24hcandlestickresponse.go b/pkg/model/market/subscribelast24hcandlestickresponse.go new file mode 100644 index 0000000..cb7f10d --- /dev/null +++ b/pkg/model/market/subscribelast24hcandlestickresponse.go @@ -0,0 +1,11 @@ +package market + +import ( + "wss-pool/pkg/model/base" +) + +type SubscribeLast24hCandlestickResponse struct { + base.WebSocketResponseBase + Data *Candlestick + Tick *Candlestick +} diff --git a/pkg/model/market/subscribemarketbypriceresponse.go b/pkg/model/market/subscribemarketbypriceresponse.go new file mode 100644 index 0000000..68ad229 --- /dev/null +++ b/pkg/model/market/subscribemarketbypriceresponse.go @@ -0,0 +1,19 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeMarketByPriceResponse struct { + base.WebSocketResponseBase + Tick *MarketByPrice + Data *MarketByPrice +} + +type MarketByPrice struct { + SeqNum int64 `json:"seqNum"` + PrevSeqNum int64 `json:"prevSeqNum"` + Bids [][]decimal.Decimal `json:"bids"` + Asks [][]decimal.Decimal `json:"asks"` +} diff --git a/pkg/model/market/subscribetraderesponse.go b/pkg/model/market/subscribetraderesponse.go new file mode 100644 index 0000000..ec224ba --- /dev/null +++ b/pkg/model/market/subscribetraderesponse.go @@ -0,0 +1,26 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type SubscribeTradeResponse struct { + base.WebSocketResponseBase + Data []Trade + Tick *TickTrade +} + +type TickTrade struct { + Id int64 `json:"id"` + Timestamp int64 `json:"ts"` + Data []Trade +} + +type Trade struct { + TradeId int64 `json:"tradeId"` + Amount decimal.Decimal `json:"amount"` + Price decimal.Decimal `json:"price"` + Timestamp int64 `json:"ts"` + Direction string `json:"direction"` +} diff --git a/pkg/model/market/subtickerwebsocketresponse.go b/pkg/model/market/subtickerwebsocketresponse.go new file mode 100644 index 0000000..d3b0f59 --- /dev/null +++ b/pkg/model/market/subtickerwebsocketresponse.go @@ -0,0 +1,39 @@ +package market + +import ( + "github.com/shopspring/decimal" + "wss-pool/pkg/model/base" +) + +type TickerWebsocketResponse struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick *TickR + Data []TickR +} + +type TickerWebsocketResponses struct { + base base.WebSocketResponseBase + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick *TickR `json:"tick"` + Data []TickR +} + +type TickR struct { + Open decimal.Decimal `json:"open"` // 本阶段开盘价(以滚动24小时计) + High decimal.Decimal `json:"high"` // 本阶段最高价(以滚动24小时计) + Low decimal.Decimal `json:"low"` // 本阶段最低价(以滚动24小时计) + Close decimal.Decimal `json:"close"` // 本阶段最新价(以滚动24小时计) + Amount decimal.Decimal `json:"amount"` // 以基础币种计量的交易量(以滚动24小时计) + Count int `json:"count"` // 交易次数(以滚动24小时计) + Bid decimal.Decimal `json:"bid"` // 当前的最高买价 + BidSize decimal.Decimal `json:"bidSize"` // 最高买价对应的量 + Ask decimal.Decimal `json:"ask"` // 当前的最低卖价 + AskSize decimal.Decimal `json:"askSize"` // 最低卖价对应的量 + LastPrice decimal.Decimal `json:"lastPrice"` // 最新成交价 + LastSize decimal.Decimal `json:"lastSize"` // 最新成交价对应的量 + Vol decimal.Decimal `json:"vol"` + IsBa int `json:"is_ba"` +} diff --git a/pkg/model/request.go b/pkg/model/request.go new file mode 100644 index 0000000..8431176 --- /dev/null +++ b/pkg/model/request.go @@ -0,0 +1,470 @@ +package model + +import ( + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type SpotsModel struct { + Symbol string `json:"symbol"` //交易对 例如:btcusdt, ethbtc + Period string `json:"period"` //返回数据时间粒度,也就是每根蜡烛的时间区间 + Size int `json:"size"` //返回K线数据条数 例如:[1-2000] + Depth int `json:"depth"` //返回深度的数量 例如:5,10,20 + Type string `json:"type"` //深度的价格聚合度,具体说明见下方 例如:step0,step1,step2,step3,step4,step5 +} + +type ContractModel struct { + ContractCode string `json:"contract_code"` //合约代码 或 合约标识 + Type string `json:"type"` //深度类型 + BusinessType string `json:"business_type"` //业务类型,不填默认永续 例如:futures:交割、swap:永续、all:全部 + Size int `json:"size"` //获取数量,默认150 [1,2000] + Period string `json:"period"` //K线类型 1min, 5min, 15min, 30min, 60min,4hour,1day,1week,1mon + From int64 `json:"from"` //开始时间戳 10位 单位S + To int64 `json:"to"` //结束时间戳 10位 单位S + Pair string `json:"pair"` //交易对 备注:BTC-USDT + ContractType string `json:"contract_type"` //备注:swap(永续)、this_week(当周)、next_week(次周)、quarter(当季)、next_quarter(次季) + AmountType int `json:"amount_type"` //备注:1:张,2:币 + BasisPriceType string `json:"basis_price_type"` //基差价格类型,表示在周期内计算基差使用的价格类型, 不填,默认使用开盘价 +} + +type GetSpotsTickCompleteList struct { + Status string `json:"status"` + Ch string `json:"ch"` + Ts int64 `json:"ts"` + Tick *SpotsTickS `json:"tick"` + Icon string `json:"icon"` + FullName string `json:"fullName"` +} + +type GetSpotsTickList struct { + Status string `json:"status"` + Ch string `json:"ch"` + Ts int64 `json:"ts"` + Tick *SpotsTickS `json:"tick"` +} + +type SpotsTickS struct { + Id int64 `json:"id"` + Version int64 `json:"version"` + Open decimal.Decimal `json:"open"` + Close decimal.Decimal `json:"close"` + Low decimal.Decimal `json:"low"` + High decimal.Decimal `json:"high"` + Amount decimal.Decimal `json:"amount"` + Vol decimal.Decimal `json:"vol"` + Count int64 `json:"count"` + Bid []decimal.Decimal `json:"bid"` + Ask []decimal.Decimal `json:"ask"` +} + +type GetContractTickCompleteList struct { + Status string `json:"status"` + Ch string `json:"ch"` + Ts int64 `json:"ts"` + Tick *ContractTick `json:"tick"` + Icon string `json:"icon"` + FullName string `json:"fullName"` +} + +type GetContractTickList struct { + Status string `json:"status"` + Ch string `json:"ch"` + Ts int64 `json:"ts"` + Tick *ContractTick `json:"tick"` +} + +type ContractTick struct { + Id int64 `json:"id"` + Version int64 `json:"version"` + Open decimal.Decimal `json:"open"` + Close decimal.Decimal `json:"close"` + Low decimal.Decimal `json:"low"` + High decimal.Decimal `json:"high"` + Amount decimal.Decimal `json:"amount"` + Vol decimal.Decimal `json:"vol"` + Count int64 `json:"count"` + Bid []decimal.Decimal `json:"bid"` + Ask []decimal.Decimal `json:"ask"` + TradeTurnover string `json:"trade_turnover"` +} + +type Subscribe struct { + Action string `json:"action"` + Symbols string `json:"symbols"` +} + +// Sequence analysis +type ClientMessage struct { + S string `json:"s,omitempty"` // 股票代码 + P decimal.Decimal `json:"p,omitempty"` // 价格 + C []decimal.Decimal `json:"c,omitempty"` // 条件,有关更多信息,请参阅贸易条件术语表 + V int64 `json:"v,omitempty"` // 交易量,代表在相应时间戳处交易的股票数量 + Dp bool `json:"dp,omitempty"` // 暗池真/假 + Ms string `json:"ms,omitempty"` // 市场状态,指示股票市场的当前状态(“开盘”、“收盘”、“延长交易时间”) + T int64 `json:"t,omitempty"` // 以毫秒为单位的时间戳 + Av int64 `json:"av,omitempty"` // 今天累计交易量 1 + Op decimal.Decimal `json:"op,omitempty"` // 今天正式开盘价格 + Vw decimal.Decimal `json:"vw,omitempty"` // 即时报价的成交量加权平均价格 + Cl decimal.Decimal `json:"cl,omitempty"` // 此聚合窗口的收盘价 + H decimal.Decimal `json:"h,omitempty"` // 此聚合窗口的最高逐笔报价 + L decimal.Decimal `json:"l,omitempty"` // 此聚合窗口的最低价格变动价格 + A decimal.Decimal `json:"a,omitempty"` // 今天的成交量加权平均价格 + Z int64 `json:"z,omitempty"` // 此聚合窗口的平均交易规模 + Se int64 `json:"se,omitempty"` // 此聚合窗口的起始报价的时间戳(以 Unix 毫秒为单位) + ClosingMarket bool `json:"closing_market"` +} + +type ClientMessageParam struct { + Data []ClientMessage `json:"data"` + Token string `json:"token"` +} + +// Original parsing +type ClientMessageNew struct { + Ev string `json:"ev,omitempty"` // 事件类型 + Sym string `json:"sym,omitempty"` // 股票代码 1 + V int64 `json:"v,omitempty"` // 报价交易量 1 + Av int64 `json:"av,omitempty"` // 今天累计交易量 1 + Op decimal.Decimal `json:"op,omitempty"` // 今天正式开盘价格 + Vw decimal.Decimal `json:"vw,omitempty"` // 即时报价的成交量加权平均价格 + O decimal.Decimal `json:"o,omitempty"` // 此聚合窗口的开盘价格 1 + C decimal.Decimal `json:"c,omitempty"` // 此聚合窗口的收盘价 + H decimal.Decimal `json:"h,omitempty"` // 此聚合窗口的最高逐笔报价 + L decimal.Decimal `json:"l,omitempty"` // 此聚合窗口的最低价格变动价格 + A decimal.Decimal `json:"a,omitempty"` // 今天的成交量加权平均价格 + Z int64 `json:"z,omitempty"` // 此聚合窗口的平均交易规模 + S int64 `json:"s,omitempty"` // 此聚合窗口的起始报价的时间戳(以 Unix 毫秒为单位) + E int64 `json:"e,omitempty"` // 此聚合窗口的结束时钟周期的时间戳(以 Unix 毫秒为单位) +} + +type FinnhubMessage struct { + Data []FinnhubMessageData `json:"data"` +} + +type FinnhubMessageNew struct { + Topic string `json:"topic"` + Content string `json:"content"` +} + +type FinnhubMessageData struct { + S string `json:"s,omitempty"` // 股票代码 + P decimal.Decimal `json:"p,omitempty"` // 此聚合窗口的落盘价 + V int64 `json:"v,omitempty"` // 交易量 + T int64 `json:"t,omitempty"` // 此聚合窗口的结束时钟周期的时间戳(以 Unix 毫秒为单位) + C []decimal.Decimal `json:"c"` +} + +type OrderWss struct { + Code int `json:"code,omitempty"` + Data Data `json:"data,omitempty"` + Msg string `json:"msg,omitempty"` +} + +type Data struct { + Id int64 `json:"id,omitempty"` + UserId int64 `json:"userId,omitempty"` + SystemBoursesId int64 `json:"systemBoursesId,omitempty"` + BourseType int `json:"bourseType,omitempty"` + StockCode string `json:"stockCode,omitempty"` + Icon string `json:"icon,omitempty"` + Unit string `json:"unit,omitempty"` + FullName string `json:"fullName,omitempty"` + BeforeClose decimal.Decimal `json:"beforeClose,omitempty"` + YesterdayClose decimal.Decimal `json:"yesterdayClose,omitempty"` + KeepDecimal int `json:"keepDecimal"` + Currency string `json:"currency"` +} + +type Token struct { + State bool `json:"state,omitempty"` + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Data string `json:"data,omitempty"` +} + +type LoginPost struct { + Check string `json:"check,omitempty"` + PhoneNumber string `json:"phoneNumber,omitempty"` + Password string `json:"password,omitempty"` + InvitationCode string `json:"invitationCode,omitempty"` + Code string `json:"code,omitempty"` + Id string `json:"id,omitempty"` + TerminalEquipmentName string `json:"terminalEquipmentName,omitempty"` + LoginCountry string `json:"loginCountry,omitempty"` + LoginCity string `json:"loginCity,omitempty"` + LoginIp string `json:"loginIp,omitempty"` +} + +type StockParam struct { + Symbol string `json:"symbol"` //交易所:股票代码 + StockCode string `json:"stock_code"` // 股票代码 + StockName string `json:"stock_name"` // 股票名,可能为空 + Price float64 `json:"price"` // 实时价格 + UpDownRate decimal.Decimal `json:"up_down_rate"` // 涨跌% + UpDown decimal.Decimal `json:"up_down"` // // 涨跌额 + TradeV decimal.Decimal `json:"trade_v"` // 技术评级指标值 + TradeK string `json:"trade_k"` // 技术评级 + Vol int64 `json:"vol"` // 成交量 + TurnoverPriceTotal decimal.Decimal `json:"turnover_price_total"` // 成交量*价格 + PriceTotal string `json:"price_total"` // 总市值 + PE string `json:"p_e"` //pe + Eps string `json:"eps"` // EPS + EmployeesNumber string `json:"employees_number"` // 雇员 + Plate string `json:"plate"` // 板块 + Desc string `json:"desc"` // 股票说明 + PriceCode string `json:"price_code"` // 货币单位 + Country string `json:"country"` //国家 + Ts int64 `json:"ts"` // 时间戳 毫秒 + Token string `json:"token"` + ClosingMarket bool `json:"closing_market"` +} + +type StockMonParam struct { + Symbol string `json:"symbol"` //交易所:股票代码 + StockCode string `json:"stock_code"` // 股票代码 + StockName string `json:"stock_name"` // 股票名,可能为空 + OpenPrice decimal.Decimal `json:"open_price"` // 开盘价 + HighPrice decimal.Decimal `json:"high_price"` // 最高价 + LowPrice decimal.Decimal `json:"low_price"` // 最低价 + ClosePrice decimal.Decimal `json:"close_price"` // 当前时段市价 + Desc string `json:"desc"` // 股票说明 + PriceCode string `json:"price_code"` // 货币单位 + Country string `json:"country"` //国家 + Vol int64 `json:"vol"` + Ts int64 `json:"ts"` // 蜡烛图的开端 时间戳 毫秒 +} + +type StockMonRes struct { + Result []StockMonParam `json:"result"` + Token string `json:"token"` + Country string `json:"country"` + Status string `json:"status"` // 5min 15min 30min 1hour 1day 1week 1mon +} + +type StockNews struct { + Country string `json:"country"` //国家 + Title string `json:"title"` //标题 + Source string `json:"source"` //来源 + Pubdate string `json:"pubdate"` //发布时间 + Link string `json:"link"` // 链接 + Code int64 `json:"code"` // 链接 +} + +type StockNewsRes struct { + Result []StockNews `json:"result"` + Token string `json:"token"` + Country string `json:"country"` +} + +type StockMogoParam struct { + Symbol string `json:"symbol"` //交易所:股票代码 + StockCode string `json:"stock_code" bson:"stock_code"` // 股票代码 + StockName string `json:"stock_name" bson:"stock_name"` // 股票名,可能为空 + Price float64 `json:"price"` // 实时价格 + OpenPrice string `json:"open_price" bson:"open_price"` // 开盘价 + PreviousPrice string `json:"previous_price" bson:"previous_price"` //上一次的闭盘价 + HighPrice string `json:"high_price" bson:"high_price"` // 最高价 + LowPrice string `json:"low_price" bson:"low_price"` // 最低价 + ClosePrice string `json:"close_price" bson:"close_price"` // 当前时段市价 + StringVal float64 `json:"stringVal" bson:"stringVal"` + UpDownRate string `json:"up_down_rate" bson:"up_down_rate"` // 涨跌% + UpDown string `json:"up_down" bson:"up_down"` // // 涨跌额 + TradeV string `json:"trade_v" bson:"trade_v"` // 技术评级指标值 + TradeK string `json:"trade_k" bson:"trade_k"` // 技术评级 + Vol interface{} `json:"vol"` // 成交量 + TurnoverPriceTotal string `json:"turnover_price_total" bson:"turnover_price_total"` // 成交量*价格 + PriceTotal string `json:"price_total" bson:"price_total"` // 总市值 + PE string `json:"p_e" bson:"p_e"` //pe + Eps string `json:"eps"` // EPS + EmployeesNumber string `json:"employees_number" bson:"employees_number"` // 雇员 + Plate string `json:"plate"` // 板块 + Desc string `json:"desc"` // 股票说明 + PriceCode string `json:"price_code" bson:"price_code"` // 货币单位 + Country string `json:"country"` //国家 + Ts int64 `json:"ts" bson:"timestamp"` // 时间戳 毫秒 + Vw string `json:"vw"` + PrimaryExchange string `json:"primary_exchange"` + NumericCode string `json:"numeric_code"` + ID primitive.ObjectID `bson:"_id"` +} + +type StockIndexParam struct { + StockCode string `json:"stock_code" bson:"stock_code"` // 股票代码 + StockName string `json:"stock_name" bson:"stock_name"` // 股票名,可能为空 + Price float64 `json:"price"` // 最新价格 + HighPrice float64 `json:"high_price" bson:"high_price"` // 最高价格 + LowPrice float64 `json:"low_price" bson:"low_price"` // 最低价格 + OpenPrice float64 `json:"open_price" bson:"open_price"` // 开盘价格 + UpDownRate decimal.Decimal `json:"up_down_rate" bson:"up_down_rate"` // 涨跌% + UpDown decimal.Decimal `json:"up_down" bson:"up_down"` // 涨跌额 + Vol int64 `json:"vol"` // 成交量 + PriceCode string `json:"price_code" bson:"price_code"` // 货币单位 + Country string `json:"country"` //国家 + Ts int64 `json:"ts" bson:"timestamp"` // 时间戳 毫秒 + Token string `json:"token"` + IsStockIndex bool `json:"is_stock_index"` + //ClosingMarket bool `json:"closing_market"` +} + +type StockMogoParams struct { + Symbol string `json:"symbol"` //交易所:股票代码 + StockCode string `json:"stock_code" bson:"stock_code"` // 股票代码 + StockName string `json:"stock_name" bson:"stock_name"` // 股票名,可能为空 + Price float64 `json:"price"` // 实时价格 + OpenPrice string `json:"open_price" bson:"open_price"` // 开盘价 + PreviousPrice string `json:"previous_price" bson:"previous_price"` //上一次的闭盘价 + HighPrice string `json:"high_price" bson:"high_price"` // 最高价 + LowPrice string `json:"low_price" bson:"low_price"` // 最低价 + ClosePrice string `json:"close_price" bson:"close_price"` // 当前时段市价 + UpDownRate interface{} `json:"up_down_rate" bson:"up_down_rate"` // 涨跌% + UpDown interface{} `json:"up_down" bson:"up_down"` // // 涨跌额 + TradeV string `json:"trade_v" bson:"trade_v"` // 技术评级指标值 + TradeK string `json:"trade_k" bson:"trade_k"` // 技术评级 + Vol interface{} `json:"vol"` // 成交量 + TurnoverPriceTotal string `json:"turnover_price_total" bson:"turnover_price_total"` // 成交量*价格 + PriceTotal string `json:"price_total" bson:"price_total"` // 总市值 + PE string `json:"p_e" bson:"p_e"` //pe + Eps string `json:"eps"` // EPS + EmployeesNumber string `json:"employees_number" bson:"employees_number"` // 雇员 + Plate string `json:"plate"` // 板块 + Desc string `json:"desc"` // 股票说明 + PriceCode string `json:"price_code" bson:"price_code"` // 货币单位 + Country string `json:"country"` //国家 + Ts int64 `json:"ts" bson:"timestamp"` // 时间戳 毫秒 + Vw string `json:"vw"` + PrimaryExchange string `json:"primary_exchange"` +} + +type StrikeInfo struct { + Strike string `json:"strike"` + IsDefault bool `json:"is_default"` + Calls StrikeInfos `json:"calls"` + Puts StrikeInfos `json:"puts"` + Compute ComputePayload `json:"compute"` +} + +type ComputePayload struct { + TickSize string `json:"tick_size"` + LotSize string `json:"lot_size"` + Multiplier string `json:"multiplier"` + ExpiryType string `json:"expiry_type"` + ScriptDate string `json:"script_date"` +} + +type StrikeInfos struct { + GammaOn string `json:"gamma_on"` + GammaOff string `json:"gamma_off"` + VegaOn string `json:"vega_on"` + VegaOff string `json:"vega_off"` + ThetaOn string `json:"theta_on"` + ThetaOff string `json:"theta_off"` + DeltaOn string `json:"delta_on"` + DeltaOff string `json:"delta_off"` + PCR string `json:"pcr,omitempty"` + POP string `json:"pop"` + Volume string `json:"volume"` + OIChg string `json:"oi_chg"` + OIChgPercent string `json:"oi_chg_percent"` //olchg% + OIlakh string `json:"oi_lakh"` + BidOn string `json:"bid_on"` + BidOff string `json:"bid_off"` + OfferOn string `json:"offer_on"` + OfferOff string `json:"offer_off"` + IntValFOn string `json:"int_val_f_on"` + IntValFOff string `json:"int_val_f_off"` + IntValSOn string `json:"int_val_s_on"` + IntValSOff string `json:"int_val_s_off"` + TimeValueOn string `json:"time_value_on"` + TimeValueOff string `json:"time_value_off"` + LTPOn string `json:"ltp_on"` //开启 一手期权 + LTPOff string `json:"ltp_off"` // 默认 一股 期权 + IV string `json:"iv,omitempty"` + IVChg string `json:"iv_chg,omitempty"` + Name string `json:"name"` +} + +type OptionInfoParam struct { + Country string `json:"country"` + Stock string `json:"stock"` // 期权code + FutPrice string `json:"fut_price"` // + Percent float64 `json:"percent"` + ATMIV string `json:"atmiv"` + IVP string `json:"ivp"` + Results map[string][]StrikeInfo `json:"results"` + Token string `json:"token"` + IsOptionInfo bool `json:"is_option_info"` + IsClose bool `json:"is_close"` + CloseDate string `json:"option_date"` +} + +type OptionInfoExchange struct { + Country string `json:"country"` + Stock string `json:"stock"` // 期权code + FutPrice string `json:"fut_price"` // + Percent float64 `json:"percent"` + ATMIV string `json:"atmiv"` + IVP string `json:"ivp"` + Strike map[string]string `json:"strike"` //key 日期 value strike + IsOptionInfo bool `json:"is_option_info"` +} + +type OptionInfoMogo struct { + Country string `json:"country" bson:"country"` + Stock string `json:"stock" bson:"code"` // 期权code + FutPrice string `json:"fut_price" bson:"fut_price"` // + Percent float64 `json:"percent" bson:"percent"` + ATMIV string `json:"atmiv" bson:"atmiv"` + IVP string `json:"ivp" bson:"ivp"` + Results string `json:"results"` +} + +type OptionInfoExpiryMogo struct { + Stock string `json:"stock" bson:"code"` // 期权code + Expiry string `json:"expiry" bson:"expiry"` + Info string `json:"info" bson:"info"` +} + +type OptionInfoRes struct { + Country string `json:"country"` + Stock string `json:"stock"` // 期权code + Results string `json:"results"` +} + +type OptionInfo struct { + Stock string `json:"stock"` // 期权code + Results []OptionPolygon `json:"results"` + Token string `json:"token"` +} + +type OptionPolygon struct { + Stock string `json:"stock" bson:"Code"` // 期权code + FutPrice string `json:"fut_price"` // + Percent float64 `json:"percent"` //涨跌幅 + IVChg string `json:"iv_chg"` + ATMIV string `json:"atmiv"` + IVP string `json:"ivp"` + Country string `json:"country"` + PrimaryExchange string `json:"primary_exchange" bson:"Exchange"` + IsOptionList bool `json:"is_option_list"` + Rate float64 `json:"rate"` + DateTime string `json:"date_time" bson:"DateTime"` + IsClose bool `json:"is_close"` + CloseDate string `json:"option_date"` +} + +type OptionIndexList struct { + Results []OptionPolygon `json:"results"` + Token string `json:"token"` +} + +// offer 替换 ask ltp 替换 price chg 换 oi chg +type StrikePrice struct { + Price string `json:"price"` + Bid string `json:"bid"` + Ask string `json:"ask"` + Code string `json:"code"` + BeforeClose string `json:"before_close"` + YesterdayClose string `json:"yesterday_close"` + CloseDate string `json:"option_date"` + DueDate int64 `json:"due_date"` +} diff --git a/pkg/model/sqlmodel/bo_user_fund_account_spots.go b/pkg/model/sqlmodel/bo_user_fund_account_spots.go new file mode 100644 index 0000000..5cd3840 --- /dev/null +++ b/pkg/model/sqlmodel/bo_user_fund_account_spots.go @@ -0,0 +1,17 @@ +package sqlmodel + +import ( + "time" +) + +type BoUserFundAccountSpots struct { + Id int64 `xorm:"pk autoincr comment('资金账户id') BIGINT"` + Fundaccountsid int64 `xorm:"not null comment('用户USDT现货资金表id') BIGINT"` + Userid int64 `xorm:"not null comment('用户id') BIGINT"` + Fundaccountspot string `xorm:"not null comment('资金账号') VARCHAR(100)"` + Fundaccountname string `xorm:"not null comment('资金账户名称-对应币名') VARCHAR(30)"` + Spotmoney string `xorm:"not null default 0.000000 comment('账户金额(与对应持仓表对应)') DECIMAL(15,6)"` + Unit string `xorm:"comment('单位') VARCHAR(60)"` + Addtime time.Time `xorm:"comment('创建时间') DATETIME"` + Updatetime time.Time `xorm:"comment('更新时间') DATETIME"` +} diff --git a/pkg/model/sqlmodel/bo_user_fund_accounts.go b/pkg/model/sqlmodel/bo_user_fund_accounts.go new file mode 100644 index 0000000..31d5107 --- /dev/null +++ b/pkg/model/sqlmodel/bo_user_fund_accounts.go @@ -0,0 +1,16 @@ +package sqlmodel + +import ( + "time" +) + +type BoUserFundAccounts struct { + Id int64 `xorm:"pk autoincr comment('资金账户id') BIGINT"` + Userid int64 `xorm:"not null comment('用户id') index(userId) BIGINT"` + Fundaccounttype int `xorm:"not null comment('资金账号类型:1 现货USDT(充值默认账户) 2 合约USDT 3 马股资产 4 美股资产 5 日股资产 6 佣金账户USDT') index(userId) TINYINT"` + Fundaccount string `xorm:"not null comment('资金账号') index(userId) CHAR(20)"` + Money string `xorm:"not null default 0.000000 comment('账户金额') DECIMAL(15,6)"` + Unit string `xorm:"comment('单位') VARCHAR(60)"` + Addtime time.Time `xorm:"comment('创建时间') DATETIME"` + Updatetime time.Time `xorm:"comment('更新时间') DATETIME"` +} diff --git a/pkg/model/sqlmodel/bo_user_optional_stocks.go b/pkg/model/sqlmodel/bo_user_optional_stocks.go new file mode 100644 index 0000000..3fd9e12 --- /dev/null +++ b/pkg/model/sqlmodel/bo_user_optional_stocks.go @@ -0,0 +1,15 @@ +package sqlmodel + +// BoUserOptionalStocks 自选股表 +type BoUserOptionalStocks struct { + Id int64 `xorm:"id pk autoincr comment('自选股id') BIGINT"` + Userid int64 `xorm:"userId not null comment('用户id') unique(userId) BIGINT"` + Boursetype int `xorm:"bourseType not null comment('所属类型:1 数字币 2 股票 (冗余)') TINYINT"` + Systemboursesid int64 `xorm:"systemBoursesId not null comment('交易所股种id:1 现货 2 合约 3 马股 4 美股 5 日股') BIGINT"` + Stockcode string `xorm:"stockCode not null comment('股票代码') unique(userId) VARCHAR(100)"` + Icon string `xorm:"icon comment('图标') VARCHAR(100)"` + Unit string `xorm:"unit comment('单位') VARCHAR(60)"` + Fullname string `xorm:"fullName comment('全称') VARCHAR(180)"` + Beforeclose string `xorm:"beforeClose DECIMAL(15,6)"` + Yesterdayclose string `xorm:"yesterdayClose DECIMAL(15,6)"` +} diff --git a/pkg/model/sqlmodel/bo_user_sms.go b/pkg/model/sqlmodel/bo_user_sms.go new file mode 100644 index 0000000..f82e8a2 --- /dev/null +++ b/pkg/model/sqlmodel/bo_user_sms.go @@ -0,0 +1,16 @@ +package sqlmodel + +import ( + "time" +) + +type BoUserSms struct { + Id int `xorm:"not null pk autoincr comment('自增长id') INT"` + From string `xorm:"not null comment('发送方标识;支持SenderId的发送,只允许数字+字母,含有字母标识最长11位,纯数字标识支持15位') VARCHAR(255)"` + To string `xorm:"not null comment('接收短信号码;号码格式为:国际区号+号码') VARCHAR(255)"` + Message string `xorm:"comment('短信内容') VARCHAR(255)"` + TaskId string `xorm:"comment('任务ID;长度不要超过255') VARCHAR(255)"` + MessageResult string `xorm:"comment('返回消息结果') VARCHAR(255)"` + CreateTime time.Time `xorm:"not null comment('创建时间') DATETIME"` + UpdateTime time.Time `xorm:"not null comment('更新时间') DATETIME"` +} diff --git a/pkg/model/sqlmodel/bo_user_terminal_equipments.go b/pkg/model/sqlmodel/bo_user_terminal_equipments.go new file mode 100644 index 0000000..7ae5d78 --- /dev/null +++ b/pkg/model/sqlmodel/bo_user_terminal_equipments.go @@ -0,0 +1,15 @@ +package sqlmodel + +import ( + "time" +) + +type BoUserTerminalEquipments struct { + TerminalEquipmentId int `xorm:"terminalEquipmentId not null pk autoincr comment('自增长id') INT"` + UserId int64 `xorm:"userId not null comment('用户表id') INT"` + TerminalEquipmentName string `xorm:"terminalEquipmentName not null comment('终端设备名称') VARCHAR(30)"` + LoginCountry string `xorm:"loginCountry not null comment('登录国家') VARCHAR(30)"` + LoginCity string `xorm:"loginCity not null comment('登录城市') VARCHAR(30)"` + LoginIp int `xorm:"loginIp not null comment('登录IP') INT"` + AddTime time.Time `xorm:"addTime not null comment('创建时间') DATETIME"` +} diff --git a/pkg/model/sqlmodel/bo_users.go b/pkg/model/sqlmodel/bo_users.go new file mode 100644 index 0000000..80153fa --- /dev/null +++ b/pkg/model/sqlmodel/bo_users.go @@ -0,0 +1,53 @@ +package sqlmodel + +import "time" + +// UsersJson 用户表 +type UsersJson struct { + Id int64 `json:"id"` //userId + Uid string `json:"uid"` //UID + CountryCode string `json:"countryCode"` //国家代码 + PhoneNumber int64 `json:"phoneNumber"` //手机号 + Email string `json:"email"` //电子邮箱 + LoginPassword string `json:"loginPassword"` //登陆密码 + TradePassword string `json:"tradePassword"` //交易密码 + Surname string `json:"surname"` //姓 + Name string `json:"name"` //名 + Sex int32 `json:"sex"` //性别:1男 2女 0未设置 + Birthday string `json:"birthday"` //出生日期 + Country string `json:"country"` //国家 + NickName string `json:"nickName"` //昵称 + Avatar string `json:"avatar"` //头像 + IsRealName int32 `json:"isRealName"` //是否已实名认证:0未认证 1已认证 + InviteCode string `json:"inviteCode"` //邀请码 + LastLoginTime string `json:"lastLoginTime"` //最近一次登陆时间 + Status int32 `json:"status"` //状态:1 启用 2禁用 3 黑名单 + AccessToken string `json:"accessToken"` //登陆令牌 + AddTime string `json:"addTime"` //创建时间 + UpdateTime string `json:"updateTime"` //更新时间 +} + +type BoUsers struct { + Id int64 `xorm:"id pk autoincr comment('userId') BIGINT"` + Uid string `xorm:"uid not null comment('UID') BIGINT"` + Countrycode string `xorm:"countryCode comment('国家代码') CHAR(6)"` + Phonenumber int64 `xorm:"phoneNumber comment('手机号') BIGINT"` + Email string `xorm:"email comment('电子邮箱') CHAR(60)"` + Loginpassword string `xorm:"loginPassword comment('登陆密码') VARCHAR(255)"` + Tradepassword string `xorm:"tradePassword comment('交易密码') VARCHAR(100)"` + Surname string `xorm:"surname comment('姓') VARCHAR(30)"` + Name string `xorm:"name comment('名') VARCHAR(60)"` + Sex int32 `xorm:"sex default 3 comment('性别:1男 2女 0未设置') TINYINT"` + Birthday time.Time `xorm:"birthday comment('出生日期') DATE"` + Country string `xorm:"country comment('国家') VARCHAR(60)"` + Nickname string `xorm:"nickName comment('昵称') VARCHAR(60)"` + Avatar string `xorm:"avatar comment('头像') VARCHAR(100)"` + Isrealname int32 `xorm:"isRealName default 0 comment('是否已实名认证:0未认证 1已认证') TINYINT"` + Invitecode string `xorm:"inviteCode comment('邀请码') VARCHAR(60)"` + Lastlogintime time.Time `xorm:"lastLoginTime comment('最近一次登陆时间') DATETIME"` + Status int32 `xorm:"status default 1 comment('状态:1 启用 2禁用 3 黑名单') TINYINT"` + Accesstoken string `xorm:"accessToken comment('登陆令牌') VARCHAR(255)"` + Addtime time.Time `xorm:"addTime comment('创建时间') DATETIME"` + Updatetime time.Time `xorm:"updateTime comment('更新时间') DATETIME"` + Deletetime time.Time `xorm:"deletetime comment('删除时间(软删除,删除写入删除时间视为删除)') DATETIME"` +} diff --git a/pkg/model/stock/forex.go b/pkg/model/stock/forex.go new file mode 100644 index 0000000..68a7101 --- /dev/null +++ b/pkg/model/stock/forex.go @@ -0,0 +1,54 @@ +package stock + +// StockData 代表外汇股票市场数据的结构 +type ForexData struct { + Ticker string `json:"ticker" bson:"ticker"` // 股票或商品的标识符 + TodaysChange float64 `json:"todaysChange" bson:"todaysChange"` // 今日价格变动 + TodaysChangePerc float64 `json:"todaysChangePerc" bson:"todaysChangePerc"` // 今日价格变动的百分比 + Updated int64 `json:"updated" bson:"updated"` // 更新时间的 Unix 时间戳(纳秒) + Day DayData `json:"day" bson:"day"` // 今日的市场数据 + LastQuote QuoteData `json:"lastQuote" bson:"lastQuote"` // 最新的报价信息 + Min MinData `json:"min" bson:"min"` // 最小价格记录 + PrevDay DayData `json:"prevDay" bson:"prevDay"` // 昨日的市场数据 +} +type ForexDataNew struct { + Code string `json:"Code"` + Category string `json:"Category"` + Name string `json:"Name"` + Symbol string `json:"Symbol"` + HighPrice string `json:"HighPrice"` + LowPrice string `json:"LowPrice"` + OpenPrice string `json:"OpenPrice"` + ClosePrice string `json:"ClosePrice"` + Timestamp string `json:"Timestamp"` +} + +// DayData 代表一天的市场数据 +type DayData struct { + O float64 `json:"o" bson:"o"` // 开盘价 + H float64 `json:"h" bson:"h"` // 最高价 + L float64 `json:"l" bson:"l"` // 最低价 + C float64 `json:"c" bson:"c"` // 当前/收盘价 + V int `json:"v" bson:"v"` // 成交量 + VW float64 `json:"vw" bson:"vw"` // 加权平均价格 +} + +// QuoteData 代表最新报价信息 +type QuoteData struct { + A float64 `json:"a" bson:"a"` // 卖价 + B float64 `json:"b" bson:"b"` // 买价 + T int64 `json:"t" bson:"t"` // 最新报价的时间戳(毫秒) + X int `json:"x" bson:"x"` // 交易所标识符 +} + +// MinData 代表最小价格记录 +type MinData struct { + T int64 `json:"t" bson:"t"` // 时间戳 + N int `json:"n" bson:"n"` // 记录数 + O float64 `json:"o" bson:"o"` // 开盘价 + H float64 `json:"h" bson:"h"` // 最高价 + L float64 `json:"l" bson:"l"` // 最低价 + C float64 `json:"c" bson:"c"` // 收盘价 + V int `json:"v" bson:"v"` // 成交量 + VW float64 `json:"vw" bson:"vw"` // 加权平均价格 +} diff --git a/pkg/model/stock/stockpublic.go b/pkg/model/stock/stockpublic.go new file mode 100644 index 0000000..00e15ba --- /dev/null +++ b/pkg/model/stock/stockpublic.go @@ -0,0 +1,182 @@ +package stock + +import "github.com/shopspring/decimal" + +// Stock List Information +type StockShare struct { + Code string `json:"Code"` + Name string `json:"Name"` + Country string `json:"Country"` + Exchange string `json:"Exchange"` + Currency string `json:"Currency"` + Type string `json:"Type"` + Isin interface{} `json:"Isin"` + YesterdayClose string `json:"YesterdayClose"` // 昨天收盘价 + BeforeClose string `json:"BeforeClose"` // 前天收盘价 +} + +type StockPolygonParam struct { + Results []StockPolygon `json:"results"` + Status string `json:"status"` + NextUrl string `json:"next_url"` +} + +type StockPolygon struct { + Code string `json:"ticker" bson:"Code"` + Name string `json:"name" bson:"Name"` + Locale string `json:"locale" bson:"Country" ` + PrimaryExchange string `json:"primary_exchange" bson:"Exchange"` + Symbol string `json:"symbol" bson:"Symbol"` + Currency string `json:"currency_name"` + Type string `json:"type"` + CIK string `json:"cik"` + CompositeFigi string `json:"composite_figi"` + ShareClassFigi string `json:"share_class_figi"` + YesterdayClose string `json:"yesterday_close" bson:"YesterdayClose"` // 昨天收盘价 + BeforeClose string `json:"BeforeClose" bson:"BeforeClose"` // 前天收盘价 + ClosePrice string `json:"ClosePrice" bson:"ClosePrice"` + LogoUrl string `json:"logo_url" bson:"LogoUrl"` + Desc string `json:"desc"` + Intro string `json:"intro"` + Tape int `bson:"Tape"` + Vol int64 `bson:"Vol" json:"vol"` + DateStr string `json:"date_str"` + TapeStr string `json:"tape_str"` + IsReal int `json:"is_real"` + Source int `json:"source"` + NumericCode string `json:"numeric_code" bson:"NumericCode"` + IsSharia int `json:"is_sharia"` //1 、符合伊斯兰教法股票 2、不符合 + DP float64 `json:"dp"` + JapanName string `json:"JapanName" bson:"JapanName"` + JapanIntro string `json:"JapanIntro" bson:"JapanIntro"` +} + +type StockUpdatePolygon struct { + OldCode string `json:"old_ticker"` + NewCode string `json:"new_ticker"` + Name string `json:"name" bson:"Name"` + Locale string `json:"locale" bson:"Country" ` + PrimaryExchange string `json:"primary_exchange" bson:"Exchange"` + Currency string `json:"currency_name"` + Source int `json:"source"` + Type string `json:"type"` + CIK string `json:"cik"` + CompositeFigi string `json:"composite_figi"` + ShareClassFigi string `json:"share_class_figi"` + YesterdayClose string `json:"yesterday_close" bson:"YesterdayClose"` // 昨天收盘价 + BeforeClose string `json:"BeforeClose" bson:"BeforeClose"` // 前天收盘价 + ClosePrice string `json:"ClosePrice" bson:"ClosePrice"` + LogoUrl string `json:"logo_url" bson:"LogoUrl"` + Desc string `json:"desc"` + Intro string `json:"intro"` + Tape int `bson:"Tape"` + Vol int64 `bson:"Vol" json:"vol"` + DateStr string `json:"date_str"` + TapeStr string `json:"tape_str"` + IsReal int `json:"is_real"` + Token string `json:"token"` + NumericCode string `json:"numeric_code"` +} + +type StockPolygonS struct { + Code string `json:"ticker" bson:"Code"` + Name string `json:"name" bson:"Name"` + Locale string `json:"locale" bson:"Country" ` + PrimaryExchange string `json:"primary_exchange" bson:"Exchange"` + Currency string `json:"currency_name"` + Type string `json:"type"` + CIK string `json:"cik"` + CompositeFigi string `json:"composite_figi"` + ShareClassFigi string `json:"share_class_figi"` + YesterdayClose string `json:"yesterday_close" bson:"YesterdayClose"` // 昨天收盘价 + BeforeClose string `json:"BeforeClose" bson:"BeforeClose"` // 前天收盘价 + ClosePrice string `json:"ClosePrice" bson:"ClosePrice"` + LogoUrl string `json:"logo_url" bson:"LogoUrl"` + Desc string `json:"desc"` + Intro string `json:"intro"` + Tape int `bson:"Tape"` + Vol int64 `bson:"Vol" json:"vol"` + TapeStr string `json:"tape_str"` + State int `json:"State"` + DateStr string `bson:"DateStr"` + Sort int `json:"Sort"` + NumericCode string `json:"numeric_code" bson:"NumericCode"` +} + +type StockList struct { + Results []StockPolygon `json:"results"` + Token string `json:"token"` +} + +type AggsTicke struct { + Ticker string `json:"ticker" from:"ticker"` + QueryCount int `json:"queryCount" from:"queryCount"` + ResultsCount int `json:"resultsCount" from:"resultsCount"` + Adjusted bool `json:"adjusted" from:"adjusted"` + Results []Results `json:"results" from:"results"` + Status string `json:"status" from:"status"` + RequestId string `json:"request_id" from:"request_id"` + Count int `json:"count" from:"count"` +} + +type Results struct { + Code string `json:"T"` + V int64 `json:"v"` + Vw decimal.Decimal `json:"vw"` + O decimal.Decimal `json:"o"` + C decimal.Decimal `json:"c"` + H decimal.Decimal `json:"h"` + L decimal.Decimal `json:"l"` + T int64 `json:"t"` + N int64 `json:"n"` +} + +type MgoPageSize struct { + PageSize int `json:"PageSize"` + PageNumber int `json:"PageNumber"` + Total int64 `json:"Total"` + Data interface{} `json:"Data"` +} + +type ClosePrice struct { + Code string `json:"T"` + ClosePrice float64 `json:"ClosePrice"` +} + +type StockIndexPolygon struct { + Code string `json:"ticker" bson:"Code"` + Name string `json:"name" bson:"Name"` + Locale string `json:"locale" bson:"Country" ` + PrimaryExchange string `json:"primary_exchange" bson:"Exchange"` + Currency string `json:"currency_name"` + YesterdayClose string `json:"yesterday_close" bson:"YesterdayClose"` // 昨天收盘价 + BeforeClose string `json:"BeforeClose" bson:"BeforeClose"` // 前天收盘价 + ClosePrice string `json:"ClosePrice" bson:"ClosePrice"` + Intro string `json:"intro"` + DateStr string `json:"date_str"` + State int `json:"state"` + Sort int `json:"sort"` + Vol int64 `bson:"Vol" json:"vol"` +} + +type StockIndexList struct { + Results []StockIndexPolygon `json:"results"` + Token string `json:"token"` +} + +// bak数据表数据备份 +type StockListBak struct { + Country string `json:"Country"` + Code string `json:"Code"` + BeforeClose string `json:"BeforeClose"` + Cik string `json:"Cik"` + CompositeFigi string `json:"CompositeFigi"` + Currency string `json:"Currency"` + Exchange string `json:"Exchange"` + Name string `json:"Name"` + ShareClassFigi string `json:"ShareClassFigi"` + Type string `json:"Type"` + YesterdayClose string `json:"YesterdayClose"` + DP float64 `json:"DP"` + DateStr string `json:"DateStr"` +} diff --git a/pkg/model/stock/stockus.go b/pkg/model/stock/stockus.go new file mode 100644 index 0000000..99f423c --- /dev/null +++ b/pkg/model/stock/stockus.go @@ -0,0 +1,180 @@ +package stock + +import ( + "github.com/shopspring/decimal" +) + +type RealTimeMessage struct { + S string `json:"s,omitempty"` // ticker code + P decimal.Decimal `json:"p,omitempty"` // price + C []decimal.Decimal `json:"c,omitempty"` // conditions, see trade conditions glossary for more information + V int64 `json:"v,omitempty"` // volume, representing the number of shares traded at the corresponding time stamp + Dp bool `json:"dp,omitempty"` // dark pool true/false + Ms string `json:"ms,omitempty"` // market status, indicating the current state of the market for the stock ( “open”, “closed”, “extended hours”) + T int64 `json:"t,omitempty"` // timestamp in milliseconds +} + +type EodTimeMessage struct { + Date string `json:"date,omitempty"` + Open decimal.Decimal `json:"open,omitempty"` + High decimal.Decimal `json:"s,omitempty"` + Low decimal.Decimal `json:"high,omitempty"` + Close decimal.Decimal `json:"close,omitempty"` + AdjustedClose decimal.Decimal `json:"adjusted_close,omitempty"` + Volume decimal.Decimal `json:"volume,omitempty"` +} + +type UsDateClose struct { + Status string `json:"status,omitempty"` + From string `json:"from,omitempty"` + Symbol string `json:"symbol,omitempty"` + Open decimal.Decimal `json:"open,omitempty"` + High decimal.Decimal `json:"high,omitempty"` + Low decimal.Decimal `json:"low,omitempty"` + Close decimal.Decimal `json:"close,omitempty"` + Volume interface{} `json:"volume,omitempty"` + AfterHours decimal.Decimal `json:"afterHours,omitempty"` + PreMarket decimal.Decimal `json:"preMarket,omitempty"` +} + +// list message +type Finance struct { + Result []Result `json:"result"` + Error interface{} `json:"error"` +} + +type Result struct { + Count decimal.Decimal `json:"count"` + JobTimestamp decimal.Decimal `json:"jobTimestamp"` + StartInterval decimal.Decimal `json:"startInterval"` + Quotes []QuotesModel `json:"quotes"` +} + +type QuotesModel struct { + Language string `json:"language"` + Region string `json:"region"` + QuoteType string `json:"quoteType"` + TypeDisp string `json:"typeDisp"` + QuoteSourceName string `json:"quoteSourceName"` + Triggerable bool `json:"triggerable"` + CustomPriceAlertConfidence string `json:"customPriceAlertConfidence"` + TrendingScore decimal.Decimal `json:"trendingScore"` + RegularMarketChange decimal.Decimal `json:"regularMarketChange"` + RegularMarketChangePercent decimal.Decimal `json:"regularMarketChangePercent"` + RegularMarketTime decimal.Decimal `json:"regularMarketTime"` + RegularMarketPrice decimal.Decimal `json:"regularMarketPrice"` + RegularMarketPreviousClose decimal.Decimal `json:"regularMarketPreviousClose"` + Exchange string `json:"exchange"` + Market string `json:"market"` + FullExchangeName string `json:"fullExchangeName"` + ShortName string `json:"shortName"` + SourceInterval decimal.Decimal `json:"sourceInterval"` + ExchangeDataDelayedBy decimal.Decimal `json:"exchangeDataDelayedBy"` + ExchangeTimezoneName string `json:"exchangeTimezoneName"` + ExchangeTimezoneShortName string `json:"exchangeTimezoneShortName"` + QmtOffSetMilliseconds decimal.Decimal `json:"gmtOffSetMilliseconds"` + EsgPopulated bool `json:"esgPopulated"` + Tradeable bool `json:"tradeable"` + CryptoTradeable bool `json:"cryptoTradeable"` + MarketState string `json:"marketState"` + FirstTradeDateMilliseconds decimal.Decimal `json:"firstTradeDateMilliseconds"` + Symbol string `json:"symbol"` +} + +type EodData struct { + Date string `json:"date" form:"date"` // 日期 + Open float64 `json:"open" form:"open"` // 开盘价格 + High float64 `json:"high" form:"high"` // 最高价格 + Low float64 `json:"low" form:"low"` // 最低价格 + Close float64 `json:"close" form:"close"` // 闭盘价格 + AdjustedClose float64 `json:"adjusted_close" form:"adjusted_close"` + Volume int64 `json:"volume" form:"volume"` // 交易量 +} + +type PHPData struct { + Name string `json:"name" redis:"name"` + Code string `json:"code" redis:"code"` + LogoLink string `json:"logo_link" redis:"logo_link"` + KeepDecimal string `json:"keep_decimal" redis:"keep_decimal"` + FaceValue int `json:"face_value" redis:"face_value"` + MinPry int `json:"min_pry" redis:"min_pry"` + MaxPry int `json:"max_pry" redis:"max_pry"` + Status int `json:"status" redis:"status"` +} + +type PHPRes struct { + Data PHPList `json:"data"` + Code string `json:"code"` +} + +type PHPList struct { + List []PHPData `json:"list"` + Code string `json:"code"` +} + +type MarketTrade struct { + OrderNumber string `json:"order_number" ` //交易数量 + DealPrice string `json:"deal_price"` //成交金额 + OrderTime int64 `json:"order_time"` //订单时间 + TradeType int `json:"trade_type"` //交易类型:1买入, 2卖出 + IsHuobi bool `json:"is_huobi"` //是否火币 + TradeTurnover decimal.Decimal `json:"trade_turnover"` + ID int64 `json:"id"` +} + +type PHPMarketTradeList struct { + List []MarketTrade `json:"data"` + Code string `json:"code"` +} + +type IntraDayData struct { + Timestamp int64 `json:"timestamp"` // 时间格式 + Gmtoffset int64 `json:"gmtoffset"` // + Datetime string `json:"datetime"` // 日期 + Open float64 `json:"open"` // 开盘价格 + High float64 `json:"high"` // 最高价格 + Low float64 `json:"low"` // 最低价格 + Close float64 `json:"close"` // 闭盘价格 + Volume int64 `json:"volume"` // 交易量 +} + +type Symbol struct { + Code string `json:"code"` + Name string `json:"name"` + Country string `json:"country"` + Exchange string `json:"exchange"` + Currency string `json:"currency"` + Type string `json:"type"` +} + +type PreviousCloseResponse struct { + Adjusted bool `json:"adjusted"` + QueryCount int `json:"queryCount"` + Results []PreviousCloseRes `json:"results"` + ResultsCount int `json:"resultsCount"` + Status string `json:"status"` + Ticker string `json:"ticker"` +} + +type PreviousCloseRes struct { + Ts string `json:"T,omitempty"` + C decimal.Decimal `json:"c,omitempty"` + H decimal.Decimal `json:"h"` + L decimal.Decimal `json:"l"` + O decimal.Decimal `json:"o"` + T int64 `json:"t"` + V decimal.Decimal `json:"v,omitempty"` + VW decimal.Decimal `json:"vw"` + N int64 `json:"n"` + PC decimal.Decimal `json:"pc"` + Error string `json:"error"` + DP decimal.Decimal `json:"dp"` +} + +type TypeTradesRes struct { + Tape int `json:"tape"` +} + +type TypeTradesResponse struct { + Results []TypeTradesRes `json:"results"` +} diff --git a/pkg/model/tojson.go b/pkg/model/tojson.go new file mode 100644 index 0000000..64cab60 --- /dev/null +++ b/pkg/model/tojson.go @@ -0,0 +1,88 @@ +package model + +import ( + "encoding/json" + "fmt" + "strings" + "wss-pool/dictionary" + "wss-pool/logging/applogger" +) + +func ToJson(v interface{}) (string, error) { + result, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(result), nil +} + +func InterceptStr(symbol string) string { + var name string + ch := strings.Split(symbol, ".") + if len(ch) >= 2 { + name = strings.Replace(ch[1], dictionary.TypeUnit, "", 1) + } + return name +} + +func InterceptCtStr(symbol string) string { + var name string + symbol = strings.Replace(symbol, "-", "", 1) + ch := strings.Split(symbol, ".") + if len(ch) >= 2 { + name = strings.Replace(ch[1], dictionary.TypeUnit, "", 1) + } + return strings.ToLower(name) +} + +// SymbolListString []string +func SymbolListString(key []string) map[string][]string { + var symbolList = make(map[string][]string) + for _, value := range dictionary.Symbol { + symbolName := fmt.Sprintf("%v%v", value, dictionary.TypeUnit) + symbolList[symbolName] = key + } + applogger.Info("symbol data-key:%v", symbolList) + return symbolList +} + +// SymbolCtListString []string +func SymbolCtListString(key []string) map[string][]string { + var symbolList = make(map[string][]string) + for _, value := range dictionary.ContractCodeList { + // TODO: 合约放开所有标识 + //for _, vue := range dictionarykey.ContractCode { + //symbolName := fmt.Sprintf("%v%v", strings.ToUpper(value), vue) + //symbolList[symbolName] = key + //} + // TODO: 目前只支持永续合约 + symbolName := fmt.Sprintf("%v", value) + symbolList[symbolName] = key + } + applogger.Info("symbol data-key:%v", symbolList) + return symbolList +} + +// SymbolCtListInt []int +func SymbolCtListInt(key []int) map[string][]int { + var symbolList = make(map[string][]int) + for _, value := range dictionary.Symbol { + for _, vue := range dictionary.ContractCode { + symbolName := fmt.Sprintf("%v%v", strings.ToUpper(value), vue) + symbolList[symbolName] = key + } + } + applogger.Info("symbol data-key:%v", symbolList) + return symbolList +} + +// SymbolListInt []int +func SymbolListInt(key []int) map[string][]int { + var symbolList = make(map[string][]int) + for _, value := range dictionary.Symbol { + symbolName := fmt.Sprintf("%v%v", value, dictionary.TypeUnit) + symbolList[symbolName] = key + } + applogger.Info("symbol data-key:%v", symbolList) + return symbolList +} diff --git a/pkg/model/tojson_test.go b/pkg/model/tojson_test.go new file mode 100644 index 0000000..8b53790 --- /dev/null +++ b/pkg/model/tojson_test.go @@ -0,0 +1 @@ +package model diff --git a/pkg/msg/aliyun.go b/pkg/msg/aliyun.go new file mode 100644 index 0000000..fa7ca81 --- /dev/null +++ b/pkg/msg/aliyun.go @@ -0,0 +1,217 @@ +package msg + +import ( + "errors" + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + dysmsapi20180501 "github.com/alibabacloud-go/dysmsapi-20180501/v2/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + "github.com/patrickmn/go-cache" + uuid "github.com/satori/go.uuid" + "sync" + "time" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/data/mysqlbusiness" + "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/sqlmodel" +) + +type aLiYun struct { + verificationCodeReqCache *cache.Cache // You can only send the verification code once within a minute +} + +var ( + aLiYunOnce sync.Once + aLiYunEntity *aLiYun +) + +// getALiYunEntity +func getALiYunEntity() *aLiYun { + aLiYunOnce.Do(func() { + aLiYunEntity = new(aLiYun) + aLiYunEntity.verificationCodeReqCache = cache.New(time.Minute, time.Minute) + }) + + return aLiYunEntity +} + +// SendVerificationCode +func (al *aLiYun) SendVerificationCode(phoneNumber string) (code string, err error) { + // Verify if the verification code can be obtained (1-minute validity period) + _, found := al.verificationCodeReqCache.Get(phoneNumber) + if found { + err = errors.New("请勿重复发送验证码") + return + } + + // Generate verification code + verifyCode := CreateRandCode() + // Generate Message Body + message := al.getVerifyCodeReq(phoneNumber, verifyCode) + + // Write to database + applogger.Debug("send message info:%v", message) + if err = mysqlbusiness.SaveBoUserSms(sqlmodel.BoUserSms{ + From: tea.StringValue(message.From), + To: tea.StringValue(message.To), + Message: tea.StringValue(message.Message), + TaskId: tea.StringValue(message.TaskId), + }); err != nil { + err = errors.New("add data db error") + return internal.ResultStr, err + } + + // Sends sms + //result, _err := al.SendSms(message) + //if _err != nil { + // _err = errors.New("请输入正确的手机号") + // return internal.ResultStr, _err + //} + result := "发送短信之后的返回值" + + applogger.Debug("Send the returned results:%v", tea.StringValue(util.ToJSONString(util.ToMap(result)))) + // Update to database + if err = mysqlbusiness.UpdateBoUserSms(sqlmodel.BoUserSms{ + From: tea.StringValue(message.From), + To: tea.StringValue(message.To), + Message: tea.StringValue(message.Message), + TaskId: tea.StringValue(message.TaskId), + MessageResult: tea.StringValue(util.ToJSONString(util.ToMap(result))), + UpdateTime: time.Now(), + }); err != nil { + err = errors.New("update data db error") + return internal.ResultStr, err + } + + // Request valid within 1 minute + al.verificationCodeReqCache.SetDefault(phoneNumber, 1) + + // Set SMS verification code cache to be valid within 5 minutes + err = redis.Set_Cache_Data(phoneNumber, verifyCode, 5) + if err != nil { + err = errors.New("cache key error") + return internal.ResultStr, err + } + + return verifyCode, nil +} + +// CheckVerificationCode +func (al *aLiYun) CheckVerificationCode(phoneNumber, verificationCode string) (err error) { + code, err := redis.Get_Cache_Data(phoneNumber) + if err != nil { + if err.Error() == "redis: nil" { + // init cache data + if err = redis.Set_Cache_Data(phoneNumber, verificationCode, 5); err != nil { + return errors.New("内部服务出错") + } + code = verificationCode + } + } + + if len(code) == 0 { + err = errors.New("验证码已失效") + return + } + + if verificationCode != code { + err = errors.New("验证码输入错误") + return + } + + return err +} + +// CheckVerificationCodeNew +func (al *aLiYun) CheckVerificationCodeNew(phoneNumber string) (string, error) { + code, err := redis.Get_Cache_Data(phoneNumber) + if err != nil { + if err.Error() == "redis: nil" { + code = "" + } else { + err = errors.New("内部服务错误") + return "", err + } + } + + return code, nil +} + +// getVerifyCodeReq +func (al *aLiYun) getVerifyCodeReq(phoneNumber, code string) (req dysmsapi20180501.SendMessageToGlobeRequest) { + verifyCodeFrom := CreateRandCodeTest() + + req = dysmsapi20180501.SendMessageToGlobeRequest{ + From: tea.String(verifyCodeFrom), // 发送方标识;支持SenderId的发送,只允许数字+字母,含有字母标识最长11位,纯数字标识支持15位 + To: tea.String(phoneNumber), // 接收短信号码;号码格式为:国际区号+号码 + Message: tea.String(code), // TODO: 修改短信内容 + TaskId: tea.String(uuid.NewV4().String()), // 任务ID;长度不要超过255 + } + + return +} + +// CreateClient Initialize the Client with the AccessKey of the account +func (al *aLiYun) CreateClient(accessKeyId *string, accessKeySecret *string) (_result *dysmsapi20180501.Client, _err error) { + configs := &openapi.Config{ + // Required, your AccessKey ID + AccessKeyId: accessKeyId, + // Required, your AccessKey secret + AccessKeySecret: accessKeySecret, + } + // Endpoint + configs.Endpoint = tea.String(config.Config.ALiYun.EndPoint) + _result = &dysmsapi20180501.Client{} + _result, _err = dysmsapi20180501.NewClient(configs) + return _result, _err +} + +// SendSms +func (al *aLiYun) SendSms(req dysmsapi20180501.SendMessageToGlobeRequest) (res *dysmsapi20180501.SendMessageToGlobeResponse, _err error) { + client, _err := al.CreateClient(tea.String(config.Config.ALiYun.AccessKeyId), tea.String(config.Config.ALiYun.AccessKeySecret)) + if _err != nil { + return nil, _err + } + + defer func() { + if r := tea.Recover(recover()); r != nil { + _err = r + } + }() + + runtime := &util.RuntimeOptions{} + + // Copy the code to run, please print the return value of the API by yourself. + result, _err := client.SendMessageToGlobeWithOptions(&req, runtime) + if _err != nil { + return nil, _err + } + + if *result.Body.ResponseCode != "OK" { + _err = errors.New(result.String()) + } + + return result, _err +} + +func RunSendSms(phoneNumber string) (string, error) { + sms := NewSms() + code, err := sms.CheckVerificationCodeNew(phoneNumber) + if err != nil { + return err.Error(), err + } + + if len(code) > 0 { + return code, err + } + + // Request verification code + codeMsg, err := sms.SendVerificationCode(phoneNumber) + if err != nil { + return internal.ResultStr, err + } + + return codeMsg, nil +} diff --git a/pkg/msg/msg_test.go b/pkg/msg/msg_test.go new file mode 100644 index 0000000..65417d0 --- /dev/null +++ b/pkg/msg/msg_test.go @@ -0,0 +1,28 @@ +package msg + +import ( + "fmt" + "testing" + "wss-pool/config" + "wss-pool/internal/data" + red "wss-pool/internal/redis" +) + +func TestCreateRandCode(t *testing.T) { + fmt.Println("生成短信编号:", CreateRandCode()) +} + +func TestCreateRandCodeTest(t *testing.T) { + fmt.Println("生成发送者编号:", CreateRandCodeTest()) +} + +func TestRun(t *testing.T) { + red.RedisClient = red.RedisInit(config.Config.Redis.DbTen) + data.InitMysql(config.Config.Bourse) + code, err := RunSendSms("6001155042087") + if err != nil { + fmt.Println("发送短信失败:", err) + return + } + fmt.Println("发送短信成功......", code) +} diff --git a/pkg/msg/sms.go b/pkg/msg/sms.go new file mode 100644 index 0000000..7a40391 --- /dev/null +++ b/pkg/msg/sms.go @@ -0,0 +1,27 @@ +package msg + +import ( + "fmt" + "math/rand" + "time" +) + +type SmsOperation interface { + SendVerificationCode(phoneNumber string) (string, error) + CheckVerificationCode(phoneNumber, verificationCode string) error + CheckVerificationCodeNew(phoneNumber string) (string, error) +} + +func NewSms() SmsOperation { + return getALiYunEntity() +} + +// Create 6-bit random numbers +func CreateRandCode() string { + return fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000)) +} + +// Create a 14 bit random number as the sender +func CreateRandCodeTest() string { + return fmt.Sprintf("%014v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000)) +} diff --git a/pkg/processor/contract_api.go b/pkg/processor/contract_api.go new file mode 100644 index 0000000..17efba4 --- /dev/null +++ b/pkg/processor/contract_api.go @@ -0,0 +1,503 @@ +package processor + +import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "net/http" + "strings" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +/* +合约-获取行情深度数据 https://api.hbdm.com/linear-swap-ex/market/depth?contract_code=BTC-USDT&type=step0 +1、contract_code 合约代码 或 合约标识 +2、type 深度类型 +*/ +func ContractDepth(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + typeS := internal.ReplaceStr(c.Query("type")) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(typeS) > 0 { + param = param + "&" + fmt.Sprintf("type=%v", typeS) + } + + url := fmt.Sprintf("https://%v/linear-swap-ex/market/depth?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + if strings.Contains(bodyStr, "invalid-parameter") { + chStep6 := fmt.Sprintf("market-%s-depth-step6", contract_code) + bodyStr, _ = red.Get_Cache_Data(chStep6) + } + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-获取市场最优挂单 https://api.hbdm.com/linear-swap-ex/market/bbo?contract_code=BTC-USDT&business_type=swap +1、contract_code 合约代码 或 合约标识 +2、business_type 业务类型,不填默认永续 例如:futures:交割、swap:永续、all:全部 +*/ +func ContractBbo(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + business_type := internal.ReplaceStr(c.Query("business_type")) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(business_type) > 0 { + param = param + "&" + fmt.Sprintf("business_type=%v", contract_code) + } + + url := fmt.Sprintf("https://%v/linear-swap-ex/market/bbo?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-K线数据获取 https://api.hbdm.com/linear-swap-ex/market/history/kline?contract_code=BTC-USDT&period=1min&size=10&from=1587052800&to=1591286400 +1、contract_code 合约代码 或 合约标识 +2、period K线类型 1min, 5min, 15min, 30min, 60min,4hour,1day,1week,1mon +3、size 获取数量,默认150 [1,2000] +4、from 开始时间戳 10位 单位S +5、to 结束时间戳 10位 单位S +*/ +func ContractHistoryKline(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + period := internal.ReplaceStr(c.Query("period")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) + to := internal.IntegerInit(internal.ReplaceStr(c.Query("to"))) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(period) > 0 { + param = param + "&" + fmt.Sprintf("period=%v", period) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + if from > 0 { + param = param + "&" + fmt.Sprintf("from=%v", from) + } + if to > 0 { + param = param + "&" + fmt.Sprintf("to=%v", to) + } + + url := fmt.Sprintf("https://%v/linear-swap-ex/market/history/kline?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-获取标记价格的K线数据 https://api.hbdm.com/index/market/history/linear_swap_mark_price_kline?contract_code=BTC-USDT&period=1min&size=10 +1、contract_code 合约代码 或 合约标识 +2、period K线类型 1min, 5min, 15min, 30min, 60min,4hour,1day, 1week,1mon +3、size K线获取数量 [1,2000] +*/ +func ContractHistoryPriceKline(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + period := internal.ReplaceStr(c.Query("period")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(period) > 0 { + param = param + "&" + fmt.Sprintf("period=%v", period) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + + url := fmt.Sprintf("https://%v/index/market/history/linear_swap_mark_price_kline?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-行情数据信息 https://api.hbdm.com/linear-swap-ex/market/detail/merged?contract_code=BTC-USDT +1、contract_code 合约代码 或 合约标识 +*/ +func ContractMerged(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + + url := fmt.Sprintf("https://%v/linear-swap-ex/market/detail/merged?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + if strings.Contains(bodyStr, "invalid-parameter") { + title := fmt.Sprintf("market-%s-detail-merged", contract_code) + bodyStr, _ = red.Get_Cache_Data(title) + } + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// 合约-行情数据列表展示 https://api.hbdm.com/linear-swap-ex/market/detail/merged?contract_code=BTC-USDT +func ContractMergedList(c *gin.Context) { + var mergedList []model.GetContractTickCompleteList + + // 获取永续合约列表 + for _, value := range dictionary.Symbol { + param := fmt.Sprintf("%v-USDT", strings.ToUpper(value)) + applogger.Debug("合约类型:%v", param) + + url := fmt.Sprintf("https://%v/linear-swap-ex/market/detail/merged?contract_code=%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + var merged model.GetContractTickList + if err := json.Unmarshal([]byte(bodyStr), &merged); err != nil { + applogger.Error("Unmarshal err:%v", err) + continue + } + + mergedCon := model.GetContractTickCompleteList{ + Tick: merged.Tick, + Ch: merged.Ch, + Status: merged.Status, + Ts: merged.Ts, + Icon: "", + FullName: "", + } + + mergedList = append(mergedList, mergedCon) + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, mergedList, internal.QuerySuccess)) +} + +/* +合约-批量获取聚合行情(V2) https://api.hbdm.com/v2/linear-swap-ex/market/detail/batch_merged?contract_code=BTC-USDT&business_type=swap +1、contract_code 合约代码 或 合约标识 +2、business_type 业务类型,不填默认永续 +*/ +func ContractBatchMerged(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + business_type := internal.ReplaceStr(c.Query("business_type")) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(business_type) > 0 { + param = param + "&" + fmt.Sprintf("business_type=%v", business_type) + } + + url := fmt.Sprintf("https://%v/v2/linear-swap-ex/market/detail/batch_merged?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-获取市场最近成交记录 https://api.hbdm.com/linear-swap-ex/market/trade?contract_code=BTC-USDT&business_type=swap +1、contract_code 合约代码 或 合约标识 +2、business_type 业务类型,不填默认永续 +*/ +func ContractTrade(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + business_type := internal.ReplaceStr(c.Query("business_type")) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(business_type) > 0 { + param = param + "&" + fmt.Sprintf("business_type=%v", business_type) + } + + url := fmt.Sprintf("https://%v/linear-swap-ex/market/trade?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-批量获取最近的交易记录 https://api.hbdm.com/linear-swap-ex/market/history/trade?contract_code=BTC-USDT&size=10 +1、contract_code 合约代码 或 合约标识 +2、size 获取交易记录的数量,默认1 例如:[1, 2000] +*/ +func ContractHistoryTrade(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + //phpRes := websocketservice.PHPMarketTrade(ContractStatus, size, contract_code) + //num := size - len(phpRes.List) + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + url := fmt.Sprintf("https://%v/linear-swap-ex/market/history/trade?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + bodyStr, _ := internal.HttpGet(url) + huobiRes := business.MarketTrade{} + json.Unmarshal([]byte(bodyStr), &huobiRes) + phpRes := stock.PHPMarketTradeList{} + for _, v := range huobiRes.Data { + item := stock.MarketTrade{ + ID: v.Data[0].ID, + OrderNumber: fmt.Sprintf("%f", v.Data[0].Amount), + DealPrice: fmt.Sprintf("%f", v.Data[0].Price), + OrderTime: v.Data[0].Ts, + TradeType: TradeMap[v.Data[0].Direction], + IsHuobi: true, + TradeTurnover: v.Data[0].TradeTurnover, + } + phpRes.List = append(phpRes.List, item) + } + if len(phpRes.List) <= 0 { + title := fmt.Sprintf("market-%s-trade-detail", contract_code) + item, _ := red.Get_Cache_Data(title) + json.Unmarshal([]byte(item), &phpRes.List) + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, phpRes.List, internal.QuerySuccess)) +} + +/* +合约-平台历史持仓量查询 https://api.hbdm.com/linear-swap-api/v1/swap_his_open_interest?contract_code=BTC-USDT&pair=BTC-USDT&contract_type=swap&period=12hour&size=10&amount_type=1 +1、contract_code 合约代码 备注:永续:"BTC-USDT"... ,交割:"BTC-USDT-210625"... +2、pair 交易对 备注:BTC-USDT +3、contract_type 合约类型 备注:swap(永续)、this_week(当周)、next_week(次周)、quarter(当季)、next_quarter(次季) +4、period 时间周期类型 备注:1小时:"60min",4小时:"4hour",12小时:"12hour",1天:"1day" +5、size 获取数量,默认为:48 备注:[1,200] +6、amount_type 计价单位 备注:1:张,2:币 +*/ +func ContractsWapHisOpenInterest(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + pair := internal.ReplaceStr(c.Query("pair")) + contract_type := internal.ReplaceStr(c.Query("contract_type")) + period := internal.ReplaceStr(c.Query("period")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + amount_type := internal.IntegerInit(internal.ReplaceStr(c.Query("amount_type"))) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(pair) > 0 { + param = param + "&" + fmt.Sprintf("pair=%v", pair) + } + if len(contract_type) > 0 { + param = param + "&" + fmt.Sprintf("contract_type=%v", contract_type) + } + if len(period) > 0 { + param = param + "&" + fmt.Sprintf("period=%v", period) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + if amount_type > 0 { + param = param + "&" + fmt.Sprintf("amount_type=%v", amount_type) + } + + url := fmt.Sprintf("https://%v/linear-swap-api/v1/swap_his_open_interest?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-获取合约的溢价指数K线 https://api.hbdm.com/index/market/history/linear_swap_premium_index_kline?contract_code=BTC-USDT&period=1min&size=10 +1、contract_code 合约代码 "BTC-USDT","ETH-USDT"... +2、period K线类型 1min, 5min, 15min, 30min, 60min,4hour,1day, 1week,1mon +3、size K线获取数量 [1,2000] +*/ +func ContractHistoryLinearSwapPremiumIndexKline(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + period := internal.ReplaceStr(c.Query("period")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(period) > 0 { + param = param + "&" + fmt.Sprintf("period=%v", period) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + + url := fmt.Sprintf("https://%v/index/market/history/linear_swap_premium_index_kline?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-获取实时预测资金费率的K线数据 https://api.hbdm.com/index/market/history/linear_swap_estimated_rate_kline?contract_code=BTC-USDT&period=1min&size=10 +1、contract_code 合约代码 "BTC-USDT","ETH-USDT"... +2、period K线类型 1min, 5min, 15min, 30min, 60min,4hour,1day, 1week,1mon +3、size K线获取数量 [1,2000] +*/ +func ContractHistoryLinearSwapEstimatedRateKline(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + period := internal.ReplaceStr(c.Query("period")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(period) > 0 { + param = param + "&" + fmt.Sprintf("period=%v", period) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + + url := fmt.Sprintf("https://%v/index/market/history/linear_swap_estimated_rate_kline?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +合约-获取基差数据 https://api.hbdm.com/index/market/history/linear_swap_basis?contract_code=BTC-USDT&period=1min&size=10 +1、contract_code 合约代码 或 合约标识 +2、period 周期 +3、basis_price_type 基差价格类型,表示在周期内计算基差使用的价格类型, 不填,默认使用开盘价 +4、size 基差获取数量,默认 150 +*/ +func ContractHistoryLinearSwapBasis(c *gin.Context) { + contract_code := internal.ReplaceStr(c.Query("contract_code")) + period := internal.ReplaceStr(c.Query("period")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + basis_price_type := internal.ReplaceStr(c.Query("basis_price_type")) + + var param string + if len(contract_code) > 0 { + param = fmt.Sprintf("contract_code=%v", contract_code) + } + if len(period) > 0 { + param = param + "&" + fmt.Sprintf("period=%v", period) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + if len(basis_price_type) > 0 { + param = param + "&" + fmt.Sprintf("basis_price_type=%v", basis_price_type) + } + + url := fmt.Sprintf("https://%v/index/market/history/linear_swap_basis?%v", config.Config.HbApi.HbContractApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} diff --git a/pkg/processor/forex_api.go b/pkg/processor/forex_api.go new file mode 100644 index 0000000..894827d --- /dev/null +++ b/pkg/processor/forex_api.go @@ -0,0 +1,948 @@ +package processor + +import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +var RedisFOREX string = "FOREX:LIST:" +var UrlKline = "https://quote.tradeswitcher.com/quote-b-api/kline" // 历史K线 +var UrlBatchKline = "https://quote.tradeswitcher.com/quote-b-api/batch-kline" // 批量K线查询 +var UrlDepthTick = "https://quote.tradeswitcher.com/quote-b-api/depth-tick" // 最新盘口报价查询 { "trace": "", "data": { "symbol_list": [ { "code": "GBPJPY" }, { "code": "CADJPY" } ] +var UrlTradeTick = "https://quote.tradeswitcher.com/quote-b-api/trade-tick" // 最新成交报价查询 { "trace": "", "data": { "symbol_list": [ { "code": "GBPJPY" }, { "code": "CADJPY" } ] +var TokenTrade = "bf8f33c446c4494286eccaa57a2e6fac-c-app" + +// ForexAggregates +// +// @Description: 外汇K线 https://api.polygon.io/v2/aggs/ticker/C:EURUSD/range/1/day/2023-01-09/2023-02-10?adjusted=true&sort=asc&apiKey=3uX9zgBRPFl6MS11t4CsIrIZ_s_o2nh9 +// @param c +func ForexAggregates(c *gin.Context) { + code := internal.ReplaceStr(c.Query("code")) // 外汇code + multiplier := internal.IntegerInit(c.Query("multiplier")) // 乘数:默认1 + timespan := internal.ReplaceStr(c.Query("timespan")) // 时间间隔:second\minute\hour\day\week\month\quarter\year(默认:day) + from := internal.ReplaceStr(internal.ReplaceStr(c.Query("from"))) // 开始时间 + to := internal.ReplaceStr(internal.ReplaceStr(c.Query("to"))) // 结束时间 + sort := internal.ReplaceStr(internal.ReplaceStr(c.Query("sort"))) // 排序(默认:asc) + + if len(from) == 0 || len(to) == 0 || len(code) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "Parameter error!", internal.QueryError)) + return + } + if multiplier == 0 { + multiplier = 1 + } + if len(timespan) == 0 { + timespan = "day" + } + if len(sort) == 0 { + sort = "asc" + } + + url := fmt.Sprintf("https://%v/v2/aggs/ticker/C:%v/range/%v/%v/%v/%v?adjusted=true&sort=%v&apiKey=%v", + config.Config.ShareGather.PolygonHost, code, multiplier, timespan, from, to, sort, config.Config.ShareGather.PolygonKey) + + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// 历史k线查询 +func ForexAggregatesNewGet(c *gin.Context) { + codeStr := internal.ReplaceStr(c.Query("code")) // 外汇code + kline_type := internal.IntegerInit(c.Query("kline_type")) // k线类型,1分钟K,2为5分钟K,3为15分钟K,4为30分钟K,5为小时K,6为2小时K,7为4小时K,8为日K,9为周K,10为月K + query_kline_num := internal.IntegerInit(c.Query("query_kline_num")) // 查询多少根K线,最多1000根 + + code := model.Check_Symbol[codeStr] + if len(code) == 0 { + code = codeStr + } + + if kline_type == 0 || query_kline_num == 0 || len(code) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + req, err := http.NewRequest("GET", UrlKline, nil) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + q := req.URL.Query() + q.Add("token", TokenTrade) + // 参数构造 + var query model.ConstructParameters + query.Trace = uuid.New().String() + query.Data.Code = code + query.Data.KlineType = kline_type + query.Data.KlineTimestampEnd = 0 + query.Data.QueryKlineNum = query_kline_num + query.Data.AdjustType = 0 + queryStr, err := json.Marshal(&query) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + q.Add("query", string(queryStr)) + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + defer resp.Body.Close() + + bodyStr, err := ioutil.ReadAll(resp.Body) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + var klineNew model.KlineGetReturnStruct + if err = json.Unmarshal(bodyStr, &klineNew); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + var group bson.D + addFields := bson.D{ + {"$addFields", bson.D{ + {"dateObj", bson.D{{"$toDate", "$timestamp"}}}, + {"num", bson.D{{"$toInt", "$volume"}}}}, + }} + match := bson.D{ + {"$match", bson.D{ + {"code", codeStr}, + {"dateObj", bson.D{{"$gte", time.Now().Add(-1 * time.Hour)}}}, + }}} + //minDate := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) + // TODO: 插针数据 + var timeStamp int64 + switch kline_type { + case 1: // 分时线 + timeStamp = int64(1 * 60 * 1000) // 1 分钟 = 1 * 60 秒 * 1000 毫秒 + case 2: // 5分钟线 + timeStamp = int64(5 * 60 * 1000) // 5分钟 = 5 * 60 秒 * 1000 毫秒 + //case 3: // 15分钟 + // timeStamp = int64(15 * 60 * 1000) // 15分钟 =15 * 60 秒 * 1000 毫秒 + //case 4: // 30分钟 + // timeStamp = int64(30 * 60 * 1000) // 30分钟 =30 * 60 秒 * 1000 毫秒 + //case 5: // 1小时 + // timeStamp = int64(60 * 60 * 1000) // 1小时 =60 *60 秒 * 1000 毫秒 + default: + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, klineNew.Data, internal.QuerySuccess)) + return + } + applogger.Debug("数据展示:%v", timeStamp) + + group = bson.D{ + {"$group", bson.D{ + {"_id", "$dateObj"}, + //{"_id", bson.M{"$subtract": []interface{}{ + // bson.M{"$subtract": []interface{}{"$dateObj", bson.M{"$dateFromString": bson.M{"dateString": "1970-01-01T00:00:00Z"}}}}, + // bson.M{"$mod": []interface{}{bson.M{"$subtract": []interface{}{"$dateObj", bson.M{"$dateFromString": bson.M{"dateString": "1970-01-01T00:00:00Z"}}}}, timeStamp}}, + //}}}, + //{"_id", bson.M{ + // "date": "$dateObj", // 用实际时间字段替换dateField + // "minutes": bson.M{"$toInt": bson.M{"$divide": []interface{}{"$dateObj", timeStamp}}}, + //}}, + {"highestPrice", bson.D{{"$max", "$high_price"}}}, + {"lowestPrice", bson.D{{"$min", "$low_price"}}}, + {"openPrice", bson.D{{"$first", "$open_price"}}}, + {"closePrice", bson.D{{"$last", "$close_price"}}}, + {"volume", bson.D{{"$max", "$num"}}}, + }}} + + project := bson.D{ + {"$project", bson.D{ + {"_id", 0}, + {"time", bson.D{ + {"$dateToString", bson.D{ + {"format", "%Y-%m-%d %H:%M:%S"}, + {"date", "$_id"}, + }}}}, + //{"time", bson.D{{"$toDate", "$_id"}}}, + //{"time", "$_id"}, + {"highestPrice", 1}, + {"lowestPrice", 1}, + {"openPrice", 1}, + {"closePrice", 1}, + {"volume", bson.D{{"$toString", "$volume"}}}, + }}} + sort := bson.D{{"$sort", bson.D{{"time", 1}}}} + + operations := mongo.Pipeline{addFields, match, group, project, sort} + mapList, err := data.MgoAggregate(data.ForexKLine, operations) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "MgoAggregate err", internal.QueryError)) + return + } + applogger.Debug("查询数据:%v", mapList) + + switch v := mapList.(type) { + case []map[string]interface{}: + v = mapList.([]map[string]interface{}) + for _, value := range v { + var md model.KlineList + if highest, ok := value["highestPrice"].(string); ok { + md.HighPrice = highest + } + if lowest, ok := value["lowestPrice"].(string); ok { + md.LowPrice = lowest + } + if open, ok := value["openPrice"].(string); ok { + md.OpenPrice = open + } + if closeP, ok := value["closePrice"].(string); ok { + md.ClosePrice = closeP + } + if volume, ok := value["volume"].(string); ok { + md.Volume = volume + } + if times, ok := value["time"].(string); ok { + loc, err := time.LoadLocation("Local") // 加载本地时区 + if err != nil { + applogger.Error("加载时区失败:", err) + continue + } + parsedTime, err := time.ParseInLocation("2006-01-02 15:04:05", times, loc) + if err != nil { + applogger.Error("解析时间字符串失败:", err) + continue + } + md.Timestamp = strconv.Itoa(int(parsedTime.Unix())) + } + md.Turnover = strconv.Itoa(0) + applogger.Debug("数据解析为:%v", md) + klineNew.Data.KlineList = append(klineNew.Data.KlineList, md) + } + default: + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, klineNew.Data, internal.QuerySuccess)) +} + +// 最新k线查询 +func ForexAggregatesNewPost(c *gin.Context) { + param := model.ConstructParametersPost{} + err := c.BindJSON(¶m) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Trace) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if len(param.Data.DataList) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } + queryStr, err := json.Marshal(¶m) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + + bodyStr, err := internal.HttpPost(fmt.Sprintf("%v?token=%v", UrlBatchKline, TokenTrade), string(queryStr)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + var klineNew model.KlinePostReturnStruct + if err = json.Unmarshal([]byte(bodyStr), &klineNew); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, klineNew.Data.KlineList, internal.QuerySuccess)) +} + +// 最新盘口报价查询 +func ForexAggregatesDepthTick(c *gin.Context) { + param := model.OrderBookOrTradeTick{} + err := c.BindJSON(¶m) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Trace) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if len(param.Data.SymbolList) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } + + req, err := http.NewRequest("GET", UrlDepthTick, nil) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + // 参数构造 + q := req.URL.Query() + q.Add("token", TokenTrade) + queryStr, err := json.Marshal(¶m) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + q.Add("query", string(queryStr)) + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + defer resp.Body.Close() + + bodyStr, err := ioutil.ReadAll(resp.Body) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + var depthNew model.DepthReturnStruct + if err = json.Unmarshal(bodyStr, &depthNew); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, depthNew.Data, internal.QuerySuccess)) +} + +// 最新成交价批量查询 +func ForexAggregatesTradeTick(c *gin.Context) { + param := model.OrderBookOrTradeTick{} + err := c.BindJSON(¶m) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Trace) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if len(param.Data.SymbolList) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } + + req, err := http.NewRequest("GET", UrlTradeTick, nil) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + // 参数构造 + q := req.URL.Query() + q.Add("token", TokenTrade) + queryStr, err := json.Marshal(¶m) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + q.Add("query", string(queryStr)) + req.URL.RawQuery = q.Encode() + resp, err := http.DefaultClient.Do(req) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, nil, internal.QueryError)) + return + } + defer resp.Body.Close() + + bodyStr, err := ioutil.ReadAll(resp.Body) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + var tradeNew model.TradeReturnStruct + if err = json.Unmarshal(bodyStr, &tradeNew); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, tradeNew.Data, internal.QuerySuccess)) +} + +// 查询大宗列表 +func ForexSymbolListNew(c *gin.Context) { + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) // 每页显示多少条数据 + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) // 第几页 + sort := internal.IntegerInit(internal.ReplaceStr(c.Query("sort"))) // Code排序 + search := internal.ReplaceStr(c.Query("search")) // 搜索数据(模糊) + category := internal.ReplaceStr(c.Query("category")) // 类型:外汇(FX)、贵金属(Metals)、能源(Energy) + if len(category) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "Parameter cannot be empty", internal.QueryError)) + return + } + + var filter bson.M + if len(search) > 0 { + // 模糊搜索 + switch category { + case "FX": // 外汇 + filter = bson.M{"$and": []bson.M{{"symbol": bson.M{"$regex": search}}, {"symbol": bson.M{"$regex": "USD$"}}, {"category": category}}} + default: // 贵金属和能源 + filter = bson.M{"symbol": bson.M{"$regex": search}, "category": category} + } + } else { + // 正常加载列表 + filter = bson.M{"$and": []bson.M{{"category": category}, {"symbol": bson.M{"$regex": "USD$"}}}} + } + + total, err := data.MgoFindTotal(data.ForexListBak, filter) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + pageData := make([]stock.ForexDataNew, 0) + if sort == 0 { + sort = -1 + } + + data.MgoPagingFindStruct(data.ForexListBak, filter, int64(pageSize), int64(pageNumber), "symbol", sort, &pageData) + + pageDataList := make([]stock.ForexDataNew, 0) + redisShield := HashValue(RedisFOREX) + // 过滤屏蔽的交易对 + for _, value := range pageData { + strMap, ok := redisShield[value.Code] + if !ok { + pageDataList = append(pageDataList, value) + } else { + if strMap.Status == 1 { + pageDataList = append(pageDataList, value) + } + } + } + + var md stock.MgoPageSize + md.PageSize = pageSize + md.PageNumber = pageNumber + md.Total = total + md.Data = pageDataList + applogger.Debug("查询数据: %v", len(pageDataList)) + + if len(pageDataList) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// 查询大宗收藏列表 +func ForexFreeSymbolListNew(c *gin.Context) { + id := internal.ReplaceStr(c.Query("id")) // 用户ID + market_type := internal.IntegerInit(internal.ReplaceStr(c.Query("market_type"))) // 外汇市场 + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) // 每页显示多少条数据 + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) // 第几页 + + if len(id) <= 0 || market_type <= 0 || pageSize <= 0 || pageNumber <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "Parameter cannot be empty", internal.QueryError)) + return + } + var md stock.MgoPageSize + pageDataList := make([]stock.ForexDataNew, 0) + md.PageSize = pageSize + md.PageNumber = pageNumber + + userIdKey := fmt.Sprintf("%v%v", FreeSymbolKey, id) + result, err := red.Get_Cache_Byte(userIdKey) + if err != nil { + md.Data = pageDataList + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + + var freeList []StockSymbol + if err = json.Unmarshal(result, &freeList); err != nil { + md.Data = pageDataList + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + + // 组合需要查询自选缓存股票code + var symbolList []string + for _, value := range freeList { + if market_type == value.MarketType { + symbolList = append(symbolList, value.Code) + } + } + + filter := bson.M{"$and": []bson.M{{"symbol": bson.M{"$in": symbolList}}, {"symbol": bson.M{"$regex": "USD$"}}}} + total, err := data.MgoFindTotal(data.ForexListBak, filter) + if err != nil { + md.Total = int64(len(symbolList)) + md.Data = pageDataList + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + pageData := make([]stock.ForexDataNew, 0) + if err = data.MgoPagingFindStruct(data.ForexListBak, filter, int64(pageSize), int64(pageNumber), "symbol", -1, &pageData); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + redisShield := HashValue(RedisFOREX) + // 过滤屏蔽的交易对 + for _, value := range pageData { + strMap, ok := redisShield[value.Code] + if !ok { + pageDataList = append(pageDataList, value) + } else { + if strMap.Status == 1 { + pageDataList = append(pageDataList, value) + } + } + } + md.Total = total + md.Data = pageDataList + applogger.Debug("查询数据: %v", len(pageDataList)) + + if len(pageDataList) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// 查询大宗成交报价 +func ForexTradeList(c *gin.Context) { + code := internal.ReplaceStr(c.Query("code")) // 外汇code + limit := internal.IntegerInit(internal.ReplaceStr(c.Query("limit"))) // 查询多少条数据 + if len(code) == 0 || limit == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "Parameter cannot be empty", internal.QueryError)) + return + } + + res := make([]model.ForexTradeList, 0) + filter := bson.M{"code": code} + + if err := data.MgoFindForexToStr(data.ForexTradeList, filter, int64(limit), &res); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, []model.ForexTradeList{}, internal.QuerySuccess)) + return + } + + if len(res) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, []model.ForexTradeList{}, internal.QuerySuccess)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, res, internal.QuerySuccess)) +} + +// ForexSymbolList +// +// @Description: 外汇代码列表查询 http://127.0.0.1:88/stock/share/exchange-symbol-list?pageNumber=1&pageSize=35&&search= +// @param c +func ForexSymbolList(c *gin.Context) { + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) // 每页显示多少条数据 + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) // 第几页 + sort := internal.IntegerInit(internal.ReplaceStr(c.Query("sort"))) // Code排序 + search := internal.ReplaceStr(c.Query("search")) // 搜索数据(模糊) + + var filter bson.M + if len(search) > 0 { // 模糊搜索 + filter = bson.M{"$and": []bson.M{{"ticker": bson.M{"$regex": search}}, {"ticker": bson.M{"$regex": "USD$"}}}} + } else { + filter = bson.M{"ticker": bson.M{"$regex": "USD$"}} + } + filter["day.o"] = bson.M{"$gt": 0.0} + filter["day.h"] = bson.M{"$gt": 0.0} + filter["day.l"] = bson.M{"$gt": 0.0} + filter["day.c"] = bson.M{"$gt": 0.0} + + total, err := data.MgoFindTotal(data.ForexList, filter) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + pageData := make([]stock.ForexData, 0) + if sort == 0 { + sort = -1 + } + + data.MgoPagingFindStruct(data.ForexList, filter, int64(pageSize), int64(pageNumber), "updated", sort, &pageData) + + pageDataList := make([]stock.ForexData, 0) + redisShield := HashValue(RedisFOREX) + // 过滤屏蔽的交易对 + for _, value := range pageData { + if value.Day.C == 0 && value.Day.O == 0 && value.Day.H == 0 && value.Day.L == 0 { + continue + } + strMap, ok := redisShield[value.Ticker] + if !ok { + pageDataList = append(pageDataList, value) + } else { + if strMap.Status == 1 { + pageDataList = append(pageDataList, value) + } + } + } + + var md stock.MgoPageSize + md.PageSize = pageSize + md.PageNumber = pageNumber + md.Total = total + md.Data = pageDataList + applogger.Debug("查询数据: %v", len(pageDataList)) + + if len(pageDataList) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// ForexFreeSymbolList +func ForexFreeSymbolList(c *gin.Context) { + id := internal.ReplaceStr(c.Query("id")) // 用户ID + market_type := internal.IntegerInit(internal.ReplaceStr(c.Query("market_type"))) // 外汇市场 + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) // 每页显示多少条数据 + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) // 第几页 + + if len(id) <= 0 || market_type <= 0 || pageSize <= 0 || pageNumber <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "参数不能为空", internal.QueryError)) + return + } + var md stock.MgoPageSize + pageDataList := make([]stock.ForexData, 0) + md.PageSize = pageSize + md.PageNumber = pageNumber + + userIdKey := fmt.Sprintf("%v%v", FreeSymbolKey, id) + result, err := red.Get_Cache_Byte(userIdKey) + if err != nil { + md.Data = pageDataList + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + var freeList []StockSymbol + if err = json.Unmarshal(result, &freeList); err != nil { + md.Data = pageDataList + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + + // 组合需要查询自选缓存股票code + var symbolList []string + for _, value := range freeList { + if market_type == value.MarketType { + symbolList = append(symbolList, value.Code) + } + } + filter := bson.M{"$and": []bson.M{{"ticker": bson.M{"$in": symbolList}}, {"ticker": bson.M{"$regex": "USD$"}}}} + filter["day.o"] = bson.M{"$gt": 0.0} + filter["day.h"] = bson.M{"$gt": 0.0} + filter["day.l"] = bson.M{"$gt": 0.0} + filter["day.c"] = bson.M{"$gt": 0.0} + + total, err := data.MgoFindTotal(data.ForexList, filter) + if err != nil { + md.Total = int64(len(symbolList)) + md.Data = pageDataList + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + pageData := make([]stock.ForexData, 0) + if err = data.MgoPagingFindStruct(data.ForexList, filter, int64(pageSize), int64(pageNumber), "updated", -1, &pageData); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + + redisShield := HashValue(RedisFOREX) + // 过滤屏蔽的交易对 + for _, value := range pageData { + if value.Day.C == 0 && value.Day.O == 0 && value.Day.H == 0 && value.Day.L == 0 { + continue + } + strMap, ok := redisShield[value.Ticker] + if !ok { + pageDataList = append(pageDataList, value) + } else { + if strMap.Status == 1 { + pageDataList = append(pageDataList, value) + } + } + } + + md.Total = total + md.Data = pageDataList + applogger.Debug("查询数据: %v", len(pageDataList)) + + if len(pageDataList) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// ForexAllTickers +// +// @Description: 查询所有外汇代码列表 https://api.polygon.io/v2/snapshot/locale/global/markets/forex/tickers?apiKey=3uX9zgBRPFl6MS11t4CsIrIZ_s_o2nh9 +// @param c +func ForexAllTickers(c *gin.Context) { + url := fmt.Sprintf("https://%v/v2/snapshot/locale/global/markets/forex/tickers?apiKey=%v", config.Config.ShareGather.PolygonHost, config.Config.ShareGather.PolygonKey) + applogger.Debug("查询数据信息:%v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + var listCode model.ForexDataResponse + if err = json.Unmarshal([]byte(bodyStr), &listCode); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + var dataList []mongo.WriteModel + for _, v := range listCode.Tickers { + code := strings.Split(v.Ticker, ":") + if len(code) < 2 { + continue + } + codeStr := code[1] + filter := bson.M{ + "ticker": codeStr, + } + updateData := bson.M{} + updateData["todaysChange"] = v.TodaysChange + updateData["todaysChangePerc"] = v.TodaysChangePerc + updateData["updated"] = v.Updated + updateData["day"] = v.Day + updateData["lastQuote"] = v.LastQuote + updateData["min"] = v.Min + updateData["prevDay"] = v.PrevDay + + update := bson.M{"$set": updateData} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err = data.MgoBulkWrite(data.ForexList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, dataList, internal.QuerySuccess)) +} + +// ForexTicker +// +// @Description: 获取单个外汇数据 https://api.polygon.io/v2/snapshot/locale/global/markets/forex/tickers/C:EURUSD?apiKey=3uX9zgBRPFl6MS11t4CsIrIZ_s_o2nh9 +// @param c +func ForexTicker(c *gin.Context) { + code := internal.ReplaceStr(c.Query("code")) // 外汇代码 + if len(code) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "Parameter error!", internal.QueryError)) + return + } + + url := fmt.Sprintf("https://%v/v2/snapshot/locale/global/markets/forex/tickers/C:%v?apiKey=%v", config.Config.ShareGather.PolygonHost, code, config.Config.ShareGather.PolygonKey) + applogger.Debug("查询数据信息:%v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// ForexPreviousClose +// +// @Description: 获取指定外汇代码前一天数据 https://api.polygon.io/v2/aggs/ticker/C:EURUSD/prev?adjusted=true&apiKey=3uX9zgBRPFl6MS11t4CsIrIZ_s_o2nh9 +// @param c +func ForexPreviousClose(c *gin.Context) { + code := internal.ReplaceStr(c.Query("code")) // 外汇代码 + if len(code) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "Parameter error!", internal.QueryError)) + return + } + + url := fmt.Sprintf("https://%v/v2/aggs/ticker/C:%v/prev?adjusted=true&apiKey=%v", config.Config.ShareGather.PolygonHost, code, config.Config.ShareGather.PolygonKey) + applogger.Debug("查询数据信息:%v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// ForexGroupedDaily +// +// @Description: 获取每日分组数据 https://api.polygon.io/v2/aggs/grouped/locale/global/market/fx/2023-01-09?adjusted=true&apiKey=3uX9zgBRPFl6MS11t4CsIrIZ_s_o2nh9 +// @param c +func ForexGroupedDaily(c *gin.Context) { + date := internal.ReplaceStr(c.Query("date")) // 查询时间 + if len(date) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "Parameter error!", internal.QueryError)) + return + } + url := fmt.Sprintf("https://%v/v2/aggs/grouped/locale/global/market/fx/%v?adjusted=true&apiKey=%v", config.Config.ShareGather.PolygonHost, date, config.Config.ShareGather.PolygonKey) + applogger.Debug("查询数据信息:%v", url) + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// ForexQuotesBBO +// +// @Description: 行情(BBO) 获取给定时间范围内某个股票代码的 BBO 报价。 https://api.polygon.io/v3/quotes/C:EUR-USD?order=desc&limit=1000&sort=timestamp&apiKey=3uX9zgBRPFl6MS11t4CsIrIZ_s_o2nh9 +// @param c +func ForexQuotesBBO(c *gin.Context) { + code := internal.ReplaceStr(c.Query("code")) // 代码 + if len(code) >= 3 { + firstThree := code[0:3] + lastThree := code[len(code)-3:] // 从字符串长度减去 3 开始到结束 + code = fmt.Sprintf("%v-%v", firstThree, lastThree) + } else { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "Parameter error!", internal.QueryError)) + return + } + url := fmt.Sprintf("https://%v/v3/quotes/C:%v?order=desc&limit=100&sort=timestamp&apiKey=%v", config.Config.ShareGather.PolygonHost, code, config.Config.ShareGather.PolygonKey) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// ForexLastQuote +// +// @Description: 货币对的最后报价 https://api.polygon.io/v1/last_quote/currencies/AUD/USD?apiKey=3uX9zgBRPFl6MS11t4CsIrIZ_s_o2nh9 +// @param c +func ForexLastQuote(c *gin.Context) { + code := internal.ReplaceStr(c.Query("code")) // 代码 + var firstCode, lastCode string + if len(code) >= 3 { + firstCode = code[0:3] + lastCode = code[len(code)-3:] // 从字符串长度减去 3 开始到结束 + } else { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "Parameter error!", internal.QueryError)) + return + } + url := fmt.Sprintf("https://%v/v1/last_quote/currencies/%v/%v?apiKey=%v", config.Config.ShareGather.PolygonHost, firstCode, lastCode, config.Config.ShareGather.PolygonKey) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// ForexRealTimeCurrency +// +// @Description: 实时货币兑换 https://api.polygon.io/v1/conversion/AUD/USD?amount=100&precision=2&apiKey=3uX9zgBRPFl6MS11t4CsIrIZ_s_o2nh9 +// @param c +func ForexRealTimeCurrency(c *gin.Context) { + var firstCode, lastCode string + code := internal.ReplaceStr(c.Query("code")) // 代码 + amount := internal.IntegerInit(internal.ReplaceStr(c.Query("amount"))) // 查询数量 默认:100 + precision := internal.IntegerInit(internal.ReplaceStr(c.Query("precision"))) // 精度 0,1,2,3,4 默认:2 + + if len(code) >= 3 { + firstCode = code[0:3] + lastCode = code[len(code)-3:] // 从字符串长度减去 3 开始到结束 + } else { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "Parameter error!", internal.QueryError)) + return + } + if amount < 1 { + amount = 100 + } + if precision < 0 || precision > 4 { + precision = 2 + } + + url := fmt.Sprintf("https://%v/v1/conversion/%v/%v?amount=%v&precision=%v&apiKey=%v", + config.Config.ShareGather.PolygonHost, firstCode, lastCode, amount, precision, config.Config.ShareGather.PolygonKey) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// HashValue +// +// @Description: 获取屏蔽的外汇交易对 +// @param hashListName +// @return []stock.PHPData +func HashValue(hashListName string) map[string]stock.PHPData { + keys := red.Scan(hashListName) + result := make(map[string]stock.PHPData, 0) + for _, v := range keys { + res, _ := red.HGetAll(v) + item := stock.PHPData{} + + for field, value := range res { + switch field { + case "name": + item.Name = value + case "code": + item.Code = value + case "keep_decimal": + item.KeepDecimal = value + case "face_value": + item.FaceValue, _ = strconv.Atoi(value) + case "max_pry": + item.MaxPry, _ = strconv.Atoi(value) + case "min_pry": + item.MinPry, _ = strconv.Atoi(value) + case "logo_link": + item.LogoLink = value + case "status": + item.Status, _ = strconv.Atoi(value) + } + } + + result[item.Code] = item + } + + return result +} diff --git a/pkg/processor/history.go b/pkg/processor/history.go new file mode 100644 index 0000000..2975f49 --- /dev/null +++ b/pkg/processor/history.go @@ -0,0 +1,657 @@ +package processor + +import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "go.mongodb.org/mongo-driver/bson" + "net/http" + "reflect" + "strings" + "time" + "wss-pool/cmd/common" + "wss-pool/cmd/websocketservice" + "wss-pool/internal" + "wss-pool/internal/data" + red "wss-pool/internal/redis" + "wss-pool/pkg/model/stock" +) + +const ( + KlinePageSize = 100 + SpotsStatus int = 1 + ContractStatus int = 2 +) + +// 现货历史记录 +// contract_code 合约代码 或 合约标识 +// period K线类型 "1min", "5min", "15min", "30min", "60min", "4hour", "1day", "1mon", "1week", "1year" +// size 获取数量,默认150 [1,2000] +// from 开始时间戳 10位 单位S +// to 结束时间戳 10位 单位S +func SpotKLineList(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) // 币种 + period := internal.ReplaceStr(c.Query("period")) // 时间颗粒度 + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) // 开始时间 + to := internal.IntegerInit(internal.ReplaceStr(c.Query("to"))) // 结束时间 + if symbol == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } else if period == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "period error", internal.QueryError)) + return + } + filter := bson.M{"channel": fmt.Sprintf("market.%susdt.kline.%s", symbol, period), "code": bson.M{"$gte": from, "$lte": to}} + if size > 0 { + filter = bson.M{"channel": fmt.Sprintf("market.%susdt.kline.%s", symbol, period)} + } + tableName := data.GetStockKLineTableName(period) + //applogger.Debug("查询数据总数: %v", total) + pagedData, err := data.MgoLimitFind(tableName, filter, int64(size)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = removeDuplicates(pagedData) + md.Data = common.MarshalToJsonWithGzip(md.Data) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func removeDuplicates(pagedData []data.MongoTick) []data.MongoTick { + result := make([]data.MongoTick, 0) + keys := make([]int64, 0) + resMap := make(map[int64]data.MongoTick) + for _, doc := range pagedData { + resMap[doc.Code] = doc + keys = append(keys, doc.Code) + } + for _, v := range keys { + result = append(result, resMap[v]) + } + return result +} + +// 合约价格历史记录 +func ContractPriceKLineList(c *gin.Context) { + contractCode := strings.ToUpper(internal.ReplaceStr(c.Query("symbol"))) // 合约 + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) // 开始时间 + to := internal.IntegerInit(internal.ReplaceStr(c.Query("to"))) // 结束时间 + period := internal.ReplaceStr(c.Query("period")) // 时间颗粒度 + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) // 结束时间 + if contractCode == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } else if period == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "period error", internal.QueryError)) + return + } + //bodyStr, err := internal.HttpGet(fmt.Sprintf("https://api.hbdm.com/index/market/history/linear_swap_mark_price_kline?contract_code=%s&period=%s", contractCode, period)) + //if err != nil { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + // return + //} + //result := business.SpotKlineRes{} + //if err = json.Unmarshal([]byte(bodyStr), &result); err != nil { + // applogger.Error("Unmarshal err: %v---%v", err) + //} + //var md stock.MgoPageSize + //md.Data = result + //c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + + //else if from == 0 { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "from error", internal.QueryError)) + // return + //} else if to == 0 { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "to error", internal.QueryError)) + // return + //} + + filter := bson.M{"channel": fmt.Sprintf("market.%s-USDT.mark_price.%s", contractCode, period), "code": bson.M{"$gte": from, "$lte": to}} + if size > 0 { + filter = bson.M{"channel": fmt.Sprintf("market.%s-USDT.mark_price.%s", contractCode, period)} + } + tableName := data.GetContractPriceKLineTableName(period) + //applogger.Debug("查询数据总数: %v", total) + pagedData, err := data.MgoLimitFind(tableName, filter, int64(size)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = removeDuplicates(pagedData) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// 合约历史记录 +func ContractKLineList(c *gin.Context) { + contractCode := strings.ToUpper(internal.ReplaceStr(c.Query("symbol"))) // 合约 + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) // 开始时间 + to := internal.IntegerInit(internal.ReplaceStr(c.Query("to"))) // 结束时间 + period := internal.ReplaceStr(c.Query("period")) // 时间颗粒度 + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) // 结束时间 + if contractCode == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } else if period == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "period error", internal.QueryError)) + return + } + //url := fmt.Sprintf("https://api.hbdm.com/linear-swap-ex/market/history/kline?contract_code=%s&period=%s&from=%d&to=%d", contractCode, period, from, to) + //if size > 0 { + // url = fmt.Sprintf("https://api.hbdm.com/linear-swap-ex/market/history/kline?contract_code=%s&period=%s&size=%d", contractCode, period, size) + //} + //bodyStr, err := internal.HttpGet(url) + //if err != nil { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + // return + //} + //result := business.ContractKlineRes{} + //if err = json.Unmarshal([]byte(bodyStr), &result); err != nil { + // applogger.Error("Unmarshal err: %v---%v", err) + //} + //var md stock.MgoPageSize + //md.Data = result + //c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + filter := bson.M{"channel": fmt.Sprintf("market.%s-USDT.kline.%s", contractCode, period), "code": bson.M{"$gte": from, "$lte": to}} + if size > 0 { + filter = bson.M{"channel": fmt.Sprintf("market.%s-USDT.kline.%s", contractCode, period)} + } + tableName := data.GetContractKLineTableName(period) + + pagedData, err := data.MgoLimitFind(tableName, filter, int64(size)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = removeDuplicates(pagedData) + md.Data = common.MarshalToJsonWithGzip(md.Data) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// 查询价格 +func InquiryPrice(c *gin.Context) { + contractCode := internal.ReplaceStr(c.Query("symbol")) + timeStr := c.Query("time") // 结束时间 + if contractCode == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } else if timeStr == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "timeStr error", internal.QueryError)) + return + } + timestamp, err := common.TimeStrToTimestamp(timeStr) + fmt.Println(timestamp) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err.Error(), internal.QueryError)) + return + } + ok := strings.Contains(contractCode, "-USDT") + filter := bson.M{"channel": fmt.Sprintf("market.%s.kline.1min", contractCode), "code": timestamp} + tableName := data.GetContractKLineTableName("1min") + if !ok { + tableName = data.GetStockKLineTableName("1min") + } + pagedData, err := data.MgoLimitFind(tableName, filter, int64(0)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = removeDuplicates(pagedData) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// 美股查询记录 +func HistoryUsList(c *gin.Context) { + code := strings.ToUpper(internal.ReplaceStr(c.Query("code"))) // 股票代码 + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) // 每页显示多少条数据 + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) // 第几页 + period := internal.ReplaceStr(c.Query("period")) // 时间颗粒度 + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) // 开始时间 + if pageNumber <= 0 { + pageNumber = 1 + } + if pageSize <= 0 { + pageSize = KlinePageSize + } + if code == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "code error", internal.QueryError)) + return + } else if period == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "period error", internal.QueryError)) + return + } + if from == 0 { + from = int(time.Now().Unix()) + } + filter := bson.M{"code": code, "timestamp": bson.M{"$lte": from}} + tableName := data.GetStockUsTableName(period) + total, err := data.MgoFindTotal(tableName, filter) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + //applogger.Debug("查询数据总数: %v", total) + pagedData, err := data.MgoPagingFind(tableName, filter, int64(pageSize), int64(pageNumber), -1) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = pagedData + md.PageSize = pageSize + md.PageNumber = pageNumber + md.Total = total + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func removeWsconn(conn interface{}, userConn *websocket.Conn, symbol string) error { + connRes := make([]*websocket.Conn, 0) + value := reflect.ValueOf(conn) + if value.Kind() != reflect.Slice && value.Kind() != reflect.Array { + return nil + } + for i := 0; i < value.Len(); i++ { + val := value.Index(i).Interface().(*websocket.Conn) + if val != userConn { + connRes = append(connRes, val) + } + } + return nil +} + +// 现货|合约|秒合约列表查询 +func MainSpotList(c *gin.Context) { + market_type := internal.IntegerInit(internal.ReplaceStr(c.Query("marketType"))) + if market_type == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "marketType error", internal.QueryError)) + return + } + result := make([]interface{}, 0) + var md stock.MgoPageSize + resMap := make(map[string]interface{}) + if market_type == SpotsStatus { + // spot := websocketservice.HashValue(websocketservice.RedisDIGITAL) + websocketservice.SpotMarketCache.Range( + func(key, value any) bool { + //for _, val := range spot { + //if strings.Contains(key.(string), fmt.Sprintf("%susdt", strings.ToLower(val.Name))) && value != nil { + resMap[key.(string)] = value + //} + return true + }) + if _, ok := resMap["btc"]; ok { + result = append(result, resMap["btc"]) + } + if _, ok := resMap["eth"]; ok { + result = append(result, resMap["eth"]) + } + if _, ok := resMap["bnb"]; ok { + result = append(result, resMap["bnb"]) + } + if _, ok := resMap["usdc"]; ok { + result = append(result, resMap["usdc"]) + } + if _, ok := resMap["xrp"]; ok { + result = append(result, resMap["xrp"]) + } + if _, ok := resMap["ada"]; ok { + result = append(result, resMap["ada"]) + } + if _, ok := resMap["doge"]; ok { + result = append(result, resMap["doge"]) + } + if _, ok := resMap["sol"]; ok { + result = append(result, resMap["sol"]) + } + if _, ok := resMap["trx"]; ok { + result = append(result, resMap["trx"]) + } + if _, ok := resMap["ltc"]; ok { + result = append(result, resMap["ltc"]) + } + if _, ok := resMap["dot"]; ok { + result = append(result, resMap["dot"]) + } + if _, ok := resMap["matic"]; ok { + result = append(result, resMap["matic"]) + } + if _, ok := resMap["bch"]; ok { + result = append(result, resMap["bch"]) + } + if _, ok := resMap["eos"]; ok { + result = append(result, resMap["eos"]) + } + if _, ok := resMap["ton"]; ok { + result = append(result, resMap["ton"]) + } + if _, ok := resMap["avax"]; ok { + result = append(result, resMap["avax"]) + } + if _, ok := resMap["shib"]; ok { + result = append(result, resMap["shib"]) + } + if _, ok := resMap["invu"]; ok { + result = append(result, resMap["invu"]) + } + if _, ok := resMap["osel"]; ok { + result = append(result, resMap["osel"]) + } + if _, ok := resMap["fmd"]; ok { + result = append(result, resMap["fmd"]) + } + if _, ok := resMap["dten"]; ok { + result = append(result, resMap["dten"]) + } + if _, ok := resMap["xnsl"]; ok { + result = append(result, resMap["xnsl"]) + } + if _, ok := resMap["kools"]; ok { + result = append(result, resMap["kools"]) + } + md.Data = result + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + // contract := websocketservice.HashValue(websocketservice.RedisCONTRACT) + websocketservice.ContractCache.Range( + func(key, value any) bool { + //for _, val := range contract { + // if strings.Contains(key.(string), val.Name) && value != nil { + resMap[key.(string)] = value + // } + // } + return true + }) + + if _, ok := resMap["BTC-USDT"]; ok { + result = append(result, resMap["BTC-USDT"]) + } + if _, ok := resMap["ETH-USDT"]; ok { + result = append(result, resMap["ETH-USDT"]) + } + if _, ok := resMap["BCH-USDT"]; ok { + result = append(result, resMap["BCH-USDT"]) + } + if _, ok := resMap["XRP-USDT"]; ok { + result = append(result, resMap["XRP-USDT"]) + } + if _, ok := resMap["EOS-USDT"]; ok { + result = append(result, resMap["EOS-USDT"]) + } + if _, ok := resMap["LTC-USDT"]; ok { + result = append(result, resMap["LTC-USDT"]) + } + if _, ok := resMap["TRX-USDT"]; ok { + result = append(result, resMap["TRX-USDT"]) + } + if _, ok := resMap["ETC-USDT"]; ok { + result = append(result, resMap["ETC-USDT"]) + } + if _, ok := resMap["LINK-USDT"]; ok { + result = append(result, resMap["LINK-USDT"]) + } + if _, ok := resMap["BNB-USDT"]; ok { + result = append(result, resMap["BNB-USDT"]) + } + if _, ok := resMap["ADA-USDT"]; ok { + result = append(result, resMap["ADA-USDT"]) + } + if _, ok := resMap["DOGE-USDT"]; ok { + result = append(result, resMap["DOGE-USDT"]) + } + if _, ok := resMap["SOL-USDT"]; ok { + result = append(result, resMap["SOL-USDT"]) + } + if _, ok := resMap["DOT-USDT"]; ok { + result = append(result, resMap["DOT-USDT"]) + } + if _, ok := resMap["MATIC-USDT"]; ok { + result = append(result, resMap["MATIC-USDT"]) + } + if _, ok := resMap["AVAX-USDT"]; ok { + result = append(result, resMap["AVAX-USDT"]) + } + if _, ok := resMap["SHIB-USDT"]; ok { + result = append(result, resMap["SHIB-USDT"]) + } + if _, ok := resMap["BNBS-USDT"]; ok { + result = append(result, resMap["BNBS-USDT"]) + } + if _, ok := resMap["INVU-USDT"]; ok { + result = append(result, resMap["INVU-USDT"]) + } + if _, ok := resMap["OSEL-USDT"]; ok { + result = append(result, resMap["OSEL-USDT"]) + } + if _, ok := resMap["FMD-USDT"]; ok { + result = append(result, resMap["FMD-USDT"]) + } + if _, ok := resMap["DTEN-USDT"]; ok { + result = append(result, resMap["DTEN-USDT"]) + } + if _, ok := resMap["XNSL-USDT"]; ok { + result = append(result, resMap["XNSL-USDT"]) + } + if _, ok := resMap["KOOLS-USDT"]; ok { + result = append(result, resMap["KOOLS-USDT"]) + } + md.Data = result + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// 现货|合约|秒合约|自选列表查询 +func MainFreeSpotList(c *gin.Context) { + id := internal.ReplaceStr(c.Query("id")) + market_type := internal.IntegerInit(internal.ReplaceStr(c.Query("marketType"))) + if market_type == 0 || len(id) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "marketType or userId error", internal.QueryError)) + return + } + var md stock.MgoPageSize + result := make([]interface{}, 0) + md.Data = result + userIdKey := fmt.Sprintf("%v%v", FreeSymbolKey, id) + resultStr, err := red.Get_Cache_Byte(userIdKey) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + var freeList []StockSymbol + if err = json.Unmarshal(resultStr, &freeList); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + // 组合需要查询自选缓存股票code + var symbolList []string + for _, value := range freeList { + if market_type == value.MarketType { + symbolList = append(symbolList, value.Code) + } + } + + resMap := make(map[string]interface{}) + switch market_type { + case SpotsStatus: + websocketservice.SpotMarketCache.Range(func(key, value any) bool { + for _, vue := range symbolList { + if vue == key.(string) { + resMap[vue] = value + } + } + return true + }) + if _, ok := resMap["btc"]; ok { + result = append(result, resMap["btc"]) + } + if _, ok := resMap["eth"]; ok { + result = append(result, resMap["eth"]) + } + if _, ok := resMap["bnb"]; ok { + result = append(result, resMap["bnb"]) + } + if _, ok := resMap["usdc"]; ok { + result = append(result, resMap["usdc"]) + } + if _, ok := resMap["xrp"]; ok { + result = append(result, resMap["xrp"]) + } + if _, ok := resMap["ada"]; ok { + result = append(result, resMap["ada"]) + } + if _, ok := resMap["doge"]; ok { + result = append(result, resMap["doge"]) + } + if _, ok := resMap["sol"]; ok { + result = append(result, resMap["sol"]) + } + if _, ok := resMap["trx"]; ok { + result = append(result, resMap["trx"]) + } + if _, ok := resMap["ltc"]; ok { + result = append(result, resMap["ltc"]) + } + if _, ok := resMap["dot"]; ok { + result = append(result, resMap["dot"]) + } + if _, ok := resMap["matic"]; ok { + result = append(result, resMap["matic"]) + } + if _, ok := resMap["bch"]; ok { + result = append(result, resMap["bch"]) + } + if _, ok := resMap["eos"]; ok { + result = append(result, resMap["eos"]) + } + if _, ok := resMap["ton"]; ok { + result = append(result, resMap["ton"]) + } + if _, ok := resMap["avax"]; ok { + result = append(result, resMap["avax"]) + } + if _, ok := resMap["shib"]; ok { + result = append(result, resMap["shib"]) + } + if _, ok := resMap["invu"]; ok { + result = append(result, resMap["invu"]) + } + if _, ok := resMap["osel"]; ok { + result = append(result, resMap["osel"]) + } + if _, ok := resMap["fmd"]; ok { + result = append(result, resMap["fmd"]) + } + if _, ok := resMap["dten"]; ok { + result = append(result, resMap["dten"]) + } + if _, ok := resMap["xnsl"]; ok { + result = append(result, resMap["xnsl"]) + } + if _, ok := resMap["kools"]; ok { + result = append(result, resMap["kools"]) + } + md.Data = result + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + default: + websocketservice.ContractCache.Range( + func(key, value any) bool { + for _, vue := range symbolList { + if vue == key.(string) { + resMap[vue] = value + } + } + return true + }) + if _, ok := resMap["BTC-USDT"]; ok { + result = append(result, resMap["BTC-USDT"]) + } + if _, ok := resMap["ETH-USDT"]; ok { + result = append(result, resMap["ETH-USDT"]) + } + if _, ok := resMap["BCH-USDT"]; ok { + result = append(result, resMap["BCH-USDT"]) + } + if _, ok := resMap["XRP-USDT"]; ok { + result = append(result, resMap["XRP-USDT"]) + } + if _, ok := resMap["EOS-USDT"]; ok { + result = append(result, resMap["EOS-USDT"]) + } + if _, ok := resMap["LTC-USDT"]; ok { + result = append(result, resMap["LTC-USDT"]) + } + if _, ok := resMap["TRX-USDT"]; ok { + result = append(result, resMap["TRX-USDT"]) + } + if _, ok := resMap["ETC-USDT"]; ok { + result = append(result, resMap["ETC-USDT"]) + } + if _, ok := resMap["LINK-USDT"]; ok { + result = append(result, resMap["LINK-USDT"]) + } + if _, ok := resMap["BNB-USDT"]; ok { + result = append(result, resMap["BNB-USDT"]) + } + if _, ok := resMap["ADA-USDT"]; ok { + result = append(result, resMap["ADA-USDT"]) + } + if _, ok := resMap["DOGE-USDT"]; ok { + result = append(result, resMap["DOGE-USDT"]) + } + if _, ok := resMap["SOL-USDT"]; ok { + result = append(result, resMap["SOL-USDT"]) + } + if _, ok := resMap["DOT-USDT"]; ok { + result = append(result, resMap["DOT-USDT"]) + } + if _, ok := resMap["MATIC-USDT"]; ok { + result = append(result, resMap["MATIC-USDT"]) + } + if _, ok := resMap["AVAX-USDT"]; ok { + result = append(result, resMap["AVAX-USDT"]) + } + if _, ok := resMap["SHIB-USDT"]; ok { + result = append(result, resMap["SHIB-USDT"]) + } + if _, ok := resMap["BNBS-USDT"]; ok { + result = append(result, resMap["BNBS-USDT"]) + } + if _, ok := resMap["INVU-USDT"]; ok { + result = append(result, resMap["INVU-USDT"]) + } + if _, ok := resMap["OSEL-USDT"]; ok { + result = append(result, resMap["OSEL-USDT"]) + } + if _, ok := resMap["FMD-USDT"]; ok { + result = append(result, resMap["FMD-USDT"]) + } + if _, ok := resMap["DTEN-USDT"]; ok { + result = append(result, resMap["DTEN-USDT"]) + } + if _, ok := resMap["XNSL-USDT"]; ok { + result = append(result, resMap["XNSL-USDT"]) + } + if _, ok := resMap["KOOLS-USDT"]; ok { + result = append(result, resMap["KOOLS-USDT"]) + } + md.Data = result + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + } +} + +func IntroList(c *gin.Context) { + symbol := strings.ToUpper(internal.ReplaceStr(c.Query("symbol"))) // 交易对 + if symbol == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = Intro[symbol] + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} diff --git a/pkg/processor/intro.go b/pkg/processor/intro.go new file mode 100644 index 0000000..c9c3cd0 --- /dev/null +++ b/pkg/processor/intro.go @@ -0,0 +1,194 @@ +package processor + +type IntroInfo struct { + Pubdate string `json:"pubdate"` + TotalIssuance string `json:"total_issuance"` + TotalCirculation string `json:"total_circulation"` + CrowdfundingPrice string `json:"crowdfunding_price"` + WhitePaper string `json:"white_paper"` + OfficialWebsite string `json:"official_website"` + BlockChainExplorer string `json:"block_chain_explorer"` +} + +var Intro = map[string]IntroInfo{ + "BTC": IntroInfo{ + Pubdate: "2009/1/3", + TotalIssuance: "21 million", + TotalCirculation: "18.70 million coins", + CrowdfundingPrice: "-", + WhitePaper: "https://bitcoin.org/bitcoin.pdf", + OfficialWebsite: "https://bitcoin.org/", + BlockChainExplorer: "https://www.blockchain.com/explorer", + }, + "ETH": IntroInfo{ + Pubdate: "2015/7/30", + TotalIssuance: "Unlimited", + TotalCirculation: "11.70 million coins", + CrowdfundingPrice: "-", + WhitePaper: "https://ethereum.org/whitepaper/", + OfficialWebsite: "https://ethereum.org/", + BlockChainExplorer: "https://etherscan.io/", + }, + "USDT": IntroInfo{ + Pubdate: "2014/11", + TotalIssuance: "No fixed limit", + TotalCirculation: "65.5 billion coins", + CrowdfundingPrice: "-", + WhitePaper: "https://tether.to/en/whitepaper", + OfficialWebsite: "https://tether.to/", + BlockChainExplorer: "https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7", + }, + "BNB": IntroInfo{ + Pubdate: "2017/7/14", + TotalIssuance: "200 million tokens", + TotalCirculation: "168 million coins", + CrowdfundingPrice: "-", + WhitePaper: "https://www.binance.com/resources/ico/Binance_WhitePaper_en.pdf", + OfficialWebsite: "https://www.binance.com/", + BlockChainExplorer: "https://bscscan.com/token/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + }, + "USDC": IntroInfo{ + Pubdate: "2018/9/26", + TotalIssuance: "No fixed limit", + TotalCirculation: "33.2 billion coins", + CrowdfundingPrice: "-", + WhitePaper: "https://www.centre.io/pdfs/centre-whitepaper.pdf", + OfficialWebsite: "https://www.circle.com/en/usdc", + BlockChainExplorer: "https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + }, + "XRP": IntroInfo{ + Pubdate: "2012", + TotalIssuance: "100 billion tokens", + TotalCirculation: "46.8 billion tokens", + CrowdfundingPrice: "-", + WhitePaper: "https://ripple.com/files/ripple_consensus_whitepaper.pdf", + OfficialWebsite: "https://ripple.com/currency/", + BlockChainExplorer: "https://bithomp.com/explorer/", + }, + "ADA": IntroInfo{ + Pubdate: "2017/9/29", + TotalIssuance: "45 billion tokens", + TotalCirculation: "31.9 billion tokens", + CrowdfundingPrice: "-", + WhitePaper: "https://www.cardanohub.org/zh/academic-papers-3", + OfficialWebsite: "https://www.cardano.org/", + BlockChainExplorer: "https://cardanoexplorer.com/", + }, + "DOGE": IntroInfo{ + Pubdate: "2013/12/6", + TotalIssuance: "Unlimited", + TotalCirculation: "Approximately 130 billion tokens.", + CrowdfundingPrice: "-", + WhitePaper: "-", + OfficialWebsite: "https://dogecoin.com/", + BlockChainExplorer: "https://blockchair.com/dogecoin", + }, + "SOL": IntroInfo{ + Pubdate: "2020/3/23", + TotalIssuance: "5 billion tokens.", + TotalCirculation: "Approximately 10.1 million tokens.", + CrowdfundingPrice: "-", + WhitePaper: "https://solana.com/solana-whitepaper.pdf", + OfficialWebsite: "https://solana.com/", + BlockChainExplorer: "https://solscan.io/", + }, + "TRX": IntroInfo{ + Pubdate: "2017/9/26", + TotalIssuance: "1 trillion tokens.", + TotalCirculation: "Approximately 716 billion tokens", + CrowdfundingPrice: "-", + WhitePaper: "https://tron.network/static/doc/white_paper_v_2_0.pdf", + OfficialWebsite: "https://tron.network/", + BlockChainExplorer: "https://tronscan.org/", + }, + "LTC": IntroInfo{ + Pubdate: "2011/10/13", + TotalIssuance: "84 million tokens", + TotalCirculation: "68 million tokens", + CrowdfundingPrice: "-", + WhitePaper: "-", + OfficialWebsite: "https://litecoin.org/", + BlockChainExplorer: "https://blockchair.com/litecoin", + }, + "DOT": IntroInfo{ + Pubdate: "2020/8/21", + TotalIssuance: "Approximately 105 million tokens", + TotalCirculation: "Approximately 102 million tokens", + CrowdfundingPrice: "-", + WhitePaper: "https://polkadot.network/PolkaDotPaper.pdf", + OfficialWebsite: "https://polkadot.network/", + BlockChainExplorer: "https://polkascan.io/", + }, + "MATIC": IntroInfo{ + Pubdate: "2019/4", + TotalIssuance: "10 billion tokens", + TotalCirculation: "Approximately 6.8 billion tokens", + CrowdfundingPrice: "-", + WhitePaper: "https://whitepaper.matic.network/", + OfficialWebsite: "https://polygon.technology/", + BlockChainExplorer: "https://polygonscan.com/", + }, + "BCH": IntroInfo{ + Pubdate: "2017/8/1", + TotalIssuance: "21 million tokens.", + TotalCirculation: "19 million tokens.", + CrowdfundingPrice: "-", + WhitePaper: "https://www.bitcoincash.org/bitcoin.pdf", + OfficialWebsite: "https://www.bitcoincash.org/", + BlockChainExplorer: "https://blockchair.com/bitcoin-cash", + }, + "WBTC": IntroInfo{ + Pubdate: "2019/1/", + TotalIssuance: "180,000 tokens.", + TotalCirculation: "180,000 tokens.", + CrowdfundingPrice: "-", + WhitePaper: "-", + OfficialWebsite: "https://wbtc.network/", + BlockChainExplorer: "https://etherscan.io/token/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + }, + "DAI": IntroInfo{ + Pubdate: "2017/12/17", + TotalIssuance: "No fixed limit", + TotalCirculation: "10.1 billion coins", + CrowdfundingPrice: "-", + WhitePaper: "https://makerdao.com/whitepaper/", + OfficialWebsite: "https://makerdao.com/", + BlockChainExplorer: "https://etherscan.io/token/0x6b175474e89094c44da98b954eedeac495271d0f", + }, + "TON": IntroInfo{ + Pubdate: "-", + TotalIssuance: "500 million", + TotalCirculation: "-", + CrowdfundingPrice: "-", + WhitePaper: "https://ton.org/whitepaper.pdf", + OfficialWebsite: "https://ton.org/", + BlockChainExplorer: "https://www.blockchain.com/explorer", + }, + "AVAX": IntroInfo{ + Pubdate: "2020/9/22", + TotalIssuance: "700 million tokens", + TotalCirculation: "251 million tokens.", + CrowdfundingPrice: "-", + WhitePaper: "https://www.avalabs.org/whitepapers", + OfficialWebsite: "https://www.avax.network/", + BlockChainExplorer: "https://cchain.explorer.avax.network/", + }, + "SHIB": IntroInfo{ + Pubdate: "2020/8/", + TotalIssuance: "10 trillion tokens", + TotalCirculation: "5 trillion", + CrowdfundingPrice: "-", + WhitePaper: "https://github.com/shytoshikusama/shibawoofpaper/raw/main/SHIBAINU_Ecosystem_WOOF_Paper.pdf", + OfficialWebsite: "https://www.shibatoken.com/", + BlockChainExplorer: "https://etherscan.io/token/0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + }, + "BUSD": IntroInfo{ + Pubdate: "2019/9/6", + TotalIssuance: "No fixed limit", + TotalCirculation: "38.3 billion coins", + CrowdfundingPrice: "-", + WhitePaper: "-", + OfficialWebsite: "https://www.binance.com/en/busd", + BlockChainExplorer: "https://bscscan.com/token/0xe9e7cea3dedca5984780bafc599bd69add087d56", + }, +} diff --git a/pkg/processor/msg_api.go b/pkg/processor/msg_api.go new file mode 100644 index 0000000..5b5ba85 --- /dev/null +++ b/pkg/processor/msg_api.go @@ -0,0 +1,258 @@ +package processor + +import ( + "github.com/gin-gonic/gin" + "net/http" + "strconv" + "wss-pool/internal" + "wss-pool/internal/data/mysqlbusiness" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/msg" +) + +/* +MsgSend +1、Users can only submit once within 1 minute +2、The verification code will not expire within five minutes +*/ +func MsgSend(c *gin.Context) { + var tmp model.LoginPost + if err := c.BindJSON(&tmp); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.ParameterError)) + return + } + phoneNumber := tmp.PhoneNumber + if len(phoneNumber) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PhoneError)) + return + } + + msgCode, err := msg.RunSendSms(phoneNumber) + if err != nil { + applogger.Error("msg RunSendSms info err: %v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err.Error(), internal.QueryError)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, msgCode, internal.QuerySuccess)) +} + +// MobileLogin 用户手机号登录 +func MobileLogin(c *gin.Context) { + var tmp model.LoginPost + if err := c.BindJSON(&tmp); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.ParameterError)) + return + } + phoneNumber := tmp.PhoneNumber + password := tmp.Password + + if len(phoneNumber) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PhoneError)) + return + } + if len(password) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PassWordError)) + return + } + + check, err := mysqlbusiness.GetBoUsersByPhoneAndPassWord(phoneNumber, password) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, check)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, check, internal.TokenError)) +} + +// ForgetPassWord 手机验证码修改密码 +func PhoneNumberByPassWord(c *gin.Context) { + var tmp model.LoginPost + if err := c.BindJSON(&tmp); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.ParameterError)) + return + } + check := tmp.Check + phoneNumber := tmp.PhoneNumber + //code := tmp.Code + password := tmp.Password + + if len(check) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.ParameterError)) + return + } + if len(phoneNumber) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PhoneError)) + return + } + if len(password) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PassWordError)) + return + } + + //if len(code) <= 0 { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + //codeData, err := redis.Get_Cache_Data(phoneNumber) + //if err != nil { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + //if codeData != code { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + + dataMsg, err := mysqlbusiness.UpdateBoUsersById(phoneNumber, password) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, dataMsg)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, internal.ResultStr, dataMsg)) +} + +// Registration 手机号注册|验证码|密码|邀请码 +func Registration(c *gin.Context) { + var tmp model.LoginPost + if err := c.BindJSON(&tmp); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.ParameterError)) + return + } + + phoneNumber := tmp.PhoneNumber + //code := tmp.Code + password := tmp.Password + invitationCode := tmp.InvitationCode + + if len(phoneNumber) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PhoneError)) + return + } + if len(password) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PassWordError)) + return + } + //if len(code) <= 0 { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + + //codeData, err := redis.Get_Cache_Data(phoneNumber) + //if err != nil { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr,internal.CodeError)) + // return + //} + + //if codeData != code { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + + // Check if the user exists + user, err := mysqlbusiness.GetBoUsersByPhoneNumber(phoneNumber) + if err != nil || len(user) > 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, user)) + return + } + + dataMsg, err := mysqlbusiness.SaveBoUsers(phoneNumber, password, invitationCode) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, dataMsg)) + return + } + + //TODO: 注册创建用户账号、创建数字币账号、写入设备、注册写用户信息缓存、写入缓存 + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, internal.ResultStr, dataMsg)) +} + +// ForgetPassWore 忘记密码-通过手机重新设置密码 +func ForgetPassWore(c *gin.Context) { + var tmp model.LoginPost + if err := c.BindJSON(&tmp); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.ParameterError)) + return + } + phoneNumber := tmp.PhoneNumber + //code := tmp.Code + password := tmp.Password + + if len(phoneNumber) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PhoneError)) + return + } + if len(password) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PassWordError)) + return + } + //if len(code) <= 0 { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + + //codeData, err := redis.Get_Cache_Data(phoneNumber) + //if err != nil { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, "internal.CodeError)) + // return + //} + + //if codeData != code { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + + dataMsg, err := mysqlbusiness.UpdateBoUsersPassWordByPhoneNumber(phoneNumber, password) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, dataMsg)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, internal.ResultStr, dataMsg)) +} + +// SetPhoneNumber 设置手机号 +func SetPhoneNumber(c *gin.Context) { + var tmp model.LoginPost + if err := c.BindJSON(&tmp); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.ParameterError)) + return + } + //code :=tmp.Code + phoneNumber := tmp.PhoneNumber + id, err := strconv.Atoi(tmp.Id) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.ParameterError)) + return + } + if len(phoneNumber) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.PhoneError)) + return + } + if id <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.UserIdError)) + return + } + //if len(code) <= 0 { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + //codeData, err := redis.Get_Cache_Data(phoneNumber) + //if err != nil { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + //if codeData != code { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, internal.CodeError)) + // return + //} + + dateMsg, err := mysqlbusiness.UpdateBoUsersPhoneNumberById(phoneNumber, int64(id)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, internal.ResultStr, dateMsg)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, internal.ResultStr, dateMsg)) +} diff --git a/pkg/processor/option.go b/pkg/processor/option.go new file mode 100644 index 0000000..0f1a70c --- /dev/null +++ b/pkg/processor/option.go @@ -0,0 +1,350 @@ +package processor + +import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "net/http" + "strings" + "wss-pool/cmd/common" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +func OptionInfoAdd(c *gin.Context) { + param := make([]model.OptionInfoParam, 0) + err := c.BindJSON(¶m) + total := len(param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if total <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param[0].Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param[0].Country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } else if param[0].CloseDate != common.TimeToNows().Format("2006-01-02") { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "option date not today ", internal.QueryError)) + return + } + country := common.CapitalizeFirstLetter(param[0].Country) + if country == "Us" { + country = "US" + } + var dataList []mongo.WriteModel + applogger.Debug("stock info add param total: %d", total) + for _, v := range param { + red.HsetMap(business.StockClosingPrice[fmt.Sprintf("Option%sNew", country)], v.Stock, v.FutPrice) + //爬虫抓取列表数据 延迟较大 统一用info 数据 + business.StockPyWsOptionList(model.OptionPolygon{ + Stock: v.Stock, + Percent: v.Percent, + FutPrice: v.FutPrice, + Country: country, + }, fmt.Sprintf("%s%s%s", common.StockOption, country, common.StockOptionList)) + filter := bson.M{"code": bson.M{"$eq": v.Stock}} + update := bson.D{{"$set", bson.D{ + {"code", v.Stock}, + {"fut_price", v.FutPrice}, + {"percent", v.Percent}, + {"atmiv", v.ATMIV}, + {"ivp", v.IVP}, + {"option_date", v.CloseDate}, + {"country", v.Country}, + }}} + code := fmt.Sprintf("%s:%s", business.StockClosingPrice[fmt.Sprintf("Option%sPrice", country)], v.Stock) + strike := make(map[string]string) + for key, val := range v.Results { + //标的名+到期时间+行权价+类型 + for key2, val2 := range val { + title := fmt.Sprintf("%s%s%s", v.Stock, strings.Replace(key, "-", "", -1), val2.Strike) + val[key2].Calls.Name = title + "CE" + business.OptionResPrice(model.StrikePrice{ + Ask: val2.Calls.OfferOff, + Bid: val2.Calls.BidOff, + Price: val2.Calls.LTPOff, + Code: v.Stock, + }, val[key2].Calls.Name, code, v.IsClose, v.CloseDate, key) + val[key2].Puts.Name = title + "PE" + business.OptionResPrice(model.StrikePrice{ + Ask: val2.Puts.OfferOff, + Bid: val2.Puts.BidOff, + Price: val2.Puts.LTPOff, + Code: v.Stock, + }, val[key2].Puts.Name, code, v.IsClose, v.CloseDate, key) + if val2.IsDefault { + strike[key] = val2.Strike + // break + } + } + val = common.QuickSort(val) + //fmt.Printf("%v",val) + //分开写入mogodb + filterExpiry := bson.M{"code": v.Stock, "expiry": key} + info, _ := json.Marshal(val) + expiryDate, _ := common.TimeStrToTimestamp(fmt.Sprintf("%s 23:59:59", key)) + updateExpiry := bson.D{{"$set", bson.D{ + {"code", v.Stock}, + {"expiry", key}, + {"expiry_date", expiryDate}, + {"info", string(info)}, + }}} + if err := data.MgoUpdateOneTrue(data.GetOptionExpiryTableName(country), filterExpiry, updateExpiry); err != nil { + applogger.Error("%s MgoInsertMany err:%v", v.Stock, err) + } + } + business.StockPyWsOptionInfo(v, fmt.Sprintf("%s%s%s", common.StockOption, country, common.StockOptionInfo)) + business.StockPyWsOptionInfoExchange(model.OptionInfoExchange{ + Country: v.Country, + Stock: v.Stock, + FutPrice: v.FutPrice, + Percent: v.Percent, + Strike: strike, + }, fmt.Sprintf("%s%s%s.%s", common.StockOption, country, common.StockOptionInfo, "Exchange")) + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.GetOptionTableName(country), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + } + + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func OptionListAdd(c *gin.Context) { + param := model.OptionIndexList{} + err := c.BindJSON(¶m) + fmt.Println(param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Results) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Results[0].Stock == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "ticker error", internal.QueryError)) + return + } else if param.Results[0].Country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "locale error", internal.QueryError)) + return + } + locale := common.CapitalizeFirstLetter(param.Results[0].Country) + closeDate := common.TimeToNows().Format("2006-01-02") + if locale == "Us" { + locale = "US" + } + var dataList []mongo.WriteModel + for _, value := range param.Results { + if closeDate != strings.TrimSpace(value.CloseDate) { + continue + } + filter := bson.D{{"Code", bson.M{ + "$eq": value.Stock, + }}, {"Country", bson.M{ + "$eq": locale, + }}} + update := bson.D{{"$set", bson.D{ + {"Code", value.Stock}, + {"Country", locale}, + {"FutPrice", value.FutPrice}, + {"Percent", value.Percent}, + {"IVChg", value.IVChg}, + {"IVP", value.IVP}, + {"Exchange", value.PrimaryExchange}, + {"DateTime", closeDate}, + {"ATMIV", value.ATMIV}}}} + if value.IsClose { + red.HsetMap(business.StockClosingPrice[fmt.Sprintf("Option%sNew", locale)], value.Stock, "0") + red.HsetMap(business.StockClosingPrice[locale], value.Stock, value.FutPrice) + list, _ := data.MgoFind(data.OptionList, bson.M{"Country": locale, "Code": value.Stock, "CloseDate": bson.M{"$ne": closeDate}}) + lists := list.([]primitive.M) + if len(lists) > 0 { + beforeClose, ok := lists[0]["YesterdayClose"].(string) + if ok { + red.HsetMap(business.StockClosingPrice[fmt.Sprintf("Option%sBeforeClose", locale)], value.Stock, beforeClose) + } + update = bson.D{{"$set", bson.D{ + {"Code", value.Stock}, + {"Country", locale}, + {"FutPrice", value.FutPrice}, + {"Percent", value.Percent}, + {"IVChg", value.IVChg}, + {"IVP", value.IVP}, + {"Exchange", value.PrimaryExchange}, + {"DateTime", closeDate}, + {"YesterdayClose", value.FutPrice}, //上一次 闭盘价 + {"BeforeClose", beforeClose}, //前一天 + {"CloseDate", closeDate}, + {"ATMIV", value.ATMIV}}}} + } else { + update = bson.D{{"$set", bson.D{ + {"Code", value.Stock}, + {"Country", locale}, + {"FutPrice", value.FutPrice}, + {"Percent", value.Percent}, + {"IVChg", value.IVChg}, + {"IVP", value.IVP}, + {"Exchange", value.PrimaryExchange}, + {"DateTime", closeDate}, + {"YesterdayClose", value.FutPrice}, //上一次 闭盘价 + {"CloseDate", closeDate}, + {"ATMIV", value.ATMIV}}}} + } + } + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + //标记 批量少于 3 不做修改 + if len(dataList) >= 3 { + data.MgoUpdateMany(data.OptionList, bson.D{{"Country", bson.M{ + "$eq": locale, + }}, {"DateTime", bson.M{ + "$ne": "0", + }}}, bson.D{{"$set", bson.D{ + {"DateTime", "0"}}}}) + } + if len(dataList) > 0 { + data.MgoBulkWrite(data.OptionList, dataList) + if err := data.MgoBulkWrite(data.OptionList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + } else { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "error operation repetition", internal.QueryError)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func ExchangeOptionList(c *gin.Context) { + country := internal.ReplaceStr(c.Query("country")) + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) + sort := internal.IntegerInit(internal.ReplaceStr(c.Query("sort"))) + search := internal.ReplaceStr(c.Query("search")) + code := internal.ReplaceStr(c.Query("code")) + if pageSize <= 0 || pageNumber <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "分页参数不能为零", internal.QueryError)) + return + } + var condition = country + var filter bson.M + if len(search) > 0 { + if len(condition) > 0 { + filter = bson.M{"Country": condition, "DateTime": bson.M{"$ne": "0"}, "$or": []bson.M{{"Code": bson.M{"$regex": search}}}} + } else { + filter = bson.M{"$or": []bson.M{{"Code": bson.M{"$regex": search}}}, "DateTime": bson.M{"$ne": "0"}} + } + } else { + filter = bson.M{"Country": condition, "DateTime": bson.M{"$ne": "0"}} + } + + strs := strings.Split(code, "-") + if len(code) > 0 { + filter = bson.M{"Country": condition, "DateTime": bson.M{"$ne": "0"}, "Code": bson.M{"$in": strs}} + } + //fmt.Println(filter) + total, err := data.MgoFindTotal(data.OptionList, filter) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("查询数据总数: %v", total) + pageData := make([]model.OptionPolygon, 0) + if sort == 0 { + sort = -1 + } + data.MgoPagingFindStructList(data.OptionList, filter, int64(pageSize), int64(pageNumber), "Percent", sort, &pageData) + var md stock.MgoPageSize + md.PageSize = pageSize + md.PageNumber = pageNumber + md.Total = total + if len(pageData) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + data := make([]model.OptionPolygon, 0) + for k, v := range pageData { + if ok, rate := common.IsExistOption(v.Country, v.Stock); ok { + pageData[k].Rate = rate + data = append(data, pageData[k]) + } + } + md.Data = data + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func OptionPHPList(c *gin.Context) { + country := internal.ReplaceStr(c.Query("country")) + var condition = country + filter := bson.M{"Country": condition, "DateTime": bson.M{"$ne": "0"}} + result, _ := data.MgoFind(data.OptionList, filter) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, result, internal.QuerySuccess)) +} + +func OptionInfo(c *gin.Context) { + code := internal.ReplaceStr(c.Query("option")) + country := internal.ReplaceStr(c.Query("country")) + if code == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "option error", internal.QueryError)) + return + } else if country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } + country = common.CapitalizeFirstLetter(country) + fmt.Println(common.TimeToNow()) + var res = make([]model.OptionInfoMogo, 1) + filter := bson.M{"code": code} + data.MgoFindToStr(data.GetOptionTableName(country), filter, int64(1), &res) + strikeMap := make(map[string][]model.StrikeInfo) + if len(res) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, model.OptionInfoParam{}, internal.QuerySuccess)) + return + } + filter = bson.M{"code": code, "expiry_date": bson.M{ + "$gte": common.TimeToNow(), + }} + info := make([]model.OptionInfoExpiryMogo, 0) + data.MgoFindToStr(data.GetOptionExpiryTableName(country), filter, int64(0), &info) + if len(info) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, model.OptionInfoParam{}, internal.QuerySuccess)) + return + } + for _, v := range info { + item := make([]model.StrikeInfo, 0) + json.Unmarshal([]byte(v.Info), &item) + strikeMap[v.Expiry] = item + } + var pageData = model.OptionInfoParam{ + Country: res[0].Country, + Stock: res[0].Stock, + FutPrice: res[0].FutPrice, + Percent: res[0].Percent, + IVP: res[0].IVP, + ATMIV: res[0].ATMIV, + Results: strikeMap, + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, pageData, internal.QuerySuccess)) +} diff --git a/pkg/processor/share_api.go b/pkg/processor/share_api.go new file mode 100644 index 0000000..b47a33d --- /dev/null +++ b/pkg/processor/share_api.go @@ -0,0 +1,1213 @@ +package processor + +import ( + "encoding/json" + "fmt" + "github.com/360EntSecGroup-Skylar/excelize" + "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "log" + "net/http" + "strconv" + "strings" + "time" + "wss-pool/cmd/common" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/internal/data/business" + "wss-pool/internal/data/mysqlbusiness" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +var topIc = "https://" +var TotalSize int = 10 +var FreeSymbolKey = "USER:MARKET:" +var MarketType = map[int]string{ + 1: "spots", + 2: "contract", + 3: "US", + 4: "Indonesia", + 5: "Malaysia", + 6: "Thailand", + 7: "India", + 9: "Singapore", + 12: "HongKong", + 14: "UK", + 15: "France", + 16: "Germany", + 17: "Brazil", + 18: "Japan", + 19: "Forex", +} + +type StockSymbol struct { + Code string `json:"code"` + Name string `json:"name"` + MarketType int `json:"market_type"` + TradeNumericCode string `json:"trade_numeric_code"` +} + +// ExchangeSymbolList Obtain stock list data +func ExchangeSymbolList(c *gin.Context) { + /* 股票代码查询业务逻辑 + 1、列表查询传入交易所代码 + 2、搜索 + 1、带有交易所代码--模糊查询当前交易所的股票 + 2、不带有交易所代码--模糊查询所有股票 + 3、查询股票列表不排序,搜索排序(通过code排序) + */ + symbol := internal.ReplaceStr(c.Query("symbol")) // 国家(美股,马股) + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) // 每页显示多少条数据 + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) // 第几页 + sort := internal.IntegerInit(internal.ReplaceStr(c.Query("sort"))) // Code排序 + search := internal.ReplaceStr(c.Query("search")) // 搜索数据(模糊 + code := internal.ReplaceStr(c.Query("code")) // 多个代码 + primaryExchange := internal.ReplaceStr(c.Query("primaryExchange")) // 交易所 + if pageSize <= 0 || pageNumber <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "分页参数不能为零", internal.QueryError)) + return + } + var condition = symbol + var filter bson.M + if len(search) > 0 { + //香港股票代码特殊处理 + if symbol == "HongKong" { + numberWithoutLeadingZeros := strings.TrimLeft(search, "0") + if numberWithoutLeadingZeros == "" { + numberWithoutLeadingZeros = "0" + } + search = numberWithoutLeadingZeros + } + if len(primaryExchange) > 0 { + filter = bson.M{"Country": condition, "Exchange": primaryExchange, "$or": []bson.M{{"Code": bson.M{"$regex": search}}, {"Name": bson.M{"$regex": search}}, {"NumericCode": bson.M{"$regex": search}}}, "YesterdayClose": bson.M{"$ne": ""}} + } else { + filter = bson.M{"Country": condition, "$or": []bson.M{{"Code": bson.M{"$regex": search}}, {"Name": bson.M{"$regex": search}}, {"NumericCode": bson.M{"$regex": search}}}, "YesterdayClose": bson.M{"$ne": ""}, "Exchange": bson.M{"$exists": true}} + } + } else { + if len(primaryExchange) > 0 { + filter = bson.M{"Country": condition, "Exchange": primaryExchange, "YesterdayClose": bson.M{"$ne": ""}} + } else { + filter = bson.M{"Country": condition, "YesterdayClose": bson.M{"$ne": ""}, "Exchange": bson.M{"$exists": true}} + } + } + codeSearch := "Code" + if symbol == "Malaysia" { + codeSearch = "NumericCode" + } + str_s := strings.Split(code, "-") + if len(code) > 0 { + if len(primaryExchange) > 0 { + filter = bson.M{"Country": condition, "Exchange": primaryExchange, "YesterdayClose": bson.M{"$ne": ""}, codeSearch: bson.M{"$in": str_s}} + } else { + filter = bson.M{"Country": condition, "YesterdayClose": bson.M{"$ne": ""}, codeSearch: bson.M{"$in": str_s}, "Exchange": bson.M{"$exists": true}} + } + } + total, err := data.MgoFindTotal(data.StockList, filter) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + //applogger.Debug("查询数据表: %v", data.StockList) + //applogger.Debug("查询数据总数: %v", total) + pageData := make([]stock.StockPolygon, 0) + if sort == 0 { + sort = -1 + } + sortField := "Vol" + if symbol == "US" { + sortField = "DP" + } + //applogger.Debug("查询条件: %v", filter) + var md stock.MgoPageSize + md.PageSize = pageSize + md.PageNumber = pageNumber + md.Total = total + if err = data.MgoPagingFindStruct(data.StockList, filter, int64(pageSize), int64(pageNumber), sortField, sort, &pageData); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + } + //applogger.Debug("查询数据: %", len(pageData)) + if len(pageData) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + var dataStockPolygon = make([]stock.StockPolygon, 0) + for k, v := range pageData { + key := business.StockClosingPrice[fmt.Sprintf("%sNew", v.Locale)] + pageData[k].ClosePrice, err = red.Hget(key, v.Code) + if err != nil { + continue + } + if common.IsExistStock(v.Locale, v.Code) { + dataStockPolygon = append(dataStockPolygon, pageData[k]) + } + } + md.Data = dataStockPolygon + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +// ExchangeFreeSymbolList exchange free symbol list +func ExchangeFreeSymbolList(c *gin.Context) { + id := internal.ReplaceStr(c.Query("id")) // 用户ID + market_type := internal.IntegerInit(internal.ReplaceStr(c.Query("market_type"))) // 市场 + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) // 每页显示多少条数据 + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) // 第几页 + if pageSize <= 0 || pageNumber <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "分页参数不能为零", internal.QueryError)) + return + } + if len(id) <= 0 || market_type <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "参数不能为空", internal.QueryError)) + return + } + var md stock.MgoPageSize + var dataStockPolygon = make([]stock.StockPolygon, 0) + md.PageSize = pageSize + md.PageNumber = pageNumber + + userIdKey := fmt.Sprintf("%v%v", FreeSymbolKey, id) + result, err := red.Get_Cache_Byte(userIdKey) + if err != nil { + md.Data = dataStockPolygon + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + var freeList []StockSymbol + if err = json.Unmarshal(result, &freeList); err != nil { + md.Data = dataStockPolygon + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + // 组合需要查询自选缓存股票code + var symbolList []string + var condition = MarketType[market_type] + for _, value := range freeList { + if market_type == value.MarketType { + symbolList = append(symbolList, value.Code) + } + } + //applogger.Debug("查询自选股票列表: %v", symbolList) + codeSearch := "Code" + if condition == "Malaysia" { + codeSearch = "NumericCode" + } + filter := bson.M{"Country": condition, "YesterdayClose": bson.M{"$ne": ""}, codeSearch: bson.M{"$in": symbolList}, "Exchange": bson.M{"$exists": true}} + total, err := data.MgoFindTotal(data.StockList, filter) + if err != nil { + md.Total = int64(len(symbolList)) + md.Data = dataStockPolygon + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QueryError)) + return + } + //applogger.Debug("查询数据表: %v", data.StockList) + //applogger.Debug("查询数据总数: %v", total) + //applogger.Debug("查询条件: %v", filter) + md.Total = total + var pageData = make([]stock.StockPolygon, 0) + sortField := "Vol" + if condition == "US" { + sortField = "DP" + } + if err = data.MgoPagingFindStruct(data.StockList, filter, int64(pageSize), int64(pageNumber), sortField, -1, &pageData); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + if len(pageData) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + for k, v := range pageData { + key := business.StockClosingPrice[fmt.Sprintf("%sNew", v.Locale)] + pageData[k].ClosePrice, err = red.Hget(key, v.Code) + if err != nil { + continue + } + if common.IsExistStock(v.Locale, v.Code) { + dataStockPolygon = append(dataStockPolygon, pageData[k]) + } + } + md.Data = dataStockPolygon + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func UpdateKeepDecimal(c *gin.Context) { + param := model.Data{} + err := c.BindJSON(¶m) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if param.StockCode == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "stock_code null", internal.QueryError)) + return + } else if param.KeepDecimal == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "keep_decimal error", internal.QueryError)) + return + } else if param.Currency == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "currency error", internal.QueryError)) + return + } + filterS := bson.D{{"Code", bson.M{ + "$eq": param.StockCode, + }}, {"Currency", bson.M{ + "$eq": param.Currency, + }}} + updateData := bson.M{ + "$set": bson.M{ + "Code": param.StockCode, + "KeepDecimal": param.KeepDecimal, + }} + if err := data.MgoUpdateOne(data.StockList, filterS, updateData); err != nil { + applogger.Error("MgoBulkWrite update err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err.Error(), internal.QueryError)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "ok", internal.QuerySuccess)) +} + +func SymbolToExcel(c *gin.Context) { + country := internal.ReplaceStr(c.Query("country")) + filter := bson.M{} + if country != "" { + filter = bson.M{"Country": country} + } else { + country = "total" + } + res := make([]stock.StockPolygon, 0) + data.MgoFindRes(data.StockList, filter, &res) + // 创建Excel文件 + file := excelize.NewFile() + sheetName := "Sheet1" + // 写入表头 + file.SetCellValue(sheetName, "A1", "Code") + file.SetCellValue(sheetName, "B1", "Name") + file.SetCellValue(sheetName, "C1", "Country") + file.SetCellValue(sheetName, "D1", "PrimaryExchange") + file.SetCellValue(sheetName, "E1", "Symbol") + file.SetCellValue(sheetName, "F1", "NumericCode") + file.SetCellValue(sheetName, "G1", "Intro") + file.SetCellValue(sheetName, "H1", "JapanIntro") + file.SetCellValue(sheetName, "I1", "JapanName") + // 写入数据 + row := 2 // 从第二行开始写入数据 + for _, val := range res { + if val.PrimaryExchange == "" { + applogger.Error(val.Locale, val.Code, " not Exchange") + continue + } + file.SetCellValue(sheetName, "A"+strconv.Itoa(row), val.Code) + file.SetCellValue(sheetName, "B"+strconv.Itoa(row), val.Name) + file.SetCellValue(sheetName, "C"+strconv.Itoa(row), val.Locale) + file.SetCellValue(sheetName, "D"+strconv.Itoa(row), val.PrimaryExchange) + file.SetCellValue(sheetName, "E"+strconv.Itoa(row), val.Symbol) + file.SetCellValue(sheetName, "F"+strconv.Itoa(row), val.NumericCode) + file.SetCellValue(sheetName, "G"+strconv.Itoa(row), val.Intro) + file.SetCellValue(sheetName, "H"+strconv.Itoa(row), val.JapanIntro) + file.SetCellValue(sheetName, "I"+strconv.Itoa(row), val.JapanName) + row++ + } + // 保存文件 + err := file.SaveAs(fmt.Sprintf("/home/ubuntu/wss-server/%s.xlsx", country)) + if err != nil { + log.Fatal(err) + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", internal.QuerySuccess)) +} + +func ExcelToSymbolByJapanJson(c *gin.Context) { + data.Mgo_init(config.Config.Mongodb) + f, err := excelize.OpenFile("/home/ubuntu/wss-server/Japan.xlsx") + if err != nil { + applogger.Error("ExcelToSymbol err:%v", err) + return + } + var dataList []mongo.WriteModel + // 获取 Sheet1 上所有单元格 + rows := f.GetRows("Sheet1") + for k, row := range rows { + if k == 0 { + continue + } + applogger.Debug("Code:%v", row[0]) + applogger.Debug("Name:%v", row[1]) + applogger.Debug("Country:%v", row[2]) + applogger.Debug("PrimaryExchange:%v", row[3]) + applogger.Debug("Symbol:%v", row[4]) + applogger.Debug("NumericCode:%v", row[5]) + applogger.Debug("Intro:%v", row[6]) + applogger.Debug("JapanIntro:%v", row[7]) + applogger.Debug("JapanName:%v", row[8]) + + filter := bson.D{{"Code", bson.M{ + "$eq": row[0], + }}, {"Country", bson.M{ + "$eq": row[2], + }}} + + update := bson.D{{"$set", bson.D{ + {"Code", row[0]}, + {"Name", row[1]}, + {"Country", row[2]}, + {"PrimaryExchange", row[3]}, + {"Symbol", row[4]}, + {"NumericCode", row[5]}, + {"Intro", row[6]}, + {"JapanIntro", row[7]}, + {"JapanName", row[8]}, + }}} + + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err = data.MgoBulkWrite(data.StockList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} +func ExcelToSymbolByJapan(c *gin.Context) { + data.Mgo_init(config.Config.Mongodb) + f, err := excelize.OpenFile("/home/ubuntu/wss-server/Japan_edited.xlsx") + if err != nil { + applogger.Error("ExcelToSymbol err:%v", err) + return + } + var dataList []mongo.WriteModel + // 获取 Sheet1 上所有单元格 + rows := f.GetRows("Sheet1") + for k, row := range rows { + if k == 0 { + continue + } + applogger.Debug("Name:%v", row[0]) + applogger.Debug("Symbol:%v", row[1]) + applogger.Debug("JapanName:%v", row[2]) + + filter := bson.D{{"Symbol", bson.M{"$eq": row[1]}}} + update := bson.D{{"$set", bson.D{{"JapanName", row[2]}}}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err = data.MgoBulkWrite(data.StockList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} +func ExcelToForexCode(c *gin.Context) { + data.Mgo_init(config.Config.Mongodb) + f, err := excelize.OpenFile("/home/ubuntu/wss-server/forex_code.xlsx") + if err != nil { + applogger.Error("ExcelToSymbol err:%v", err) + return + } + var dataList []mongo.WriteModel + applogger.Debug("这里执行了。。。。。") + // 获取 Sheet1 上所有单元格 + rows := f.GetRows("Sheet1") + for k, row := range rows { + if k == 0 { + continue + } + applogger.Debug("code:%v", row[0]) + applogger.Debug("name:%v", row[1]) + applogger.Debug("category:%v", row[2]) + applogger.Debug("symbol:%v", row[3]) + + filter := bson.D{{"code", bson.M{"$eq": row[0]}}} + update := bson.D{{"$set", bson.D{ + {"code", row[0]}, + {"name", row[1]}, + {"category", row[2]}, + {"symbol", row[3]}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + applogger.Debug("这里执行了。。。。。111111") + if len(dataList) > 0 { + if err = data.MgoBulkWrite(data.ForexListBak, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func TickerToExcel(c *gin.Context) { + filter := bson.M{} + res := make([]stock.ForexData, 0) + data.MgoFindRes(data.StockList, filter, &res) + // 创建Excel文件 + file := excelize.NewFile() + sheetName := "Sheet1" + // 写入表头 + file.SetCellValue(sheetName, "A1", "code") + file.SetCellValue(sheetName, "B1", "Name") + file.SetCellValue(sheetName, "C1", "Country") + file.SetCellValue(sheetName, "D1", "PrimaryExchange") + file.SetCellValue(sheetName, "E1", "Symbol") + file.SetCellValue(sheetName, "F1", "NumericCode") + // 写入数据 + row := 2 // 从第二行开始写入数据 + for _, val := range res { + file.SetCellValue(sheetName, "A"+strconv.Itoa(row), val.Ticker) + file.SetCellValue(sheetName, "B"+strconv.Itoa(row), "") + file.SetCellValue(sheetName, "C"+strconv.Itoa(row), "") + file.SetCellValue(sheetName, "D"+strconv.Itoa(row), "") + file.SetCellValue(sheetName, "E"+strconv.Itoa(row), "") + file.SetCellValue(sheetName, "F"+strconv.Itoa(row), "") + row++ + } + // 保存文件 + //err := file.SaveAs(fmt.Sprintf("/home/ubuntu/wss-server/%s.xlsx", "forex")) + err := file.SaveAs(fmt.Sprintf("./cmd/%s.xlsx", "forex")) + if err != nil { + log.Fatal(err) + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", internal.QuerySuccess)) +} + +func OptionToExcel(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("option")) + filter := bson.M{"Country": symbol} + res := make([]model.OptionPolygon, 0) + data.MgoFindRes(data.OptionList, filter, &res) + // 创建Excel文件 + file := excelize.NewFile() + sheetName := "Sheet1" + // 写入表头 + file.SetCellValue(sheetName, "A1", "Code") + file.SetCellValue(sheetName, "B1", "Name") + file.SetCellValue(sheetName, "C1", "Country") + file.SetCellValue(sheetName, "D1", "Tape") + // 写入数据 + row := 2 // 从第二行开始写入数据 + for _, val := range res { + file.SetCellValue(sheetName, "A"+strconv.Itoa(row), val.Stock) + file.SetCellValue(sheetName, "B"+strconv.Itoa(row), val.Stock) + file.SetCellValue(sheetName, "C"+strconv.Itoa(row), val.Country) + file.SetCellValue(sheetName, "D"+strconv.Itoa(row), val.PrimaryExchange) + row++ + } + // 保存文件 + err := file.SaveAs(fmt.Sprintf("%s.xlsx", symbol)) + if err != nil { + log.Fatal(err) + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", internal.QuerySuccess)) +} + +// IntraDisCal IntraDisCal data +func IntraDisCal(c *gin.Context) { + symbol := strings.ToUpper(internal.ReplaceStr(c.Query("symbol"))) // 股票代码 + intMin := internal.IntegerInit(internal.ReplaceStr(c.Query("interval"))) // 查询间隔 + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) // 开始时间 + applogger.Debug("Incoming parameters:%v", symbol, intMin) + + if from <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusUnauthorized, "parameter error", internal.QueryError)) + return + } + + //filter := bson.M{"Code": symbol, "YesterdayClose": bson.M{"$ne": ""}, "BeforeClose": bson.M{"$ne": ""}} + intTime := intMin * 60 * 1000 + match := bson.D{ + {"$match", bson.D{ + {"s", symbol}, + {"t", bson.D{{"$gte", from}}}}, + }} + group := bson.D{{ + "$group", bson.D{ + {"_id", bson.D{{"$subtract", bson.A{"$t", bson.D{{"$mod", bson.A{"$t", intTime}}}}}}}, + {"fisrtTime", bson.D{{"$first", "$t"}}}, + {"lastTime", bson.D{{"$last", "$t"}}}, + {"datetime", bson.D{{"$first", bson.D{{"$dateToString", + bson.D{{"format", "%Y-%m-%d %H:%M:%S"}, + {"date", bson.D{{"$add", bson.A{time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), "$t", 28800000}}}}}}}}}}, + {"timestamp", bson.D{{"$first", "$t"}}}, + {"open", bson.D{{"$max", bson.D{{"$toDouble", "$p"}}}}}, + {"high", bson.D{{"$max", bson.D{{"$toDouble", "$h"}}}}}, + {"low", bson.D{{"$min", bson.D{{"$toDouble", "$l"}}}}}, + {"close", bson.D{{"$min", bson.D{{"$toDouble", "$cl"}}}}}, + {"volume", bson.D{{"$sum", "$v"}}}, + }}} + sort := bson.D{{"$sort", bson.D{{"timestamp", 1}}}} + + operations := mongo.Pipeline{match, group, sort} + applogger.Debug("mongodb filter info: %v", operations) + + mapList, err := data.MgoAggregate(data.StockUs, operations) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusUnauthorized, "MgoAggregate err", internal.QueryError)) + return + } + applogger.Debug("data info: %v", mapList) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, mapList, internal.QuerySuccess)) +} + +// FindShareBySymbol 自选列表查询服务 +func FindShareBySymbol(c *gin.Context) { + auth := internal.ReplaceStr(c.Query("auth")) + systemBoursesId := internal.IntegerInit(internal.ReplaceStr(c.Query("systemBoursesId"))) + bourseType := internal.IntegerInit(internal.ReplaceStr(c.Query("bourseType"))) + + if len(auth) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusUnauthorized, internal.QueryToken, internal.QueryError)) + return + } + check, userId, err := mysqlbusiness.GetBoUsers(auth) + if err != nil { + applogger.Error("GetBoUsers err: %v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusUnauthorized, internal.QueryToken, internal.QueryError)) + return + } + + if !check { + c.JSON(http.StatusOK, internal.GinResult(http.StatusUnauthorized, internal.QueryToken, internal.QueryError)) + return + } + + usList, err := mysqlbusiness.GetBoUserOptionalStocksNew(bourseType, systemBoursesId, userId) + if err != nil { + applogger.Error("GetBoUserOptionalStocks err : %v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusUnauthorized, "", internal.QueryError)) + return + } + + var keys []bson.M + for _, value := range usList { + code := value.Stockcode + keys = append(keys, bson.M{"Code": code}) + } + + filter := bson.M{"$or": keys} + pagedData, err := data.MgoFind(data.StockList, filter) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusInternalServerError, err, internal.QueryError)) + return + } + pagedDataMap := make(map[string]stock.StockShare) + for _, vue := range pagedData.([]primitive.M) { + var stockM stock.StockShare + code := vue["Code"] + beforeClose := vue["BeforeClose"] + yesterdayClose := vue["YesterdayClose"] + fullName := vue["Name"] + stockM.BeforeClose = beforeClose.(string) + stockM.YesterdayClose = yesterdayClose.(string) + stockM.Name = fullName.(string) + pagedDataMap[code.(string)] = stockM + } + + applogger.Debug("") + + var dataList []model.Data + for _, value := range usList { + var md model.Data + dataBool := false + vue, ok := pagedDataMap[value.Stockcode] + if ok { + md.BeforeClose = decimal.RequireFromString(vue.BeforeClose) + md.YesterdayClose = decimal.RequireFromString(vue.YesterdayClose) + md.FullName = vue.Name + + md.Id = value.Id + md.BourseType = value.Boursetype + md.SystemBoursesId = value.Systemboursesid + md.UserId = value.Userid + md.StockCode = value.Stockcode + + dataList = append(dataList, md) + + dataBool = true + } + if !dataBool { + md.Id = value.Id + md.BourseType = value.Boursetype + md.SystemBoursesId = value.Systemboursesid + md.UserId = value.Userid + md.StockCode = value.Stockcode + md.FullName = "" + dataList = append(dataList, md) + } + dataBool = false + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, dataList, internal.QuerySuccess)) +} + +// Fundamentals 获取个人股票信息 +func Fundamentals(c *gin.Context) { + // https://eodhistoricaldata.com/api/fundamentals/AAPL.US?api_token=demo + symbol := internal.ReplaceStr(c.Query("symbol")) + region := internal.ReplaceStr(c.Query("region")) + filter := internal.ReplaceStr(c.Query("filter")) + + var param string + param = fmt.Sprintf("api_token=%v", config.Config.ShareGather.FinancialKey) + if len(filter) > 0 { + param = param + "&" + fmt.Sprintf("filter=%v", filter) + } + if len(symbol) == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "参数错误", internal.QueryError)) + return + } + + qe := fmt.Sprintf("%v.%v", symbol, region) + url := fmt.Sprintf("%v%v/api/fundamentals/%v?%v", topIc, config.Config.ShareGather.FinancialHost, qe, param) + + applogger.Debug("url info:%v", url) + + bodyStr := make(map[string]interface{}) + data, err := red.Get_Cache_Data(symbol) + applogger.Debug("数据信息:%v", data) + if err != nil { + applogger.Error("Get_Cache_Data err: %v", err) + } + if len(data) == 0 { + bodyStr, err = internal.HttpGetDoNew(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + //applogger.Debug("bodyStr info:%v", bodyStr) + jsonStr, err := json.Marshal(bodyStr) + if err != nil { + applogger.Debug("http data json Marshal err: %v", bodyStr) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + if err = red.Set_Cache_Data(symbol, jsonStr, 1440); err != nil { + applogger.Error("write Set_Cache_Data info err: %v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + } else { + if err := json.Unmarshal([]byte(data), &bodyStr); err != nil { + applogger.Error("select redis data json Unmarshal err: %v", err) + } + } + //applogger.Debug("data info:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// FundamentalsNew Obtain individual stock information data (add close) +func FundamentalsNew(c *gin.Context) { + // https://eodhistoricaldata.com/api/fundamentals/AAPL.US?api_token=demo + symbol := internal.ReplaceStr(c.Query("symbol")) + region := internal.ReplaceStr(c.Query("region")) + filter := internal.ReplaceStr(c.Query("filter")) + + filterM := bson.M{"Code": symbol} + pagedData, err := data.MgoFind(data.StockList, filterM) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var yesterdayClose, beforeClose string + for _, vue := range pagedData.([]primitive.M) { + yesterdayClose = vue["YesterdayClose"].(string) + beforeClose = vue["BeforeClose"].(string) + } + applogger.Debug("data info: %v", yesterdayClose, beforeClose) + + var param string + param = fmt.Sprintf("api_token=%v", config.Config.ShareGather.FinancialKey) + if len(filter) > 0 { + param = param + "&" + fmt.Sprintf("filter=%v", filter) + } + + qe := fmt.Sprintf("%v.%v", symbol, region) + url := fmt.Sprintf("%v%v/api/fundamentals/%v?%v", topIc, config.Config.ShareGather.FinancialHost, qe, param) + + applogger.Debug("info url:%v", url) + + bodyStr := make(map[string]interface{}) + data, err := red.Get_Cache_Data(symbol) + if err != nil { + applogger.Error("Get_Cache_Data err: %v", err) + } + if len(data) == 0 { + bodyStr, err = internal.HttpGetDoNew(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + //applogger.Debug("bodyStr info:%v", bodyStr) + jsonStr, err := json.Marshal(bodyStr) + if err != nil { + //applogger.Debug("http data json Marshal err: %v", bodyStr) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + if err := red.Set_Cache_Data(symbol, jsonStr, 1440); err != nil { + applogger.Error("write Set_Cache_Data info err: %v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + } else { + if err := json.Unmarshal([]byte(data), &bodyStr); err != nil { + applogger.Error("select redis data json Unmarshal err: %v", err) + } + } + + bodyStr["YesterdayClose"] = yesterdayClose + bodyStr["BeforeClose"] = beforeClose + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// Intraday Daily historical data +func Intraday(c *gin.Context) { + // https://eodhistoricaldata.com/api/intraday/AAPL.US?api_token=647dd6744b94f4.20894198&fmt=json&from=1564752900&to=1564753200&interval=1m + symbol := internal.ReplaceStr(c.Query("symbol")) + region := internal.ReplaceStr(c.Query("region")) + from := internal.ReplaceStr(c.Query("from")) + interval := internal.ReplaceStr(c.Query("interval")) + to := internal.ReplaceStr(c.Query("to")) + + var param string + param = fmt.Sprintf("api_token=%v", config.Config.ShareGather.FinancialKey) + if len(from) > 0 { + param = param + "&" + fmt.Sprintf("from=%v", from) + } + if len(to) > 0 { + param = param + "&" + fmt.Sprintf("to=%v", to) + } + if len(interval) > 0 { + param = param + "&" + fmt.Sprintf("interval=%v", interval) + } + + qe := fmt.Sprintf("%v.%v", symbol, region) + url := fmt.Sprintf("%v%v/api/intraday/%v?fmt=json&%v", topIc, config.Config.ShareGather.FinancialHost, qe, param) + + applogger.Debug("url data info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// Eod End of Day Historical Data +func Eod(c *gin.Context) { + // https://eodhistoricaldata.com/api/eod/MCD.US?api_token=647dd6744b94f4.20894198&period=d&order=d&from=2017-01-05&to=2017-02-10&fmt=json + symbol := internal.ReplaceStr(c.Query("symbol")) + region := internal.ReplaceStr(c.Query("region")) + period := internal.ReplaceStr(c.Query("period")) + order := internal.ReplaceStr(c.Query("order")) + from := internal.ReplaceStr(c.Query("from")) + to := internal.ReplaceStr(c.Query("to")) + + // 条件组装 + var param string + param = fmt.Sprintf("api_token=%v", config.Config.ShareGather.FinancialKey) + if len(order) > 0 { + param = param + "&" + fmt.Sprintf("from=%v", from) + } + if len(period) > 0 { + param = param + "&" + fmt.Sprintf("period=%v", period) + } + if len(from) > 0 { + param = param + "&" + fmt.Sprintf("from=%v", from) + } + if len(to) > 0 { + param = param + "&" + fmt.Sprintf("to=%v", to) + } + + qe := fmt.Sprintf("%v.%v", symbol, region) + url := fmt.Sprintf("%v%v/api/eod/%v?fmt=json&%v", topIc, config.Config.ShareGather.FinancialHost, qe, param) + + applogger.Debug("url data info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/****https://polygon.io/docs/stocks/get_v2_aggs_grouped_locale_us_market_stocks__date*****/ +// Aggregates 股票聚合条形图 +func Aggregates(c *gin.Context) { + stocksTicker := internal.ReplaceStr(c.Query("stocksTicker")) // AAPL + resolution := internal.ReplaceStr(c.Query("multiplier")) //multiplier + from := internal.ReplaceStr(c.Query("from")) // from + to := internal.ReplaceStr(c.Query("to")) + //fmt.Println(resolution, to) + if strings.Contains("5,15,30,60,1", resolution) && !common.IsOpeningUS() { + to = fmt.Sprintf("%d", common.GetToTime()/1000) + } + //fmt.Println(to) + //else if timespan == "minute" && multiplier == 15 && common.IsOpeningUS() { + // to = fmt.Sprintf("%d", common.GenerateSingaporeMinTimestamp(15).UnixMilli()) + //} else if timespan == "minute" && multiplier == 5 && common.IsOpeningUS() { + // to = fmt.Sprintf("%d", common.GenerateSingaporeMinTimestamp(5).UnixMilli()) + //} + url := fmt.Sprintf("%v%vstock/candle?symbol=%s&resolution=%s&from=%s&to=%s&token=%s", + topIc, config.Config.FinnhubUs.FinnhubHost, stocksTicker, resolution, from, to, config.Config.FinnhubUs.FinnhubKey) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// Grouped 获取整个股票/股票市场的每日开盘价、最高价、最低价和收盘价 (OHLC) +func Grouped(c *gin.Context) { + // /v2/aggs/grouped/locale/us/market/stocks/{date} + // https://api.polygon.io/v2/aggs/grouped/locale/us/market/stocks/2023-01-09?adjusted=true&apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + date := internal.ReplaceStr(c.Query("date")) // date + + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/v2/aggs/grouped/locale/us/market/stocks/%v?adjusted=true&%v", + topIc, config.Config.ShareGather.PolygonHost, date, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// OpenClose 股票每日开盘/收盘 +func OpenClose(c *gin.Context) { + // /v1/open-close/{stocksTicker}/{date} + // https://api.polygon.io/v1/open-close/AAPL/2023-01-09?adjusted=true&apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + stocksTicker := internal.ReplaceStr(c.Query("stocksTicker")) // AAPL + date := internal.ReplaceStr(c.Query("date")) // date + + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/v1/open-close/%v/%v?adjusted=true&%v", + topIc, config.Config.ShareGather.PolygonHost, stocksTicker, date, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// PreviousClose 上一收盘价 +func PreviousClose(c *gin.Context) { + // /v2/aggs/ticker/{stocksTicker}/prev + // https://api.polygon.io/v2/aggs/ticker/AAPL/prev?adjusted=true&apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + stocksTicker := internal.ReplaceStr(c.Query("stocksTicker")) // AAPL + + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + var tickerList []string + if len(stocksTicker) > 0 { + tickerList = strings.Split(stocksTicker, ",") + } + var codeCloseList []stock.Results + for _, ticker := range tickerList { + url := fmt.Sprintf("%v%v/v2/aggs/ticker/%v/prev?adjusted=true&%v", + topIc, config.Config.ShareGather.PolygonHost, ticker, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //var code string + //var closePrice float64 + //for key, value := range bodyStr { + // switch key { + // case "results": + // boDay := value.([]interface{}) + // for _, vue := range boDay { + // switch vue.(type) { + // case map[string]interface{}: + // da := vue.(map[string]interface{}) + // for e, v := range da { + // switch e { + // case "T": + // code = v.(string) + // case "c": + // closePrice = v.(float64) + // default: + // } + // } + // default: + // } + // } + // } + //} + item := stock.AggsTicke{} + json.Unmarshal([]byte(bodyStr), &item) + if len(item.Results) > 0 { + codeCloseList = append(codeCloseList, item.Results[0]) + } + } + applogger.Debug("data info:%v", codeCloseList) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, codeCloseList, internal.QuerySuccess)) +} + +// Trades 获取给定时间范围内股票代码的交易 +func Trades(c *gin.Context) { + ticker := internal.ReplaceStr(c.Query("ticker")) // AAPL + date := internal.ReplaceStr(c.Query("date")) + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/trades/%s?timestamp=%s&order=desc&%v", + topIc, config.Config.ShareGather.PolygonHost, ticker, date, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + //applogger.Debug("第三方数据接收:%v", bodyStr) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) + +} + +// LastTrade 获取给定股票的最新交易 +func LastTrade(c *gin.Context) { + // /v2/last/trade/{stocksTicker} + // https://api.polygon.io/v2/last/trade/AAPL?apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + stocksTicker := internal.ReplaceStr(c.Query("stocksTicker")) // AAPL + + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/v2/last/trade/%v?%v", + topIc, config.Config.ShareGather.PolygonHost, stocksTicker, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// Quotes TODO: 获取给定时间范围内股票代码的交易 +func Quotes(c *gin.Context) { + // https://api.polygon.io/v3/quotes/AAPL?apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + +} + +// LastQuote 股票的最新NBBO(报价)刻度 +func LastQuote(c *gin.Context) { + // /v2/last/nbbo/{stocksTicker} + // https://api.polygon.io/v2/last/nbbo/AAPL?apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + stocksTicker := internal.ReplaceStr(c.Query("stocksTicker")) // AAPL + + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/v2/last/nbbo/%v?%v", + topIc, config.Config.ShareGather.PolygonHost, stocksTicker, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// SnapshotAllTickers 所有交易股票代码的最新市场数据 +func SnapshotAllTickers(c *gin.Context) { + // /v2/snapshot/locale/us/markets/stocks/tickers + // https://api.polygon.io/v2/snapshot/locale/us/markets/stocks/tickers?include_otc=true&apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/v2/snapshot/locale/us/markets/stocks/tickers?include_otc=true&%v", + topIc, config.Config.ShareGather.PolygonHost, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// SnapshotGainersLosers 获取股票/股票市场当前前20名涨幅或跌幅的最新市场数据 +func SnapshotGainersLosers(c *gin.Context) { + // /v2/snapshot/locale/us/markets/stocks/{direction} + // https://api.polygon.io/v2/snapshot/locale/us/markets/stocks/gainers?include_otc=true&apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + // https://api.polygon.io/v2/snapshot/locale/us/markets/stocks/losers?include_otc=true&apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + direction := internal.ReplaceStr(c.Query("direction")) // AAPL + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/v2/snapshot/locale/us/markets/stocks/%v?include_otc=true&%v", + topIc, config.Config.ShareGather.PolygonHost, direction, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// SnapshotOneTicker 获取单个交易股票行情的最新市场数据 +func SnapshotOneTicker(c *gin.Context) { + // /v2/snapshot/locale/us/markets/stocks/tickers/{stocksTicker} + // https://api.polygon.io/v2/snapshot/locale/us/markets/stocks/tickers/AAPL?apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + + stocksTicker := internal.ReplaceStr(c.Query("stocksTicker")) // AAPL + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/v2/snapshot/locale/us/markets/stocks/tickers/%v?%v", + topIc, config.Config.ShareGather.PolygonHost, stocksTicker, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// ReferenceTicker TODO: 所有股票代码 +func ReferenceTicker(c *gin.Context) { + // /v3/reference/tickers + // https://api.polygon.io/v3/reference/tickers?active=true&apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + + url := fmt.Sprintf("%v%v/v3/reference/tickers?active=true&%v", topIc, config.Config.ShareGather.PolygonHost, param) + + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// ReferenceTickerDetails 股票代码详细信息 +func ReferenceTickerDetails(c *gin.Context) { + // /v3/reference/tickers/{ticker} + // https://api.polygon.io/v3/reference/tickers/AAPL?apiKey=CDGMfPJmyiEX5dbjagLSEipf5Y4XbXVb + ticker := internal.ReplaceStr(c.Query("ticker")) // AAPL + //param := fmt.Sprintf("apiKey=%v", config.Config.ShareGather.PolygonKey) + // + //url := fmt.Sprintf("%v%v/v3/reference/tickers/%v?%v", + // topIc, config.Config.ShareGather.PolygonHost, ticker, param) + // + //applogger.Debug("Url info:%v", url) + // + //bodyStr, err := internal.HttpGet(url) + //if err != nil { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + // return + //} + var bodyStr string + //if strings.Contains(bodyStr, "Ticker not found") { + filter := bson.M{"Country": "US", "Code": ticker} + projection := bson.M{} + sort := bson.M{} + result, _ := data.MgoFindProjection(data.StockList, filter, projection, sort, 0) + if len(result) > 0 { + bodyStr = fmt.Sprintf(`{"results": {"ticker": "%s","name": "%s","market": "stocks","locale": "us","primary_exchange": "%s","description": "%s"},"status": "OK"}`, business.TypeCheck(result[0]["Code"]), business.TypeCheck(result[0]["Name"]), business.TypeCheck(result[0]["Exchange"]), business.TypeCheck(result[0]["Intro"])) + } + // } + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// 股票资讯 +func ReferenceTickerNews(c *gin.Context) { + ticker := internal.ReplaceStr(c.Query("ticker")) // AAPL + url := fmt.Sprintf("%v%vcompany-news?symbol=%s&from=%s&to=%s&token=%s", + topIc, config.Config.FinnhubUs.FinnhubHost, ticker, common.NewsUsTime(-7), common.NewsUsTime(0), config.Config.FinnhubUs.FinnhubKey) + if ticker == "" { + url = fmt.Sprintf("%v%vnews?category=general&token=%s", + topIc, config.Config.FinnhubUs.FinnhubHost, config.Config.FinnhubUs.FinnhubKey) + } + applogger.Debug("Url info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + //applogger.Debug("第三方数据接收:%v", bodyStr) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} diff --git a/pkg/processor/spots_api.go b/pkg/processor/spots_api.go new file mode 100644 index 0000000..a0a5734 --- /dev/null +++ b/pkg/processor/spots_api.go @@ -0,0 +1,281 @@ +package processor + +import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "net/http" + "strings" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +/* +现货-K线数据(蜡烛图) https://api.huobi.pro/market/history/kline?symbol=btcusdt&period=1min&size=20 +1、symbol 交易对 例如:btcusdt, ethbtc +2、period 返回数据时间粒度,也就是每根蜡烛的时间区间 目前提供:[1min, 5min, 15min, 30min, 60min, 4hour, 1day, 1mon, 1week, 1year] +3、size 返回K线数据条数 例如:[1-2000] +*/ + +var TradeMap = map[string]int{ + "buy": 1, + "sell": 2, +} + +func SpotsKline(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + period := internal.ReplaceStr(c.Query("period")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + + var param string + if len(symbol) > 0 { + param = fmt.Sprintf("symbol=%v", symbol) + } + if len(period) > 0 { + param = param + "&" + fmt.Sprintf("period=%v", period) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + + url := fmt.Sprintf("https://%v/market/history/kline?%v", config.Config.HbApi.HbSpotsApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +现货-聚合行情(Ticker) https://api.huobi.pro/market/detail/merged?symbol=btcusdt +1、symbol 交易对 例如:btcusdt, ethbtc +*/ +func SpotsMerged(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + + var param string + if len(symbol) > 0 { + param = fmt.Sprintf("symbol=%v", symbol) + } + + url := fmt.Sprintf("https://%v/market/detail/merged?%v", config.Config.HbApi.HbSpotsApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +// 现货列表 https://api.huobi.pro/market/detail/merged?symbol=btcusdt +func SpotsMergedList(c *gin.Context) { + var mergedList []model.GetSpotsTickCompleteList + + // 循环获取现货列表 + for _, value := range dictionary.Symbol { + param := fmt.Sprintf("%vusdt", value) + url := fmt.Sprintf("https://%v/market/detail/merged?symbol=%v", config.Config.HbApi.HbSpotsApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + applogger.Debug("第三方数据接收:%v", bodyStr) + + var merged model.GetSpotsTickList + if err := json.Unmarshal([]byte(bodyStr), &merged); err != nil { + applogger.Error("Unmarshal err:%v", err) + continue + } + + mergedCon := model.GetSpotsTickCompleteList{ + Tick: merged.Tick, + Ch: merged.Ch, + Status: merged.Status, + Ts: merged.Ts, + Icon: "", + FullName: "", + } + + mergedList = append(mergedList, mergedCon) + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, mergedList, internal.QuerySuccess)) +} + +/* +现货-所有交易对的最新 Tickers https://api.huobi.pro/market/tickers +*/ +func SpotsTickers(c *gin.Context) { + url := fmt.Sprintf("https://%v/market/tickers", config.Config.HbApi.HbSpotsApiHost) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +现货-市场深度数据 https://api.huobi.pro/market/depth?symbol=btcusdt&depth=5&type=step0 +1、symbol 交易对 例如:btcusdt, ethbtc +2、depth 返回深度的数量 例如:5,10,20 +3、type 深度的价格聚合度,具体说明见下方 例如:step0,step1,step2,step3,step4,step5 +*/ +func SpotsDepth(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + depth := internal.IntegerInit(internal.ReplaceStr(c.Query("depth"))) + typeS := internal.ReplaceStr(c.Query("type")) + + var param string + if len(symbol) > 0 { + param = fmt.Sprintf("symbol=%v", symbol) + } + if depth > 0 { + param = param + "&" + fmt.Sprintf("depth=%v", depth) + } + if len(typeS) > 0 { + param = param + "&" + fmt.Sprintf("type=%v", typeS) + } + + url := fmt.Sprintf("https://%v/market/depth?%v", config.Config.HbApi.HbSpotsApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + //return + } + if strings.Contains(bodyStr, "invalid-parameter") { + chStep6 := fmt.Sprintf("market-%s-depth-step0", symbol) + bodyStr, _ = red.Get_Cache_Data(chStep6) + } + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +现货-最近市场成交记录 https://api.huobi.pro/market/trade?symbol=btcusdt +1、symbol 交易对 例如:btcusdt, ethbtc +*/ +func SpotsTrade(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + + var param string + if len(symbol) > 0 { + param = fmt.Sprintf("symbol=%v", symbol) + } + + url := fmt.Sprintf("https://%v/market/trade?%v", config.Config.HbApi.HbSpotsApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} + +/* +现货-获得近期交易记录 https://api.huobi.pro/market/history/trade?symbol=btcusdt&size=2 +1、symbol 交易对 例如:btcusdt, ethbtc... +2、size 返回的交易记录数量,最大值2000 +*/ +func SpotsHistoryTrade(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + //symbols := strings.Split(symbol, "usdt") + //phpRes := websocketservice.PHPMarketTrade(SpotsStatus, size, symbols[0]) + //num := size - len(phpRes.List) + var param string + if len(symbol) > 0 { + param = fmt.Sprintf("symbol=%v", symbol) + } + if size > 0 { + param = param + "&" + fmt.Sprintf("size=%v", size) + } + //if num > 0 { + phpRes := stock.PHPMarketTradeList{} + url := fmt.Sprintf("https://%v/market/history/trade?%v", config.Config.HbApi.HbSpotsApiHost, param) + applogger.Debug("查询数据信息:%v", url) + huobiRes := business.MarketTrade{} + bodyStr, _ := internal.HttpGet(url) + json.Unmarshal([]byte(bodyStr), &huobiRes) + for _, v := range huobiRes.Data { + item := stock.MarketTrade{ + ID: v.Data[0].ID, + OrderNumber: fmt.Sprintf("%f", v.Data[0].Amount), + DealPrice: fmt.Sprintf("%f", v.Data[0].Price), + OrderTime: v.Data[0].Ts, + TradeType: TradeMap[v.Data[0].Direction], + IsHuobi: true, + } + phpRes.List = append(phpRes.List, item) + } + if len(phpRes.List) <= 0 { + title := fmt.Sprintf("market-%s-trade-detail", symbol) + item, _ := red.Get_Cache_Data(title) + json.Unmarshal([]byte(item), &phpRes.List) + } + //} + //applogger.Debug("第三方数据接收:%v", bodyStr) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, phpRes.List, internal.QuerySuccess)) +} + +/* +现货-最近24小时行情数据 https://api.huobi.pro/market/detail?symbol=btcusdt +1、symbol 交易对 例如:btcusdt, ethbtc +*/ +func SpotsDetail(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + + var param string + if len(symbol) > 0 { + param = fmt.Sprintf("symbol=%v", symbol) + } + + url := fmt.Sprintf("https://%v/market/detail?%v", config.Config.HbApi.HbSpotsApiHost, param) + applogger.Debug("查询数据信息:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, err, internal.QueryError)) + return + } + if strings.Contains(bodyStr, "invalid-parameter") { + title := fmt.Sprintf("market-%s-detail-merged", symbol) + bodyStr, _ = red.Get_Cache_Data(title) + } + //applogger.Debug("第三方数据接收:%v", bodyStr) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, bodyStr, internal.QuerySuccess)) +} diff --git a/pkg/processor/stock.go b/pkg/processor/stock.go new file mode 100644 index 0000000..eca219f --- /dev/null +++ b/pkg/processor/stock.go @@ -0,0 +1,813 @@ +package processor + +import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "net/http" + "strconv" + "strings" + "time" + "wss-pool/cmd/common" + "wss-pool/config" + "wss-pool/dictionary" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +var Token = "asdfsnl123jlknl3nksdf32345ln98sdfsfs8891232nsdfsdfsdfsdxcfvbhnfgh" +var TapsMap = map[string]int{ + "NYSE": 1, + "NYSEARCA": 2, + "NASDAQ": 3, +} + +func UsMessage(c *gin.Context) { + param := model.ClientMessageParam{} + err := c.BindJSON(¶m) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } + for _, message := range param.Data { + msgStr, _ := json.Marshal(message) + business.JudgeHsetMap("US", business.StockClosingPrice[fmt.Sprintf("%sNew", "US")], message.S, message.Cl.String()) + business.JudgePublishMap("US", message.S, fmt.Sprintf("%s.%s", message.S, "US"), string(msgStr)) + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockInfoAdd(c *gin.Context) { + param := make([]model.StockParam, 0) + err := c.BindJSON(¶m) + total := len(param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if total <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param[0].Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param[0].Country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } + country := common.CapitalizeFirstLetter(param[0].Country) + var dataList []mongo.WriteModel + applogger.Debug("stock info add param total: %d", total) + for _, v := range param { + var price string + //if v.Country == "indonesia" { + // price = strconv.FormatFloat(v.Price, 'f', -1, 64) + //} else { + //price = strconv.FormatFloat(v.Price, 'f', 2, 64) + price = strconv.FormatFloat(v.Price, 'f', -1, 64) + //v.Price, err = strconv.ParseFloat(price, 64) + //if err != nil { + // applogger.Error(v.Country, v.StockCode, err) + //} + // } + //if !common.GetIndiaStockBool(v.Symbol,country) { + // applogger.Error("not india stock :%v", v.Symbol) + // continue + //} + if !business.IsPriceTime(v.Symbol, price, country) { + continue + } + business.StockWs(v, country) + //更新最新价格 + //business.UpdateStockBeforeClose(v.Symbol, price, country) + business.JudgeHsetMap(country, business.StockClosingPrice[fmt.Sprintf("%sNew", country)], v.Symbol, price) + filter := bson.M{"timestamp": bson.M{"$eq": v.Ts}, "symbol": bson.M{"$eq": v.Symbol}} + update := bson.D{{"$set", bson.D{ + {"symbol", v.Symbol}, + {"stock_code", v.StockCode}, + {"stock_name", v.StockName}, + {"price", v.Price}, + {"up_down_rate", v.UpDownRate.String()}, + {"up_down", v.UpDown.String()}, + {"trade_v", v.TradeV.String()}, + {"trade_k", v.TradeK}, + {"vol", v.Vol}, + {"turnover_price_total", v.TurnoverPriceTotal.String()}, + {"price_total", v.PriceTotal}, + {"p_e", v.PE}, + {"eps", v.Eps}, + {"employees_number", v.EmployeesNumber}, + {"plate", v.Plate}, + {"desc", v.Desc}, + {"price_code", v.PriceCode}, + {"country", v.Country}, + {"timestamp", v.Ts}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.GetStockTableName(country), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + } + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func UpdateImg(c *gin.Context) { + // 获取上传文件 + file, err := c.FormFile("image") + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, fmt.Sprintf("file error %s", err.Error()), internal.QueryError)) + return + } + //不能大于10M + if file.Size > 10*1024*1024 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, fmt.Sprintf("file too big"), internal.QueryError)) + return + } + // 打开上传文件 + src, err := file.Open() + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, fmt.Sprintf("file error %s", err.Error()), internal.QueryError)) + return + } + defer src.Close() + name := fmt.Sprintf("%d-%s", time.Now().Unix(), file.Filename) + if err := common.UpdateImage(name, src); err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, fmt.Sprintf("file error %s", err.Error()), internal.QueryError)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, common.Path+name, "ok")) +} + +func Visit(c *gin.Context) { + fileName := internal.ReplaceStr(c.Query("fileName")) + if fileName == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, fmt.Sprintf("fileName error "), internal.QueryError)) + return + } + url, err := common.VisitImage(fileName) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, fmt.Sprintf("file error %s", err.Error()), internal.QueryError)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, url, "ok")) +} + +func StockListAdd(c *gin.Context) { + param := stock.StockList{} + err := c.BindJSON(¶m) + fmt.Println(param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Results) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Results[0].Code == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "ticker error", internal.QueryError)) + return + } else if param.Results[0].Locale == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "locale error", internal.QueryError)) + return + } else if param.Results[0].PrimaryExchange == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "primary exchange error", internal.QueryError)) + return + } + ////遇到排重 直接返回 + //if len(param.Results) == 1 { + // value := param.Results[0] + // if !common.GetIndiaStockBool(fmt.Sprintf("%s:%s", value.PrimaryExchange, value.Code),common.CapitalizeFirstLetter(value.Locale)) { + // c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) + // return + // } + //} + var dataList []mongo.WriteModel + for _, value := range param.Results { + //if !common.IsLetter(value.Code) { + // applogger.Debug(value.Code, "包含其他符号") + // continue + //} + locale := common.CapitalizeFirstLetter(value.Locale) + value.Code = common.GetNewCode(value.PrimaryExchange, value.Code, locale) + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}, {"Country", bson.M{ + "$eq": locale, + }}} + update := bson.D{{"$set", bson.D{ + {"Code", value.Code}, + {"Name", value.Name}, + {"Country", locale}, + {"Exchange", value.PrimaryExchange}, + {"Symbol", common.GetOldCode(value.Code)}, + {"Currency", value.Currency}, + {"Intro", value.Intro}, + {"LogoUrl", value.LogoUrl}}}} + //第二次 + if value.Currency == "" && value.YesterdayClose != "" { + _, err := decimal.NewFromString(value.YesterdayClose) + if err != nil { + applogger.Debug(value.Code, value.YesterdayClose, err.Error()) + continue + } + value.DateStr = strings.TrimSpace(value.DateStr) + //if common.TimeToNows().Format("2006-01-02") != value.DateStr { + // applogger.Error(value.Code, "不是今日的闭盘价") + // continue + //} + update = bson.D{{"$set", bson.D{ + {"YesterdayClose", value.YesterdayClose}, //上一次 闭盘价 + {"Vol", value.Vol}, //当天的交易量 + {"BeforeClose", value.BeforeClose}, + {"ClosePrice", "0"}, + {"Symbol", common.GetOldCode(value.Code)}, + {"Exchange", value.PrimaryExchange}, + {"IsSharia", value.IsSharia}, //是否符合伊斯兰股票 只针对 马来、印尼市场 其他市场不需要 + {"DateStr", value.DateStr}, + }}} + red.HsetMap(business.StockClosingPrice[locale], value.Code, value.YesterdayClose) + red.HsetMap(business.StockClosingPrice[fmt.Sprintf("%sBeforeClose", locale)], value.Code, value.BeforeClose) + red.HsetMap(business.StockClosingPrice[fmt.Sprintf("%sNew", locale)], value.Code, "0") + } + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.StockList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + // TODO: 更改副表 没有公用数据的服务不需要 暂用redis 参数 + dbs := common.GetRedisDBMore(config.Config.Redis.DbMore) + if len(dbs) > 1 { + applogger.Info("StockListAdd update table db", dbs[1]) + data.MgoBulkWrite(fmt.Sprintf("%s%s", data.StockList, dbs[1]), dataList) + } + } else { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "error operation repetition", internal.QueryError)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockListUpdate(c *gin.Context) { + param := stock.StockList{} + err := c.BindJSON(¶m) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Results) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Results[0].Code == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "ticker error", internal.QueryError)) + return + } else if param.Results[0].Locale == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "locale error", internal.QueryError)) + return + } + for _, value := range param.Results { + locale := common.CapitalizeFirstLetter(value.Locale) + value.Code = common.GetNewCode(value.PrimaryExchange, value.Code, locale) + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}, {"Country", bson.M{ + "$eq": common.CapitalizeFirstLetter(value.Locale), + }}} + updateData := bson.D{{"$set", bson.D{ + {"Name", value.Name}, + {"Country", common.CapitalizeFirstLetter(value.Locale)}, + {"Exchange", value.PrimaryExchange}, + {"Currency", value.Currency}, + {"Intro", value.Intro}, + {"LogoUrl", value.LogoUrl}}}} + if err := data.MgoUpdateOne(data.StockList, filter, updateData); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + //return + } + postData := make(map[string]string) + postData["stock_code"] = value.Code + postData["stock_name"] = value.Name + postData["tape"] = value.PrimaryExchange + postData["country"] = fmt.Sprintf("%d", business.StockClosedDataList[common.CapitalizeFirstLetter(value.Locale)]) + applogger.Info("php ", postData) + bodyStr, err := internal.HttpPostFrom(config.Config.PhpHost.URL, postData) + if err != nil { + applogger.Error("Failed to query data:%v", err) + } + applogger.Info(bodyStr) + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockListGet(c *gin.Context) { + country := strings.TrimSpace(c.Query("country")) + isDeficiency := strings.TrimSpace(c.Query("is_deficiency")) // true 查找缺失数据股票 false 查找全部 + sourceVal := strings.TrimSpace(c.Query("source")) // 1 tradingview 2 其他 + source, _ := strconv.Atoi(sourceVal) + token := internal.ReplaceStr(c.Query("token")) + fmt.Println("source : ", source) + if token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } + country = common.CapitalizeFirstLetter(internal.ReplaceStr(country)) + filter := bson.M{"Country": country} + if isDeficiency == "true" { + filter = bson.M{"Country": country, "Name": bson.M{"$exists": false}, "YesterdayClose": bson.M{"$ne": ""}} + } + if source != 0 { + filter = bson.M{"Country": country, "Source": source} + } + projection := bson.M{"Code": 1, "Country": 1, "Symbol": 1, "Exchange": 1, "Source": 1, "Name": 1} + sort := bson.M{} + var md stock.MgoPageSize + md.Total, _ = data.MgoFindTotal(data.StockList, filter) + md.Data, _ = data.MgoFindProjection(data.StockList, filter, projection, sort, 0) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, "ok")) +} + +func StockInfoMon(c *gin.Context) { + param := model.StockMonRes{} + err := c.BindJSON(¶m) + fmt.Println(param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Result) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } else if param.Status == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "status error", internal.QueryError)) + return + } + var dataList []mongo.WriteModel + for _, v := range param.Result { + filter := bson.M{"timestamp": bson.M{"$eq": v.Ts}, "symbol": bson.M{"$eq": v.Symbol}} + update := bson.D{{"$set", bson.D{ + {"symbol", v.Symbol}, + {"stock_code", v.StockCode}, + {"stock_name", v.StockName}, + {"open_price", v.OpenPrice.String()}, + {"high_price", v.HighPrice.String()}, + {"low_price", v.LowPrice.String()}, + {"close_price", v.ClosePrice.String()}, + {"desc", v.Desc}, + {"price_code", v.PriceCode}, + {"country", v.Country}, + {"vol", v.Vol}, + {"timestamp", v.Ts}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if err := data.MgoBulkWrite(data.GetStockSouthAsiaTableName(common.CapitalizeFirstLetter(param.Country), param.Status), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockKLineList(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + country := internal.ReplaceStr(c.Query("country")) + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) // 开始时间 + to := internal.IntegerInit(internal.ReplaceStr(c.Query("to"))) // 结束时间 + period := internal.ReplaceStr(c.Query("period")) // 时间颗粒度 + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) // 结束时间 + if period == "60min" { + period = "1hour" + } + if symbol == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } else if period == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "period error", internal.QueryError)) + return + } + filter := bson.M{"symbol": symbol, "timestamp": bson.M{"$gte": from, "$lte": to}} + if size > 0 { + filter = bson.M{"symbol": symbol} + } + tableName := data.GetStockSouthAsiaTableName(common.CapitalizeFirstLetter(country), period) + + pagedData, err := data.MgoFinds(tableName, filter, int64(size)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = pagedData + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func StockKLineUsList(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) // 开始时间 + to := internal.IntegerInit(internal.ReplaceStr(c.Query("to"))) // 结束时间 + period := internal.ReplaceStr(c.Query("period")) // 时间颗粒度 + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) // 结束时间 + if period == "60min" { + period = "1hour" + } + if symbol == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } else if period == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "period error", internal.QueryError)) + return + } + filter := bson.M{"code": symbol, "timestamp": bson.M{"$gte": from, "$lte": to}} + if size > 0 { + filter = bson.M{"code": symbol} + } + tableName := data.GetStockUsTableName(period) + + pagedData, err := data.MgoFinds(tableName, filter, int64(size)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = pagedData + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func StockNewAdd(c *gin.Context) { + param := model.StockNewsRes{} + err := c.BindJSON(¶m) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Result) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } + var bsonEod []interface{} + for _, v := range param.Result { + bsonEod = append(bsonEod, bson.D{ + {"country", v.Country}, + {"pubdate", v.Pubdate}, + {"title", v.Title}, + {"link", v.Link}, + {"source", v.Source}, + {"code", common.TimeToNow()}, + }) + } + if len(bsonEod) > 0 { + if err := data.MgoInsertMany(data.StockNews, bsonEod); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + return + } + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockNewsList(c *gin.Context) { + country := internal.ReplaceStr(c.Query("country")) + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) + if country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } + switch country { + case "Forex": + country = country + case "Encryption": + country = country + case "UK": + country = country + default: + country = strings.ToLower(country) + } + if size == 0 { + size = TotalSize + } + var filter = bson.M{} + if country != "" { + filter = bson.M{"country": country} + } + applogger.Debug("StockNewsList filter: %v", filter) + pagedData, err := data.MgoFindsCode(data.StockNews, filter, int64(size)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = pagedData + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func StockSouthAsiaInfo(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + country := internal.ReplaceStr(c.Query("country")) + if symbol == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } else if country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } + country = common.CapitalizeFirstLetter(country) + var pageData = model.StockMogoParam{} + filter := bson.M{"symbol": symbol} + res, _ := data.MgoFinds(data.GetStockSouthAsiaTableName(country, "1day"), filter, int64(1)) + pageData.StockCode = symbol + if len(res) > 0 { + pageData.HighPrice = business.TypeCheck(res[0]["high_price"]) + pageData.LowPrice = business.TypeCheck(res[0]["low_price"]) + pageData.Vol = res[0]["vol"] + pageData.PriceTotal = business.TypeCheck(res[0]["price_total"]) + pageData.TurnoverPriceTotal = business.TypeCheck(res[0]["turnover_price_total"]) + } + pageData.OpenPrice, _ = red.Hget(business.StockClosingPrice[country], symbol) + pageData.ClosePrice, _ = red.Hget(business.StockClosingPrice[fmt.Sprintf("%sNew", country)], symbol) + if pageData.ClosePrice == "" || pageData.ClosePrice == "0" { //闭盘期间 + pageData.ClosePrice = pageData.OpenPrice + pageData.OpenPrice, _ = red.Hget(business.StockClosingPrice[fmt.Sprintf("%sBeforeClose", country)], symbol) + } + //if country == "India" { + filter = bson.M{"Country": country, "Code": symbol} + projection := bson.M{"Exchange": 1, "Symbol": 1, "NumericCode": 1} + sort := bson.M{} + result, _ := data.MgoFindProjection(data.StockList, filter, projection, sort, 0) + if len(result) > 0 { + pageData.PrimaryExchange = business.TypeCheck(result[0]["Exchange"]) + pageData.Symbol = business.TypeCheck(result[0]["Symbol"]) + pageData.NumericCode = business.TypeCheck(result[0]["NumericCode"]) + } + // } + var md stock.MgoPageSize + md.Data = pageData + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func StockUsInfo(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + if symbol == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } + var pageData = model.StockMogoParam{} + res := business.GetFinnhubBeforClose(symbol) + if res.C.GreaterThan(decimal.Zero) { + pageData.OpenPrice = res.O.String() + pageData.HighPrice = res.H.String() + pageData.LowPrice = res.L.String() + pageData.ClosePrice = res.C.String() + //pageData.Vol = res.Results[0].V.String() + //pageData.Vw = res.Results[0].VW.String() + pageData.Symbol = symbol + } + filter := bson.M{"Country": "US", "Code": symbol} + projection := bson.M{"Exchange": 1} + usData, _ := data.MgoFindProjection(data.StockList, filter, projection, bson.M{}, 1) + if len(usData) > 0 { + pageData.PrimaryExchange = usData[0]["Exchange"].(string) + } + pageData.PreviousPrice, _ = red.Hget(business.StockClosingPrice[fmt.Sprintf("US")], symbol) + pageData.ClosePrice, _ = red.Hget(business.StockClosingPrice[fmt.Sprintf("USNew")], symbol) //市价 + if pageData.ClosePrice == "" || pageData.ClosePrice == "0" { //闭盘期间 + pageData.ClosePrice = pageData.PreviousPrice + pageData.PreviousPrice, _ = red.Hget(business.StockClosingPrice[fmt.Sprintf("USBeforeClose")], symbol) + } + + var md stock.MgoPageSize + md.Data = pageData + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func getCodePrice(code, country string) (string, string) { + if country != "US" { + return "", "" + } + eodModel, _ := business.PreviousClose(code) + if len(eodModel.Results) <= 0 { + return "", "" + } + dateStrs := common.ConvertToTimezones(eodModel.Results[0].T) + yesterday := dateStrs + var i int +Loop: + yesterday = yesterday.AddDate(0, 0, -1) + yesterdayClose, _ := business.UsData(code, yesterday.Format("2006-01-02")) + if yesterdayClose == "" { + if i <= 2 { + time.Sleep(1 * time.Second) + i++ + goto Loop + } + } + if yesterdayClose == "" { + return "", "" + } + return eodModel.Results[0].C.String(), yesterdayClose +} + +func StockListUpdateToPHP(c *gin.Context) { + param := stock.StockUpdatePolygon{} + err := c.BindJSON(¶m) + fmt.Printf("%+v", param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if param.NewCode == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "new ticker error", internal.QueryError)) + return + } else if param.Locale == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "locale error", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Source == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "source error", internal.QueryError)) + return + } + param.Locale = common.CapitalizeFirstLetter(param.Locale) + filter := bson.M{"Code": param.NewCode, + "Country": param.Locale, + } + updateData := bson.D{{"$set", bson.D{ + {"Source", param.Source}}}} + if err := data.MgoUpdateOne(data.StockList, filter, updateData); err != nil { + applogger.Error("php update err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockListAddToPHP(c *gin.Context) { + param := stock.StockUpdatePolygon{} + err := c.BindJSON(¶m) + fmt.Printf("%+v", param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if param.NewCode == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "new ticker error", internal.QueryError)) + return + } else if param.OldCode == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "old ticker error", internal.QueryError)) + return + } else if param.Locale == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "locale error", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.PrimaryExchange == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "primary exchange error", internal.QueryError)) + return + } + //转换股票code类型 + param.Locale = common.CapitalizeFirstLetter(param.Locale) + param.OldCode = common.GetNewCode(param.PrimaryExchange, param.OldCode, param.Locale) + param.NewCode = common.GetNewCode(param.PrimaryExchange, param.NewCode, param.Locale) + var dataList []mongo.WriteModel + filter := bson.M{"Code": param.OldCode, + "Country": param.Locale, + } + yesterdayClose, beforeClose := getCodePrice(param.OldCode, param.Locale) + if yesterdayClose != "" { + param.YesterdayClose = yesterdayClose + } else { + beforeClose = param.YesterdayClose + } + updateData := bson.M{} + updateData["Code"] = param.NewCode + updateData["Country"] = param.Locale + if param.Name != "" { + updateData["Name"] = param.Name + } + if param.NumericCode != "" { + updateData["NumericCode"] = param.NumericCode + } + if param.PrimaryExchange != "" { + updateData["Exchange"] = param.PrimaryExchange + updateData["Tape"] = TapsMap[strings.ReplaceAll(param.PrimaryExchange, " ", "")] + } + if param.YesterdayClose != "" { + updateData["YesterdayClose"] = param.YesterdayClose + } + if beforeClose != "" { + updateData["BeforeClose"] = beforeClose + } + if param.IsReal != 0 { + updateData["IsReal"] = param.IsReal + } + if param.Intro != "" { + updateData["Intro"] = param.Intro + } + if param.Currency != "" { + updateData["Currency"] = param.Currency + } + if param.Source != 0 { + updateData["Source"] = param.Source + } + updateData["Symbol"] = common.GetOldCode(param.NewCode) + update := bson.M{"$set": updateData} + //修改股票CODE + if param.OldCode != param.NewCode { + //判断修改原数据是否存在 + total, _ := data.MgoFindTotal(data.StockList, filter) + if total == 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "stock code not exist", internal.QueryError)) + return + } + if param.Locale != "US" { + go func(param stock.StockUpdatePolygon) { + for _, v := range dictionary.StockSouthAsiaListTime { + tableName := data.GetStockSouthAsiaTableName(param.Locale, v) + if err := data.MgoUpdateMany(tableName, bson.D{{"symbol", bson.M{ + "$eq": param.OldCode, + }}}, bson.D{{"$set", bson.D{ + {"stock_code", common.GetOldCode(param.NewCode)}, + {"symbol", param.NewCode}}}}); err != nil { + applogger.Error("Mgo update Many err:%v", err) + } + } + }(param) + } + param.YesterdayClose, _ = red.Hget(business.StockClosingPrice[param.Locale], param.OldCode) + beforeClose, _ = red.Hget(business.StockClosingPrice[fmt.Sprintf("%sBeforeClose", param.Locale)], param.OldCode) + } + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + red.Hset(business.StockClosingPrice[param.Locale], param.NewCode, param.YesterdayClose) + red.Hset(business.StockClosingPrice[fmt.Sprintf("%sBeforeClose", param.Locale)], param.NewCode, beforeClose) + red.Hset(business.StockClosingPrice[fmt.Sprintf("%sNew", param.Locale)], param.NewCode, "0") + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.StockList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + } else { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "error operation repetition", internal.QueryError)) + return + } + + // 新增美股行情分发股票代码列表 + WriteShareUs(param.NewCode) + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +// WriteShareUs +// +// @Description: 新增美股行情分发股票代码列表 +// @param code +func WriteShareUs(code string) { + url := fmt.Sprintf("http://%v/usWss/add/code?code=%v", config.Config.FinnhubUs.DispenseWss, code) + applogger.Debug("url data info:%v", url) + + bodyStr, err := internal.HttpGet(url) + if err != nil { + applogger.Error("WriteShareUs err info:%v", err) + return + } + applogger.Debug("new add shareUs code info:%v", bodyStr) +} diff --git a/pkg/processor/stock_index.go b/pkg/processor/stock_index.go new file mode 100644 index 0000000..413b817 --- /dev/null +++ b/pkg/processor/stock_index.go @@ -0,0 +1,414 @@ +package processor + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "net/http" + "strconv" + "strings" + "wss-pool/cmd/common" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/internal/data/business" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" + "wss-pool/pkg/model/stock" +) + +func StockIndexInfoAdd(c *gin.Context) { + param := make([]model.StockIndexParam, 0) + err := c.BindJSON(¶m) + total := len(param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if total <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param[0].Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param[0].Country == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "country error", internal.QueryError)) + return + } + country := common.CapitalizeFirstLetter(param[0].Country) + if country == "Us" { + country = "US" + } + var dataList []mongo.WriteModel + applogger.Debug("stock info add param total: %d", total) + for _, v := range param { + price := strconv.FormatFloat(v.Price, 'f', 4, 64) + v.Price, err = strconv.ParseFloat(price, 64) + if err != nil { + applogger.Error(v.Country, v.StockCode, err) + } + if !business.IsPriceTime(v.StockCode, price, common.StockIndexPrefix) { + continue + } + business.StockPyWsStockIndex(v, common.StockIndexPrefix) + //更新最新价格 + red.HsetMap(business.StockClosingPrice["StockIndexNew"], v.StockCode, price) + filter := bson.M{"timestamp": bson.M{"$eq": v.Ts}, "stock_code": bson.M{"$eq": v.StockCode}} + update := bson.D{{"$set", bson.D{ + {"stock_code", v.StockCode}, + {"stock_name", v.StockName}, + {"price", v.Price}, + {"high_price", v.HighPrice}, + {"low_price", v.LowPrice}, + {"open_price", v.OpenPrice}, + {"up_down_rate", v.UpDownRate}, + {"up_down", v.UpDown}, + {"vol", v.Vol}, + {"price_code", v.PriceCode}, + {"country", v.Country}, + {"timestamp", v.Ts}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.GetStockIndexTableName(), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + } + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockIndexListAdd(c *gin.Context) { + param := stock.StockIndexList{} + err := c.BindJSON(¶m) + fmt.Println(param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Results) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Results[0].Code == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "ticker error", internal.QueryError)) + return + } else if param.Results[0].Locale == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "locale error", internal.QueryError)) + return + } + var dataList []mongo.WriteModel + for _, value := range param.Results { + locale := common.CapitalizeFirstLetter(value.Locale) + if locale == "Us" { + locale = "US" + } + if !common.IsLetter(value.Code) { + applogger.Debug(value.Code, "包含其他符号") + continue + } + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}, {"Country", bson.M{ + "$eq": locale, + }}} + update := bson.D{{"$set", bson.D{ + {"Code", value.Code}, + {"Name", value.Name}, + {"Country", locale}, + {"Exchange", value.PrimaryExchange}, + {"Currency", value.Currency}, + {"State", common.StockIndexOn}, + {"Intro", value.Intro}}}} + //第二次 + if value.Currency == "" && value.YesterdayClose != "" { + _, err := decimal.NewFromString(value.YesterdayClose) + if err != nil { + applogger.Debug(value.Code, value.YesterdayClose, err.Error()) + continue + } + value.DateStr = strings.TrimSpace(value.DateStr) + //if common.TimeToNows().Format("2006-01-02") != value.DateStr { + // applogger.Error(value.Code, "不是今日的闭盘价") + // continue + //} + update = bson.D{{"$set", bson.D{ + {"YesterdayClose", value.YesterdayClose}, //上一次 闭盘价 + {"BeforeClose", value.BeforeClose}, + {"ClosePrice", "0"}, + {"Vol", value.Vol}, + {"DateStr", value.DateStr}, + }}} + red.HsetMap(business.StockClosingPrice["StockIndex"], value.Code, value.YesterdayClose) + red.HsetMap(business.StockClosingPrice["StockIndexBeforeClose"], value.Code, value.BeforeClose) + red.HsetMap(business.StockClosingPrice["StockIndexNew"], value.Code, "0") + } + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.StockIndexList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + } else { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "error operation repetition", internal.QueryError)) + return + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockIndexListUpdate(c *gin.Context) { + param := stock.StockIndexList{} + err := c.BindJSON(¶m) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Results) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Results[0].Code == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "ticker error", internal.QueryError)) + return + } else if param.Results[0].Locale == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "locale error", internal.QueryError)) + return + } + for _, value := range param.Results { + locale := common.CapitalizeFirstLetter(value.Locale) + if locale == "Us" { + locale = "US" + } + filter := bson.D{{"Code", bson.M{ + "$eq": value.Code, + }}, {"Country", bson.M{ + "$eq": locale, + }}} + updateData := bson.D{{"$set", bson.D{ + {"Code", value.Code}, + {"Name", value.Name}, + {"Currency", value.Currency}, + {"Exchange", value.PrimaryExchange}, + {"Intro", value.Intro}}}} + if err := data.MgoUpdateOne(data.StockIndexList, filter, updateData); err != nil { + applogger.Error("MgoInsertMany info err: %v", err) + } + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockIndexListGet(c *gin.Context) { + token := internal.ReplaceStr(c.Query("token")) + if token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } + filter := bson.M{} + projection := bson.M{"Code": 1, "Country": 1} + sort := bson.M{} + var md stock.MgoPageSize + md.Data, _ = data.MgoFindProjection(data.StockIndexList, filter, projection, sort, 0) + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, "ok")) +} + +func StockIndexInfoMon(c *gin.Context) { + param := model.StockMonRes{} + err := c.BindJSON(¶m) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if len(param.Result) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param null", internal.QueryError)) + return + } else if param.Token != Token { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "token error", internal.QueryError)) + return + } else if param.Status == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "status error", internal.QueryError)) + return + } + var dataList []mongo.WriteModel + for _, v := range param.Result { + filter := bson.M{"timestamp": bson.M{"$eq": v.Ts}, "stock_code": bson.M{"$eq": v.StockCode}} + update := bson.D{{"$set", bson.D{ + {"stock_code", v.StockCode}, + {"stock_name", v.StockName}, + {"open_price", v.OpenPrice.String()}, + {"high_price", v.HighPrice.String()}, + {"low_price", v.LowPrice.String()}, + {"close_price", v.ClosePrice.String()}, + {"country", v.Country}, + {"vol", v.Vol}, + {"timestamp", v.Ts}, + }}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + } + if err := data.MgoBulkWrite(data.GetStockIndixKlineTableName(param.Status), dataList); err != nil { + applogger.Error("stock MgoInsertMany err:%v", err) + } + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func StockIndexKLineList(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + from := internal.IntegerInit(internal.ReplaceStr(c.Query("from"))) // 开始时间 + to := internal.IntegerInit(internal.ReplaceStr(c.Query("to"))) // 结束时间 + period := internal.ReplaceStr(c.Query("period")) // 时间颗粒度 + size := internal.IntegerInit(internal.ReplaceStr(c.Query("size"))) // 结束时间 + if period == "60min" { + period = "1hour" + } + if symbol == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } else if period == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "period error", internal.QueryError)) + return + } + filter := bson.M{"stock_code": symbol, "timestamp": bson.M{"$gte": from, "$lte": to}} + if size > 0 { + filter = bson.M{"stock_code": symbol} + } + tableName := data.GetStockIndixKlineTableName(period) + + pagedData, err := data.MgoFinds(tableName, filter, int64(size)) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + var md stock.MgoPageSize + md.Data = pagedData + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func StockIndexInfo(c *gin.Context) { + symbol := internal.ReplaceStr(c.Query("symbol")) + if symbol == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "symbol error", internal.QueryError)) + return + } + var pageData = model.StockMogoParam{} + filter := bson.M{"stock_code": symbol} + res, _ := data.MgoFinds(data.GetStockIndixKlineTableName("1day"), filter, int64(1)) + pageData.StockCode = symbol + if len(res) > 0 { + pageData.HighPrice = business.TypeCheck(res[0]["high_price"]) + pageData.LowPrice = business.TypeCheck(res[0]["low_price"]) + pageData.Vol = res[0]["vol"] + pageData.PriceTotal = business.TypeCheck(res[0]["price_total"]) + pageData.TurnoverPriceTotal = business.TypeCheck(res[0]["turnover_price_total"]) + } + pageData.OpenPrice, _ = red.Hget(business.StockClosingPrice["StockIndex"], symbol) + pageData.ClosePrice, _ = red.Hget(business.StockClosingPrice["StockIndexNew"], symbol) + if pageData.ClosePrice == "" || pageData.ClosePrice == "0" { //闭盘期间 + pageData.ClosePrice = pageData.OpenPrice + pageData.OpenPrice, _ = red.Hget(business.StockClosingPrice["StockIndexBeforeClose"], symbol) + } + var md stock.MgoPageSize + md.Data = pageData + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +} + +func StockIndexListUpdateToPHP(c *gin.Context) { + param := stock.StockIndexPolygon{} + err := c.BindJSON(¶m) + fmt.Println(param) + if err != nil { + applogger.Error("BindJSON", err.Error()) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "param error", internal.QueryError)) + return + } else if param.Code == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "ticker error", internal.QueryError)) + return + } else if param.Locale == "" { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "locale error", internal.QueryError)) + return + } + var dataList []mongo.WriteModel + if !common.IsLetter(param.Code) { + applogger.Debug(param.Code, "包含其他符号") + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "code error", internal.QueryError)) + return + } + filter := bson.D{{"Code", bson.M{ + "$eq": param.Code, + }}, {"Country", bson.M{ + "$eq": param.Locale, + }}} + update := bson.D{{"$set", bson.D{ + {"Code", param.Code}, + {"Country", param.Locale}, + {"State", param.State}, + {"Sort", param.Sort}}}} + models := mongo.NewUpdateOneModel().SetFilter(filter).SetUpdate(update).SetUpsert(true) + dataList = append(dataList, models) + if len(dataList) > 0 { + if err := data.MgoBulkWrite(data.StockIndexList, dataList); err != nil { + applogger.Error("MgoInsertMany err:%v", err) + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "operation failure", internal.QueryError)) + return + } + } else { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "error operation repetition", internal.QueryError)) + return + } + + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, "", "ok")) +} + +func ExchangeSymbolIndexList(c *gin.Context) { + pageSize := internal.IntegerInit(internal.ReplaceStr(c.Query("pageSize"))) // 每页显示多少条数据 + pageNumber := internal.IntegerInit(internal.ReplaceStr(c.Query("pageNumber"))) // 第几页 + search := internal.ReplaceStr(c.Query("search")) // 搜索数据(模糊 + code := internal.ReplaceStr(c.Query("code")) + + if pageSize <= 0 || pageNumber <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, "分页参数不能为零", internal.QueryError)) + return + } + var filter bson.M + if len(search) > 0 { + filter = bson.M{"$or": []bson.M{{"Code": bson.M{"$regex": search}}, {"Name": bson.M{"$regex": search}}}, "State": common.StockIndexOn} + } else { + filter = bson.M{"State": common.StockIndexOn} + } + + strs := strings.Split(code, "-") + if len(code) > 0 { + filter = bson.M{"State": common.StockIndexOn, "Code": bson.M{"$in": strs}} + } + //fmt.Println(filter) + total, err := data.MgoFindTotal(data.StockIndexList, filter) + if err != nil { + c.JSON(http.StatusOK, internal.GinResult(http.StatusBadRequest, err, internal.QueryError)) + return + } + pageData := make([]stock.StockIndexPolygon, 0) + data.MgoPagingFindStructSort(data.StockIndexList, filter, int64(pageSize), int64(pageNumber), bson.M{"Sort": -1}, &pageData) + var md stock.MgoPageSize + md.PageSize = pageSize + md.PageNumber = pageNumber + md.Total = total + if len(pageData) <= 0 { + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) + return + } + for k, v := range pageData { + key := business.StockClosingPrice["StockIndexNew"] + pageData[k].ClosePrice, _ = red.Hget(key, v.Code) + } + md.Data = pageData + c.JSON(http.StatusOK, internal.GinResult(http.StatusOK, md, internal.QuerySuccess)) +}