commit e71bae6f149710029cb1803177be8d861a7ab94b Author: 1447560092@qq.com <1447560092@qq.com> Date: Thu Jan 2 13:13:59 2025 +0800 fix:add code 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 0000000..0a36c9e Binary files /dev/null and b/cmd/websocketcollect/db/forex/MANIFEST-000003 differ diff --git a/cmd/websocketcollect/db/us/000016.ldb b/cmd/websocketcollect/db/us/000016.ldb new file mode 100644 index 0000000..47a042f Binary files /dev/null and b/cmd/websocketcollect/db/us/000016.ldb differ diff --git a/cmd/websocketcollect/db/us/000033.ldb b/cmd/websocketcollect/db/us/000033.ldb new file mode 100644 index 0000000..9f4f14d Binary files /dev/null and b/cmd/websocketcollect/db/us/000033.ldb differ diff --git a/cmd/websocketcollect/db/us/CURRENT b/cmd/websocketcollect/db/us/CURRENT new file mode 100644 index 0000000..29c0a00 --- /dev/null +++ b/cmd/websocketcollect/db/us/CURRENT @@ -0,0 +1 @@ +MANIFEST-000037 diff --git a/cmd/websocketcollect/db/us/CURRENT.bak b/cmd/websocketcollect/db/us/CURRENT.bak new file mode 100644 index 0000000..29a53d8 --- /dev/null +++ b/cmd/websocketcollect/db/us/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000035 diff --git a/cmd/websocketcollect/db/us/LOCK b/cmd/websocketcollect/db/us/LOCK new file mode 100644 index 0000000..e69de29 diff --git a/cmd/websocketcollect/db/us/LOG b/cmd/websocketcollect/db/us/LOG new file mode 100644 index 0000000..e23cbee --- /dev/null +++ b/cmd/websocketcollect/db/us/LOG @@ -0,0 +1,161 @@ +=============== Oct 21, 2024 (CST) =============== +14:05:51.117469 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:05:51.125464 db@open opening +14:05:51.125464 version@stat F·[] S·0B[] Sc·[] +14:05:51.126502 db@janitor F·2 G·0 +14:05:51.127014 db@open done T·1.0377ms +=============== Oct 21, 2024 (CST) =============== +14:18:26.670497 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:18:26.676241 version@stat F·[] S·0B[] Sc·[] +14:18:26.676241 db@open opening +14:18:26.676241 journal@recovery F·1 +14:18:26.676241 journal@recovery recovering @1 +14:18:26.677423 version@stat F·[] S·0B[] Sc·[] +14:18:26.693376 db@janitor F·2 G·0 +14:18:26.693376 db@open done T·17.1353ms +=============== Oct 21, 2024 (CST) =============== +14:18:58.327918 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:18:58.333562 version@stat F·[] S·0B[] Sc·[] +14:18:58.333562 db@open opening +14:18:58.333562 journal@recovery F·1 +14:18:58.334153 journal@recovery recovering @2 +14:18:58.334656 version@stat F·[] S·0B[] Sc·[] +14:18:58.349518 db@janitor F·2 G·0 +14:18:58.349518 db@open done T·15.9557ms +=============== Oct 21, 2024 (CST) =============== +14:19:32.429756 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:19:32.435404 version@stat F·[] S·0B[] Sc·[] +14:19:32.435404 db@open opening +14:19:32.435404 journal@recovery F·1 +14:19:32.435404 journal@recovery recovering @4 +14:19:32.436432 version@stat F·[] S·0B[] Sc·[] +14:19:32.459359 db@janitor F·2 G·0 +14:19:32.459359 db@open done T·23.955ms +=============== Oct 21, 2024 (CST) =============== +14:20:20.289425 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:20:20.294887 version@stat F·[] S·0B[] Sc·[] +14:20:20.294887 db@open opening +14:20:20.294887 journal@recovery F·1 +14:20:20.295433 journal@recovery recovering @6 +14:20:20.296072 version@stat F·[] S·0B[] Sc·[] +14:20:20.310833 db@janitor F·2 G·0 +14:20:20.310833 db@open done T·15.9465ms +=============== Oct 21, 2024 (CST) =============== +14:20:36.927623 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:20:36.934195 version@stat F·[] S·0B[] Sc·[] +14:20:36.934195 db@open opening +14:20:36.934281 journal@recovery F·1 +14:20:36.934281 journal@recovery recovering @8 +14:20:36.935338 version@stat F·[] S·0B[] Sc·[] +14:20:36.938053 db@janitor F·2 G·0 +14:20:36.938053 db@open done T·3.8583ms +=============== Oct 21, 2024 (CST) =============== +14:22:23.986904 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:22:23.992574 version@stat F·[] S·0B[] Sc·[] +14:22:23.992574 db@open opening +14:22:23.992574 journal@recovery F·1 +14:22:23.992574 journal@recovery recovering @10 +14:22:23.993680 version@stat F·[] S·0B[] Sc·[] +14:22:24.008658 db@janitor F·2 G·0 +14:22:24.008658 db@open done T·16.0838ms +=============== Oct 21, 2024 (CST) =============== +14:24:51.467782 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:24:51.473434 version@stat F·[] S·0B[] Sc·[] +14:24:51.473434 db@open opening +14:24:51.473434 journal@recovery F·1 +14:24:51.473938 journal@recovery recovering @12 +14:24:51.474495 version@stat F·[] S·0B[] Sc·[] +14:24:51.477297 db@janitor F·2 G·0 +14:24:51.477297 db@open done T·3.8623ms +=============== Oct 21, 2024 (CST) =============== +14:46:39.100971 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:46:39.106984 version@stat F·[] S·0B[] Sc·[] +14:46:39.106984 db@open opening +14:46:39.106984 journal@recovery F·1 +14:46:39.106984 journal@recovery recovering @14 +14:46:39.115173 memdb@flush created L0@16 N·10706 S·117KiB "A,v10697":"ZZZ,v3592" +14:46:39.115693 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +14:46:39.118621 db@janitor F·3 G·0 +14:46:39.118621 db@open done T·11.6369ms +=============== Oct 21, 2024 (CST) =============== +14:49:38.789393 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:49:38.795391 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +14:49:38.795391 db@open opening +14:49:38.795391 journal@recovery F·1 +14:49:38.795981 journal@recovery recovering @17 +14:49:38.797010 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +14:49:38.799205 db@janitor F·3 G·0 +14:49:38.799205 db@open done T·3.8134ms +=============== Oct 22, 2024 (CST) =============== +13:43:35.290331 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:43:35.296999 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:43:35.296999 db@open opening +13:43:35.296999 journal@recovery F·1 +13:43:35.296999 journal@recovery recovering @19 +13:43:35.297504 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:43:35.312184 db@janitor F·3 G·0 +13:43:35.312184 db@open done T·15.1845ms +=============== Oct 22, 2024 (CST) =============== +13:44:56.419488 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:44:56.425242 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:44:56.425242 db@open opening +13:44:56.425242 journal@recovery F·1 +13:44:56.425242 journal@recovery recovering @21 +13:44:56.426359 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:44:56.441861 db@janitor F·3 G·0 +13:44:56.441861 db@open done T·16.6193ms +=============== Oct 22, 2024 (CST) =============== +13:46:42.392191 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:46:42.398184 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:46:42.398184 db@open opening +13:46:42.398184 journal@recovery F·1 +13:46:42.398706 journal@recovery recovering @23 +13:46:42.399245 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:46:42.402856 db@janitor F·3 G·0 +13:46:42.402856 db@open done T·4.6718ms +=============== Oct 22, 2024 (CST) =============== +13:47:34.914821 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:47:34.920792 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:47:34.920792 db@open opening +13:47:34.920792 journal@recovery F·1 +13:47:34.920792 journal@recovery recovering @25 +13:47:34.921296 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:47:34.924466 db@janitor F·3 G·0 +13:47:34.924466 db@open done T·3.6741ms +=============== Oct 22, 2024 (CST) =============== +13:48:23.970763 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:48:23.976619 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:48:23.976619 db@open opening +13:48:23.976619 journal@recovery F·1 +13:48:23.977144 journal@recovery recovering @27 +13:48:23.977671 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:48:23.979746 db@janitor F·3 G·0 +13:48:23.979746 db@open done T·3.1275ms +=============== Oct 22, 2024 (CST) =============== +13:50:04.249322 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:50:04.255596 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:50:04.255596 db@open opening +13:50:04.255596 journal@recovery F·1 +13:50:04.255596 journal@recovery recovering @29 +13:50:04.256320 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:50:04.259469 db@janitor F·3 G·0 +13:50:04.259469 db@open done T·3.8727ms +=============== Oct 22, 2024 (CST) =============== +13:56:29.194742 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:56:29.200860 version@stat F·[1] S·117KiB[117KiB] Sc·[0.25] +13:56:29.200860 db@open opening +13:56:29.200860 journal@recovery F·1 +13:56:29.200860 journal@recovery recovering @31 +13:56:29.202562 memdb@flush created L0@33 N·1 S·185B "JBKMM,v10708":"JBKMM,v10708" +13:56:29.203065 version@stat F·[2] S·117KiB[117KiB] Sc·[0.50] +13:56:29.204707 db@janitor F·4 G·0 +13:56:29.204707 db@open done T·3.847ms +=============== Oct 22, 2024 (CST) =============== +13:58:29.143667 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +13:58:29.150187 version@stat F·[2] S·117KiB[117KiB] Sc·[0.50] +13:58:29.150187 db@open opening +13:58:29.150187 journal@recovery F·1 +13:58:29.150187 journal@recovery recovering @34 +13:58:29.150993 version@stat F·[2] S·117KiB[117KiB] Sc·[0.50] +13:58:29.165607 db@janitor F·4 G·0 +13:58:29.165607 db@open done T·15.4202ms diff --git a/cmd/websocketcollect/db/us/MANIFEST-000037 b/cmd/websocketcollect/db/us/MANIFEST-000037 new file mode 100644 index 0000000..099b42c Binary files /dev/null and b/cmd/websocketcollect/db/us/MANIFEST-000037 differ diff --git a/cmd/websocketcollect/forex/route.go b/cmd/websocketcollect/forex/route.go new file mode 100644 index 0000000..cd234ee --- /dev/null +++ b/cmd/websocketcollect/forex/route.go @@ -0,0 +1,36 @@ +package forex + +import ( + "fmt" + "net/http" + "wss-pool/config" + "wss-pool/internal/data" + "wss-pool/logging/applogger" +) + +// SubscribeForex +// +// @Description: 外汇行情采集分发 +// @param ipServer +// @param addrServer +func SubscribeForex(ipServer, addrServer string) { + data.Mgo_init(config.Config.Mongodb) + codeList := GetMongodbForexCode() + applogger.Debug("foreList data:%v", codeList) + hub := newHub() + go hub.ForexMarketBBOSwitcher(codeList) // 订阅盘口(买一卖一)报价 + go hub.ForexMarketTradeSwitcher(codeList) // 订阅成交报价 + go hub.ForexMarketBatchPrice(codeList) // 订阅实时报价 + go hub.ForexMarketBatchDayPrice(codeList) // 订阅每天报价-数据(实时-最高-最低-开盘价) + go hub.run() + http.HandleFunc("/forexWs", func(w http.ResponseWriter, r *http.Request) { + handleConnection(hub, w, r) + }) + + addr := fmt.Sprintf("%v%v", ipServer, addrServer) + applogger.Info("wss-pool server start at %v", addr) + + if err := http.ListenAndServe(addr, nil); err != nil { + applogger.Error("ListenAndServe:%v", err) + } +} diff --git a/cmd/websocketcollect/forex/wss.go b/cmd/websocketcollect/forex/wss.go new file mode 100644 index 0000000..3a9f12d --- /dev/null +++ b/cmd/websocketcollect/forex/wss.go @@ -0,0 +1,725 @@ +package forex + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "log" + "net/http" + "strings" + "sync" + "time" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/data" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var topic = "forex" // 实时报价 +var topic_d = "forexDay" // 实时天报价 +var topic_t = "trade" // 成交报价 +var topic_s = "tradeStorage" // 成交报价存储 +var topic_c = "quotes" // 盘口(买一卖一)报价 +var upGrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + // Verification request` + CheckOrigin: func(r *http.Request) bool { + // If it is not a get request, return an error + if r.Method != "GET" { + fmt.Println("Request method error") + return false + } + // If chat is not included in the path, an error is returned + if r.URL.Path != "/forexWs" { + fmt.Println("Request path error") + return false + } + // Verification rules can also be customized according to other needs + return true + }, +} +var forexLastKline = make(chan []model.ForexJsonData) +var forexDayLastKline = make(chan []model.ForexJsonData) + +// 订阅数据类型 +type Symbol struct { + Code string `json:"code"` + DepthLevel int `json:"depth_level"` +} +type Request struct { + CmdID int `json:"cmd_id"` + SeqID int `json:"seq_id"` + Trace string `json:"trace"` + Data DataList `json:"data"` +} +type DataList struct { + SymbolList []Symbol `json:"symbol_list"` +} +type ResultBbo struct { + CmdID int `json:"cmd_id"` + Data struct { + Code string `json:"code"` + Seq string `json:"seq"` + TickTime string `json:"tick_time"` + Bids []struct { + Price string `json:"price"` + Volume string `json:"volume"` + } `json:"bids"` + Asks []struct { + Price string `json:"price"` + Volume string `json:"volume"` + } `json:"asks"` + } `json:"data"` +} +type ResultsTrade struct { + CmdID int `json:"cmd_id"` + Data struct { + Code string `json:"code"` + Seq string `json:"seq"` + TickTime string `json:"tick_time"` + Price string `json:"price"` + Volume string `json:"volume"` + Turnover string `json:"turnover"` + TradeDirection int `json:"trade_direction"` + } `json:"data"` +} + +// Client represents a WebSocket client. +type Client struct { + conn *websocket.Conn + subscriptions map[string]bool // Tracks which topics the client is subscribed to +} + +// Hub maintains the set of active clients and broadcasts messages to the clients. +type Hub struct { + clients map[*Client]bool // All connected clients + broadcast chan Message // Broadcast channel for messages + topics map[string][]*Client // Topic to clients mapping for subscriptions + mu sync.Mutex // Mutex for safe concurrent access +} + +// Message structure to hold the message content and the topic +type Message struct { + Topic string `json:"topic"` + Content string `json:"content"` +} + +// newHub Initialize a new Hub +func newHub() *Hub { + return &Hub{ + clients: make(map[*Client]bool), + broadcast: make(chan Message), + topics: make(map[string][]*Client), + } +} + +// run Start the Hub to listen for messages +func (h *Hub) run() { + for { + msg := <-h.broadcast + // Lock the hub to safely access the topics map + h.mu.Lock() + if clients, ok := h.topics[msg.Topic]; ok { + for _, client := range clients { + err := client.conn.WriteJSON(msg) + if err != nil { + log.Printf("Error writing message to client: %v", err) + client.conn.Close() + delete(h.clients, client) + } + } + } + h.mu.Unlock() + } +} + +// Handle WebSocket connections +func handleConnection(hub *Hub, w http.ResponseWriter, r *http.Request) { + conn, err := upGrader.Upgrade(w, r, nil) + if err != nil { + log.Println("Error during connection upgrade:", err) + return + } + client := &Client{conn: conn, subscriptions: make(map[string]bool)} + hub.clients[client] = true + + go func() { + defer func() { + delete(hub.clients, client) + for topic := range client.subscriptions { + hub.mu.Lock() + hub.topics[topic] = removeClientFromTopic(hub.topics[topic], client) + hub.mu.Unlock() + } + }() + for { + var msg Message + if err = conn.ReadJSON(&msg); err != nil { + log.Println("Error reading message:", err) + break + } + applogger.Info("message info:%v---%v", msg.Topic, msg.Content) + + if msg.Content == "ping" || msg.Content == "subscribe" { + client.subscriptions[msg.Topic] = true + hub.mu.Lock() + hub.topics[msg.Topic] = append(hub.topics[msg.Topic], client) + hub.mu.Unlock() + } + switch msg.Content { + case "subscribe": + hub.broadcast <- Message{Topic: msg.Topic, Content: "subscribe success"} + case "ping": + hub.broadcast <- Message{Topic: msg.Topic, Content: "pong"} + default: + // TODO: 广播客户端订阅消息 + //hub.broadcast <- msg + } + } + }() +} + +// subscribeTradeSwitcher content new +func subscribeTradeSwitcher() *websocket.Conn { + url := "wss://quote.tradeswitcher.com/quote-b-ws-api?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" + applogger.Info("subscribePolygon info Url:%v", url) + conn, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("Failed to link to wss server:%v", err) + return nil + } + + return conn +} +func (h *Hub) ForexMarketBBOSwitcher(codeList []string) { + var dataList DataList // 构造订阅参数 + for _, value := range codeList { + dataList.SymbolList = append(dataList.SymbolList, Symbol{ + Code: value, + DepthLevel: 1, + }) + } + ticker := time.NewTicker(time.Second * 1) + defer ticker.Stop() + for { + select { + case <-ticker.C: + applogger.Info("Execute automatic subscription........") + if conn := subscribeTradeSwitcher(); conn != nil { + // Send heartbeat every 10 seconds + go func() { + for range time.NewTicker(10 * time.Second).C { + req := Request{ + CmdID: 22000, + SeqID: 1447560092, + Trace: "b9e14618-b43f-4fb3-bf08-0bd1b60f285d", + Data: DataList{}, + } + messageBytes, err := json.Marshal(req) + if err != nil { + applogger.Error("json.Marshal err:%v", err) + return + } + applogger.Debug("Message send info:%v", string(messageBytes)) + err = conn.WriteMessage(websocket.TextMessage, messageBytes) + if err != nil { + applogger.Error("write heartbeat err:%v", err) + } + } + }() + // Construct subscription parameters + req := Request{ + CmdID: 22002, + SeqID: 1447560092, + Trace: uuid.New().String(), + Data: dataList, + } + messageBytes, err := json.Marshal(req) + if err != nil { + applogger.Error("json.Marshal err:%v", err) + return + } + applogger.Debug("Message send info:%v", string(messageBytes)) + err = conn.WriteMessage(websocket.TextMessage, messageBytes) + if err != nil { + applogger.Debug("write err:%v", err) + return + } + // 客户端broadcast,消息推送 + h.SendAllClientBBOSwitcher(conn) + } + } + } +} +func (h *Hub) ForexMarketTradeSwitcher(codeList []string) { + var dataList DataList // 构造订阅参数 + for _, value := range codeList { + dataList.SymbolList = append(dataList.SymbolList, Symbol{ + Code: value, + DepthLevel: 1, + }) + } + ticker := time.NewTicker(time.Second * 1) + defer ticker.Stop() + for { + select { + case <-ticker.C: + applogger.Info("Execute automatic subscription........") + if conn := subscribeTradeSwitcher(); conn != nil { + // Send heartbeat every 10 seconds + go func() { + for range time.NewTicker(10 * time.Second).C { + req := Request{ + CmdID: 22000, + SeqID: 1148509458, + Trace: "a5e37d07-168f-403a-b4cb-08be813e0b91", + Data: DataList{}, + } + messageBytes, err := json.Marshal(req) + if err != nil { + applogger.Error("json.Marshal err:%v", err) + return + } + applogger.Debug("Message send info:%v", string(messageBytes)) + err = conn.WriteMessage(websocket.TextMessage, messageBytes) + if err != nil { + applogger.Error("heartbeat write err:%v", err) + } + } + }() + // Construct subscription parameters + req := Request{ + CmdID: 22004, + SeqID: 1148509458, + Trace: uuid.New().String(), + Data: dataList, + } + messageBytes, err := json.Marshal(req) + if err != nil { + applogger.Error("json.Marshal err:%v", err) + return + } + applogger.Debug("Message send info:%v", string(messageBytes)) + err = conn.WriteMessage(websocket.TextMessage, messageBytes) + if err != nil { + applogger.Debug("write err:%v", err) + return + } + // 客户端broadcast,消息推送 + h.SendAllClientTradeSwitcher(conn) + } + } + } +} +func (h *Hub) ForexMarketBatchPrice(codeList []string) { + var dataList model.ConstructParametersPost + for _, value := range codeList { + dataList.Trace = uuid.New().String() + dataList.Data.DataList = append(dataList.Data.DataList, model.DataParameters{ + Code: value, + KlineType: 1, + KlineTimestampEnd: 0, + QueryKlineNum: 1, + AdjustType: 0, + }) + } + go func() { + for { + UrlBatchKline := "https://quote.tradeswitcher.com/quote-b-api/batch-kline?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" + queryStr, err := json.Marshal(&dataList) + if err != nil { + return + } + bodyStr, err := internal.HttpPost(UrlBatchKline, string(queryStr)) + if err != nil { + return + } + //applogger.Debug("klineBatchPrice data:%v", bodyStr) + var klineNew model.KlinePostReturnStruct + if err = json.Unmarshal([]byte(bodyStr), &klineNew); err != nil { + time.Sleep(3 * time.Second) + continue + } + //applogger.Debug("klineNew count:%v", len(klineNew.Data.KlineList)) + // 将数据写入缓存用来推送K线 + for _, value := range klineNew.Data.KlineList { + if len(value.KlineData) == 0 { + continue + } + code := model.Check_Code[value.Code] + if len(code) == 0 { + code = value.Code + } + openPrice := decimal.RequireFromString(value.KlineData[0].OpenPrice).InexactFloat64() + closePrice := decimal.RequireFromString(value.KlineData[0].ClosePrice).InexactFloat64() + highPrice := decimal.RequireFromString(value.KlineData[0].HighPrice).InexactFloat64() + lowPrice := decimal.RequireFromString(value.KlineData[0].LowPrice).InexactFloat64() + volume := decimal.RequireFromString(value.KlineData[0].Volume).InexactFloat64() + timestamp := decimal.RequireFromString(value.KlineData[0].Timestamp).IntPart() + forexJson := []model.ForexJsonData{ + { + Event: "CAS", + Pair: code, + Open: openPrice, + Close: closePrice, + High: highPrice, + Low: lowPrice, + Volume: int(volume), + Timestamp: timestamp, + }, + } + forexLastKline <- forexJson + } + time.Sleep(200 * time.Millisecond) + } + }() + h.ForexMarketBatchPriceBatch() +} +func (h *Hub) ForexMarketBatchDayPrice(codeList []string) { + var dataList model.ConstructParametersPost + for _, value := range codeList { + dataList.Trace = uuid.New().String() + dataList.Data.DataList = append(dataList.Data.DataList, model.DataParameters{ + Code: value, + KlineType: 8, + KlineTimestampEnd: 0, + QueryKlineNum: 1, + AdjustType: 0, + }) + } + go func() { + for { + UrlBatchKline := "https://quote.tradeswitcher.com/quote-b-api/batch-kline?token=bf8f33c446c4494286eccaa57a2e6fac-c-app" + queryStr, err := json.Marshal(&dataList) + if err != nil { + return + } + bodyStr, err := internal.HttpPost(UrlBatchKline, string(queryStr)) + if err != nil { + return + } + //applogger.Debug("klineBatchPrice data:%v", bodyStr) + var klineNew model.KlinePostReturnStruct + if err = json.Unmarshal([]byte(bodyStr), &klineNew); err != nil { + time.Sleep(3 * time.Second) + continue + } + //applogger.Debug("klineNew count:%v", len(klineNew.Data.KlineList)) + // 将数据写入缓存用来推送K线 + for _, value := range klineNew.Data.KlineList { + if len(value.KlineData) == 0 { + continue + } + code := model.Check_Code[value.Code] + if len(code) == 0 { + code = value.Code + } + openPrice := decimal.RequireFromString(value.KlineData[0].OpenPrice).InexactFloat64() + closePrice := decimal.RequireFromString(value.KlineData[0].ClosePrice).InexactFloat64() + highPrice := decimal.RequireFromString(value.KlineData[0].HighPrice).InexactFloat64() + lowPrice := decimal.RequireFromString(value.KlineData[0].LowPrice).InexactFloat64() + volume := decimal.RequireFromString(value.KlineData[0].Volume).InexactFloat64() + timestamp := decimal.RequireFromString(value.KlineData[0].Timestamp).IntPart() + forexJson := []model.ForexJsonData{ + { + Event: "CAS-D", + Pair: code, + Open: openPrice, + Close: closePrice, + High: highPrice, + Low: lowPrice, + Volume: int(volume), + Timestamp: timestamp, + }, + } + forexDayLastKline <- forexJson + } + time.Sleep(200 * time.Millisecond) + } + }() + h.ForexMarketBatchDayPriceBatch() +} + +// SendAllClientTradeSwitcher send message to all clients +func (h *Hub) SendAllClientBBOSwitcher(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + applogger.Error("IsUnexpectedCloseError err:%v", err) + } + break + } + var messageToJson ResultBbo + if err = json.Unmarshal([]byte(string(msg)), &messageToJson); err != nil { + applogger.Error("SendAllClientTradeSwitcher err:%v", err) + time.Sleep(5 * time.Second) + continue + } + switch messageToJson.CmdID { + case 22001: // 心跳 + // {"ret":200,"msg":"ok","cmd_id":22001,"seq_id":123,"trace":"3380a7a-3e1f-c3a5-5ee3-9e5be0ec8c241692805462"} + applogger.Debug("Heartbeat results:%v", string(msg)) + case 22999: // 处理订阅数据 + // {"cmd_id":22999,"data":{"code":"EURUSD","seq":"114101723","tick_time":"1732168153591","bids":[{"price":"1.05478","volume":"100000.00"}],"asks":[{"price":"1.05479","volume":"100000.00"}]}} + code := model.Check_Code[messageToJson.Data.Code] + if len(code) == 0 { + code = messageToJson.Data.Code + } + tickTime := decimal.RequireFromString(messageToJson.Data.TickTime).IntPart() + bids := decimal.RequireFromString(messageToJson.Data.Bids[0].Price).InexactFloat64() + asks := decimal.RequireFromString(messageToJson.Data.Asks[0].Price).InexactFloat64() + volume := decimal.RequireFromString(messageToJson.Data.Asks[0].Volume).IntPart() + quotesMsg := &[]model.ForexLastQuote{ + { + Ev: "C", + P: code, + T: tickTime, + B: bids, + A: asks, + X: int(volume), + }, + } + msgStr, err := json.Marshal(quotesMsg) + if err != nil { + applogger.Error("json.Marshal err:%v", err) + time.Sleep(5 * time.Second) + continue + } + applogger.Info("Message result:%v", string(msgStr)) + // 广播数据 + h.broadcast <- Message{Topic: topic_c, Content: string(msgStr)} + default: + applogger.Debug("Message result:%v", string(msg)) + } + } +} +func (h *Hub) SendAllClientTradeSwitcher(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + applogger.Error("IsUnexpectedCloseError err:%v", err) + } + break + } + var messageToJson ResultsTrade + if err = json.Unmarshal(msg, &messageToJson); err != nil { + applogger.Error("json.Unmarshal err:%v", err) + continue + } + switch messageToJson.CmdID { + case 22001: // 处理心跳 + // {"ret":200,"msg":"ok","cmd_id":22001,"seq_id":123456,"trace":"3380a7a-3e1f-c3a5-5ee3-9e5be0ec8c241692805462787878"} + applogger.Debug("Heartbeat results:%v", string(msg)) + case 22998: // 处理订阅数据 + // {"cmd_id":22998,"data":{"code":"XAUUSD","seq":"65087341","tick_time":"1732267727182","price":"2694.84","volume":"95.00","turnover":"0.00","trade_direction":0}} + code := model.Check_Code[messageToJson.Data.Code] + if len(code) == 0 { + code = messageToJson.Data.Code + } + tradeMsg := &[]model.ForexTrade{ + { + Ev: "T", + Code: code, + Seq: messageToJson.Data.Seq, + TickTime: messageToJson.Data.TickTime, + Price: messageToJson.Data.Price, + Volume: messageToJson.Data.Volume, + Turnover: messageToJson.Data.Turnover, + TradeDirection: messageToJson.Data.TradeDirection, + }, + } + msgStr, err := json.Marshal(tradeMsg) + if err != nil { + applogger.Error("json.Marshal err:%v", err) + time.Sleep(5 * time.Second) + continue + } + applogger.Info("Message result:%v", string(msgStr)) + // 广播数据 + h.broadcast <- Message{ + Topic: topic_t, + Content: string(msgStr), + } + h.broadcast <- Message{ + Topic: topic_s, + Content: string(msgStr), + } + default: + applogger.Debug("Message result:%v", string(msg)) + } + } +} +func (h *Hub) ForexMarketBatchPriceBatch() { + for dataValue := range forexLastKline { + applogger.Info("Message result:%v", dataValue) + message, err := json.Marshal(&dataValue) + if err != nil { + time.Sleep(5 * time.Second) + continue + } + // 广播数据 + h.broadcast <- Message{Topic: topic, Content: string(message)} + } +} +func (h *Hub) ForexMarketBatchDayPriceBatch() { + for dataValue := range forexDayLastKline { + applogger.Info("Message result:%v", dataValue) + message, err := json.Marshal(&dataValue) + if err != nil { + time.Sleep(5 * time.Second) + continue + } + // 广播数据 + h.broadcast <- Message{Topic: topic_d, Content: string(message)} + } +} + +// ForexMarket content +func (h *Hub) ForexMarket() { + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + applogger.Info("Execute automatic subscription........") + if conn := subscribePolygon(); conn != nil { + h.SendAllClientNew(conn) // 客户端broadcast,消息推送 + } + } + } +} + +// subscribePolygon Link to third-party services +func subscribePolygon() *websocket.Conn { + url := fmt.Sprintf("wss://%v/forex", config.Config.ShareGather.PolygonWss) + applogger.Info("subscribePolygon info Url:%v", url) + conn, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("Failed to link to wss server:%v", err) + return nil + } + + return conn +} + +// SendAllClientNew send message to all clients +func (h *Hub) SendAllClientNew(conn *websocket.Conn) { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + applogger.Error("IsUnexpectedCloseError error: %v", err) + } + break + } + + applogger.Debug("ReadMessage data info:%v", string(msg)) + + message := string(msg) + if strings.Contains(message, "Connected Successfully") { + // 鉴权 + // {"action":"auth","params":"vG4tCD5emAFPkS4kWtXxJntMASyN4dnv"} + keyStr := model.SendAuthority{ + Action: "auth", + Params: config.Config.ShareGather.PolygonKey, + } + authStr, err := JsonMarshal(keyStr) + if err != nil { + break + } + if err = Send(conn, string(authStr)); err != nil { + break + } + } else if strings.Contains(message, "authenticated") { + // 发起实时价格订阅 + // {"action":"subscribe", "params":"CAS.*"} + subStr := model.SendAuthority{Action: "subscribe", Params: "CAS.*"} + resultStr, err := JsonMarshal(subStr) + if err != nil { + break + } + if err = Send(conn, string(resultStr)); err != nil { + break + } + // 发起报价(卖一|买一)订阅 + // {"action":"subscribe", "params":"C.*"} + subStrC := model.SendAuthority{Action: "subscribe", Params: "C.*"} + resultStrC, err := JsonMarshal(subStrC) + if err != nil { + break + } + if err = Send(conn, string(resultStrC)); err != nil { + break + } + } else { + // 广播数据 + //[{"ev":"CAS","sym":"C.C:EUR-USD","i":"50578","x":4,"p":215.9721,"s":100,"t":1611082428813,"z":3}] + //[{"ev":"C","p":"PLN/NOK","i":0,"a":2.7225,"b":2.7214,"x":48,"t":1730194133000}] + var topicTop string + if strings.Contains(message, "\"ev\":\"CAS\"") { + topicTop = topic + } else if strings.Contains(message, "\"ev\":\"C\"") { + topicTop = topic_c + } + h.broadcast <- Message{Topic: topicTop, Content: message} + } + } +} + +// Send Subscribe send message +func Send(conn *websocket.Conn, data string) error { + if conn == nil { + applogger.Error("WebSocket sent error: no connection available") + return errors.New("WebSocket sent error: no connection available") + } + if err := conn.WriteMessage(websocket.TextMessage, []byte(data)); err != nil { + applogger.Error("WebSocket sent error: data=%s, error=%s", data, err) + return err + } + return nil +} + +// JsonMarshal marshal json +func JsonMarshal(v interface{}) ([]byte, error) { + str, err := json.Marshal(v) + if err != nil { + applogger.Error("json.Marshal error: %v", err) + return []byte{}, err + } + return str, nil +} + +// removeClientFromTopic Remove a client from a topic +func removeClientFromTopic(clients []*Client, c *Client) []*Client { + for i, client := range clients { + if client == c { + return append(clients[:i], clients[i+1:]...) + } + } + return clients +} + +// GetMongodbForexCode select forex code +func GetMongodbForexCode() []string { + dateList, err := data.MgoFind(data.ForexListBak, bson.M{}) + if err != nil { + applogger.Error("MgoFind info err: %v", err) + return []string{} + } + //applogger.Info("MgoFind info len: %v", dateList) + var dateListStr []string + for _, value := range dateList.([]primitive.M) { + code := value["code"].(string) + dateListStr = append(dateListStr, code) + } + return dateListStr +} diff --git a/cmd/websocketcollect/us/route.go b/cmd/websocketcollect/us/route.go new file mode 100644 index 0000000..96cc766 --- /dev/null +++ b/cmd/websocketcollect/us/route.go @@ -0,0 +1,75 @@ +package us + +import ( + "encoding/json" + "fmt" + "net/http" + "wss-pool/cmd/websocketcollect/cache" + "wss-pool/logging/applogger" +) + +// SubscribeShareUs +// +// @Description: 广播美股实时行情/新增code +// @param addr +func SubscribeShareUs(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)) + // 初始化美股 + //InitShareUsCode() + cache.InitDB("us") + hub := newHub() + go hub.ShareMarket() + go hub.run() + http.HandleFunc("/usWss/add/code", serveHome) + http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + handleConnection(hub, w, r) + }) + + addr := fmt.Sprintf("%v%v", ipServer, addrServer) + + applogger.Info("wss-pool server start at %v", addr) + if err := http.ListenAndServe(addr, nil); err != nil { + applogger.Error("ListenAndServe:%v", err) + } +} + +// serveHome +// +// @Description: 新增美股code +// @param w +// @param r +func serveHome(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/usWss/add/code" { + http.Error(w, "Not found", http.StatusNotFound) + return + } + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + value := r.FormValue("code") + if value == "" { + http.Error(w, "code is empty", http.StatusBadRequest) + return + } + applogger.Info("subscribe share us code:%v", value) + if err := cache.WriteDB(value, value); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // 确保返回 JSON 内容类型 + w.Header().Set("Content-Type", "application/json") + // 设置状态码 + w.WriteHeader(http.StatusOK) + // 将响应编码为 JSON 并写入响应体 + if err := json.NewEncoder(w).Encode(Message{ + Topic: value, + Content: "新增美股code成功!", + }); err != nil { + http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) + } +} diff --git a/cmd/websocketcollect/us/wss.go b/cmd/websocketcollect/us/wss.go new file mode 100644 index 0000000..0029fc5 --- /dev/null +++ b/cmd/websocketcollect/us/wss.go @@ -0,0 +1,241 @@ +package us + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/gorilla/websocket" + "log" + "net/http" + "sync" + "time" + "wss-pool/cmd/websocketcollect/cache" + "wss-pool/config" + "wss-pool/logging/applogger" + "wss-pool/pkg/model" +) + +var ( + UsNewCode = make(map[string]bool) + ctx, cancel = context.WithCancel(context.Background()) + wg sync.WaitGroup +) +var upGrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + // Verification request` + CheckOrigin: func(r *http.Request) bool { + // If it is not a get request, return an error + if r.Method != "GET" { + fmt.Println("Request method error") + return false + } + // If chat is not included in the path, an error is returned + if r.URL.Path != "/ws" { + fmt.Println("Request path error") + return false + } + // Verification rules can also be customized according to other needs + return true + }, +} + +// Client represents a WebSocket client. +type Client struct { + conn *websocket.Conn + subscriptions map[string]bool // Tracks which topics the client is subscribed to +} + +// Hub maintains the set of active clients and broadcasts messages to the clients. +type Hub struct { + clients map[*Client]bool // All connected clients + broadcast chan Message // Broadcast channel for messages + topics map[string][]*Client // Topic to clients mapping for subscriptions + mu sync.Mutex // Mutex for safe concurrent access +} + +// Message structure to hold the message content and the topic +type Message struct { + Topic string `json:"topic"` + Content string `json:"content"` +} + +// Initialize a new Hub +func newHub() *Hub { + return &Hub{ + clients: make(map[*Client]bool), + broadcast: make(chan Message), + topics: make(map[string][]*Client), + } +} + +// Start the Hub to listen for messages +func (h *Hub) run() { + for { + msg := <-h.broadcast + // Lock the hub to safely access the topics map + h.mu.Lock() + if clients, ok := h.topics[msg.Topic]; ok { + for _, client := range clients { + err := client.conn.WriteJSON(msg) + if err != nil { + log.Printf("Error writing message to client: %v", err) + client.conn.Close() + delete(h.clients, client) + } + } + } + h.mu.Unlock() + } +} + +// Handle WebSocket connections +func handleConnection(hub *Hub, w http.ResponseWriter, r *http.Request) { + conn, err := upGrader.Upgrade(w, r, nil) + if err != nil { + log.Println("Error during connection upgrade:", err) + return + } + client := &Client{conn: conn, subscriptions: make(map[string]bool)} + hub.clients[client] = true + + go func() { + defer func() { + delete(hub.clients, client) + for topic := range client.subscriptions { + hub.mu.Lock() + hub.topics[topic] = removeClientFromTopic(hub.topics[topic], client) + hub.mu.Unlock() + } + }() + for { + var msg Message + if err = conn.ReadJSON(&msg); err != nil { + log.Println("Error reading message:", err) + break + } + applogger.Info("message info:%v---%v", msg.Topic, msg.Content) + + if msg.Content == "ping" || msg.Content == "subscribe" { + client.subscriptions[msg.Topic] = true + hub.mu.Lock() + hub.topics[msg.Topic] = append(hub.topics[msg.Topic], client) + hub.mu.Unlock() + } + switch msg.Content { + case "subscribe": + hub.broadcast <- Message{Topic: msg.Topic, Content: "subscribe success"} + case "ping": + hub.broadcast <- Message{Topic: msg.Topic, Content: "pong"} + default: + // TODO: 广播客户端订阅消息 + //hub.broadcast <- msg + } + } + }() +} + +// ShareMarket content +func (h *Hub) ShareMarket() { + ticker := time.NewTicker(time.Second * 10) + defer ticker.Stop() + for { + select { + case <-ticker.C: + applogger.Info("Execute automatic subscription........") + if conn := subscribeFinnHub(); conn != nil { + // 客户端broadcast,消息推送 + if err := h.SendAllClientNew(conn); err != nil { + break + } + } + } + } +} +func (h *Hub) SendAllClientNew(conn *websocket.Conn) error { + sendUsCodeNew(conn, false) // 初始化所有项目股票code订阅 + go singleSendUsCodeNew(conn) // 单线程 订阅后续新增的股票 + for { + _, msg, err := conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + applogger.Error("IsUnexpectedCloseError error: %v", err) + } + return errors.New("Error reading message") + } + + 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 + } + // send to websocket + h.broadcast <- Message{Topic: "us", Content: string(msg)} + } +} + +// Link to third-party services +func subscribeFinnHub() *websocket.Conn { + url := fmt.Sprintf("wss://%v?token=%v", config.Config.FinnhubUs.FinnhubWss, config.Config.FinnhubUs.FinnhubKey) + applogger.Info("subscribeFinnHub info Url:%v", url) + conn, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + applogger.Error("Failed to link to wss server:%v", err) + return nil + } + + return conn +} +func singleSendUsCodeNew(conn *websocket.Conn) { + for { + time.Sleep(10 * time.Minute) + //发送测试数据检查该连接是否有效 + //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") + sendUsCodeNew(conn, true) + } +} +func sendUsCodeNew(conn *websocket.Conn, isSingle bool) { + stockRes := cache.ReadListDB() + for _, code := range stockRes { + sendPing := fmt.Sprintf("{\"type\":\"subscribe\",\"symbol\":\"%v\"}", code) + if !isSingle { //订阅全部美股 + fmt.Println("subscribe:", code) + Send(conn, sendPing) + UsNewCode[code] = true + continue + } + if !UsNewCode[code] { //追加订阅新股票 + applogger.Debug("new us stock:%v", sendPing) + Send(conn, sendPing) + UsNewCode[code] = true + } + } +} +func Send(conn *websocket.Conn, data string) error { + if conn == nil { + applogger.Error("WebSocket sent error: no connection available") + return errors.New("WebSocket sent error: no connection available") + } + if err := conn.WriteMessage(websocket.TextMessage, []byte(data)); err != nil { + applogger.Error("WebSocket sent error: data=%s, error=%s", data, err) + return err + } + return nil +} + +// Remove a client from a topic +func removeClientFromTopic(clients []*Client, c *Client) []*Client { + for i, client := range clients { + if client == c { + return append(clients[:i], clients[i+1:]...) + } + } + return clients +} diff --git a/cmd/websocketservice/websocketclient.go b/cmd/websocketservice/websocketclient.go new file mode 100644 index 0000000..7c1fbc6 --- /dev/null +++ b/cmd/websocketservice/websocketclient.go @@ -0,0 +1,540 @@ +package websocketservice + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/gorilla/websocket" + "github.com/satori/go.uuid" + "go.uber.org/zap" + "log" + "net/http" + "strconv" + "strings" + "sync" + "time" + "wss-pool/config" + "wss-pool/internal" + "wss-pool/internal/gzip" + "wss-pool/internal/model" + red "wss-pool/internal/redis" + "wss-pool/logging/applogger" + "wss-pool/pkg/model/market" + "wss-pool/pkg/model/stock" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 512 + SpotsStatus int = 1 + ContractStatus int = 2 + RedisCONTRACT string = "CONTRACT:LIST" + RedisDIGITAL string = "DIGITAL:LIST" + StatusPass int = 1 +) + +// Define a websocket connection object that contains information for each connection +type User struct { + Id string // Client ID + conn *websocket.Conn // Define websocket link objects + msg chan []byte // Define messages received and distributed + symbol sync.Map // Define a collection of user subscription types + stop chan string + start chan string + mux sync.Mutex +} + +var ( + wsConMap = map[string][]*websocket.Conn{} + mutex = sync.RWMutex{} + msgChan = make(chan []byte) + mutexSpotConn = sync.RWMutex{} +) + +// Define the message body for the output +type Message struct { + ServersId string `json:"serversId,omitempty"` // Service ID (used to receive the client push ID of the third-party push server) + Sender string `json:"sender,omitempty"` // sender + Recipient string `json:"recipient,omitempty"` // recipient + Content any `json:"content,omitempty"` // send content + Symbol string `json:"symbol,omitempty"` // Subscription type + Logo string `json:"logo_link,omitempty"` // Subscription type + KeepDecimal string `json:"keep_decimal"` +} + +var SpotMarketCache = sync.Map{} +var ContractCache = sync.Map{} + +// Define an UpGrader to upgrade a regular HTTP connection to a websocket connection +var up = &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-wss" { + fmt.Println("Request path error") + return false + } + // Verification rules can also be customized according to other needs + return true + }, +} + +func Connect(host, addr string) { + //广播启动 + go write() + http.HandleFunc("/quotes-wss", wsHandle) + url := fmt.Sprintf("%v%v", host, addr) + fmt.Println(url) + err := http.ListenAndServe(url, nil) + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} + +func wsHandle(w http.ResponseWriter, r *http.Request) { + // Obtain a link through the upgraded upgrade tool + conn, err := up.Upgrade(w, r, nil) + if err != nil { + applogger.Info("获取连接失败:%v", err) + return + } + // Register users after successful connection + user := &User{ + Id: uuid.NewV4().String(), + conn: conn, + msg: make(chan []byte), + stop: make(chan string), + start: make(chan string), + symbol: sync.Map{}, + mux: sync.Mutex{}, + } + read(user) + //go hearBeat(user) +} + +// Read the message sent by the client and process the return response +func read(user *User) { + defer func() { + user.conn.Close() + }() + for { + _, msg, err := user.conn.ReadMessage() + if err != nil { + offLine(user.conn) + applogger.Info("user exit:%v", user.conn.RemoteAddr().String()) + return + } + // Process business logic + psgMsg := model.SubMessage(string(msg)) + fmt.Println(psgMsg) + if psgMsg != nil { + switch psgMsg.Type { + case "ping": // Receiving ping + aloneSend(user.conn, []byte(model.ReturnValue("pong"))) + case "subscribe": // Receive subscription + applogger.Info("接收到订阅消息体:", string(msg)) + aloneSend(user.conn, []byte(model.ReturnValue("subscribe success"))) + //获取 服务是否 订阅该数据 + mutex.RLock() + conns, ok := wsConMap[psgMsg.Symbol] + mutex.RUnlock() + if ok { + //查询client是否订阅 + conns = checkClient(conns, user.conn) + } else { + //添加订阅 + conns = make([]*websocket.Conn, 0) + conns = append(conns, user.conn) + } + applogger.Info("psgMsg.Symbol", psgMsg.Symbol) + mutex.Lock() + wsConMap[psgMsg.Symbol] = conns + mutex.Unlock() + if !ok { + go user.userSubscribe(psgMsg.Symbol) + } + case "unSubscribe": // Receive unsubscribe + applogger.Info("Received unsubscribe message body:", string(msg)) + mutex.RLock() + conns, ok := wsConMap[psgMsg.Symbol] + mutex.RUnlock() + if ok { + //取消订阅 + removeWsconn(conns, user.conn, psgMsg.Symbol) + } + aloneSend(user.conn, []byte(model.ReturnValue("unSubscribe success"))) + applogger.Debug("Subscription type after current user deletion:%v", ok) + default: + // TODO: Handling other situations transmitted by customers + } + } + } +} + +// 清理客户端 +func removeWsconn(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:]...) + 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)) +}