Compare commits

..

129 Commits

Author SHA1 Message Date
injoyai
36a8479aa4 优化codes-server,待完成 2025-11-26 17:00:05 +08:00
injoyai
0d1e6b6b51 修复指数判断不全的问题 2025-11-26 16:49:11 +08:00
injoyai
6fd178245a 增加LICENSE文件 2025-11-26 15:41:18 +08:00
injoyai
0efd0735e6 Merge pull request #27 from jingmian/patch-1
Update main.go
2025-11-25 19:08:44 +08:00
镜面王子
b01da71236 Update main.go
NewManageMysql函数不接受*ManageConfig类型参数,而是接受Option函数类型参数
2025-11-25 17:34:49 +08:00
injoyai
26e2479e2f 细节优化 2025-11-21 16:08:46 +08:00
injoyai
6d0125afef 优化manage,修改成Option的方式,老版本的名称从NewManage改成MewManageSqlite 2025-11-21 14:17:09 +08:00
injoyai
2d77d769fd 增加Codes2的Option名称前缀 2025-11-21 14:14:54 +08:00
injoyai
33627c3d6c 增加新版迭代器,Iter和IterYear 2025-11-21 14:13:40 +08:00
injoyai
1ff1ceb8d7 开放interval包,命名为lib 2025-11-21 08:44:37 +08:00
injoyai
233d1b689e 更新etf的判断 2025-11-20 09:31:08 +08:00
injoyai
2a27eea873 增加指数代码的判断 2025-11-20 09:08:22 +08:00
injoyai
fcfb329712 增加指数代码的判断 2025-11-20 08:56:53 +08:00
injoyai
ed2c814fab 增加指数分钟k线的方法 2025-11-19 15:57:02 +08:00
injoyai
fc04b5042a 重新定义接口 2025-11-17 15:48:57 +08:00
injoyai
b2a4c00253 重新定义接口 2025-11-17 15:48:35 +08:00
injoyai
c68c7582bc go版本升级到1.23 2025-11-17 15:40:41 +08:00
injoyai
4e62ee1c5e 更新文档 2025-11-17 14:44:25 +08:00
injoyai
8eeab6f533 增加Trades生成Kline的单数字段,当天数据才有效 2025-11-17 11:05:35 +08:00
injoyai
5fd492e881 优化Codes2 2025-11-17 11:04:55 +08:00
injoyai
fcb6c995ad 优化Codes2 2025-11-17 09:48:20 +08:00
injoyai
e6411858e9 定义ICodes接口 2025-11-17 09:48:06 +08:00
injoyai
f4b2497e92 把manage中的Codes改成接口 2025-11-17 09:47:29 +08:00
injoyai
e250223e57 增加codes2的示例 2025-11-17 09:46:58 +08:00
injoyai
d19cfb4416 优化gbbq 2025-11-17 09:45:33 +08:00
injoyai
530de4fa5a Merge remote-tracking branch 'origin/master' 2025-11-13 16:58:10 +08:00
injoyai
ffe0cb2c92 准备增加流通股总股本 2025-11-13 16:57:53 +08:00
钱纯净
4d1487f0b6 Merge remote-tracking branch 'origin/master' 2025-11-02 21:59:20 +08:00
钱纯净
bd3863d3c5 优化etf的判断,sh51xxxx,sh56xxxx,sh68xxxx,sz15xxxx,sz16xxxx 2025-11-02 21:58:44 +08:00
injoyai
5fb1a212c6 增加tdx.DialWorkday 2025-10-31 10:45:22 +08:00
injoyai
f381d72ec4 优化Workday.Range 2025-10-30 10:45:26 +08:00
injoyai
5fa572f298 把集合竞价合并到931里面 2025-10-28 08:41:04 +08:00
injoyai
61b2e737b3 把集合竞价从931剥离出来到930 2025-10-27 09:09:19 +08:00
injoyai
5654065954 把集合竞价从931剥离出来到930 2025-10-27 08:49:22 +08:00
injoyai
d7dd7fe0bf 优化extend.GetBjCodes,直接输出代码 2025-10-22 14:25:30 +08:00
injoyai
9fb1a3b651 增加SetTimeout 设置超时时间 2025-10-21 09:57:03 +08:00
injoyai
f8a24c0cf1 优化GetHistoryTradeBefore,增加失败重试,共尝试3次,都失败则返回错误 2025-10-21 09:39:16 +08:00
injoyai
ad0abbe6ba 优化GetHistoryTradeFull,需要传入Workday,减少非工作日的查询操作 2025-10-20 16:00:09 +08:00
injoyai
0b35006323 优化GetHistoryTradeFull,需要传入Workday,减少非工作日的查询操作 2025-10-20 15:37:04 +08:00
injoyai
3b823e2e54 优化GetHistoryTradeFull,需要传入Workday,减少非工作日的查询操作 2025-10-20 15:35:20 +08:00
injoyai
5c8091ac26 修复深圳指数历史分时成交价格小10倍的问题 2025-10-20 09:49:01 +08:00
钱纯净
d7b6963bd6 修复workday没有要更新时会报错的问题 "no element on slice when insert" 2025-10-19 22:46:06 +08:00
injoyai
456a0af9a5 增加Manage对mysql的支持 2025-10-16 14:00:31 +08:00
injoyai
84404bcb2c 增加Manage对mysql的支持 2025-10-16 11:00:58 +08:00
injoyai
716e35122f 增加Manage对mysql的支持 2025-10-16 10:54:44 +08:00
injoyai
c4866a2f2e 增加Manage对mysql的支持 2025-10-16 10:47:24 +08:00
injoyai
fa98199dae 增加Codes和Workday对mysql的支持 2025-10-16 10:33:04 +08:00
injoyai
29882ea5c0 修复IsSZStock的判断 2025-10-14 13:07:59 +08:00
injoyai
37eb34beaa 优化 2025-10-14 09:23:45 +08:00
钱纯净
7bf4839310 修改GetHistoryTradeAll为GetHistoryTradeDay,增加GetHistoryTradeFull,获取上市至今的分时成交 2025-10-13 20:04:47 +08:00
钱纯净
cbf56d936d 优化Klines的Kline和Merge 2025-10-13 19:53:19 +08:00
injoyai
80ecdec737 增加extend.ListenCodesHTTP,因为北交所代码是网页爬虫,爬被封ip,感觉好像用处也不大 2025-10-13 15:05:21 +08:00
injoyai
ecf0365879 优化protocol.Trades,能直接生成完整的分时k线,和实际误差很小 2025-10-13 15:01:44 +08:00
injoyai
f1da1182ce 优化protocol.Trades,能直接生成完整的分时k线,和实际误差很小 2025-10-13 15:01:27 +08:00
injoyai
8fb069b855 简单测试北交所数据 2025-09-30 11:44:12 +08:00
injoyai
110eaddc4d 兼容北交所代码到GetCodeAll,方便原先的逻辑不用改动 2025-09-30 11:43:37 +08:00
injoyai
aec2cf1518 优化NewWorkday 2025-09-30 11:42:33 +08:00
injoyai
a596139d3e 移动GetBjCodes至tdx包 2025-09-30 11:42:10 +08:00
injoyai
578617e458 修复北交所(历史)分时成交小10倍的问题 2025-09-30 11:41:13 +08:00
钱纯净
fab9e92fcd 43开头也是北京交易所的 2025-09-27 22:32:22 +08:00
钱纯净
47084b1112 增加同花顺复权数据计算复权因子 2025-09-27 17:39:11 +08:00
钱纯净
2566ef5cec 增加获取北京交易所股票列表,通达信好像没有北交所股票列表 2025-09-27 17:38:43 +08:00
钱纯净
86404db551 Merge remote-tracking branch 'origin/master' 2025-09-21 15:28:48 +08:00
钱纯净
3f3438fca8 增加北京交易所数据 2025-09-21 15:28:32 +08:00
injoyai
1656b41f02 go1.25版本有点不兼容老版本,增加对go1.25版本的支持 2025-08-29 09:43:00 +08:00
injoyai
8090cc7216 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	go.mod
#	go.sum
2025-08-29 09:41:42 +08:00
injoyai
a3169c67da go1.25版本有点不兼容老版本,增加对go1.25版本的支持 2025-08-29 09:41:01 +08:00
钱纯净
0a1a6194af 升级引用库版本 2025-08-24 20:20:53 +08:00
钱纯净
8b3ff3af01 升级引用库版本 2025-08-24 20:09:38 +08:00
钱纯净
c4c0d1dfa4 Merge remote-tracking branch 'origin/master' 2025-08-18 22:31:17 +08:00
钱纯净
73b80857cc 修复未交易(停牌等情况)超过一年复权数据不对的bug,例000633 2025-08-18 22:30:07 +08:00
injoyai
e7e8c6a46a 用60Minute来替代Hour,保留Hour作为60Minute的别名 2025-07-10 09:47:17 +08:00
injoyai
2e17db7faf 增加GetStockAll和GetETFAll,用于实时获取代码 2025-07-08 10:45:37 +08:00
injoyai
accda98f3b protocol.HistoryTradeResp替换成protocol.TradeResp 2025-07-02 15:53:38 +08:00
injoyai
1783643f47 升级引用库版本 2025-07-01 16:13:06 +08:00
injoyai
b9f0951b15 Manage的Codes和Workday共用一个客户端 2025-07-01 16:01:55 +08:00
injoyai
2e4ecd034c Manage的Codes和Workday共用一个客户端 2025-07-01 16:00:16 +08:00
injoyai
9269bca388 增加Manage.RangeStocks和增加Manage.RangeETFs 2025-07-01 15:46:44 +08:00
injoyai
fc1f25c6c9 优化FastHosts,增加Spend(耗时) 2025-06-18 13:51:45 +08:00
injoyai
705f6e4e3a 优化GetTrade示例 2025-06-18 13:50:50 +08:00
injoyai
12079f1ee2 优化FastHosts,增加Spend(耗时) 2025-06-18 13:50:23 +08:00
injoyai
34701c4197 优化PullTrade 2025-06-10 15:34:14 +08:00
钱纯净
1187e1dcdf 优化PullTrade 2025-06-09 22:26:00 +08:00
钱纯净
2adcc9a322 修改名称 2025-06-09 22:23:40 +08:00
injoyai
b035cc7fbf 统一分时成交的结构名称为Trade,和K线Kline一样简洁 2025-06-09 16:14:02 +08:00
injoyai
e58130b3c3 统一分时成交的结构名称为Trade,和K线Kline一样简洁 2025-06-09 16:13:34 +08:00
钱纯净
d840e0d33d Merge remote-tracking branch 'origin/master' 2025-06-09 00:19:43 +08:00
钱纯净
78843ed6b7 增加PullTrade,用于拉取分时成交,然后计算成K线 2025-06-09 00:19:30 +08:00
钱纯净
04378dadf0 增加HistoryMinuteTrades和Klines 2025-06-09 00:18:51 +08:00
钱纯净
e74aeba7b2 增加RangeYear和Range 2025-06-09 00:18:11 +08:00
injoyai
bea9762c50 增加extend.GetTHSDayKlineFull获取前复权和后复权数据,并补充成交金额数据 2025-06-04 14:03:59 +08:00
钱纯净
6750edb3ec 修改同花顺的复权日线数据接口 2025-06-03 20:47:23 +08:00
钱纯净
2efd7e089e 增加同花顺的复权日线数据接口,拉取测试通过(无成交金额数据),后续应该会转移位置 2025-05-29 22:42:14 +08:00
injoyai
6d849a8756 增加tdx.FastHosts,用于测试排序更快的服务器地址 2025-05-29 16:35:49 +08:00
injoyai
bc45a72d8d 修复codes的LastPrice价格不对的问题,只做了简单验证 2025-05-29 09:19:34 +08:00
injoyai
c5f62c15b3 Merge remote-tracking branch 'origin/master' 2025-05-29 08:33:22 +08:00
injoyai
ecaad0e85f 增加GetETFs,用于获取基金代码 2025-05-29 08:33:08 +08:00
钱纯净
9ddaafbf48 测试服务地址,全部测试通过 2025-05-21 22:25:56 +08:00
injoyai
84b0ec6f6c 增加income,用于计算未来收益 2025-05-20 17:10:10 +08:00
injoyai
73d068002c 增加枚举 2025-05-20 17:09:05 +08:00
injoyai
abe39fb2ae 修复盘内下午(13~15点)拉取K线数据的时候,11.30的时间会变成13.00 2025-05-13 14:17:33 +08:00
injoyai
3ebd6e3fb6 修复盘内下午(13~15点)拉取K线数据的时候,11.30的时间会变成13.00 2025-05-13 14:17:08 +08:00
injoyai
1afa0c7c6d 升级引用库版本 2025-05-13 11:17:01 +08:00
injoyai
9bad0908b7 升级引用库版本 2025-05-13 11:16:21 +08:00
injoyai
360d0e861b 升级引用库版本 2025-05-13 11:16:04 +08:00
injoyai
b86e7dcacf 优化首次拉取会报错的问题(未创建目录) 2025-04-18 13:33:59 +08:00
injoyai
af505eb55e 优化首次拉取会报错的问题 2025-04-18 13:29:15 +08:00
钱纯净
630cbb8939 Merge remote-tracking branch 'origin/master' 2025-04-17 23:17:28 +08:00
钱纯净
0b89aadd7f 优化 2025-04-17 23:17:09 +08:00
injoyai
7c8b4989f6 增加pull-kline的mysql版本,待测试 2025-04-16 14:22:10 +08:00
injoyai
b00f1b65d5 默认按股票处理,不用配置DefaultCodes,基金才需要 2025-04-16 10:44:06 +08:00
钱纯净
78e2ead79c 修复分时成交金额不准的问题 2025-04-09 23:18:00 +08:00
钱纯净
f69dd66ecb 测试分时成交数据不准的问题 2025-04-09 23:17:30 +08:00
钱纯净
4ce4adbfea 查询盘口信息得初始化DefaultCodes 2025-04-09 23:16:53 +08:00
injoyai
20de683bca Merge remote-tracking branch 'origin/master' 2025-04-09 17:02:47 +08:00
injoyai
49c6deb9c4 测试分时部分股票值不对的问题 2025-04-09 17:02:33 +08:00
钱纯净
5c0bc2a772 优化 2025-04-01 18:50:00 +08:00
钱纯净
0f75b402bc 优化PullKline 2025-03-26 19:21:31 +08:00
钱纯净
ce6718831c Merge remote-tracking branch 'origin/master' 2025-03-25 20:39:28 +08:00
钱纯净
031d9f6509 调整获取指数示例 2025-03-25 20:39:13 +08:00
injoyai
5e4115d045 修改DefaultCodes,需要手动赋值 2025-03-24 17:01:24 +08:00
injoyai
1cae60c65e 修改NewRangeDial等待间隔为2秒 2025-03-24 16:40:37 +08:00
injoyai
e76043dc29 可以自定义连接方式 2025-03-24 16:33:52 +08:00
injoyai
ff7fc6aba0 增加RangDial等待时间 2025-03-24 16:29:03 +08:00
injoyai
0c27cc4276 默认开启日志,日志等级为Info 2025-03-24 16:28:30 +08:00
injoyai
80d6d6dfc5 优化 2025-03-20 13:37:34 +08:00
injoyai
a27461740a Merge remote-tracking branch 'origin/master' 2025-03-20 13:19:05 +08:00
injoyai
7a9d59f8f1 优化 2025-03-20 13:18:49 +08:00
钱纯净
cba35308f6 优化GetQuote方法 2025-03-19 22:47:46 +08:00
65 changed files with 3935 additions and 622 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 injoyai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,9 +1,8 @@
### 说明
* 参考golang库 [`https://github.com/bensema/gotdx`](https://github.com/bensema/gotdx)
* 参考python库 [`https://github.com/mootdx/mootdx`](https://github.com/mootdx/mootdx)
* 数据入库示例(开发中) [`https://github.com/injoyai/stock`](https://github.com/injoyai/stock)
* 参考 [`https://github.com/bensema/gotdx`](https://github.com/bensema/gotdx)
* 参考 [`https://github.com/mootdx/mootdx`](https://github.com/mootdx/mootdx)
* 参考 [`https://github.com/jing2uo/tdx2db`](https://github.com/jing2uo/tdx2db)
### 如何使用
@@ -60,43 +59,43 @@ func main() {
| IP | 测试时间 | 所属地区 | 运营商 |
|-----------------|------------|------|-----|
| 124.71.187.122 | 2025-03-15 | 上海 | 华为 |
| 122.51.120.217 | 2025-03-15 | 上海 | 腾讯 |
| 111.229.247.189 | 2025-03-15 | 上海 | 腾讯 |
| 124.70.176.52 | 2025-03-15 | 上海 | 华为 |
| 123.60.186.45 | 2025-03-15 | 上海 | 华为 |
| 122.51.232.182 | 2025-03-15 | 上海 | 腾讯 |
| 118.25.98.114 | 2025-03-15 | 上海 | 腾讯 |
| 124.70.199.56 | 2025-03-15 | 上海 | 华为 |
| 121.36.225.169 | 2025-03-15 | 上海 | 华为 |
| 123.60.70.228 | 2025-03-15 | 上海 | 华为 |
| 123.60.73.44 | 2025-03-15 | 上海 | 华为 |
| 124.70.133.119 | 2025-03-15 | 上海 | 华为 |
| 124.71.187.72 | 2025-03-15 | 上海 | 华为 |
| 123.60.84.66 | 2025-03-15 | 上海 | 华为 |
| 124.71.187.122 | 2025-05-21 | 上海 | 华为 |
| 122.51.120.217 | 2025-05-21 | 上海 | 腾讯 |
| 111.229.247.189 | 2025-05-21 | 上海 | 腾讯 |
| 124.70.176.52 | 2025-05-21 | 上海 | 华为 |
| 123.60.186.45 | 2025-05-21 | 上海 | 华为 |
| 122.51.232.182 | 2025-05-21 | 上海 | 腾讯 |
| 118.25.98.114 | 2025-05-21 | 上海 | 腾讯 |
| 124.70.199.56 | 2025-05-21 | 上海 | 华为 |
| 121.36.225.169 | 2025-05-21 | 上海 | 华为 |
| 123.60.70.228 | 2025-05-21 | 上海 | 华为 |
| 123.60.73.44 | 2025-05-21 | 上海 | 华为 |
| 124.70.133.119 | 2025-05-21 | 上海 | 华为 |
| 124.71.187.72 | 2025-05-21 | 上海 | 华为 |
| 123.60.84.66 | 2025-05-21 | 上海 | 华为 |
| | | | |
| 121.36.54.217 | 2025-03-15 | 北京 | 华为 |
| 121.36.81.195 | 2025-03-15 | 北京 | 华为 |
| 123.249.15.60 | 2025-03-15 | 北京 | 华为 |
| 124.70.75.113 | 2025-03-15 | 北京 | 华为 |
| 120.46.186.223 | 2025-03-15 | 北京 | 华为 |
| 124.70.22.210 | 2025-03-15 | 北京 | 华为 |
| 139.9.133.247 | 2025-03-15 | 北京 | 华为 |
| 121.36.54.217 | 2025-05-21 | 北京 | 华为 |
| 121.36.81.195 | 2025-05-21 | 北京 | 华为 |
| 123.249.15.60 | 2025-05-21 | 北京 | 华为 |
| 124.70.75.113 | 2025-05-21 | 北京 | 华为 |
| 120.46.186.223 | 2025-05-21 | 北京 | 华为 |
| 124.70.22.210 | 2025-05-21 | 北京 | 华为 |
| 139.9.133.247 | 2025-05-21 | 北京 | 华为 |
| | | | |
| 124.71.85.110 | 2025-03-15 | 广州 | 华为 |
| 139.9.51.18 | 2025-03-15 | 广州 | 华为 |
| 139.159.239.163 | 2025-03-15 | 广州 | 华为 |
| 124.71.9.153 | 2025-03-15 | 广州 | 华为 |
| 116.205.163.254 | 2025-03-15 | 广州 | 华为 |
| 116.205.171.132 | 2025-03-15 | 广州 | 华为 |
| 116.205.183.150 | 2025-03-15 | 广州 | 华为 |
| 111.230.186.52 | 2025-03-15 | 广州 | 腾讯 |
| 110.41.4.4 | 2025-03-15 | 广州 | 华为 |
| 110.41.2.72 | 2025-03-15 | 广州 | 华为 |
| 110.41.154.219 | 2025-03-15 | 广州 | 华为 |
| 110.41.147.114 | 2025-03-15 | 广州 | 华为 |
| 124.71.85.110 | 2025-05-21 | 广州 | 华为 |
| 139.9.51.18 | 2025-05-21 | 广州 | 华为 |
| 139.159.239.163 | 2025-05-21 | 广州 | 华为 |
| 124.71.9.153 | 2025-05-21 | 广州 | 华为 |
| 116.205.163.254 | 2025-05-21 | 广州 | 华为 |
| 116.205.171.132 | 2025-05-21 | 广州 | 华为 |
| 116.205.183.150 | 2025-05-21 | 广州 | 华为 |
| 111.230.186.52 | 2025-05-21 | 广州 | 腾讯 |
| 110.41.4.4 | 2025-05-21 | 广州 | 华为 |
| 110.41.2.72 | 2025-05-21 | 广州 | 华为 |
| 110.41.154.219 | 2025-05-21 | 广州 | 华为 |
| 110.41.147.114 | 2025-05-21 | 广州 | 华为 |
| | | | |
| 119.97.185.59 | 2025-03-15 | 武汉 | 电信 |
| 119.97.185.59 | 2025-05-21 | 武汉 | 电信 |

327
client.go
View File

@@ -3,16 +3,29 @@ package tdx
import (
"errors"
"fmt"
"github.com/injoyai/base/maps"
"github.com/injoyai/base/maps/wait/v2"
"github.com/injoyai/conv"
"github.com/injoyai/ios"
"github.com/injoyai/ios/client"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/protocol"
"runtime/debug"
"sync/atomic"
"time"
"github.com/injoyai/base/maps"
"github.com/injoyai/base/maps/wait"
"github.com/injoyai/conv"
"github.com/injoyai/ios"
"github.com/injoyai/ios/client"
"github.com/injoyai/ios/module/common"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/lib/bse"
"github.com/injoyai/tdx/protocol"
)
const (
LevelNone = common.LevelNone
LevelDebug = common.LevelDebug
LevelWrite = common.LevelWrite
LevelRead = common.LevelRead
LevelInfo = common.LevelInfo
LevelError = common.LevelError
LevelAll = common.LevelAll
)
// WithDebug 是否打印通讯数据
@@ -22,6 +35,12 @@ func WithDebug(b ...bool) client.Option {
}
}
func WithLevel(level int) client.Option {
return func(c *client.Client) {
c.Logger.SetLevel(level)
}
}
// WithRedial 断线重连
func WithRedial(b ...bool) client.Option {
return func(c *client.Client) {
@@ -31,6 +50,7 @@ func WithRedial(b ...bool) client.Option {
// DialDefault 默认连接方式
func DialDefault(op ...client.Option) (cli *Client, err error) {
op = append([]client.Option{WithRedial()}, op...)
return DialHostsRange(Hosts, op...)
}
@@ -63,21 +83,24 @@ func DialWith(dial ios.DialFunc, op ...client.Option) (cli *Client, err error) {
}
cli.Client, err = client.Dial(dial, func(c *client.Client) {
c.Logger.Debug(false) //关闭日志打印
c.Logger.Debug(true) //关闭日志打印
c.Logger.SetLevel(LevelInfo) //设置日志级别
c.Logger.WithHEX() //以HEX显示
c.SetOption(op...) //自定义选项
c.Event.OnReadFrom = protocol.ReadFrom //分包
c.Event.OnDealMessage = cli.handlerDealMessage //解析数据并处理
//无数据超时时间是60秒,30秒发送一个心跳包
c.GoTimerWriter(30*time.Second, func(w ios.MoreWriter) error {
bs := protocol.MHeart.Frame().Bytes()
_, err := w.Write(bs)
return err
})
f := protocol.MConnect.Frame()
if _, err = c.Write(f.Bytes()); err != nil {
c.Close()
c.Event.OnConnected = func(c *client.Client) error {
//无数据超时时间是60秒,30秒发送一个心跳包
c.GoTimerWriter(30*time.Second, func(w ios.MoreWriter) error {
bs := protocol.MHeart.Frame().Bytes()
_, err := w.Write(bs)
return err
})
f := protocol.MConnect.Frame()
if _, err = c.Write(f.Bytes()); err != nil {
c.Close()
}
return nil
}
})
if err != nil {
@@ -86,16 +109,6 @@ func DialWith(dial ios.DialFunc, op ...client.Option) (cli *Client, err error) {
go cli.Client.Run()
/*
部分接口需要通过代码信息计算得出
*/
codesOnce.Do(func() {
//初始化代码管理
if DefaultCodes == nil {
DefaultCodes, err = NewCodes(cli, "./codes.db")
}
})
return cli, err
}
@@ -148,10 +161,10 @@ func (this *Client) handlerDealMessage(c *client.Client, msg ios.Acker) {
resp, err = protocol.MHistoryMinute.Decode(f.Data)
case protocol.TypeMinuteTrade:
resp, err = protocol.MMinuteTrade.Decode(f.Data, conv.String(val))
resp, err = protocol.MTrade.Decode(f.Data, val.(protocol.TradeCache))
case protocol.TypeHistoryMinuteTrade:
resp, err = protocol.MHistoryMinuteTrade.Decode(f.Data, conv.String(val))
resp, err = protocol.MHistoryTrade.Decode(f.Data, val.(protocol.TradeCache))
case protocol.TypeKline:
resp, err = protocol.MKline.Decode(f.Data, val.(protocol.KlineCache))
@@ -170,6 +183,11 @@ func (this *Client) handlerDealMessage(c *client.Client, msg ios.Acker) {
}
// SetTimeout 设置超时时间
func (this *Client) SetTimeout(t time.Duration) {
this.Wait.SetTimeout(t)
}
// SendFrame 发送数据,并等待响应
func (this *Client) SendFrame(f *protocol.Frame, cache ...any) (any, error) {
f.MsgID = atomic.AddUint32(&this.msgID, 1)
@@ -209,6 +227,26 @@ func (this *Client) GetCode(exchange protocol.Exchange, start uint16) (*protocol
// GetCodeAll 通过多次请求的方式获取全部证券代码
func (this *Client) GetCodeAll(exchange protocol.Exchange) (*protocol.CodeResp, error) {
resp := &protocol.CodeResp{}
//通达信没有北交所代码列表,通过爬虫的方式从北交所官网获取,放在这里是为了方便业务逻辑
//不放在extend包时防止循环引用
//todo 这是临时方案,等通达信有北交所代码列表时再改
if exchange == protocol.ExchangeBJ {
codes, err := bse.GetCodes()
if err != nil {
return nil, err
}
resp.Count = uint16(len(codes))
for _, v := range codes {
resp.List = append(resp.List, &protocol.Code{
Code: v.Code,
Name: v.Name,
LastPrice: v.Last,
})
}
return resp, nil
}
size := uint16(1000)
for start := uint16(0); ; start += size {
r, err := this.GetCode(exchange, start)
@@ -224,11 +262,55 @@ func (this *Client) GetCodeAll(exchange protocol.Exchange) (*protocol.CodeResp,
return resp, nil
}
// GetStockAll 获取所有股票代码
func (this *Client) GetStockAll() ([]string, error) {
ls := []string(nil)
for _, ex := range []protocol.Exchange{protocol.ExchangeSH, protocol.ExchangeSZ, protocol.ExchangeBJ} {
resp, err := this.GetCodeAll(ex)
if err != nil {
return nil, err
}
for _, v := range resp.List {
if protocol.IsStock(v.Code) {
ls = append(ls, v.Code)
}
}
}
return ls, nil
}
// GetETFAll 获取所有ETF代码
func (this *Client) GetETFAll() ([]string, error) {
ls := []string(nil)
for _, ex := range []protocol.Exchange{protocol.ExchangeSH, protocol.ExchangeSZ} {
resp, err := this.GetCodeAll(ex)
if err != nil {
return nil, err
}
for _, v := range resp.List {
if protocol.IsETF(v.Code) {
ls = append(ls, v.Code)
}
}
}
return ls, nil
}
// GetQuote 获取盘口五档报价
func (this *Client) GetQuote(codes ...string) (protocol.QuotesResp, error) {
if DefaultCodes == nil {
return nil, errors.New("DefaultCodes未初始化")
for i := range codes {
//如果是股票代码,则加上前缀
codes[i] = protocol.AddPrefix(codes[i])
if !protocol.IsStock(codes[i]) {
if DefaultCodes == nil {
return nil, errors.New("DefaultCodes未初始化")
}
//不是股票代码的话根据codes的信息加上前缀
//codes[i] = DefaultCodes.AddExchange(codes[i])
codes[i] = protocol.AddPrefix(codes[i])
}
}
f, err := protocol.MQuote.Frame(codes...)
if err != nil {
return nil, err
@@ -244,26 +326,25 @@ func (this *Client) GetQuote(codes ...string) (protocol.QuotesResp, error) {
if len(quotes) != len(codes) {
return nil, fmt.Errorf("预期%d个实际%d个", len(codes), len(quotes))
}
if DefaultCodes == nil {
return nil, errors.New("DefaultCodes未初始化")
}
for i, code := range codes {
m := DefaultCodes.Get(code)
if m == nil {
return nil, fmt.Errorf("未查询到代码[%s]相关信息", code)
}
for ii, v := range quotes[i].SellLevel {
quotes[i].SellLevel[ii].Price = m.Price(v.Price)
}
for ii, v := range quotes[i].BuyLevel {
quotes[i].BuyLevel[ii].Price = m.Price(v.Price)
}
quotes[i].K = protocol.K{
Last: m.Price(quotes[i].K.Last),
Open: m.Price(quotes[i].K.Open),
High: m.Price(quotes[i].K.High),
Low: m.Price(quotes[i].K.Low),
Close: m.Price(quotes[i].K.Close),
if !protocol.IsStock(code) {
m := DefaultCodes.Get(code)
if m == nil {
return nil, fmt.Errorf("未查询到代码[%s]相关信息", code)
}
for ii, v := range quotes[i].SellLevel {
quotes[i].SellLevel[ii].Price = m.Price(v.Price)
}
for ii, v := range quotes[i].BuyLevel {
quotes[i].BuyLevel[ii].Price = m.Price(v.Price)
}
quotes[i].K = protocol.K{
Last: m.Price(quotes[i].K.Last),
Open: m.Price(quotes[i].K.Open),
High: m.Price(quotes[i].K.High),
Low: m.Price(quotes[i].K.Low),
Close: m.Price(quotes[i].K.Close),
}
}
}
}
@@ -299,23 +380,34 @@ func (this *Client) GetHistoryMinute(date, code string) (*protocol.MinuteResp, e
return result.(*protocol.MinuteResp), nil
}
func (this *Client) GetTrade(code string, start, count uint16) (*protocol.TradeResp, error) {
return this.GetMinuteTrade(code, start, count)
}
// GetMinuteTrade 获取分时交易详情,服务器最多返回1800条,count-start<=1800
func (this *Client) GetMinuteTrade(code string, start, count uint16) (*protocol.MinuteTradeResp, error) {
func (this *Client) GetMinuteTrade(code string, start, count uint16) (*protocol.TradeResp, error) {
code = protocol.AddPrefix(code)
f, err := protocol.MMinuteTrade.Frame(code, start, count)
f, err := protocol.MTrade.Frame(code, start, count)
if err != nil {
return nil, err
}
result, err := this.SendFrame(f, code)
result, err := this.SendFrame(f, protocol.TradeCache{
Date: time.Now().Format("20060102"),
Code: code,
})
if err != nil {
return nil, err
}
return result.(*protocol.MinuteTradeResp), nil
return result.(*protocol.TradeResp), nil
}
func (this *Client) GetTradeAll(code string) (*protocol.TradeResp, error) {
return this.GetMinuteTradeAll(code)
}
// GetMinuteTradeAll 获取分时全部交易详情,todo 只做参考 因为交易实时在进行,然后又是分页读取的,所以会出现读取间隔内产生的交易会丢失
func (this *Client) GetMinuteTradeAll(code string) (*protocol.MinuteTradeResp, error) {
resp := &protocol.MinuteTradeResp{}
func (this *Client) GetMinuteTradeAll(code string) (*protocol.TradeResp, error) {
resp := &protocol.TradeResp{}
size := uint16(1800)
for start := uint16(0); ; start += size {
r, err := this.GetMinuteTrade(code, start, size)
@@ -332,26 +424,71 @@ func (this *Client) GetMinuteTradeAll(code string) (*protocol.MinuteTradeResp, e
return resp, nil
}
// GetHistoryMinuteTrade 获取历史分时交易
// 只能获取昨天及之前的数据,服务器最多返回2000条,count-start<=2000,如果日期输入错误,则返回0
// 历史数据sz000001在20241116只能查到21111112,13年差几天,3141天,或者其他规则
func (this *Client) GetHistoryMinuteTrade(date, code string, start, count uint16) (*protocol.HistoryMinuteTradeResp, error) {
code = protocol.AddPrefix(code)
f, err := protocol.MHistoryMinuteTrade.Frame(date, code, start, count)
if err != nil {
return nil, err
}
result, err := this.SendFrame(f, code)
if err != nil {
return nil, err
}
return result.(*protocol.HistoryMinuteTradeResp), nil
func (this *Client) GetHistoryTrade(date, code string, start, count uint16) (*protocol.TradeResp, error) {
return this.GetHistoryMinuteTrade(date, code, start, count)
}
// GetHistoryMinuteTradeAll 获取历史分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
// 历史数据sz000001在20241116只能查到21111112,13年差几天,3141天,或者其他规则
func (this *Client) GetHistoryMinuteTradeAll(date, code string) (*protocol.HistoryMinuteTradeResp, error) {
resp := &protocol.HistoryMinuteTradeResp{}
// GetHistoryMinuteTrade 获取历史分时交易
// 只能获取昨天及之前的数据,服务器最多返回2000条,count-start<=2000,如果日期输入错误,则返回0
// 历史数据只能查到20000609
func (this *Client) GetHistoryMinuteTrade(date, code string, start, count uint16) (*protocol.TradeResp, error) {
code = protocol.AddPrefix(code)
f, err := protocol.MHistoryTrade.Frame(date, code, start, count)
if err != nil {
return nil, err
}
result, err := this.SendFrame(f, protocol.TradeCache{
Date: date,
Code: code,
})
if err != nil {
return nil, err
}
return result.(*protocol.TradeResp), nil
}
// GetHistoryTradeFull 获取上市至今的分时成交
func (this *Client) GetHistoryTradeFull(code string, w *Workday) (protocol.Trades, error) {
return this.GetHistoryTradeBefore(code, w, time.Now())
}
// GetHistoryTradeBefore 获取上市至今的分时成交
func (this *Client) GetHistoryTradeBefore(code string, w *Workday, before time.Time) (protocol.Trades, error) {
ls := protocol.Trades(nil)
resp, err := this.GetKlineMonthAll(code)
if err != nil {
return nil, err
}
if len(resp.List) == 0 {
return nil, nil
}
start := time.Date(resp.List[0].Time.Year(), resp.List[0].Time.Month(), 1, 0, 0, 0, 0, resp.List[0].Time.Location())
var res *protocol.TradeResp
w.Range(start, before, func(t time.Time) bool {
for i := 0; i < 3; i++ {
res, err = this.GetHistoryTradeDay(t.Format("20060102"), code)
if err == nil {
break
}
}
if err != nil {
return false
}
ls = append(ls, res.List...)
return true
})
return ls, err
}
// GetHistoryTradeDay 获取历史某天分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
func (this *Client) GetHistoryTradeDay(date, code string) (*protocol.TradeResp, error) {
return this.GetHistoryMinuteTradeDay(date, code)
}
// GetHistoryMinuteTradeDay 获取历史某天分时全部交易,通过多次请求来拼接,只能获取昨天及之前的数据
// 历史数据只能查到20000609
func (this *Client) GetHistoryMinuteTradeDay(date, code string) (*protocol.TradeResp, error) {
resp := &protocol.TradeResp{}
size := uint16(2000)
for start := uint16(0); ; start += size {
r, err := this.GetHistoryMinuteTrade(date, code, start, size)
@@ -424,6 +561,26 @@ func (this *Client) GetIndexAll(Type uint8, code string) (*protocol.KlineResp, e
return this.GetIndexUntil(Type, code, func(k *protocol.Kline) bool { return false })
}
func (this *Client) GetIndexMinute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKlineMinute, code, start, count)
}
func (this *Client) GetIndex5Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKline5Minute, code, start, count)
}
func (this *Client) GetIndex15Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKline15Minute, code, start, count)
}
func (this *Client) GetIndex30Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKline30Minute, code, start, count)
}
func (this *Client) GetIndex60Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKline60Minute, code, start, count)
}
func (this *Client) GetIndexDay(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetIndex(protocol.TypeKlineDay, code, start, count)
}
@@ -564,18 +721,32 @@ func (this *Client) GetKline30MinuteUntil(code string, f func(k *protocol.Kline)
return this.GetKlineUntil(protocol.TypeKline30Minute, code, f)
}
// GetKline60Minute 获取60分钟k线数据
func (this *Client) GetKline60Minute(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetKline(protocol.TypeKline60Minute, code, start, count)
}
// GetKlineHour 获取小时k线数据
func (this *Client) GetKlineHour(code string, start, count uint16) (*protocol.KlineResp, error) {
return this.GetKline(protocol.TypeKlineHour, code, start, count)
return this.GetKline(protocol.TypeKline60Minute, code, start, count)
}
// GetKline60MinuteAll 获取60分钟k线全部数据
func (this *Client) GetKline60MinuteAll(code string) (*protocol.KlineResp, error) {
return this.GetKlineAll(protocol.TypeKline60Minute, code)
}
// GetKlineHourAll 获取小时k线全部数据
func (this *Client) GetKlineHourAll(code string) (*protocol.KlineResp, error) {
return this.GetKlineAll(protocol.TypeKlineHour, code)
return this.GetKlineAll(protocol.TypeKline60Minute, code)
}
func (this *Client) GetKline60MinuteUntil(code string, f func(k *protocol.Kline) bool) (*protocol.KlineResp, error) {
return this.GetKlineUntil(protocol.TypeKline60Minute, code, f)
}
func (this *Client) GetKlineHourUntil(code string, f func(k *protocol.Kline) bool) (*protocol.KlineResp, error) {
return this.GetKlineUntil(protocol.TypeKlineHour, code, f)
return this.GetKlineUntil(protocol.TypeKline60Minute, code, f)
}
// GetKlineDay 获取日k线数据

263
codes.go
View File

@@ -1,26 +1,63 @@
package tdx
import (
"github.com/injoyai/conv"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"errors"
"iter"
"math"
"os"
"path/filepath"
"sync"
"time"
"github.com/injoyai/conv"
"github.com/injoyai/ios/client"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"xorm.io/core"
"xorm.io/xorm"
)
// 增加单例,部分数据需要通过Codes里面的信息计算
var (
DefaultCodes *Codes
codesOnce sync.Once
)
type ICodes interface {
Iter() iter.Seq2[string, *CodeModel]
Get(code string) *CodeModel
GetName(code string) string
GetStocks(limit ...int) CodeModels
GetStockCodes(limit ...int) []string
GetETFs(limit ...int) CodeModels
GetETFCodes(limit ...int) []string
GetIndexes(limits ...int) CodeModels
GetIndexCodes(limits ...int) []string
}
func NewCodes(c *Client, filename string) (*Codes, error) {
// DefaultCodes 增加单例,部分数据需要通过Codes里面的信息计算
var DefaultCodes *Codes
func DialCodes(filename string, op ...client.Option) (*Codes, error) {
c, err := DialDefault(op...)
if err != nil {
return nil, err
}
return NewCodesSqlite(c, filename)
}
func NewCodesMysql(c *Client, dsn string) (*Codes, error) {
//连接数据库
db, err := xorm.NewEngine("mysql", dsn)
if err != nil {
return nil, err
}
db.SetMapper(core.SameMapper{})
return NewCodes(c, db)
}
func NewCodesSqlite(c *Client, filenames ...string) (*Codes, error) {
//如果没有指定文件名,则使用默认
defaultFilename := filepath.Join(DefaultDatabaseDir, "codes.db")
filename := conv.Default(defaultFilename, filenames...)
filename = conv.Select(filename == "", defaultFilename, filename)
//如果文件夹不存在就创建
dir, _ := filepath.Split(filename)
@@ -33,6 +70,12 @@ func NewCodes(c *Client, filename string) (*Codes, error) {
}
db.SetMapper(core.SameMapper{})
db.DB().SetMaxOpenConns(1)
return NewCodes(c, db)
}
func NewCodes(c *Client, db *xorm.Engine) (*Codes, error) {
if err := db.Sync2(new(CodeModel)); err != nil {
return nil, err
}
@@ -42,10 +85,11 @@ func NewCodes(c *Client, filename string) (*Codes, error) {
update := new(UpdateModel)
{ //查询或者插入一条数据
has, err := db.Get(update)
has, err := db.Where("`Key`=?", "codes").Get(update)
if err != nil {
return nil, err
} else if !has {
update.Key = "codes"
if _, err := db.Insert(update); err != nil {
return nil, err
}
@@ -59,9 +103,10 @@ func NewCodes(c *Client, filename string) (*Codes, error) {
{ //设置定时器,每天早上9点更新数据
task := cron.New(cron.WithSeconds())
task.AddFunc("0 0 9 * * *", func() {
task.AddFunc("10 0 9 * * *", func() {
for i := 0; i < 3; i++ {
if err := cc.Update(); err == nil {
err := cc.Update()
if err == nil {
return
}
logs.Err(err)
@@ -92,12 +137,28 @@ func NewCodes(c *Client, filename string) (*Codes, error) {
return cc, cc.Update(true)
}
var _ ICodes = &Codes{}
type Codes struct {
*Client //客户端
db *xorm.Engine //数据库实例
Map map[string]*CodeModel //股票缓存
list []*CodeModel //列表方式缓存
exchanges map[string]string //交易所缓存
exchanges map[string][]string //交易所缓存
}
func (this *Codes) Get(code string) *CodeModel {
return this.Map[code]
}
func (this *Codes) Iter() iter.Seq2[string, *CodeModel] {
return func(yield func(string, *CodeModel) bool) {
for _, code := range this.list {
if !yield(code.FullCode(), code) {
break
}
}
}
}
// GetName 获取股票名称
@@ -109,13 +170,13 @@ func (this *Codes) GetName(code string) string {
}
// GetStocks 获取股票代码,sh6xxx sz0xx sz30xx
func (this *Codes) GetStocks(limits ...int) []string {
limit := conv.DefaultInt(-1, limits...)
ls := []string(nil)
func (this *Codes) GetStocks(limits ...int) CodeModels {
limit := conv.Default(-1, limits...)
ls := []*CodeModel(nil)
for _, m := range this.list {
code := m.FullCode()
if protocol.IsStock(code) {
ls = append(ls, code)
ls = append(ls, m)
}
if limit > 0 && len(ls) >= limit {
break
@@ -124,34 +185,53 @@ func (this *Codes) GetStocks(limits ...int) []string {
return ls
}
func (this *Codes) Get(code string) *CodeModel {
return this.Map[code]
func (this *Codes) GetStockCodes(limits ...int) []string {
return this.GetStocks(limits...).Codes()
}
// GetExchange 获取股票交易所,这里的参数不需要带前缀
func (this *Codes) GetExchange(code string) protocol.Exchange {
if len(code) == 6 {
switch {
case code[:1] == "6":
return protocol.ExchangeSH
case code[:1] == "0":
return protocol.ExchangeSZ
case code[:2] == "30":
return protocol.ExchangeSZ
// GetETFs 获取基金代码,sz159xxx,sh510xxx,sh511xxx
func (this *Codes) GetETFs(limits ...int) CodeModels {
limit := conv.Default(-1, limits...)
ls := []*CodeModel(nil)
for _, m := range this.list {
code := m.FullCode()
if protocol.IsETF(code) {
ls = append(ls, m)
}
if limit > 0 && len(ls) >= limit {
break
}
}
exchange := this.exchanges[code]
if len(code) == 8 {
exchange = code[0:2]
}
switch exchange {
case protocol.ExchangeSH.String():
return protocol.ExchangeSH
case protocol.ExchangeSZ.String():
return protocol.ExchangeSZ
default:
return protocol.ExchangeSH
return ls
}
// GetETFCodes 获取基金代码,sz159xxx,sh510xxx,sh511xxx
func (this *Codes) GetETFCodes(limits ...int) []string {
return this.GetETFs(limits...).Codes()
}
// GetIndexes 获取基金代码,sz159xxx,sh510xxx,sh511xxx
func (this *Codes) GetIndexes(limits ...int) CodeModels {
limit := conv.Default(-1, limits...)
ls := []*CodeModel(nil)
for _, m := range this.list {
code := m.FullCode()
if protocol.IsIndex(code) {
ls = append(ls, m)
}
if limit > 0 && len(ls) >= limit {
break
}
}
return ls
}
func (this *Codes) GetIndexCodes(limits ...int) []string {
return this.GetIndexes(limits...).Codes()
}
func (this *Codes) AddExchange(code string) string {
return protocol.AddPrefix(code)
}
// Update 更新数据,从服务器或者数据库
@@ -161,22 +241,26 @@ func (this *Codes) Update(byDB ...bool) error {
return err
}
codeMap := make(map[string]*CodeModel)
exchanges := make(map[string]string)
exchanges := make(map[string][]string)
for _, code := range codes {
codeMap[code.Exchange+code.Code] = code
exchanges[code.Code] = code.Exchange
exchanges[code.Code] = append(exchanges[code.Code], code.Exchange)
}
this.Map = codeMap
this.list = codes
this.exchanges = exchanges
//更新时间
_, err = this.db.Update(&UpdateModel{Time: time.Now().Unix()})
_, err = this.db.Where("`Key`=?", "codes").Update(&UpdateModel{Time: time.Now().Unix()})
return err
}
// GetCodes 更新股票并返回结果
func (this *Codes) GetCodes(byDatabase bool) ([]*CodeModel, error) {
if this.Client == nil {
return nil, errors.New("client is nil")
}
//2. 查询数据库所有股票
list := []*CodeModel(nil)
if err := this.db.Find(&list); err != nil {
@@ -196,7 +280,7 @@ func (this *Codes) GetCodes(byDatabase bool) ([]*CodeModel, error) {
//3. 从服务器获取所有股票代码
insert := []*CodeModel(nil)
update := []*CodeModel(nil)
for _, exchange := range []protocol.Exchange{protocol.ExchangeSH, protocol.ExchangeSZ} {
for _, exchange := range []protocol.Exchange{protocol.ExchangeSH, protocol.ExchangeSZ, protocol.ExchangeBJ} {
resp, err := this.Client.GetCodeAll(exchange)
if err != nil {
return nil, err
@@ -229,29 +313,52 @@ func (this *Codes) GetCodes(byDatabase bool) ([]*CodeModel, error) {
}
}
//4. 插入或者更新数据库
err := NewSessionFunc(this.db, func(session *xorm.Session) error {
for _, v := range insert {
if _, err := session.Insert(v); err != nil {
return err
switch this.db.Dialect().URI().DBType {
case "mysql":
// 1⃣ 清空
if _, err := this.db.Exec("TRUNCATE TABLE codes"); err != nil {
return nil, err
}
data := append(insert, update...)
// 2⃣ 直接批量插入
batchSize := 3000 // 8000(2m16s) 5000(43s) 3000(11s) 1000(59s)
for i := 0; i < len(data); i += batchSize {
end := i + batchSize
if end > len(data) {
end = len(data)
}
slice := conv.Array(data[i:end])
if _, err := this.db.Insert(slice); err != nil {
return nil, err
}
}
for _, v := range update {
if _, err := session.Where("Exchange=? and Code=? ", v.Exchange, v.Code).Cols("Name,LastPrice").Update(v); err != nil {
return err
case "sqlite3":
//4. 插入或者更新数据库
err := NewSessionFunc(this.db, func(session *xorm.Session) error {
for _, v := range insert {
if _, err := session.Insert(v); err != nil {
return err
}
}
for _, v := range update {
if _, err := session.Where("Exchange=? and Code=? ", v.Exchange, v.Code).Cols("Name,LastPrice").Update(v); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
return nil
})
if err != nil {
return nil, err
}
return list, nil
}
type UpdateModel struct {
Key string
Time int64 //更新时间
}
@@ -260,27 +367,35 @@ func (*UpdateModel) TableName() string {
}
type CodeModel struct {
ID int64 `json:"id"` //主键
Name string `json:"name"` //名称,有时候名称会变,例STxxx
Code string `json:"code" xorm:"index"` //代码
Exchange string `json:"exchange" xorm:"index"` //交易所
Multiple uint16 `json:"multiple"` //倍数
Decimal int8 `json:"decimal"` //小数位
LastPrice float64 `json:"lastPrice"` //昨收价格
EditDate int64 `json:"editDate" xorm:"updated"` //修改时间
InDate int64 `json:"inDate" xorm:"created"` //创建时间
ID int64 `json:"id"` //主键
Name string `json:"name"` //名称,有时候名称会变,例STxxx
Code string `json:"code" xorm:"index"` //代码
Exchange string `json:"exchange" xorm:"index"` //交易所
Multiple uint16 `json:"multiple"` //倍数
Decimal int8 `json:"decimal"` //小数位
LastPrice float64 `json:"lastPrice"` //昨收价格
FloatStock float64 `json:"floatStock"` //流通股
TotalStock float64 `json:"totalStock"` //总股本
EditDate int64 `json:"editDate" xorm:"updated"` //修改时间
InDate int64 `json:"inDate" xorm:"created"` //创建时间
}
func (*CodeModel) TableName() string {
return "codes"
}
// FullCode 获取完整代码 sz000001
func (this *CodeModel) FullCode() string {
return this.Exchange + this.Code
}
// Turnover 换手率
func (this *CodeModel) Turnover(volume float64) float64 {
return volume / (this.FloatStock * 100)
}
func (this *CodeModel) Price(p protocol.Price) protocol.Price {
return p * protocol.Price(math.Pow10(int(3-this.Decimal)))
return protocol.Price(float64(p) * math.Pow10(int(2-this.Decimal)))
}
func NewSessionFunc(db *xorm.Engine, fn func(session *xorm.Session) error) error {
@@ -300,3 +415,13 @@ func NewSessionFunc(db *xorm.Engine, fn func(session *xorm.Session) error) error
}
return nil
}
type CodeModels []*CodeModel
func (this CodeModels) Codes() []string {
codes := make([]string, len(this))
for i, v := range this {
codes[i] = v.FullCode()
}
return codes
}

380
codes_v2.go Normal file
View File

@@ -0,0 +1,380 @@
package tdx
import (
"errors"
"iter"
"os"
"path/filepath"
"time"
"github.com/injoyai/base/maps"
"github.com/injoyai/base/types"
"github.com/injoyai/conv"
"github.com/injoyai/ios"
"github.com/injoyai/ios/client"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/lib/gbbq"
"github.com/injoyai/tdx/lib/xorms"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"xorm.io/xorm"
)
type Codes2Option func(*Codes2)
func WithCodes2Database(filename string) Codes2Option {
return func(c *Codes2) {
c.dbFilename = filename
}
}
func WithCodes2TempDir(dir string) Codes2Option {
return func(c *Codes2) {
c.tempDir = dir
}
}
func WithCodes2Spec(spec string) Codes2Option {
return func(c *Codes2) {
c.spec = spec
}
}
func WithCodes2UpdateKey(key string) Codes2Option {
return func(c *Codes2) {
c.updateKey = key
}
}
func WithCodes2Retry(retry int) Codes2Option {
return func(c *Codes2) {
c.retry = retry
}
}
func WithCodes2Client(c *Client) Codes2Option {
return func(cs *Codes2) {
cs.c = c
}
}
func WithCodes2Dial(dial ios.DialFunc, op ...client.Option) Codes2Option {
return func(c *Codes2) {
c.dial = dial
c.dialOption = op
}
}
func WithCodes2DialOption(op ...client.Option) Codes2Option {
return func(c *Codes2) {
c.dialOption = op
}
}
func NewCodes2(op ...Codes2Option) (*Codes2, error) {
cs := &Codes2{
dbFilename: filepath.Join(DefaultDatabaseDir, "codes2.db"),
tempDir: filepath.Join(DefaultDataDir, "temp"),
spec: "10 0 9 * * *",
updateKey: "codes",
retry: DefaultRetry,
dial: NewRangeDial(Hosts),
dialOption: nil,
m: maps.NewGeneric[string, *CodeModel](),
}
for _, o := range op {
o(cs)
}
os.MkdirAll(cs.tempDir, 0777)
var err error
// 初始化连接
if cs.c == nil {
cs.c, err = DialWith(cs.dial, cs.dialOption...)
if err != nil {
return nil, err
}
}
// 初始化数据库
cs.db, err = xorms.NewSqlite(cs.dbFilename)
if err != nil {
return nil, err
}
if err = cs.db.Sync2(new(CodeModel), new(UpdateModel)); err != nil {
return nil, err
}
// 立即更新
err = cs.Update()
if err != nil {
return nil, err
}
// 定时更新
cr := cron.New(cron.WithSeconds())
_, err = cr.AddFunc(cs.spec, func() {
for i := 0; i == 0 || i < cs.retry; i++ {
if err := cs.Update(); err != nil {
logs.Err(err)
<-time.After(time.Minute * 5)
} else {
break
}
}
})
if err != nil {
return nil, err
}
cr.Start()
return cs, nil
}
var _ ICodes = &Codes2{}
type Codes2 struct {
dbFilename string //数据库文件
tempDir string //临时目录
spec string //定时规则
updateKey string //标识
retry int //重试次数
dial ios.DialFunc //连接
dialOption []client.Option //
/*
内部字段
*/
c *Client //
db *xorms.Engine //
stocks types.List[*CodeModel] //股票缓存
etfs types.List[*CodeModel] //etf缓存
indexes types.List[*CodeModel] //指数缓存
all types.List[*CodeModel] //全部缓存
m *maps.Generic[string, *CodeModel] //缓存
}
func (this *Codes2) Get(code string) *CodeModel {
v, _ := this.m.Get(code)
return v
}
func (this *Codes2) Iter() iter.Seq2[string, *CodeModel] {
return func(yield func(string, *CodeModel) bool) {
for _, code := range this.all {
if !yield(code.FullCode(), code) {
break
}
}
}
}
func (this *Codes2) GetName(code string) string {
v, _ := this.m.Get(code)
if v == nil {
return "未知"
}
return v.Name
}
func (this *Codes2) GetStocks(limit ...int) CodeModels {
size := conv.Default(this.stocks.Len(), limit...)
return CodeModels(this.stocks.Limit(size))
}
func (this *Codes2) GetStockCodes(limit ...int) []string {
return this.GetStocks(limit...).Codes()
}
func (this *Codes2) GetETFs(limit ...int) CodeModels {
size := conv.Default(this.etfs.Len(), limit...)
return CodeModels(this.etfs.Limit(size))
}
func (this *Codes2) GetETFCodes(limit ...int) []string {
return this.GetETFs(limit...).Codes()
}
func (this *Codes2) GetIndexes(limit ...int) CodeModels {
size := conv.Default(this.etfs.Len(), limit...)
return CodeModels(this.indexes.Limit(size))
}
func (this *Codes2) GetIndexCodes(limit ...int) []string {
return this.GetIndexes(limit...).Codes()
}
func (this *Codes2) updated() (bool, error) {
update := new(UpdateModel)
{ //查询或者插入一条数据
has, err := this.db.Where("`Key`=?", this.updateKey).Get(update)
if err != nil {
return true, err
} else if !has {
update.Key = this.updateKey
if _, err = this.db.Insert(update); err != nil {
return true, err
}
return false, nil
}
}
{ //判断是否更新过,更新过则不更新
now := time.Now()
node := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, time.Local)
updateTime := time.Unix(update.Time, 0)
if now.Sub(node) > 0 {
//当前时间在9点之后,且更新时间在9点之前,需要更新
if updateTime.Sub(node) < 0 {
return false, nil
}
} else {
//当前时间在9点之前,且更新时间在上个节点之前
if updateTime.Sub(node.Add(time.Hour*24)) < 0 {
return false, nil
}
}
}
return true, nil
}
func (this *Codes2) Update() error {
codes, err := this.update()
if err != nil {
return err
}
stocks := []*CodeModel(nil)
etfs := []*CodeModel(nil)
indexes := []*CodeModel(nil)
for _, v := range codes {
fullCode := v.FullCode()
this.m.Set(fullCode, v)
switch {
case protocol.IsStock(fullCode):
stocks = append(stocks, v)
case protocol.IsETF(fullCode):
etfs = append(etfs, v)
case protocol.IsIndex(fullCode):
indexes = append(indexes, v)
}
}
this.stocks = stocks
this.etfs = etfs
this.indexes = indexes
this.all = codes
return nil
}
// GetCodes 更新股票并返回结果
func (this *Codes2) update() ([]*CodeModel, error) {
if this.c == nil {
return nil, errors.New("client is nil")
}
//2. 查询数据库所有股票
list := []*CodeModel(nil)
if err := this.db.Find(&list); err != nil {
return nil, err
}
//如果更新过,则不更新
updated, err := this.updated()
if err == nil && updated {
return list, nil
}
mCode := make(map[string]*CodeModel, len(list))
for _, v := range list {
mCode[v.FullCode()] = v
}
//3. 从服务器获取所有股票代码
insert := []*CodeModel(nil)
update := []*CodeModel(nil)
for _, exchange := range []protocol.Exchange{protocol.ExchangeSH, protocol.ExchangeSZ, protocol.ExchangeBJ} {
resp, err := this.c.GetCodeAll(exchange)
if err != nil {
return nil, err
}
for _, v := range resp.List {
code := &CodeModel{
Name: v.Name,
Code: v.Code,
Exchange: exchange.String(),
Multiple: v.Multiple,
Decimal: v.Decimal,
LastPrice: v.LastPrice,
}
if val, ok := mCode[exchange.String()+v.Code]; ok {
if val.Name != v.Name {
update = append(update, code)
}
delete(mCode, exchange.String()+v.Code)
} else {
insert = append(insert, code)
list = append(list, code)
}
}
}
//4. 获取gbbq
ss, err := gbbq.DownloadAndDecode(this.tempDir)
if err != nil {
logs.Err(err)
return nil, err
}
mStock := map[string]gbbq.Stock{}
for _, v := range ss {
mStock[protocol.AddPrefix(v.Code)] = v
}
//5. 赋值流通股和总股本
for _, v := range insert {
if protocol.IsStock(v.FullCode()) {
v.FloatStock, v.TotalStock = ss.GetStock(v.Code)
}
}
for _, v := range update {
if stock, ok := mStock[v.FullCode()]; ok {
v.FloatStock = stock.Float
v.TotalStock = stock.Total
}
}
//6. 插入或者更新数据库
err = this.db.SessionFunc(func(session *xorm.Session) error {
for _, v := range mCode {
if _, err = session.Where("Exchange=? and Code=? ", v.Exchange, v.Code).Delete(v); err != nil {
return err
}
}
for _, v := range insert {
if _, err := session.Insert(v); err != nil {
return err
}
}
for _, v := range update {
if _, err = session.Where("Exchange=? and Code=? ", v.Exchange, v.Code).Cols("Name,LastPrice").Update(v); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
//更新时间
_, err = this.db.Where("`Key`=?", this.updateKey).Update(&UpdateModel{Time: time.Now().Unix()})
return list, err
}

View File

@@ -73,7 +73,8 @@ func NewRangeDial(hosts []string) ios.DialFunc {
}
if i < len(hosts)-1 {
//最后一个错误返回出去
logs.Err(err)
logs.Err(err, "等待2秒后尝试下一个服务地址...")
<-time.After(time.Second * 2)
}
}
return

View File

@@ -0,0 +1,47 @@
package main
import (
"time"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
m, err := tdx.NewManage()
logs.PanicErr(err)
codes := m.Codes.GetStocks().Codes()
//codes = []string{
// "sz000001",
// "sz000002",
//}
for _, code := range codes {
m.Do(func(c *tdx.Client) error {
resp, err := c.GetHistoryMinute(time.Now().Format("20060102"), code)
logs.PanicErr(err)
resp2, err := c.GetKlineDay(code, 0, 1)
logs.PanicErr(err)
if len(resp2.List) == 0 {
logs.Debug(code)
return nil
}
if len(resp.List) == 0 {
logs.Debug(code)
return nil
}
if resp2.List[0].Close != resp.List[len(resp.List)-1].Price {
logs.Debug(code)
}
return nil
})
}
}

25
example/Codes2/main.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"fmt"
"strings"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
cs, err := tdx.NewCodes2()
logs.PanicErr(err)
c := cs.Get("sz000001")
fmt.Println(c.FloatStock, c.TotalStock)
for _, v := range cs.GetIndexes().Codes() {
if strings.HasPrefix(v, "sz") {
logs.Debug(v)
}
}
}

20
example/CodesHTTP/main.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx/extend"
"time"
)
func main() {
go extend.ListenCodesHTTP(10033)
<-time.After(time.Second * 3)
c := extend.DialCodesHTTP("http://localhost:10033")
stocks, err := c.GetStocks()
logs.PanicErr(err)
for _, v := range stocks {
println(v)
}
}

14
example/FastHosts/main.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
ls := tdx.FastHosts(tdx.Hosts...)
for _, v := range ls {
logs.Debug(v.Host, v.Spend)
}
logs.Debug("总数量:", len(ls))
}

View File

@@ -0,0 +1,18 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx/extend"
)
func main() {
ls, err := extend.GetBjCodes()
if err != nil {
logs.Err(err)
return
}
for _, v := range ls {
logs.Debug(v)
}
logs.Debug("总数量:", len(ls))
}

View File

@@ -3,20 +3,18 @@ package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/example/common"
"github.com/injoyai/tdx/protocol"
)
func main() {
c, err := tdx.Dial("124.71.187.122:7709")
logs.PanicErr(err)
common.Test(func(c *tdx.Client) {
resp, err := c.GetCode(protocol.ExchangeSH, 369)
logs.PanicErr(err)
resp, err := c.GetCode(protocol.ExchangeSH, 369)
logs.PanicErr(err)
for i, v := range resp.List {
logs.Debug(i, v)
}
logs.Debug("总数:", resp.Count)
select {}
for i, v := range resp.List {
logs.Debug(i, v, v.LastPrice)
}
logs.Debug("总数:", resp.Count)
})
}

View File

@@ -0,0 +1,29 @@
package main
import (
"strings"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
cs, err := tdx.NewCodes2()
logs.PanicErr(err)
ls := cs.GetETFCodes()
shNumber := 0
szNumber := 0
for _, v := range ls {
switch {
case strings.HasPrefix(v, "sh"):
shNumber++
case strings.HasPrefix(v, "sz"):
szNumber++
}
}
logs.Debug("sh:", shNumber)
logs.Debug("sz:", szNumber)
}

View File

@@ -8,7 +8,7 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetHistoryMinuteTrade("20241025", "sz000001", 0, 20)
resp, err := c.GetHistoryMinuteTrade("20250929", "bj838971", 0, 20)
logs.PanicErr(err)
for _, v := range resp.List {

View File

@@ -8,7 +8,7 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetHistoryMinuteTradeAll("20241025", "sz000001")
resp, err := c.GetHistoryMinuteTradeDay("20170704", "sh000001")
logs.PanicErr(err)
for _, v := range resp.List {

View File

@@ -8,7 +8,7 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetKlineDay("sh000001", 0, 10)
resp, err := c.GetIndexDay("sh000001", 0, 10)
logs.PanicErr(err)
for _, v := range resp.List {

View File

@@ -0,0 +1,20 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/example/common"
)
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetKline30Minute("sz000001", 0, 20)
logs.PanicErr(err)
for _, v := range resp.List {
logs.Debug(v)
}
logs.Debug("总数:", resp.Count)
})
}

View File

@@ -8,7 +8,7 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetKlineDay("000001", 0, 10)
resp, err := c.GetKlineDay("920992", 0, 20)
logs.PanicErr(err)
for _, v := range resp.List {

View File

@@ -0,0 +1,32 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/extend"
"time"
)
func main() {
c, err := tdx.DialDefault()
logs.PanicErr(err)
ks, fs, err := extend.GetTHSDayKlineFactorFull("000001", c)
logs.PanicErr(err)
m := map[int64]*extend.THSFactor{}
for _, v := range fs {
m[v.Date] = v
}
for _, v := range ks[0] {
logs.Debugf("%s 不复权:%.2f 前复权:%.2f 后复权:%.2f \n",
time.Unix(v.Date, 0).Format(time.DateOnly),
v.Close.Float64(),
v.Close.Float64()*m[v.Date].QFactor,
v.Close.Float64()*m[v.Date].HFactor,
)
}
}

View File

@@ -10,6 +10,9 @@ func main() {
c, err := tdx.Dial("124.71.187.122:7709", tdx.WithDebug())
logs.PanicErr(err)
tdx.DefaultCodes, err = tdx.NewCodesSqlite(c, "./codes.db")
logs.PanicErr(err)
_ = c
/*
@@ -21,7 +24,7 @@ func main() {
b1cb74001c00000000000d005100bd00789c6378c1cecb252ace6066c5b4898987b9050ed1f90cc5b74c18a5bc18c1b43490fecff09c81819191f13fc3c9f3bb169f5e7dfefeb5ef57f7199a305009308208e5b32bb6bcbf70148712002d7f1e13
b1cb74000c02000000003e05ac00ac000102020000303030303031601294121a1c2d4eadabcf0ed412aae5fc01afb0024561124fbcc08301afa47900b2e3174100bf68871a4201b741b6144302bb09af334403972e96354504ac09b619560e00000000f8ff601201363030303038b60fba04060607429788a70efa04ada37ab2531c12974d91e7449dbc354184b6010001844bad324102b5679ea1014203a65abd8d0143048a6ba4dd01440587e101b3d2029613000000000000b60f
*/
resp, err := c.GetQuote("sz000001", "sh600000", "sz159558")
resp, err := c.GetQuote("000001", "600000", "159558", "010504")
logs.PanicErr(err)
for _, v := range resp {

24
example/GetTrade/main.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/example/common"
)
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetTrade("sz000001", 0, 200)
logs.PanicErr(err)
for _, v := range resp.List {
logs.Debug(v)
}
for _, v := range resp.List.Klines() {
logs.Debug(v, v.Order)
}
logs.Debug("总数:", resp.Count)
})
}

36
example/Income/main.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx/extend"
"time"
)
func main() {
code := "sz000001"
pull := extend.NewPullKline(extend.PullKlineConfig{
Codes: []string{code},
Tables: []string{extend.Day},
})
//m, err := tdx.NewManage(nil)
//logs.PanicErr(err)
//err = pull.Run(context.Background(), m)
//logs.PanicErr(err)
ks, err := pull.DayKlines(code)
logs.PanicErr(err)
t := time.Now().AddDate(0, -1, -9)
logs.Debug(t.Format(time.DateOnly))
ls := extend.DoIncomes(ks, t, 5, 10, 20)
logs.Debug(len(ls))
for _, v := range ls {
logs.Info(v)
}
}

23
example/Manage/main.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
m, err := tdx.NewManage()
logs.PanicErr(err)
err = m.Do(func(c *tdx.Client) error {
resp, err := c.GetIndexDayAll("sh000001")
if err != nil {
return err
}
for _, v := range resp.List {
logs.Debug(v)
}
return nil
})
logs.PanicErr(err)
}

View File

@@ -0,0 +1,16 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
)
func main() {
_, err := tdx.NewManageMysql(
tdx.WithClients(2),
tdx.WithCodesDatabase("root:root@tcp(192.168.1.105:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local"),
tdx.WithWorkdayDatabase("root:root@tcp(192.168.1.105:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local"),
)
logs.PanicErr(err)
logs.Debug("done")
}

View File

@@ -2,23 +2,26 @@ package main
import (
"context"
"path/filepath"
"time"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/extend"
"path/filepath"
)
func main() {
m, err := tdx.NewManage(nil)
m, err := tdx.NewManage()
logs.PanicErr(err)
err = extend.NewPullKline(
[]string{"sz000001"},
[]string{extend.Year},
filepath.Join(tdx.DefaultDatabaseDir, "kline"),
1,
).Run(context.Background(), m)
err = extend.NewPullKline(extend.PullKlineConfig{
Codes: []string{"sz000001"},
Tables: []string{extend.Year},
Dir: filepath.Join(tdx.DefaultDatabaseDir, "kline"),
Limit: 1,
StartAt: time.Time{},
}).Run(context.Background(), m)
logs.PanicErr(err)
}

21
example/PullTrade/main.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"context"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/extend"
)
func main() {
pt := extend.NewPullTrade("./data/trade")
m, err := tdx.NewManage()
logs.PanicErr(err)
err = pt.PullYear(context.Background(), m, 2025, "sz000001")
logs.Err(err)
}

View File

@@ -0,0 +1,22 @@
package main
import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/example/common"
)
func main() {
common.Test(func(c *tdx.Client) {
resp, err := c.GetHistoryTradeDay("20251010", "sz000001")
logs.PanicErr(err)
ks := resp.List.Klines()
for _, v := range ks {
logs.Debug(v)
}
})
}

View File

@@ -9,11 +9,12 @@ import (
func main() {
common.Test(func(c *tdx.Client) {
_, err := tdx.NewWorkday(c, "./workday.db")
_, err := tdx.NewWorkdaySqlite(c) //"./workday.db"
logs.PanicErr(err)
_, err = tdx.NewCodes(c, "./codes.db")
_, err = tdx.NewCodesSqlite(c) //"./codes.db"
logs.PanicErr(err)
c.Close()
})
}

17
extend/codes-bj.go Normal file
View File

@@ -0,0 +1,17 @@
package extend
import (
"github.com/injoyai/tdx/lib/bse"
)
func GetBjCodes() ([]string, error) {
cs, err := bse.GetCodes()
if err != nil {
return nil, err
}
ls := []string(nil)
for _, v := range cs {
ls = append(ls, "bj"+v.Code)
}
return ls, nil
}

162
extend/codes-server.go Normal file
View File

@@ -0,0 +1,162 @@
package extend
import (
"encoding/json"
"fmt"
"io"
"iter"
"net/http"
"github.com/injoyai/base/maps"
"github.com/injoyai/conv"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/robfig/cron/v3"
)
func ListenCodesHTTP(port int, op ...tdx.Codes2Option) error {
code, err := tdx.NewCodes2(op...)
if err != nil {
return nil
}
succ := func(w http.ResponseWriter, data any) {
w.WriteHeader(http.StatusOK)
w.Write(conv.Bytes(data))
}
return http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/all":
case "/stocks":
succ(w, code.GetStocks())
case "/etfs":
succ(w, code.GetETFs())
case "/indexes":
succ(w, code.GetIndexes())
default:
http.NotFound(w, r)
}
}))
}
func DialCodesHTTP(address string) (c *CodesHTTP, err error) {
c = &CodesHTTP{address: address}
cr := cron.New(cron.WithSeconds())
_, err = cr.AddFunc("0 20 9 * * *", func() { logs.PrintErr(c.Update()) })
if err != nil {
return
}
err = c.Update()
if err != nil {
return
}
cr.Start()
return c, nil
}
type CodesHTTP struct {
address string
stocks tdx.CodeModels
etfs tdx.CodeModels
indexes tdx.CodeModels
m maps.Generic[string, *tdx.CodeModel]
}
func (this *CodesHTTP) Iter() iter.Seq2[string, *tdx.CodeModel] {
return func(yield func(string, *tdx.CodeModel) bool) {
for _, v := range this.stocks {
if !yield(v.FullCode(), v) {
return
}
}
for _, v := range this.etfs {
if !yield(v.FullCode(), v) {
return
}
}
for _, v := range this.indexes {
if !yield(v.FullCode(), v) {
return
}
}
}
}
func (this *CodesHTTP) Get(code string) *tdx.CodeModel {
return this.m.MustGet(code)
}
func (this *CodesHTTP) GetName(code string) string {
v := this.m.MustGet(code)
if v != nil {
return v.Name
}
return ""
}
func (this *CodesHTTP) GetStocks(limit ...int) tdx.CodeModels {
return this.stocks
}
func (this *CodesHTTP) GetStockCodes(limit ...int) []string {
return this.stocks.Codes()
}
func (this *CodesHTTP) GetETFs(limit ...int) tdx.CodeModels {
return this.etfs
}
func (this *CodesHTTP) GetETFCodes(limit ...int) []string {
return this.etfs.Codes()
}
func (this *CodesHTTP) GetIndexes(limits ...int) tdx.CodeModels {
return this.indexes
}
func (this *CodesHTTP) GetIndexCodes(limits ...int) []string {
return this.indexes.Codes()
}
func (this *CodesHTTP) Update() (err error) {
this.stocks, err = this.getList("/stocks")
if err != nil {
return
}
for _, v := range this.stocks {
this.m.Set(v.FullCode(), v)
}
this.etfs, err = this.getList("/etfs")
if err != nil {
return
}
for _, v := range this.etfs {
this.m.Set(v.FullCode(), v)
}
this.indexes, err = this.getList("/indexes")
if err != nil {
return
}
for _, v := range this.indexes {
this.m.Set(v.FullCode(), v)
}
return
}
func (this *CodesHTTP) getList(path string) (tdx.CodeModels, error) {
resp, err := http.DefaultClient.Get(this.address + path)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http code:%d", resp.StatusCode)
}
bs, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
ls := tdx.CodeModels{}
err = json.Unmarshal(bs, &ls)
return ls, err
}

65
extend/income.go Normal file
View File

@@ -0,0 +1,65 @@
package extend
import (
"fmt"
"github.com/injoyai/tdx/protocol"
"time"
)
func DoIncomes(ks Klines, startAt time.Time, days ...int) Incomes {
year, month, day := startAt.Date()
start := time.Date(year, month, day, 15, 0, 0, 0, startAt.Location()).Unix()
for i, v := range ks {
if v.Date >= start {
ks = ks[i:]
break
}
}
ls := Incomes{}
for _, v := range days {
if v < len(ks) {
x := ks[v]
ls = append(ls, &Income{
Offset: v,
Time: time.Unix(x.Date, 0),
Source: protocol.K{
Open: ks[0].Open,
High: ks[0].High,
Low: ks[0].Low,
Close: ks[0].Close,
},
Current: protocol.K{
Open: x.Open,
High: x.High,
Low: x.Low,
Close: x.Close,
},
})
}
}
return ls
}
type Incomes []*Income
type Income struct {
Offset int //偏移量
Time time.Time //时间
Source protocol.K //源
Current protocol.K //当前
}
func (this *Income) String() string {
return fmt.Sprintf("偏移: %d, 时间: %s, 涨幅: %.1f%%", this.Offset, this.Time.Format(time.DateOnly), this.RiseRate()*100)
}
func (this *Income) Rise() protocol.Price {
return this.Current.Close - this.Source.Close
}
func (this *Income) RiseRate() float64 {
return this.Rise().Float64() / this.Source.Close.Float64()
}

148
extend/pull-kline-mysql.go Normal file
View File

@@ -0,0 +1,148 @@
package extend
import (
"context"
_ "github.com/go-sql-driver/mysql"
"github.com/injoyai/base/chans"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/protocol"
"xorm.io/core"
"xorm.io/xorm"
)
func NewPullKlineMysql(cfg PullKlineConfig) (*PullKlineMysql, error) {
db, err := xorm.NewEngine("mysql", cfg.Dir)
if err != nil {
return nil, err
}
db.SetMapper(core.SameMapper{})
_tables := []*KlineTable(nil)
for _, v := range cfg.Tables {
table := KlineTableMap[v]
if err = db.Sync2(table); err != nil {
return nil, err
}
_tables = append(_tables, table)
}
return &PullKlineMysql{
tables: _tables,
Config: cfg,
DB: db,
}, nil
}
type PullKlineMysql struct {
tables []*KlineTable
Config PullKlineConfig
DB *xorm.Engine
}
func (this *PullKlineMysql) Name() string {
return "拉取k线数据"
}
func (this *PullKlineMysql) Run(ctx context.Context, m *tdx.Manage) error {
limit := chans.NewWaitLimit(this.Config.Limit)
//1. 获取所有股票代码
codes := this.Config.Codes
if len(codes) == 0 {
codes = m.Codes.GetStockCodes()
}
for _, v := range codes {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
limit.Add()
go func(code string) {
defer limit.Done()
for _, table := range this.tables {
if table == nil {
continue
}
select {
case <-ctx.Done():
return
default:
}
var err error
//2. 获取最后一条数据
last := new(Kline)
if _, err = this.DB.Table(table).Where("Code=?", code).Desc("Date").Get(last); err != nil {
logs.Err(err)
return
}
//3. 从服务器获取数据
insert := Klines{}
err = m.Do(func(c *tdx.Client) error {
insert, err = this.pull(code, last.Date, table.Handler(c))
return err
})
if err != nil {
logs.Err(err)
return
}
//4. 插入数据库
err = tdx.NewSessionFunc(this.DB, func(session *xorm.Session) error {
for i, v := range insert {
if i == 0 {
if _, err := session.Table(table).Where("Code=? and Date >= ?", code, v.Date).Delete(); err != nil {
return err
}
}
if _, err := session.Table(table).Insert(v); err != nil {
return err
}
}
return nil
})
logs.PrintErr(err)
}
}(v)
}
limit.Wait()
return nil
}
func (this *PullKlineMysql) pull(code string, lastDate int64, f func(code string, f func(k *protocol.Kline) bool) (*protocol.KlineResp, error)) (Klines, error) {
if lastDate == 0 {
lastDate = protocol.ExchangeEstablish.Unix()
}
resp, err := f(code, func(k *protocol.Kline) bool {
return k.Time.Unix() <= lastDate || k.Time.Unix() <= this.Config.StartAt.Unix()
})
if err != nil {
return nil, err
}
ks := Klines{}
for _, v := range resp.List {
ks = append(ks, &Kline{
Code: code,
Date: v.Time.Unix(),
Open: v.Open,
High: v.High,
Low: v.Low,
Close: v.Close,
Volume: v.Volume,
Amount: v.Amount,
})
}
return ks, nil
}

View File

@@ -7,8 +7,10 @@ import (
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/protocol"
"os"
"path/filepath"
"sort"
"time"
"xorm.io/core"
"xorm.io/xorm"
)
@@ -24,55 +26,91 @@ const (
Month = "month"
Quarter = "quarter"
Year = "year"
tableMinute = "MinuteKline"
table5Minute = "Minute5Kline"
table15Minute = "Minute15Kline"
table30Minute = "Minute30Kline"
tableHour = "HourKline"
tableDay = "DayKline"
tableWeek = "WeekKline"
tableMonth = "MonthKline"
tableQuarter = "QuarterKline"
tableYear = "YearKline"
)
var (
AllKlineType = []string{Minute, Minute5, Minute15, Minute30, Hour, Day, Week, Month, Quarter, Year}
KlineTableMap = map[string]*KlineTable{
Minute: NewKlineTable("MinuteKline", func(c *tdx.Client) KlineHandler { return c.GetKlineMinuteUntil }),
Minute5: NewKlineTable("Minute5Kline", func(c *tdx.Client) KlineHandler { return c.GetKline5MinuteUntil }),
Minute15: NewKlineTable("Minute15Kline", func(c *tdx.Client) KlineHandler { return c.GetKline15MinuteUntil }),
Minute30: NewKlineTable("Minute30Kline", func(c *tdx.Client) KlineHandler { return c.GetKline30MinuteUntil }),
Hour: NewKlineTable("HourKline", func(c *tdx.Client) KlineHandler { return c.GetKlineHourUntil }),
Day: NewKlineTable("DayKline", func(c *tdx.Client) KlineHandler { return c.GetKlineDayUntil }),
Week: NewKlineTable("WeekKline", func(c *tdx.Client) KlineHandler { return c.GetKlineWeekUntil }),
Month: NewKlineTable("MonthKline", func(c *tdx.Client) KlineHandler { return c.GetKlineMonthUntil }),
Quarter: NewKlineTable("QuarterKline", func(c *tdx.Client) KlineHandler { return c.GetKlineQuarterUntil }),
Year: NewKlineTable("YearKline", func(c *tdx.Client) KlineHandler { return c.GetKlineYearUntil }),
Minute: NewKlineTable(tableMinute, func(c *tdx.Client) KlineHandler { return c.GetKlineMinuteUntil }),
Minute5: NewKlineTable(table5Minute, func(c *tdx.Client) KlineHandler { return c.GetKline5MinuteUntil }),
Minute15: NewKlineTable(table15Minute, func(c *tdx.Client) KlineHandler { return c.GetKline15MinuteUntil }),
Minute30: NewKlineTable(table30Minute, func(c *tdx.Client) KlineHandler { return c.GetKline30MinuteUntil }),
Hour: NewKlineTable(tableHour, func(c *tdx.Client) KlineHandler { return c.GetKlineHourUntil }),
Day: NewKlineTable(tableDay, func(c *tdx.Client) KlineHandler { return c.GetKlineDayUntil }),
Week: NewKlineTable(tableWeek, func(c *tdx.Client) KlineHandler { return c.GetKlineWeekUntil }),
Month: NewKlineTable(tableMonth, func(c *tdx.Client) KlineHandler { return c.GetKlineMonthUntil }),
Quarter: NewKlineTable(tableQuarter, func(c *tdx.Client) KlineHandler { return c.GetKlineQuarterUntil }),
Year: NewKlineTable(tableYear, func(c *tdx.Client) KlineHandler { return c.GetKlineYearUntil }),
}
)
func NewPullKline(codes, tables []string, dir string, limit int) *PullKline {
type PullKlineConfig struct {
Codes []string //操作代码
Tables []string //数据类型
Dir string //数据位置
Limit int //协程数量
StartAt time.Time //数据开始时间
}
func NewPullKline(cfg PullKlineConfig) *PullKline {
_tables := []*KlineTable(nil)
for _, v := range tables {
for _, v := range cfg.Tables {
_tables = append(_tables, KlineTableMap[v])
}
if cfg.Limit <= 0 {
cfg.Limit = 1
}
if len(cfg.Dir) == 0 {
cfg.Dir = filepath.Join(tdx.DefaultDatabaseDir, "kline")
}
return &PullKline{
tables: _tables,
dir: dir,
Codes: codes,
limit: limit,
Config: cfg,
}
}
type PullKline struct {
tables []*KlineTable
dir string //数据目录
Codes []string //指定的代码
limit int //并发数量
Config PullKlineConfig
}
func (this *PullKline) Name() string {
return "拉取k线数据"
}
func (this *PullKline) DayKlines(code string) (Klines, error) {
//连接数据库
db, err := xorm.NewEngine("sqlite", filepath.Join(this.Config.Dir, code+".db"))
if err != nil {
return nil, err
}
db.SetMapper(core.SameMapper{})
db.DB().SetMaxOpenConns(1)
defer db.Close()
data := Klines{}
err = db.Table(tableDay).Asc("date").Find(&data)
return data, err
}
func (this *PullKline) Run(ctx context.Context, m *tdx.Manage) error {
limit := chans.NewWaitLimit(uint(this.limit))
limit := chans.NewWaitLimit(this.Config.Limit)
//1. 获取所有股票代码
codes := this.Codes
codes := this.Config.Codes
if len(codes) == 0 {
codes = m.Codes.GetStocks()
codes = m.Codes.GetStockCodes()
}
for _, v := range codes {
@@ -86,12 +124,15 @@ func (this *PullKline) Run(ctx context.Context, m *tdx.Manage) error {
go func(code string) {
defer limit.Done()
_ = os.MkdirAll(this.Config.Dir, 0777)
//连接数据库
db, err := xorm.NewEngine("sqlite", filepath.Join(this.dir, code+".db"))
db, err := xorm.NewEngine("sqlite", filepath.Join(this.Config.Dir, code+".db"))
if err != nil {
logs.Err(err)
return
}
defer db.Close()
db.SetMapper(core.SameMapper{})
db.DB().SetMaxOpenConns(1)
@@ -106,7 +147,7 @@ func (this *PullKline) Run(ctx context.Context, m *tdx.Manage) error {
default:
}
db.Sync2(table)
logs.PrintErr(db.Sync2(table))
//2. 获取最后一条数据
last := new(Kline)
@@ -157,7 +198,7 @@ func (this *PullKline) pull(code string, lastDate int64, f func(code string, f f
}
resp, err := f(code, func(k *protocol.Kline) bool {
return k.Time.Unix() <= lastDate
return k.Time.Unix() <= lastDate || k.Time.Unix() <= this.Config.StartAt.Unix()
})
if err != nil {
return nil, err
@@ -181,7 +222,7 @@ func (this *PullKline) pull(code string, lastDate int64, f func(code string, f f
}
type Kline struct {
Code string `json:"code" xorm:"-"` //代码
Code string `json:"code"` //代码
Date int64 `json:"date"` //时间节点 2006-01-02 15:00
Open protocol.Price `json:"open"` //开盘价
High protocol.Price `json:"high"` //最高价

168
extend/pull-trade.go Normal file
View File

@@ -0,0 +1,168 @@
package extend
import (
"context"
"github.com/injoyai/conv"
"github.com/injoyai/logs"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/protocol"
"path/filepath"
"time"
)
func NewPullTrade(dir string) *PullTrade {
return &PullTrade{
Dir: dir,
}
}
type PullTrade struct {
Dir string
}
func (this *PullTrade) Pull(ctx context.Context, m *tdx.Manage, code string) error {
for i := 2000; i <= time.Now().Year(); i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if err := this.PullYear(ctx, m, i, code); err != nil {
return err
}
}
return nil
}
func (this *PullTrade) PullYear(ctx context.Context, m *tdx.Manage, year int, code string) (err error) {
tss := protocol.Trades{}
kss1 := protocol.Klines(nil)
kss5 := protocol.Klines(nil)
kss15 := protocol.Klines(nil)
kss30 := protocol.Klines(nil)
kss60 := protocol.Klines(nil)
m.Workday.RangeYear(year, func(t time.Time) bool {
select {
case <-ctx.Done():
err = ctx.Err()
return false
default:
}
date := t.Format("20060102")
var resp *protocol.TradeResp
err = m.Do(func(c *tdx.Client) error {
resp, err = c.GetHistoryTradeDay(date, code)
return err
})
if err != nil {
logs.Err(err)
return false
}
tss = append(tss, resp.List...)
//转成分时K线
ks := resp.List.Klines()
kss1 = append(kss1, ks...)
kss5 = append(kss5, ks.Merge(5)...)
kss15 = append(kss5, ks.Merge(15)...)
kss30 = append(kss5, ks.Merge(30)...)
kss60 = append(kss5, ks.Merge(60)...)
return true
})
if err != nil {
return
}
filename := filepath.Join(this.Dir, "分时成交", code+"-"+conv.String(year)+".csv")
filename1 := filepath.Join(this.Dir, "1分钟", code+"-"+conv.String(year)+".csv")
filename5 := filepath.Join(this.Dir, "5分钟", code+"-"+conv.String(year)+".csv")
filename15 := filepath.Join(this.Dir, "15分钟", code+"-"+conv.String(year)+".csv")
filename30 := filepath.Join(this.Dir, "30分钟", code+"-"+conv.String(year)+".csv")
filename60 := filepath.Join(this.Dir, "60分钟", code+"-"+conv.String(year)+".csv")
name := m.Codes.GetName(code)
err = TradeToCsv(filename, tss)
if err != nil {
return err
}
err = KlinesToCsv(filename1, code, name, kss1)
if err != nil {
return err
}
err = KlinesToCsv(filename5, code, name, kss5)
if err != nil {
return err
}
err = KlinesToCsv(filename15, code, name, kss15)
if err != nil {
return err
}
err = KlinesToCsv(filename30, code, name, kss30)
if err != nil {
return err
}
err = KlinesToCsv(filename60, code, name, kss60)
if err != nil {
return err
}
return nil
}
func KlinesToCsv(filename string, code, name string, ks protocol.Klines) error {
data := [][]any{{"日期", "时间", "代码", "名称", "开盘", "最高", "最低", "收盘", "总手", "金额"}}
for _, v := range ks {
data = append(data, []any{
v.Time.Format("20060102"),
v.Time.Format("15:04"),
code,
name,
v.Open.Float64(),
v.High.Float64(),
v.Low.Float64(),
v.Close.Float64(),
v.Volume,
v.Amount.Float64(),
})
}
buf, err := toCsv(data)
if err != nil {
return err
}
return newFile(filename, buf)
}
func TradeToCsv(filename string, ts protocol.Trades) error {
data := [][]any{{"日期", "时间", "价格", "成交量(手)", "成交额", "方向(0买,1卖)"}}
for _, v := range ts {
data = append(data, []any{
v.Time.Format(time.DateOnly),
v.Time.Format("15:04"),
v.Price.Float64(),
v.Volume,
v.Amount().Float64(),
v.Status,
})
}
buf, err := toCsv(data)
if err != nil {
return err
}
return newFile(filename, buf)
}

198
extend/spider-ths.go Normal file
View File

@@ -0,0 +1,198 @@
package extend
import (
"bytes"
"encoding/json"
"fmt"
"github.com/injoyai/conv"
"github.com/injoyai/tdx"
"github.com/injoyai/tdx/protocol"
"io"
"net/http"
"strings"
"time"
)
const (
UrlTHSDayKline = "http://d.10jqka.com.cn/v6/line/hs_%s/0%d/all.js"
THS_BFQ uint8 = 0 //不复权
THS_QFQ uint8 = 1 //前复权
THS_HFQ uint8 = 2 //后复权
)
// GetTHSDayKlineFactorFull 增加计算复权因子
func GetTHSDayKlineFactorFull(code string, c *tdx.Client) ([3][]*Kline, []*THSFactor, error) {
ks, err := GetTHSDayKlineFull(code, c)
if err != nil {
return [3][]*Kline{}, nil, err
}
mQPrice := make(map[int64]float64)
for _, v := range ks[1] {
mQPrice[v.Date] = v.Close.Float64()
}
mHPrice := make(map[int64]float64)
for _, v := range ks[2] {
mHPrice[v.Date] = v.Close.Float64()
}
fs := make([]*THSFactor, 0, len(ks[0]))
for _, v := range ks[0] {
fs = append(fs, &THSFactor{
Date: v.Date,
QFactor: mQPrice[v.Date] / v.Close.Float64(),
HFactor: mHPrice[v.Date] / v.Close.Float64(),
})
}
return ks, fs, nil
}
/*
GetTHSDayKlineFull
获取[不复权,前复权,后复权]数据,并补充成交金额数据
前复权,和通达信对的上,和东方财富对不上
后复权,和通达信,东方财富都对不上
*/
func GetTHSDayKlineFull(code string, c *tdx.Client) ([3][]*Kline, error) {
resp, err := c.GetKlineDayAll(code)
if err != nil {
return [3][]*Kline{}, err
}
mAmount := make(map[int64]protocol.Price)
bfq := []*Kline(nil)
for _, v := range resp.List {
mAmount[v.Time.Unix()] = v.Amount
bfq = append(bfq, &Kline{
Code: code,
Date: v.Time.Unix(),
Open: v.Open,
High: v.High,
Low: v.Low,
Close: v.Close,
Volume: v.Volume,
Amount: v.Amount,
})
}
//前复权
qfq, err := GetTHSDayKline(code, THS_QFQ)
if err != nil {
return [3][]*Kline{}, err
}
for i := range qfq {
qfq[i].Amount = mAmount[qfq[i].Date]
}
//后复权
hfq, err := GetTHSDayKline(code, THS_HFQ)
if err != nil {
return [3][]*Kline{}, err
}
for i := range hfq {
hfq[i].Amount = mAmount[hfq[i].Date]
}
return [3][]*Kline{bfq, qfq, hfq}, nil
}
/*
GetTHSDayKline
前复权,和通达信对的上,和东方财富对不上
后复权,和通达信,东方财富都对不上
*/
func GetTHSDayKline(code string, _type uint8) ([]*Kline, error) {
if _type != THS_BFQ && _type != THS_QFQ && _type != THS_HFQ {
return nil, fmt.Errorf("数据类型错误,例如:不复权0或前复权1或后复权2")
}
code = protocol.AddPrefix(code)
if len(code) != 8 {
return nil, fmt.Errorf("股票代码错误,例如:SZ000001或000001")
}
u := fmt.Sprintf(UrlTHSDayKline, code[2:], _type)
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
/*
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/90.0.4430.212 Safari/537.36',
'Referer': 'http://stockpage.10jqka.com.cn/',
'DNT': '1',
*/
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36 Edg/89.0.774.54")
req.Header.Set("Referer", "http://stockpage.10jqka.com.cn/")
req.Header.Set("DNT", "1")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
bs, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
n := bytes.IndexByte(bs, '(')
bs = bs[n+1 : len(bs)-1]
m := map[string]any{}
err = json.Unmarshal(bs, &m)
if err != nil {
return nil, err
}
total := conv.Int(m["total"])
sortYears := conv.Interfaces(m["sortYear"])
priceFactor := conv.Float64(m["priceFactor"])
prices := strings.Split(conv.String(m["price"]), ",")
dates := strings.Split(conv.String(m["dates"]), ",")
volumes := strings.Split(conv.String(m["volumn"]), ",")
//好像到了22点,总数量会比实际多1
if total == len(dates)+1 && total == len(volumes)+1 {
total -= 1
}
//判断数量是否对应
if total*4 != len(prices) || total != len(dates) || total != len(volumes) {
return nil, fmt.Errorf("total=%d prices=%d dates=%d volumns=%d", total, len(prices), len(dates), len(volumes))
}
mYear := make(map[int][]string)
index := 0
for i, v := range sortYears {
if ls := conv.Ints(v); len(ls) == 2 {
year := conv.Int(ls[0])
length := conv.Int(ls[1])
if i == len(sortYears)-1 {
mYear[year] = dates[index:]
break
}
mYear[year] = dates[index : index+length]
index += length
}
}
ls := []*Kline(nil)
i := 0
nowYear := time.Now().Year()
for year := 1990; year <= nowYear; year++ {
for _, d := range mYear[year] {
x, err := time.Parse("0102", d)
if err != nil {
return nil, err
}
x = time.Date(year, x.Month(), x.Day(), 15, 0, 0, 0, time.Local)
low := protocol.Price(conv.Float64(prices[i*4+0]) * 1000 / priceFactor)
ls = append(ls, &Kline{
Code: protocol.AddPrefix(code),
Date: x.Unix(),
Open: protocol.Price(conv.Float64(prices[i*4+1])*1000/priceFactor) + low,
High: protocol.Price(conv.Float64(prices[i*4+2])*1000/priceFactor) + low,
Low: low,
Close: protocol.Price(conv.Float64(prices[i*4+3])*1000/priceFactor) + low,
Volume: (conv.Int64(volumes[i]) + 50) / 100,
})
i++
}
}
return ls, nil
}

16
extend/spider-ths_test.go Normal file
View File

@@ -0,0 +1,16 @@
package extend
import (
"testing"
)
func TestNewSpiderTHS(t *testing.T) {
ls, err := GetTHSDayKline("sz000001", THS_HFQ)
if err != nil {
t.Error(err)
return
}
for _, v := range ls {
t.Log(v)
}
}

12
extend/ths-factor.go Normal file
View File

@@ -0,0 +1,12 @@
package extend
//const (
// // UrlTHSFactor https://d.10jqka.com.cn/v6/line/hs_000001/01/2016.js
// UrlTHSFactor = "https://d.10jqka.com.cn/v6/line/hs_%s/0%d/%d.js"
//)
type THSFactor struct {
Date int64 `json:"date"` //时间
QFactor float64 `json:"q_factor"` //前复权因子
HFactor float64 `json:"h_factor"` //后复权因子
}

58
extend/util.go Normal file
View File

@@ -0,0 +1,58 @@
package extend
import (
"bytes"
"encoding/csv"
"github.com/injoyai/conv"
"io"
"os"
"path/filepath"
)
func toCsv(data [][]interface{}) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
buf.WriteString("\xEF\xBB\xBF")
w := csv.NewWriter(buf)
for _, rows := range data {
if err := w.Write(conv.Strings(rows)); err != nil {
return nil, err
}
}
w.Flush()
return buf, nil
}
// newFile 新建文件,会覆盖
func newFile(filename string, v ...interface{}) error {
if len(v) == 0 {
return os.MkdirAll(filename, 0777)
}
dir, name := filepath.Split(filename)
if len(dir) > 0 {
if err := os.MkdirAll(dir, 0777); err != nil {
return err
}
}
if len(name) == 0 {
return nil
}
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
for _, k := range v {
switch r := k.(type) {
case nil:
case io.Reader:
if _, err = io.Copy(f, r); err != nil {
return err
}
default:
if _, err = f.Write(conv.Bytes(r)); err != nil {
return err
}
}
}
return nil
}

18
go.mod
View File

@@ -1,13 +1,14 @@
module github.com/injoyai/tdx
go 1.20
go 1.23
require (
github.com/glebarez/go-sqlite v1.22.0
github.com/injoyai/base v1.0.18
github.com/injoyai/conv v1.1.10
github.com/injoyai/ios v0.0.4
github.com/injoyai/logs v1.0.9
github.com/go-sql-driver/mysql v1.7.0
github.com/injoyai/base v1.2.17
github.com/injoyai/conv v1.2.5
github.com/injoyai/ios v1.2.2
github.com/injoyai/logs v1.0.12
github.com/robfig/cron/v3 v3.0.1
golang.org/x/text v0.16.0
xorm.io/core v0.7.3
@@ -16,22 +17,21 @@ require (
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/goccy/go-json v0.8.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/sys v0.25.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.37.6 // indirect

67
go.sum
View File

@@ -6,56 +6,51 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/injoyai/base v1.0.18 h1:AMB1NhPe9acvMp04sThE8Ji/SRS1/MHsx19NmSoD8LA=
github.com/injoyai/base v1.0.18/go.mod h1:2JSv8ndJ/FmKyazfhDBewzylEbVfJV0EE/r6kOvnzwU=
github.com/injoyai/conv v1.1.5/go.mod h1:PYoJcbqaz4eyQUovzErRFCuDSPGB4L07AvdmFsTrTew=
github.com/injoyai/conv v1.1.10 h1:LefmOA4SKTEw6gGHXZ6UT8n2K+OzCttYSW8lVmvgQd8=
github.com/injoyai/conv v1.1.10/go.mod h1:PYoJcbqaz4eyQUovzErRFCuDSPGB4L07AvdmFsTrTew=
github.com/injoyai/ios v0.0.4 h1:yEZ6wN5uCSjAJB4qwpT6R77aMtjkblZo4giKwu9/s7Y=
github.com/injoyai/ios v0.0.4/go.mod h1:heABkaIUwoRRe424otl6mKgdU6LYt5gadav/V4gaojA=
github.com/injoyai/logs v1.0.9 h1:Wq7rCVIQKcPx+z+lzKQb2qyDK4TML/cgmaSZN9tx33c=
github.com/injoyai/logs v1.0.9/go.mod h1:CLchJCGhb39Obyrci816R+KMtbxZhgPs0FuikhyixK4=
github.com/injoyai/base v1.2.17 h1:+qYeCSeEMWgmTla+LBC0Ozan9ysS4mV0ne5nfMt9opU=
github.com/injoyai/base v1.2.17/go.mod h1:NfCQjml3z2pCvQ3J3YcOXtecqXD0xVPKjo4YTsMLhr8=
github.com/injoyai/conv v1.2.5 h1:G4OCyF0NTZul5W1u9IgXDOhW4/zmIigdPKXFHQGmv1M=
github.com/injoyai/conv v1.2.5/go.mod h1:s05l3fQJQ4mT4VX+KIdbvCWQB0YzZHprmUfUu2uxd1k=
github.com/injoyai/ios v1.2.2 h1:fAPWBL6t22DiE2ZEpBgf5bzyVQTcm2ZhLMkM+JFPhZA=
github.com/injoyai/ios v1.2.2/go.mod h1:DJVJGQFQvqF80CeJVabFOm6AKilqc/m8MFvz39Uy5ow=
github.com/injoyai/logs v1.0.12 h1:f7syIGZMTg9ZzhJhdd3tzaPdxkMhdKsncGaxljqIiYE=
github.com/injoyai/logs v1.0.12/go.mod h1:+dKEL6GvaFqqVRatqUBiCicJbZnAgtj7hVs824Src4s=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -68,14 +63,15 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -88,31 +84,26 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -123,14 +114,17 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
@@ -138,10 +132,13 @@ modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWP
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0=

View File

@@ -1,5 +1,14 @@
package tdx
import (
"github.com/injoyai/base/types"
"github.com/injoyai/logs"
"net"
"strings"
"sync"
"time"
)
var (
// Hosts 所有服务器地址(2024-11-30测试通过)
@@ -67,3 +76,44 @@ var (
"119.97.185.59", //电信
}
)
// FastHosts 通过tcp(ping不可用)连接速度的方式筛选排序可用的地址
func FastHosts(hosts ...string) []DialResult {
wg := sync.WaitGroup{}
wg.Add(len(hosts))
mu := sync.Mutex{}
ls := types.List[DialResult](nil)
for _, host := range hosts {
go func(host string) {
defer wg.Done()
addr := host
if !strings.Contains(addr, ":") {
addr += ":7709"
}
now := time.Now()
c, err := net.Dial("tcp", addr)
if err != nil {
logs.Err(err)
return
}
spend := time.Since(now)
c.Close()
mu.Lock()
ls = append(ls, DialResult{
Host: host,
Spend: spend,
})
mu.Unlock()
}(host)
}
wg.Wait()
return ls.Sort(func(a, b DialResult) bool {
return a.Spend < b.Spend
})
}
// DialResult 连接结果
type DialResult struct {
Host string
Spend time.Duration
}

102
lib/bse/codes.go Normal file
View File

@@ -0,0 +1,102 @@
package bse
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/injoyai/conv"
"io"
"net/http"
"strings"
"time"
)
const (
// UrlCodes 最后跟的是时间戳(ms),但是随便什么时间戳都能请求成功
UrlCodes = "https://www.bse.cn/nqhqController/nqhq_en.do?callback=jQuery3710848510589806625_%d"
)
func GetCodes() ([]*Code, error) {
list := []*Code(nil)
//这个200预防下bug,除非北京上市公司有4000个
for page := 0; page < 200; page++ {
ls, done, err := getCodes(page)
if err != nil {
return nil, err
}
list = append(list, ls...)
if done {
break
}
<-time.After(time.Millisecond * 100)
}
return list, nil
}
func getCodes(page int) (_ []*Code, last bool, err error) {
url := fmt.Sprintf(UrlCodes, time.Now().UnixMilli())
bodyStr := "page=" + conv.String(page) + "&type_en=%5B%22B%22%5D&sortfield=hqcjsl&sorttype=desc&xxfcbj_en=%5B2%5D&zqdm="
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(bodyStr))
if err != nil {
return nil, false, err
}
req.Header.Set("X-Requested-With", "XMLHttpRequest")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.39 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, false, err
}
defer resp.Body.Close()
bs, err := io.ReadAll(resp.Body)
if err != nil {
return nil, false, err
}
//处理数据
i := bytes.IndexByte(bs, '(')
if len(bs) < 1 || len(bs) <= i {
return nil, false, errors.New("未知错误: " + string(bs))
}
bs = bs[i+1 : len(bs)-1]
ls := []*Codes(nil)
err = json.Unmarshal(bs, &ls)
if err != nil {
return nil, false, err
}
if len(ls) == 0 {
return nil, false, errors.New("未知错误: " + string(bs))
}
return ls[0].Data, ls[0].LastPage, nil
}
type Codes struct {
Data []*Code `json:"content"`
TotalNumber int `json:"totalElements"`
TotalPage int `json:"totalPages"`
LastPage bool `json:"lastPage"`
}
type Code struct {
Date string `json:"hqjsrq"` //日期
Code string `json:"hqzqdm"` //代码
Name string `json:"hqzqjc"` //名称
LastClose float64 `json:"hqzrsp"` //前一天收盘价
Open float64 `json:"hqjrkp"` //开盘价
High float64 `json:"hqzgcj"` //最高价
Low float64 `json:"hqzdcj"` //最低价
Last float64 `json:"hqzjcj"` //最新价/收盘价
Volume int `json:"hqcjsl"` //成交量,股
Amount float64 `json:"hqcjje"` //成交额,元
}

197
lib/gbbq/gbbq.go Normal file

File diff suppressed because one or more lines are too long

118
lib/xorms/engine.go Normal file
View File

@@ -0,0 +1,118 @@
package xorms
import (
_ "github.com/glebarez/go-sqlite"
_ "github.com/go-sql-driver/mysql"
"os"
"path/filepath"
"time"
"xorm.io/core"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
func NewMysql(dsn string, options ...Option) (*Engine, error) {
return New("mysql", dsn, options...)
}
func NewSqlite(filename string, options ...Option) (*Engine, error) {
dir, _ := filepath.Split(filename)
_ = os.MkdirAll(dir, 0777)
//sqlite是文件数据库,只能打开一次(即一个连接)
options = append(options, WithMaxOpenConns(1))
return New("sqlite", filename, options...)
}
/*
New 需要手动引用驱动
mysql _ "github.com/go-sql-driver/mysql"
sqlite _ "github.com/glebarez/go-sqlite"
sqlserver _ "github.com/denisenkom/go-mssqldb"
*/
func New(Type, dsn string, options ...Option) (*Engine, error) {
db, err := xorm.NewEngine(Type, dsn)
if err != nil {
return nil, err
}
//默认同步字段
WithSyncField(true)(db)
for _, v := range options {
v(db)
}
return &Engine{Engine: db}, nil
}
type Engine struct {
*xorm.Engine
}
func (this *Engine) TableName(v any) string {
return this.Engine.TableName(v)
}
func (this *Engine) Tables() []*schemas.Table {
list, _ := this.DBMetas()
return list
}
// SetTablePrefix 前缀
func (this *Engine) SetTablePrefix(s string) *Engine {
this.SetTableMapper(core.NewPrefixMapper(core.SameMapper{}, s))
return this
}
// SetSyncField 字段同步
func (this *Engine) SetSyncField() *Engine {
this.SetMapper(core.SameMapper{})
return this
}
// SetConnMaxLifetime 设置连接超时时间(超时会断开连接)
func (this *Engine) SetConnMaxLifetime(d time.Duration) *Engine {
this.DB().SetConnMaxLifetime(d)
return this
}
// SetMaxIdleConns 设置空闲数(一直连接不断开)
func (this *Engine) SetMaxIdleConns(n int) *Engine {
this.DB().SetMaxIdleConns(n)
return this
}
// SetMaxOpenConns 设置连接数(超出最大数量会等待)
func (this *Engine) SetMaxOpenConns(n int) *Engine {
this.DB().SetMaxOpenConns(n)
return this
}
// NewSession 新建自动关闭事务
func (this *Engine) NewSession() *Session {
return newSession(this.Engine.Where(""))
}
func (this *Engine) SessionFunc(fn func(session *xorm.Session) error) error {
return NewSessionFunc(this.Engine, fn)
}
func (this *Engine) Like(param, arg string) *Session {
return newSession(this.Engine.Where(param+" like ?", "%"+arg+"%"))
}
func (this *Engine) Desc(colNames ...string) *Session {
return newSession(this.Engine.Desc(colNames...))
}
func (this *Engine) Asc(colNames ...string) *Session {
return newSession(this.Engine.Asc(colNames...))
}
func (this *Engine) Limit(limit int, start ...int) *Session {
if limit > 0 {
return newSession(this.Engine.Limit(limit, start...))
}
return newSession(this.Engine.Where(""))
}
func (this *Engine) Where(query any, args ...any) *Session {
return newSession(this.Engine.Where(query, args...))
}

70
lib/xorms/option.go Normal file
View File

@@ -0,0 +1,70 @@
package xorms
import (
"github.com/injoyai/conv"
"github.com/injoyai/conv/cfg"
"time"
"xorm.io/core"
"xorm.io/xorm"
"xorm.io/xorm/names"
)
type Option func(*xorm.Engine)
func WithCfg(path ...string) Option {
return WithDMap(cfg.Default.GetDMap(conv.Default[string]("database", path...)))
}
func WithDMap(m *conv.Map) Option {
return func(e *xorm.Engine) {
if v := m.GetVar("fieldSync"); !v.IsNil() {
WithSyncField(v.Bool())(e)
}
if v := m.GetVar("tablePrefix"); !v.IsNil() {
WithTablePrefix(v.String())(e)
}
if v := m.GetVar("connMaxLifetime"); !v.IsNil() {
WithConnMaxLifetime(v.Duration())(e)
}
if v := m.GetVar("maxIdleConns"); !v.IsNil() {
WithMaxIdleConns(v.Int())(e)
}
if v := m.GetVar("maxOpenConns"); !v.IsNil() {
WithMaxOpenConns(v.Int())(e)
}
}
}
func WithTablePrefix(prefix string) Option {
return func(e *xorm.Engine) {
e.SetTableMapper(core.NewPrefixMapper(core.SameMapper{}, prefix))
}
}
func WithSyncField(b bool) Option {
return func(e *xorm.Engine) {
if b {
e.SetMapper(core.SameMapper{})
} else {
e.SetMapper(names.NewCacheMapper(new(names.SnakeMapper)))
}
}
}
func WithConnMaxLifetime(d time.Duration) Option {
return func(e *xorm.Engine) {
e.DB().SetConnMaxLifetime(d)
}
}
func WithMaxIdleConns(n int) Option {
return func(e *xorm.Engine) {
e.DB().SetMaxIdleConns(n)
}
}
func WithMaxOpenConns(n int) Option {
return func(e *xorm.Engine) {
e.DB().SetMaxOpenConns(n)
}
}

61
lib/xorms/session.go Normal file
View File

@@ -0,0 +1,61 @@
package xorms
import "xorm.io/xorm"
type Session struct {
*xorm.Session
}
func newSession(session *xorm.Session) *Session {
return &Session{session}
}
func (this *Session) Like(param, arg string) *Session {
this.Session.Where(param+" like ?", "%"+arg+"%")
return this
}
func (this *Session) Desc(colNames ...string) *Session {
this.Session.Desc(colNames...)
return this
}
func (this *Session) Asc(colNames ...string) *Session {
this.Session.Asc(colNames...)
return this
}
func (this *Session) Limit(limit int, start ...int) *Session {
if limit > 0 {
this.Session.Limit(limit, start...)
}
return this
}
func (this *Session) Where(query any, args ...any) *Session {
this.Session.Where(query, args...)
return this
}
func (this *Session) And(query any, args ...any) *Session {
this.Session.And(query, args...)
return this
}
func NewSessionFunc(db *xorm.Engine, fn func(session *xorm.Session) error) error {
session := db.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
session.Rollback()
return err
}
if err := fn(session); err != nil {
session.Rollback()
return err
}
if err := session.Commit(); err != nil {
session.Rollback()
return err
}
return nil
}

119
lib/zip/zip_func.go Normal file
View File

@@ -0,0 +1,119 @@
package zip
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strings"
)
// Encode 压缩文件
// @filePath,文件路径
// @zipName,压缩名称
func Encode(filePath, zipName string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
os.MkdirAll(filepath.Dir(zipName), os.ModePerm)
zipFile, err := os.Create(zipName)
if err != nil {
return err
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
return compareZip(file, zipWriter, "", !strings.HasSuffix(filePath, "/"))
}
// 压缩文件
func compareZip(file *os.File, zipWriter *zip.Writer, prefix string, join bool) error {
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(fileInfo)
if err != nil {
return err
}
if join {
header.Name = filepath.Join(prefix, header.Name)
prefix = filepath.Join(prefix, fileInfo.Name())
header.Name = strings.ReplaceAll(header.Name, "\\", "/")
if fileInfo.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate //压缩的关键
}
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
if !fileInfo.IsDir() {
_, err = io.Copy(writer, file)
return err
}
}
fileInfoChildList, err := file.Readdir(-1)
if err != nil {
return err
}
for _, fileInfoChild := range fileInfoChildList {
fileChild, err := os.Open(filepath.Join(file.Name(), fileInfoChild.Name()))
if err != nil {
return err
}
if err := compareZip(fileChild, zipWriter, prefix, true); err != nil {
return err
}
}
return nil
}
// Decode 解压zip
func Decode(zipName, filePath string) error {
r, err := zip.OpenReader(zipName)
if err != nil {
return err
}
defer r.Close()
for _, k := range r.Reader.File {
if k.FileInfo().IsDir() {
if err := os.MkdirAll(filepath.Join(filePath, k.Name), os.ModePerm); err != nil {
return err
}
} else {
err := func() error {
f, err := k.Open()
if err != nil {
return err
}
defer f.Close()
w, err := os.Create(filepath.Join(filePath, k.Name))
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, f)
return err
}()
if err != nil {
return err
}
}
}
return nil
}

282
manage.go
View File

@@ -1,83 +1,259 @@
package tdx
import (
"errors"
"sync"
"github.com/injoyai/conv"
"github.com/injoyai/ios/client"
"github.com/robfig/cron/v3"
"path/filepath"
"time"
)
const (
DefaultClients = 1
DefaultRetry = 3
DefaultDataDir = "./data"
DefaultDatabaseDir = "./data/database"
)
func NewManage(cfg *ManageConfig, op ...client.Option) (*Manage, error) {
//初始化配置
if cfg == nil {
cfg = &ManageConfig{}
}
if len(cfg.Hosts) == 0 {
cfg.Hosts = Hosts
}
if cfg.CodesDir == "" {
cfg.CodesDir = DefaultDatabaseDir
}
if cfg.WorkdayDir == "" {
cfg.WorkdayDir = DefaultDatabaseDir
func NewManageMysql(op ...Option) (*Manage, error) {
return NewManage(
WithOptions(op...),
WithDialCodes(func(c *Client, database string) (ICodes, error) {
if database == "" {
return nil, errors.New("未配置Codes的数据库")
}
return NewCodesMysql(c, database)
}),
WithDialWorkday(func(c *Client, database string) (*Workday, error) {
if database == "" {
return nil, errors.New("未配置Workday的数据库")
}
return NewWorkdayMysql(c, database)
}),
)
}
func NewManageSqlite(op ...Option) (*Manage, error) {
return NewManage(
WithCodesDatabase(DefaultDatabaseDir+"/codes.db"),
WithWorkdayDatabase(DefaultDatabaseDir+"/workday.db"),
WithOptions(op...),
WithDialCodes(func(c *Client, database string) (ICodes, error) {
return NewCodesSqlite(c, database)
}),
WithDialWorkday(func(c *Client, database string) (*Workday, error) {
return NewWorkdaySqlite(c, database)
}),
)
}
func NewManageSqlite2(op ...Option) (*Manage, error) {
return NewManage(
WithCodesDatabase(DefaultDatabaseDir+"/codes2.db"),
WithWorkdayDatabase(DefaultDatabaseDir+"/workday.db"),
WithOptions(op...),
WithDialCodes(func(c *Client, database string) (ICodes, error) {
return NewCodes2(
WithCodes2Client(c),
WithCodes2Database(database),
)
}),
WithDialWorkday(func(c *Client, database string) (*Workday, error) {
return NewWorkdaySqlite(c, database)
}),
)
}
func NewManage(op ...Option) (m *Manage, err error) {
m = &Manage{
clients: DefaultClients,
dial: DialDefault,
dialOptions: nil,
dialCodes: nil,
codesDatabase: DefaultDatabaseDir + "/codes2.db",
dialWorkday: nil,
workdayDatabase: DefaultDatabaseDir + "/workday.db",
Pool: nil,
Codes: nil,
Workday: nil,
cron: nil,
once: sync.Once{},
}
//代码
DefaultCodes = &Codes{}
codesClient, err := DialHostsRange(cfg.Hosts, op...)
if err != nil {
return nil, err
for _, v := range op {
if v != nil {
v(m)
}
}
codesClient.Wait.SetTimeout(time.Second * 5)
codes, err := NewCodes(codesClient, filepath.Join(cfg.CodesDir, "codes.db"))
if err != nil {
return nil, err
}
DefaultCodes = codes
m.clients = conv.Select(m.clients <= 0, 1, m.clients)
m.dial = conv.Select(m.dial == nil, DialDefault, m.dial)
//连接池
p, err := NewPool(func() (*Client, error) {
return DialHostsRange(cfg.Hosts, op...)
}, cfg.Number)
m.Pool, err = NewPool(func() (*Client, error) { return m.dial(m.dialOptions...) }, m.clients)
if err != nil {
return nil, err
}
//工作日
workdayClient, err := DialHostsRange(cfg.Hosts, op...)
if err != nil {
return nil, err
}
workdayClient.Wait.SetTimeout(time.Second * 5)
workday, err := NewWorkday(workdayClient, filepath.Join(cfg.WorkdayDir, "workday.db"))
if err != nil {
return nil, err
//代码管理
if m.Codes == nil {
if m.dialCodes == nil {
m.dialCodes = func(c *Client, database string) (ICodes, error) {
return NewCodes2(WithCodes2Client(c), WithCodes2Database(database))
}
}
err = m.Pool.Do(func(c *Client) error {
m.Codes, err = m.dialCodes(c, m.codesDatabase)
return err
})
if err != nil {
return nil, err
}
}
return &Manage{
Pool: p,
Config: cfg,
Codes: codes,
Workday: workday,
Cron: cron.New(cron.WithSeconds()),
}, nil
//工作日管理
if m.Workday == nil {
if m.dialWorkday == nil {
m.dialWorkday = func(c *Client, database string) (*Workday, error) {
return NewWorkdaySqlite(c, database)
}
}
err = m.Pool.Do(func(c *Client) error {
m.Workday, err = m.dialWorkday(c, m.workdayDatabase)
return err
})
if err != nil {
return nil, err
}
}
return
}
/*
*/
type Option func(m *Manage)
type DialWorkdayFunc func(c *Client, database string) (*Workday, error)
type DialCodesFunc func(c *Client, database string) (ICodes, error)
func WithClients(clients int) Option {
return func(m *Manage) {
m.clients = clients
}
}
func WithDial(dial func(op ...client.Option) (*Client, error), op ...client.Option) Option {
return func(m *Manage) {
m.dial = dial
m.dialOptions = op
}
}
func WithDialOptions(op ...client.Option) Option {
return func(m *Manage) {
m.dialOptions = op
}
}
func WithCodes(codes ICodes) Option {
return func(m *Manage) {
m.Codes = codes
}
}
func WithDialCodes(dial DialCodesFunc) Option {
return func(m *Manage) {
m.dialCodes = dial
}
}
func WithCodesDatabase(database string) Option {
return func(m *Manage) {
m.codesDatabase = database
}
}
func WithWorkday(w *Workday) Option {
return func(m *Manage) {
m.Workday = w
}
}
func WithDialWorkday(dial DialWorkdayFunc) Option {
return func(m *Manage) {
m.dialWorkday = dial
}
}
func WithWorkdayDatabase(database string) Option {
return func(m *Manage) {
m.workdayDatabase = database
}
}
func WithOptions(op ...Option) Option {
return func(m *Manage) {
for _, v := range op {
v(m)
}
}
}
type Manage struct {
clients int
dial func(op ...client.Option) (cli *Client, err error)
dialOptions []client.Option
dialCodes func(c *Client, database string) (ICodes, error)
codesDatabase string
dialWorkday DialWorkdayFunc
workdayDatabase string
/*
*/
*Pool
Config *ManageConfig
Codes *Codes
Codes ICodes
Workday *Workday
Cron *cron.Cron
cron *cron.Cron
once sync.Once
}
// RangeStocks 遍历所有股票
func (this *Manage) RangeStocks(f func(code string)) {
for _, v := range this.Codes.GetStocks() {
f(v.FullCode())
}
}
// RangeETFs 遍历所有ETF
func (this *Manage) RangeETFs(f func(code string)) {
for _, v := range this.Codes.GetETFs() {
f(v.FullCode())
}
}
// RangeIndexes 遍历所有指数
func (this *Manage) RangeIndexes(f func(code string)) {
for _, v := range this.Codes.GetETFs() {
f(v.FullCode())
}
}
// AddWorkdayTask 添加工作日任务
func (this *Manage) AddWorkdayTask(spec string, f func(m *Manage)) {
this.Cron.AddFunc(spec, func() {
this.once.Do(func() {
this.cron = cron.New(cron.WithSeconds())
this.cron.Start()
})
this.cron.AddFunc(spec, func() {
if this.Workday.TodayIs() {
f(this)
}
@@ -85,8 +261,8 @@ func (this *Manage) AddWorkdayTask(spec string, f func(m *Manage)) {
}
type ManageConfig struct {
Hosts []string //服务端IP
Number int //客户端数量
CodesDir string //代码数据库位置
WorkdayDir string //工作日数据库位置
Number int //客户端数量
CodesFilename string //代码数据库位置
WorkdayFileName string //工作日数据库位置
Dial func(op ...client.Option) (cli *Client, err error) //默认连接方式
}

View File

@@ -1,11 +1,11 @@
package protocol
import (
"bytes"
"compress/zlib"
"errors"
"fmt"
"github.com/injoyai/base/bytes"
"github.com/injoyai/base/g"
"github.com/injoyai/base/types"
"github.com/injoyai/conv"
"io"
)
@@ -19,7 +19,7 @@ const (
)
type Message interface {
Bytes() g.Bytes
Bytes() types.Bytes
}
/*
@@ -39,7 +39,20 @@ type Frame struct {
Data []byte //数据
}
func (this *Frame) Bytes() g.Bytes {
/*
Bytes
0c00000000011c001c002d0500003030303030310900010000000a0000000000000000000000
Prefix: 0c
MsgID: 0208d301
Control: 01
Length: 1c00
Length: 1c00
Type: 2d05
000030303030303104000100a401a40100000000000000000000
*/
func (this *Frame) Bytes() types.Bytes {
length := uint16(len(this.Data) + 2)
data := make([]byte, 12+len(this.Data))
data[0] = Prefix

View File

@@ -49,7 +49,7 @@ func (code) Decode(bs []byte) (*CodeResp, error) {
Multiple: Uint16(bs[6:8]),
Name: string(UTF8ToGBK(bs[8:16])),
Decimal: int8(bs[20]),
LastPrice: getVolume(Uint32(bs[21:25])),
LastPrice: getVolume2(Uint32(bs[21:25])),
}
//logs.Debug(bs[25:29]) //26和28字节 好像是枚举(基本是44,45和34,35)
bs = bs[29:]

View File

@@ -5,16 +5,16 @@ import (
)
var (
MConnect = connect{}
MHeart = heart{}
MCount = count{}
MQuote = quote{}
MCode = code{}
MMinute = minute{}
MHistoryMinute = historyMinute{}
MMinuteTrade = minuteTrade{}
MHistoryMinuteTrade = historyMinuteTrade{}
MKline = kline{}
MConnect = connect{}
MHeart = heart{}
MCount = count{}
MQuote = quote{}
MCode = code{}
MMinute = minute{}
MHistoryMinute = historyMinute{}
MTrade = trade{}
MHistoryTrade = historyTrade{}
MKline = kline{}
)
type ConnectResp struct {

View File

@@ -33,10 +33,10 @@ func (this historyMinute) Decode(bs []byte) (*MinuteResp, error) {
Count: Uint16(bs[:2]),
}
multiple := Price(1)
if bs[5] > 0x40 {
multiple = 10
}
multiple := Price(1) * 10
//if bs[5] > 0x40 {
//multiple = 10
//}
//2-4字节是啥?
bs = bs[6:]

View File

@@ -1,94 +0,0 @@
package protocol
import (
"errors"
"fmt"
"github.com/injoyai/conv"
)
// HistoryMinuteTradeResp 历史分时交易比实时少了单量
type HistoryMinuteTradeResp struct {
Count uint16
List []*HistoryMinuteTrade
}
type HistoryMinuteTrade struct {
Time string //时间
Price Price //价格
Volume int //成交量
Status int //0是买1是卖2无效汇总出现中途也可能出现2,例20241115(sz000001)的14:56
}
func (this *HistoryMinuteTrade) String() string {
return fmt.Sprintf("%s \t%s \t%-6s \t%-6d(手) \t%-4s", this.Time, this.Price, this.Amount(), this.Volume, this.StatusString())
}
// Amount 成交额
func (this *HistoryMinuteTrade) Amount() Price {
return this.Price * Price(this.Volume*100)
}
func (this *HistoryMinuteTrade) StatusString() string {
switch this.Status {
case 0:
return "买入"
case 1:
return "卖出"
default:
return ""
}
}
type historyMinuteTrade struct{}
func (historyMinuteTrade) Frame(date, code string, start, count uint16) (*Frame, error) {
exchange, number, err := DecodeCode(code)
if err != nil {
return nil, err
}
dataBs := Bytes(conv.Uint32(date)) //req.Time.Format("20060102"))
dataBs = append(dataBs, exchange.Uint8(), 0x0)
dataBs = append(dataBs, []byte(number)...)
dataBs = append(dataBs, Bytes(start)...)
dataBs = append(dataBs, Bytes(count)...)
return &Frame{
Control: Control01,
Type: TypeHistoryMinuteTrade,
Data: dataBs,
}, nil
}
func (historyMinuteTrade) Decode(bs []byte, code string) (*HistoryMinuteTradeResp, error) {
if len(bs) < 2 {
return nil, errors.New("数据长度不足")
}
_, number, err := DecodeCode(code)
if err != nil {
return nil, err
}
resp := &HistoryMinuteTradeResp{
Count: Uint16(bs[:2]),
}
//第2-6字节不知道是啥
bs = bs[2+4:]
lastPrice := Price(0)
for i := uint16(0); i < resp.Count; i++ {
mt := &HistoryMinuteTrade{
Time: GetHourMinute([2]byte(bs[:2])),
}
var sub Price
bs, sub = GetPrice(bs[2:])
lastPrice += sub * 10 //把分转成厘
mt.Price = lastPrice / basePrice(number)
bs, mt.Volume = CutInt(bs)
bs, mt.Status = CutInt(bs)
bs, _ = CutInt(bs) //这个得到的是0不知道是啥
resp.List = append(resp.List, mt)
}
return resp, nil
}

View File

@@ -0,0 +1,67 @@
package protocol
import (
"errors"
"github.com/injoyai/conv"
"time"
)
// HistoryTradeResp 兼容之前的版本
type HistoryTradeResp = TradeResp
type historyTrade struct{}
func (historyTrade) Frame(date, code string, start, count uint16) (*Frame, error) {
exchange, number, err := DecodeCode(code)
if err != nil {
return nil, err
}
dataBs := Bytes(conv.Uint32(date)) //req.Time.Format("20060102"))
dataBs = append(dataBs, exchange.Uint8(), 0x0)
dataBs = append(dataBs, []byte(number)...)
dataBs = append(dataBs, Bytes(start)...)
dataBs = append(dataBs, Bytes(count)...)
return &Frame{
Control: Control01,
Type: TypeHistoryMinuteTrade,
Data: dataBs,
}, nil
}
func (historyTrade) Decode(bs []byte, c TradeCache) (*TradeResp, error) {
if len(bs) < 2 {
return nil, errors.New("数据长度不足")
}
_, number, err := DecodeCode(c.Code)
if err != nil {
return nil, err
}
resp := &TradeResp{
Count: Uint16(bs[:2]),
}
//第2-6字节不知道是啥
bs = bs[2+4:]
lastPrice := Price(0)
for i := uint16(0); i < resp.Count; i++ {
timeStr := GetHourMinute([2]byte(bs[:2]))
t, err := time.Parse("2006010215:04", c.Date+timeStr)
if err != nil {
return nil, err
}
mt := &Trade{Time: t}
var sub Price
bs, sub = GetPrice(bs[2:])
lastPrice += sub * 10 //把分转成厘
mt.Price = lastPrice / basePrice(number)
bs, mt.Volume = CutInt(bs)
bs, mt.Status = CutInt(bs)
bs, _ = CutInt(bs) //这个得到的是0不知道是啥
resp.List = append(resp.List, mt)
}
return resp, nil
}

View File

@@ -7,7 +7,7 @@ import (
func Test_stockHistoryMinuteTrade_Frame(t *testing.T) {
// 预期 0c 02000000 00 1200 1200 b50f 84da3401 0000 30303030303100006400
// 0c000000000112001200b50f84da3401000030303030303100006400
f, err := MHistoryMinuteTrade.Frame("20241028", "sz000001", 0, 100)
f, err := MHistoryTrade.Frame("20241028", "sz000001", 0, 100)
if err != nil {
t.Error(err)
return

View File

@@ -3,8 +3,9 @@ package protocol
import (
"errors"
"fmt"
"github.com/injoyai/base/g"
"github.com/injoyai/base/types"
"github.com/injoyai/conv"
"sort"
"time"
)
@@ -15,7 +16,7 @@ type KlineReq struct {
Count uint16
}
func (this *KlineReq) Bytes(Type uint8) (g.Bytes, error) {
func (this *KlineReq) Bytes(Type uint8) (types.Bytes, error) {
if this.Count > 800 {
return nil, errors.New("单次数量不能超过800")
}
@@ -43,6 +44,7 @@ type Kline struct {
High Price //最高价
Low Price //最低价
Close Price //收盘价,如果是当天,则是最新价/实时价
Order int //成交单数,不一定有值
Volume int64 //成交量
Amount Price //成交额
Time time.Time //时间
@@ -77,11 +79,35 @@ func (this *Kline) RisePrice() Price {
// RiseRate 涨跌比例/涨跌幅,第一个数据不准,仅做参考
func (this *Kline) RiseRate() float64 {
return float64(this.RisePrice()) / float64(this.Open) * 100
if this.Last == 0 {
return float64(this.Close-this.Open) / float64(this.Open) * 100
}
return float64(this.Close-this.Last) / float64(this.Last) * 100
}
type kline struct{}
/*
Frame
Prefix: 0c
MsgID: 0208d301
Control: 01
Length: 1c00
Length: 1c00
Type: 2d05
Data: 000030303030303104000100a401a40100000000000000000000
Data:
Exchange: 00
Unknown: 00
Code: 303030303031
Type: 04
Unknown: 00
Unknown: 0100
Start: a401
Count: a401
Append: 00000000000000000000
*/
func (kline) Frame(Type uint8, code string, start, count uint16) (*Frame, error) {
if count > 800 {
return nil, errors.New("单次数量不能超过800")
@@ -112,7 +138,6 @@ func (kline) Decode(bs []byte, c KlineCache) (*KlineResp, error) {
if len(bs) < 2 {
return nil, errors.New("数据长度不足")
}
resp := &KlineResp{
Count: Uint16(bs[:2]),
}
@@ -157,7 +182,7 @@ func (kline) Decode(bs []byte, c KlineCache) (*KlineResp, error) {
k.Volume = int64(getVolume(Uint32(bs[:4])))
bs = bs[4:]
switch c.Type {
case TypeKlineMinute, TypeKline5Minute, TypeKlineMinute2, TypeKline15Minute, TypeKline30Minute, TypeKlineHour, TypeKlineDay2:
case TypeKlineMinute, TypeKline5Minute, TypeKlineMinute2, TypeKline15Minute, TypeKline30Minute, TypeKline60Minute, TypeKlineDay2:
k.Volume /= 100
}
k.Amount = Price(getVolume(Uint32(bs[:4])) * 1000) //从元转为厘,并去除多余的小数
@@ -174,7 +199,7 @@ func (kline) Decode(bs []byte, c KlineCache) (*KlineResp, error) {
resp.List = append(resp.List, k)
}
resp.List = FixKlineTime(resp.List)
return resp, nil
}
@@ -182,3 +207,156 @@ type KlineCache struct {
Type uint8 //1分钟,5分钟,日线等
Kind string //指数,个股等
}
// FixKlineTime 修复盘内下午(13~15点)拉取数据的时候,11.30的时间变成13.00
func FixKlineTime(ks []*Kline) []*Kline {
if len(ks) == 0 {
return ks
}
now := time.Now()
//只有当天下午13~15点之间才会出现的时间问题
node1 := time.Date(now.Year(), now.Month(), now.Day(), 13, 0, 0, 0, now.Location())
node2 := time.Date(now.Year(), now.Month(), now.Day(), 15, 0, 0, 0, now.Location())
if ks[len(ks)-1].Time.Unix() < node1.Unix() || ks[len(ks)-1].Time.Unix() > node2.Unix() {
return ks
}
ls := ks
if len(ls) >= 120 {
ls = ls[len(ls)-120:]
}
for i, v := range ls {
if v.Time.Unix() == node1.Unix() {
ls[i].Time = time.Date(now.Year(), now.Month(), now.Day(), 11, 30, 0, 0, now.Location())
}
}
return ks
}
type Klines []*Kline
// LastPrice 获取最后一个K线的收盘价
func (this Klines) LastPrice() Price {
if len(this) == 0 {
return 0
}
return this[len(this)-1].Close
}
func (this Klines) Len() int {
return len(this)
}
func (this Klines) Swap(i, j int) {
this[i], this[j] = this[j], this[i]
}
func (this Klines) Less(i, j int) bool {
return this[i].Time.Before(this[j].Time)
}
func (this Klines) Sort() {
sort.Sort(this)
}
func (this Klines) Kline(t time.Time, last Price) *Kline {
k := &Kline{
Time: t,
Open: last,
High: last,
Low: last,
Close: last,
Volume: 0,
Amount: 0,
}
for i, v := range this {
switch i {
case 0:
k.Open = v.Open
k.High = v.High
k.Low = v.Low
k.Close = v.Close
default:
if k.Open == 0 {
k.Open = v.Open
}
k.High = conv.Select(k.High < v.High, v.High, k.High)
k.Low = conv.Select(k.Low > v.Low, v.Low, k.Low)
}
k.Close = v.Close
k.Volume += v.Volume
k.Amount += v.Amount
}
return k
}
// Merge 合并成其他类型的K线
func (this Klines) Merge(n int) Klines {
if n <= 1 {
return this
}
ks := Klines(nil)
ls := Klines(nil)
for i := 0; ; i++ {
if len(this) <= i*n {
break
}
if len(this) < (i+1)*n {
ls = this[i*n:]
} else {
ls = this[i*n : (i+1)*n]
}
if len(ls) == 0 {
break
}
last := ls[len(ls)-1]
k := ls.Kline(last.Time, ls[0].Open)
ks = append(ks, k)
}
return ks
}
//// Kline 计算多个K线,成一个K线
//func (this Klines) Kline() *Kline {
// if this == nil {
// return new(Kline)
// }
// k := new(Kline)
// for i, v := range this {
// switch i {
// case 0:
// k.Open = v.Open
// k.High = v.High
// k.Low = v.Low
// k.Close = v.Close
// case len(this) - 1:
// k.Close = v.Close
// k.Time = v.Time
// }
// if v.High > k.High {
// k.High = v.High
// }
// if v.Low < k.Low {
// k.Low = v.Low
// }
// k.Volume += v.Volume
// k.Amount += v.Amount
// }
// return k
//}
//// Merge 合并K线,1分钟转成5,15,30分钟等
//func (this Klines) Merge(n int) Klines {
// if this == nil {
// return nil
// }
// ks := []*Kline(nil)
// for i := 0; i < len(this); i += n {
// if i+n > len(this) {
// ks = append(ks, this[i:].Kline())
// } else {
// ks = append(ks, this[i:i+n].Kline())
// }
// }
// return ks
//}

View File

@@ -19,7 +19,10 @@ func Test_stockKline_Decode(t *testing.T) {
t.Error(err)
return
}
resp, err := MKline.Decode(bs, 9)
resp, err := MKline.Decode(bs, KlineCache{
Type: 9,
Kind: "",
})
if err != nil {
t.Error(err)
return

View File

@@ -1,116 +0,0 @@
package protocol
import (
"errors"
"fmt"
)
type MinuteTradeResp struct {
Count uint16
List []*MinuteTrade
}
// MinuteTrade 分时成交todo 时间没有到秒,客户端上也没有,东方客户端能显示秒
type MinuteTrade struct {
Time string //时间
Price Price //价格
Volume int //成交量
Number int //单数,历史数据该字段无效
Status int //0是买1是卖2无效汇总出现
}
func (this *MinuteTrade) String() string {
return fmt.Sprintf("%s \t%-6s \t%-6s \t%-6d(手) \t%-4d(单) \t%-4s",
this.Time, this.Price, this.Amount(), this.Volume, this.Number, this.StatusString())
}
// Amount 成交额
func (this *MinuteTrade) Amount() Price {
return this.Price * Price(this.Volume) * 100
}
func (this *MinuteTrade) StatusString() string {
switch this.Status {
case 0:
return "买入"
case 1:
return "卖出"
default:
return ""
}
}
// AvgVolume 平均每单成交量
func (this *MinuteTrade) AvgVolume() float64 {
return float64(this.Volume) / float64(this.Number)
}
// AvgPrice 平均每单成交金额
func (this *MinuteTrade) AvgPrice() Price {
return Price(this.AvgVolume() * float64(this.Price) * 100)
}
// IsBuy 是否是买单
func (this *MinuteTrade) IsBuy() bool {
return this.Status == 0
}
// IsSell 是否是卖单
func (this *MinuteTrade) IsSell() bool {
return this.Status == 1
}
type minuteTrade struct{}
func (minuteTrade) Frame(code string, start, count uint16) (*Frame, error) {
exchange, number, err := DecodeCode(code)
if err != nil {
return nil, err
}
codeBs := []byte(number)
codeBs = append(codeBs, Bytes(start)...)
codeBs = append(codeBs, Bytes(count)...)
return &Frame{
Control: Control01,
Type: TypeMinuteTrade,
Data: append([]byte{exchange.Uint8(), 0x0}, codeBs...),
}, nil
}
func (minuteTrade) Decode(bs []byte, code string) (*MinuteTradeResp, error) {
var err error
_, code, err = DecodeCode(code)
if err != nil {
return nil, err
}
if len(bs) < 2 {
return nil, errors.New("数据长度不足")
}
resp := &MinuteTradeResp{
Count: Uint16(bs[:2]),
}
bs = bs[2:]
lastPrice := Price(0)
for i := uint16(0); i < resp.Count; i++ {
mt := &MinuteTrade{
Time: GetHourMinute([2]byte(bs[:2])),
}
var sub Price
bs, sub = GetPrice(bs[2:])
lastPrice += sub * 10 //把分转换成厘
mt.Price = lastPrice / basePrice(code)
bs, mt.Volume = CutInt(bs)
bs, mt.Number = CutInt(bs)
bs, mt.Status = CutInt(bs)
bs, _ = CutInt(bs) //这个得到的是0不知道是啥
resp.List = append(resp.List, mt)
}
return resp, nil
}

View File

@@ -47,7 +47,7 @@ type Quote struct {
func (this *Quote) String() string {
return fmt.Sprintf(`%s%s
%s
%s, 现量:%s, 总金额:%s, 内盘:%s, 外盘:%s
%s, 现量:%s, 总金额:%s, 内盘:%s, 外盘:%s
%s%s
`,
this.Exchange.String(), this.Code, this.K,
@@ -142,9 +142,9 @@ func (this quote) Decode(bs []byte) QuotesResp {
sellLevel := PriceLevel{}
bs, p = GetPrice(bs)
buyLevel.Price = p + sec.K.Close
buyLevel.Price = p*10 + sec.K.Close
bs, p = GetPrice(bs)
sellLevel.Price = p + sec.K.Close
sellLevel.Price = p*10 + sec.K.Close
bs, buyLevel.Number = CutInt(bs)
bs, sellLevel.Number = CutInt(bs)

233
protocol/model_trade.go Normal file
View File

@@ -0,0 +1,233 @@
package protocol
import (
"errors"
"fmt"
"github.com/injoyai/base/types"
"github.com/injoyai/conv"
"time"
)
type TradeResp struct {
Count uint16
List Trades
}
// Trade 分时成交todo 时间没有到秒,客户端上也没有,东方客户端能显示秒
type Trade struct {
Time time.Time //时间, 09:30
Price Price //价格
Volume int //成交量,手
Status int //0是买1是卖2中性/汇总 中途也可能出现2,例20241115(sz000001)的14:56
Number int //单数,历史数据该字段无效
}
func (this *Trade) String() string {
return fmt.Sprintf("%s \t%-6s \t%-6s \t%-6d(手) \t%-4d(单) \t%-4s",
this.Time, this.Price, this.Amount(), this.Volume, this.Number, this.StatusString())
}
// Amount 成交额
func (this *Trade) Amount() Price {
return this.Price * Price(this.Volume*100)
}
func (this *Trade) StatusString() string {
switch this.Status {
case 0:
return "买入"
case 1:
return "卖出"
default:
return ""
}
}
// AvgVolume 平均每单成交量
func (this *Trade) AvgVolume() float64 {
return float64(this.Volume) / float64(this.Number)
}
// AvgPrice 平均每单成交金额
func (this *Trade) AvgPrice() Price {
return Price(this.AvgVolume() * float64(this.Price) * 100)
}
// IsBuy 是否是买单
func (this *Trade) IsBuy() bool {
return this.Status == 0
}
// IsSell 是否是卖单
func (this *Trade) IsSell() bool {
return this.Status == 1
}
type trade struct{}
func (trade) Frame(code string, start, count uint16) (*Frame, error) {
exchange, number, err := DecodeCode(code)
if err != nil {
return nil, err
}
codeBs := []byte(number)
codeBs = append(codeBs, Bytes(start)...)
codeBs = append(codeBs, Bytes(count)...)
return &Frame{
Control: Control01,
Type: TypeMinuteTrade,
Data: append([]byte{exchange.Uint8(), 0x0}, codeBs...),
}, nil
}
func (trade) Decode(bs []byte, c TradeCache) (*TradeResp, error) {
_, code, err := DecodeCode(c.Code)
if err != nil {
return nil, err
}
if len(bs) < 2 {
return nil, errors.New("数据长度不足")
}
resp := &TradeResp{
Count: Uint16(bs[:2]),
}
bs = bs[2:]
lastPrice := Price(0)
for i := uint16(0); i < resp.Count; i++ {
timeStr := GetHourMinute([2]byte(bs[:2]))
t, err := time.Parse("2006010215:04", c.Date+timeStr)
if err != nil {
return nil, err
}
mt := &Trade{Time: t}
var sub Price
bs, sub = GetPrice(bs[2:])
lastPrice += sub * 10 //把分转换成厘
mt.Price = lastPrice / basePrice(code)
bs, mt.Volume = CutInt(bs)
bs, mt.Number = CutInt(bs)
bs, mt.Status = CutInt(bs)
bs, _ = CutInt(bs) //这个得到的是0不知道是啥
resp.List = append(resp.List, mt)
}
return resp, nil
}
type Trades []*Trade
// Klines 合并分时成交成k线
func (this Trades) Klines() Klines {
//按天分割
m := make(types.SortMap[int64, Trades])
for _, v := range this {
//获取当天零点的时间戳
unix := time.Date(v.Time.Year(), v.Time.Month(), v.Time.Day(), 0, 0, 0, 0, v.Time.Location()).Unix()
m[unix] = append(m[unix], v)
}
//按天排序
mKline := types.SortMap[int64, Klines]{}
for date, v := range m {
//生成一分钟k线
t := time.Unix(date, 0)
mKline[date] = v.klinesForDay(t)
}
//按时间排序
lss := mKline.Sort()
ls := Klines{}
for _, v := range lss {
ls = append(ls, v...)
}
return ls
}
// Kline 合并分时成交成1个k线,注意分时成交时间保持一致
func (this Trades) Kline(t time.Time, last Price) *Kline {
k := &Kline{
Time: t,
Last: last,
Open: last,
High: last,
Low: last,
Close: last,
}
first := 0
for _, v := range this {
if v.Price <= 0 {
continue
}
switch first {
case 0:
k.Open = v.Price
k.High = v.Price
k.Low = v.Price
k.Close = v.Price
default:
k.High = conv.Select(k.High < v.Price, v.Price, k.High)
k.Low = conv.Select(k.Low > v.Price, v.Price, k.Low)
}
k.Close = v.Price
k.Volume += int64(v.Volume)
k.Order += v.Number
k.Amount += v.Price * Price(v.Volume) * 100
first++
}
return k
}
// kline1 生成一分钟k线,一天
func (this Trades) klinesForDay(date time.Time) Klines {
_930 := 570 //9:30 的分钟
_1130 := 690 //11:30 的分钟
_1300 := 780 //13:00 的分钟
_1500 := 900 //15:00 的分钟
keys := []int(nil)
//早上
m := map[int]Trades{}
for i := 1; i <= 120; i++ {
keys = append(keys, _930+i)
m[_930+i] = []*Trade{}
}
//下午
for i := 1; i <= 120; i++ {
keys = append(keys, _1300+i)
m[_1300+i] = []*Trade{}
}
//获取开盘价,有可能前几分钟没有数据,先遍历一遍
var open Price
for _, v := range this {
if v.Price > 0 {
open = v.Price
break
}
}
//分组,按
for _, v := range this {
ms := minutes(v.Time)
t := conv.Select(ms < _930, _930, ms)
t++
t = conv.Select(t > _1130 && t <= _1300, _1130, t)
t = conv.Select(t > _1500, _1500, t)
m[t] = append(m[t], v)
}
//合并
ls := []*Kline(nil)
for _, v := range keys {
k := m[v].Kline(time.Date(date.Year(), date.Month(), date.Day(), v/60, v%60, 0, 0, date.Location()), open)
open = k.Close
ls = append(ls, k)
}
return ls
}
type TradeCache struct {
Date string //日期
Code string //计算倍数
}

View File

@@ -20,8 +20,8 @@ func (this Exchange) String() string {
return "sz"
case ExchangeSH:
return "sh"
//case ExchangeBJ:
//return "bj"
case ExchangeBJ:
return "bj"
default:
return "unknown"
}
@@ -33,8 +33,8 @@ func (this Exchange) Name() string {
return "上海"
case ExchangeSZ:
return "深圳"
//case ExchangeBJ:
//return "北京"
case ExchangeBJ:
return "北京"
default:
return "未知"
}
@@ -43,13 +43,14 @@ func (this Exchange) Name() string {
const (
ExchangeSZ Exchange = iota //深圳交易所
ExchangeSH //上海交易所
//ExchangeBJ //北京交易所
ExchangeBJ //北京交易所
)
const (
TypeKline5Minute uint8 = 0 // 5分钟K 线
TypeKline15Minute uint8 = 1 // 15分钟K 线
TypeKline30Minute uint8 = 2 // 30分钟K 线
TypeKline60Minute uint8 = 3 // 60分钟K 线
TypeKlineHour uint8 = 3 // 1小时K 线
TypeKlineDay2 uint8 = 4 // 日K 线, 发现和Day的区别是这个要除以100,其他未知
TypeKlineWeek uint8 = 5 // 周K 线

View File

@@ -78,6 +78,13 @@ func DecodeK(bs []byte) ([]byte, K) {
bs, k.Low = GetPrice(bs)
k.Low += k.Close
//默认按股票展示
k.Last *= 10
k.Open *= 10
k.Close *= 10
k.High *= 10
k.Low *= 10
return bs, k
}

View File

@@ -3,24 +3,24 @@ package protocol
import (
"bytes"
"fmt"
bytes2 "github.com/injoyai/base/bytes"
"github.com/injoyai/conv"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io"
"math"
"strings"
"time"
"github.com/injoyai/conv"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
// String 字节先转小端,再转字符
func String(bs []byte) string {
return string(bytes2.Reverse(bs))
return string(Reverse(bs))
}
// Bytes 任意类型转小端字节
func Bytes(n any) []byte {
return bytes2.Reverse(conv.Bytes(n))
return Reverse(conv.Bytes(n))
}
// Reverse 字节倒序
@@ -34,12 +34,12 @@ func Reverse(bs []byte) []byte {
// Uint32 字节通过小端方式转为uint32
func Uint32(bs []byte) uint32 {
return conv.Uint32(bytes2.Reverse(bs))
return conv.Uint32(Reverse(bs))
}
// Uint16 字节通过小端方式转为uint16
func Uint16(bs []byte) uint16 {
return conv.Uint16(bytes2.Reverse(bs))
return conv.Uint16(Reverse(bs))
}
func UTF8ToGBK(text []byte) []byte {
@@ -59,6 +59,8 @@ func DecodeCode(code string) (Exchange, string, error) {
return ExchangeSH, code[2:], nil
case ExchangeSZ.String():
return ExchangeSZ, code[2:], nil
case ExchangeBJ.String():
return ExchangeBJ, code[2:], nil
default:
return 0, "", fmt.Errorf("股票代码错误,例如:SZ000001")
}
@@ -103,7 +105,7 @@ func GetHourMinute(bs [2]byte) string {
func GetTime(bs [4]byte, Type uint8) time.Time {
switch Type {
case TypeKlineMinute, TypeKlineMinute2, TypeKline5Minute, TypeKline15Minute, TypeKline30Minute, TypeKlineHour:
case TypeKlineMinute, TypeKlineMinute2, TypeKline5Minute, TypeKline15Minute, TypeKline30Minute, TypeKline60Minute:
yearMonthDay := Uint16(bs[:2])
hourMinute := Uint16(bs[2:4])
@@ -126,14 +128,18 @@ func GetTime(bs [4]byte, Type uint8) time.Time {
}
func basePrice(code string) Price {
if len(code) == 0 {
if len(code) < 2 {
return 1
}
switch code[:1] {
case "8":
return 1
}
switch code[:2] {
case "60", "30", "68", "00":
case "60", "30", "68", "00", "92", "43", "39":
return 1
default:
return 10
return 1
}
}
@@ -196,35 +202,140 @@ func getVolume(val uint32) (volume float64) {
return
}
func getVolume2(val uint32) float64 {
ivol := int32(val)
logpoint := ivol >> 24 // 提取最高字节原8*3移位
hleax := (ivol >> 16) & 0xff // 提取次高字节
lheax := (ivol >> 8) & 0xff // 提取第三字节
lleax := ivol & 0xff // 提取最低字节
dwEcx := logpoint*2 - 0x7f // 基础指数计算
dbl_xmm6 := math.Exp2(float64(dwEcx)) // 核心指数计算仅一次
// 计算dbl_xmm4
var dbl_xmm4 float64
if hleax > 0x80 {
// 高位分支:合并指数计算
dbl_xmm4 = dbl_xmm6 * (64.0 + float64(hleax&0x7f)) / 64.0
} else {
// 低位分支:复用核心指数
dbl_xmm4 = dbl_xmm6 * float64(hleax) / 128.0
}
// 计算缩放因子
scale := 1.0
if (hleax & 0x80) != 0 {
scale = 2.0
}
// 预计算常量的倒数,优化除法
const (
inv32768 = 1.0 / 32768.0 // 2^15
inv8388608 = 1.0 / 8388608.0 // 2^23
)
// 计算低位分量
dbl_xmm3 := dbl_xmm6 * float64(lheax) * inv32768 * scale
dbl_xmm1 := dbl_xmm6 * float64(lleax) * inv8388608 * scale
// 合计最终结果
return dbl_xmm6 + dbl_xmm4 + dbl_xmm3 + dbl_xmm1
}
// IsStock 是否是股票,示例sz000001
func IsStock(code string) bool {
return IsSZStock(code) || IsSHStock(code) || IsBJStock(code)
//if len(code) != 8 {
// return false
//}
//code = strings.ToLower(code)
//switch {
//case code[0:2] == ExchangeSH.String() &&
// (code[2:3] == "6"):
// return true
//
//case code[0:2] == ExchangeSZ.String() &&
// (code[2:3] == "0" || code[2:4] == "30"):
// return true
//}
//return false
}
func IsSZStock(code string) bool {
return len(code) == 8 && strings.ToLower(code[0:2]) == ExchangeSZ.String() && (code[2:3] == "0" || code[2:4] == "30")
}
func IsSHStock(code string) bool {
return len(code) == 8 && strings.ToLower(code[0:2]) == ExchangeSH.String() && code[2:3] == "6"
}
func IsBJStock(code string) bool {
return len(code) == 8 && strings.ToLower(code[0:2]) == ExchangeBJ.String() && (code[2:4] == "92" || code[2:4] == "43" || code[2:3] == "8")
}
// IsETF 是否是基金,示例sz159558
func IsETF(code string) bool {
if len(code) != 8 {
return false
}
code = strings.ToLower(code)
switch {
case code[0:2] == ExchangeSH.String() &&
(code[2:3] == "6"):
(code[2:4] == "51" || code[2:4] == "56" || code[2:4] == "58"):
return true
case code[0:2] == ExchangeSZ.String() &&
(code[2:3] == "0" || code[2:4] == "30"):
(code[2:4] == "15"):
return true
}
return false
}
// AddPrefix 添加股票代码前缀,针对股票生效,例如000001,会增加前缀sz000001(平安银行),而不是sh000001(上证指数)
// IsIndex 是否是指数,sh000001,sz399001,bj899100
func IsIndex(code string) bool {
if len(code) != 8 {
return false
}
code = strings.ToLower(code)
switch {
case code[0:2] == ExchangeSH.String() && code[2:5] == "000":
return true
case code[0:2] == ExchangeSZ.String() && code[2:5] == "399":
return true
case code[0:2] == ExchangeBJ.String() && code[2:5] == "899":
return true
}
return false
}
// AddPrefix 添加股票/基金代码前缀,针对股票/基金生效,例如000001,会增加前缀sz000001(平安银行),而不是sh000001(上证指数)
func AddPrefix(code string) string {
if len(code) == 6 {
switch {
case code[:1] == "6":
//上海股票
code = ExchangeSH.String() + code
case code[:1] == "0":
//深圳股票
code = ExchangeSZ.String() + code
case code[:2] == "30":
//深圳股票
code = ExchangeSZ.String() + code
case code[:3] == "510" || code[:3] == "511" || code[:3] == "512" || code[:3] == "513" || code[:3] == "515":
//上海基金
code = ExchangeSH.String() + code
case code[:3] == "159":
//深圳基金
code = ExchangeSZ.String() + code
case code[:1] == "8" || code[:2] == "92" || code[:2] == "43":
//北京股票
code = ExchangeBJ.String() + code
}
}
return code
}
func minutes(t time.Time) int {
return t.Hour()*60 + t.Minute()
}

View File

@@ -20,5 +20,6 @@ func TestUTF8ToGBK(t *testing.T) {
func Test_getVolume(t *testing.T) {
t.Log(getVolume(1237966432))
t.Log(getVolume(1237966432))
t.Log(getVolume2(1237966432))
}

View File

@@ -1,18 +1,48 @@
package tdx
import (
_ "github.com/glebarez/go-sqlite"
"github.com/injoyai/base/maps"
"github.com/injoyai/logs"
"github.com/robfig/cron/v3"
"errors"
"iter"
"os"
"path/filepath"
"time"
_ "github.com/glebarez/go-sqlite"
_ "github.com/go-sql-driver/mysql"
"github.com/injoyai/base/maps"
"github.com/injoyai/conv"
"github.com/injoyai/ios/client"
"github.com/injoyai/logs"
"github.com/injoyai/tdx/protocol"
"github.com/robfig/cron/v3"
"xorm.io/core"
"xorm.io/xorm"
)
func NewWorkday(c *Client, filename string) (*Workday, error) {
func DialWorkday(op ...client.Option) (*Workday, error) {
c, err := DialDefault(op...)
if err != nil {
return nil, err
}
return NewWorkdaySqlite(c)
}
func NewWorkdayMysql(c *Client, dsn string) (*Workday, error) {
//连接数据库
db, err := xorm.NewEngine("mysql", dsn)
if err != nil {
return nil, err
}
db.SetMapper(core.SameMapper{})
return NewWorkday(c, db)
}
func NewWorkdaySqlite(c *Client, filenames ...string) (*Workday, error) {
defaultFilename := filepath.Join(DefaultDatabaseDir, "workday.db")
filename := conv.Default(defaultFilename, filenames...)
//如果文件夹不存在就创建
dir, _ := filepath.Split(filename)
@@ -25,6 +55,11 @@ func NewWorkday(c *Client, filename string) (*Workday, error) {
}
db.SetMapper(core.SameMapper{})
db.DB().SetMaxOpenConns(1)
return NewWorkday(c, db)
}
func NewWorkday(c *Client, db *xorm.Engine) (*Workday, error) {
if err := db.Sync2(new(WorkdayModel)); err != nil {
return nil, err
}
@@ -34,12 +69,12 @@ func NewWorkday(c *Client, filename string) (*Workday, error) {
db: db,
cache: maps.NewBit(),
}
//设置定时器,每天早上9点更新数据,8点多获取不到今天的数据
task := cron.New(cron.WithSeconds())
task.AddFunc("0 0 9 * * *", func() {
for i := 0; i < 3; i++ {
if err := w.Update(); err == nil {
err := w.Update()
if err == nil {
return
}
logs.Err(err)
@@ -47,7 +82,6 @@ func NewWorkday(c *Client, filename string) (*Workday, error) {
}
})
task.Start()
return w, w.Update()
}
@@ -59,6 +93,11 @@ type Workday struct {
// Update 更新
func (this *Workday) Update() error {
if this.Client == nil {
return errors.New("client is nil")
}
//获取沪市指数的日K线,用作历史是否节假日的判断依据
//判断日K线是否拉取过
@@ -76,27 +115,30 @@ func (this *Workday) Update() error {
}
now := time.Now()
if lastWorkday == nil || lastWorkday.Unix < IntegerDay(now).Unix() {
resp, err := this.Client.GetKlineDayAll("sh000001")
if lastWorkday.Unix < IntegerDay(now).Unix() {
resp, err := this.Client.GetIndexDayAll("sh000001")
if err != nil {
logs.Err(err)
return err
}
return NewSessionFunc(this.db, func(session *xorm.Session) error {
for _, v := range resp.List {
if unix := v.Time.Unix(); unix > lastWorkday.Unix {
_, err = session.Insert(&WorkdayModel{Unix: unix, Date: v.Time.Format("20060102"), Is: true})
if err != nil {
return err
}
this.cache.Set(uint64(unix), true)
}
inserts := []any(nil)
for _, v := range resp.List {
if unix := v.Time.Unix(); unix > lastWorkday.Unix {
inserts = append(inserts, &WorkdayModel{Unix: unix, Date: v.Time.Format("20060102")})
this.cache.Set(uint64(unix), true)
}
}
if len(inserts) == 0 {
return nil
})
}
_, err = this.db.Insert(inserts)
return err
}
return nil
}
@@ -110,24 +152,67 @@ func (this *Workday) TodayIs() bool {
return this.Is(time.Now())
}
// RangeDesc 倒序遍历工作日,从今天-1990年12月19日(上海交易所成立时间)
func (this *Workday) RangeDesc(f func(t time.Time) bool) {
t := IntegerDay(time.Now())
for ; t.After(time.Date(1990, 12, 18, 0, 0, 0, 0, time.Local)); t = t.Add(-time.Hour * 24) {
if this.Is(t) {
if !f(t) {
// RangeYear 遍历一年的所有工作日
func (this *Workday) RangeYear(year int, f func(t time.Time) bool) {
this.Range(
time.Date(year, 1, 1, 0, 0, 0, 0, time.Local),
time.Date(year, 12, 31, 0, 0, 0, 1, time.Local),
f,
)
}
// Range 遍历指定范围的工作日,推荐start带上时间15:00,这样当天小于15点不会触发
func (this *Workday) Range(start, end time.Time, f func(t time.Time) bool) {
start = conv.Select(start.Before(protocol.ExchangeEstablish), protocol.ExchangeEstablish, start)
for ; start.Before(end); start = start.Add(time.Hour * 24) {
if this.Is(start) {
if !f(start) {
return
}
}
}
}
func (this *Workday) IterYear(year int, desc ...bool) iter.Seq[time.Time] {
return this.Iter(
time.Date(year, 1, 1, 0, 0, 0, 0, time.Local),
time.Date(year, 12, 31, 0, 0, 0, 1, time.Local),
desc...,
)
}
// Iter 遍历指定范围的工作日,推荐start带上时间15:00,这样当天小于15点不会触发
func (this *Workday) Iter(start, end time.Time, desc ...bool) iter.Seq[time.Time] {
start = conv.Select(start.Before(protocol.ExchangeEstablish), protocol.ExchangeEstablish, start)
if len(desc) > 0 && desc[0] {
//倒序遍历
return func(yield func(time.Time) bool) {
for ; end.After(start); end = end.Add(-time.Hour * 24) {
if this.Is(end) {
if !yield(end) {
return
}
}
}
}
}
//正序遍历
return func(yield func(time.Time) bool) {
for ; start.Before(end); start = start.Add(time.Hour * 24) {
if this.Is(start) {
if !yield(start) {
return
}
}
}
}
}
// WorkdayModel 工作日
type WorkdayModel struct {
ID int64 `json:"id"` //主键
Unix int64 `json:"unix"` //时间戳
Date string `json:"date"` //日期
Is bool `json:"is"` //是否是工作日
}
func (this *WorkdayModel) TableName() string {