1oscon commited on
Commit
eae54e4
·
verified ·
1 Parent(s): 768c0d0

Upload 4 files

Browse files
插件_行情/K线聚合器.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ from typing import Optional, List, Dict, Iterable
3
+ from .类型定义 import 蜡烛, 成交
4
+
5
+ def _分钟起点(ms: int) -> int:
6
+ return (ms // 60000) * 60000
7
+
8
+ class 一分钟K线生成器:
9
+ """用成交流生成1mK;每分钟切换时封口上一根"""
10
+ def __init__(self, 合约: str):
11
+ self.合约 = 合约
12
+ self._分钟: Optional[int] = None
13
+ self._开 = self._高 = self._低 = self._收 = None
14
+ self._量 = 0.0
15
+
16
+ def 更新成交(self, t: 成交) -> Optional[蜡烛]:
17
+ m = _分钟起点(t.时间戳)
18
+ if self._分钟 is not None and m > self._分钟:
19
+ 完 = 蜡烛(self.合约, '1m', self._分钟, float(self._开), float(self._高), float(self._低), float(self._收), float(self._量), True, {})
20
+ self._分钟 = m
21
+ self._开 = self._高 = self._低 = self._收 = t.价
22
+ self._量 = abs(t.量)
23
+ return 完
24
+ if self._分钟 is None:
25
+ self._分钟 = m
26
+ self._开 = self._高 = self._低 = self._收 = t.价
27
+ self._量 = abs(t.量)
28
+ else:
29
+ self._收 = t.价
30
+ self._高 = max(self._高, t.价)
31
+ self._低 = min(self._低, t.价)
32
+ self._量 += abs(t.量)
33
+ return None
34
+
35
+ def 当前未封口(self) -> Optional[蜡烛]:
36
+ if self._分钟 is None: return None
37
+ return 蜡烛(self.合约,'1m',self._分钟,float(self._开),float(self._高),float(self._低),float(self._收),float(self._量),False,{})
38
+
39
+ class 多周期聚合器:
40
+ """把已收盘的1mK聚合到更大周期;仅在1m收盘时推进"""
41
+ def __init__(self, 合约: str, 周期集合_分钟: Iterable[int]):
42
+ self.合约 = 合约
43
+ self.周期 = sorted(set(int(x) for x in 周期集合_分钟))
44
+ self._缓冲: Dict[int, Dict[str, float]] = {} # tf: {'开始':ts,'开':,'高':,'低':,'收':,'量':}
45
+
46
+ def _周期起点(self, ts_ms: int, tf: int) -> int:
47
+ 分 = ts_ms // 60000
48
+ 起 = (分 // tf) * tf
49
+ return 起 * 60000
50
+
51
+ def 接收_1m收盘(self, 一分K: 蜡烛) -> List[蜡烛]:
52
+ assert 一分K.周期 == '1m' and 一分K.已收盘
53
+ 输出: List[蜡烛] = []
54
+ for tf in self.周期:
55
+ 起 = self._周期起点(一分K.时间戳, tf)
56
+ buf = self._缓冲.get(tf)
57
+ if buf is None or buf['开始'] != 起:
58
+ if buf is not None:
59
+ 输出.append(蜡烛(self.合约, f'{tf}m', int(buf['开始']), float(buf['开']), float(buf['高']), float(buf['低']), float(buf['收']), float(buf['量']), True, {}))
60
+ self._缓冲[tf] = {'开始': 起, '开': 一分K.开, '高': 一分K.高, '低': 一分K.低, '收': 一分K.收, '量': 一分K.量}
61
+ else:
62
+ buf['收'] = 一分K.收
63
+ buf['高'] = max(buf['高'], 一分K.高)
64
+ buf['低'] = min(buf['低'], 一分K.低)
65
+ buf['量'] += 一分K.量
66
+ return 输出
67
+
68
+ def 当前未封口(self) -> List[蜡烛]:
69
+ res=[]
70
+ for tf, b in self._缓冲.items():
71
+ res.append(蜡烛(self.合约, f'{tf}m', int(b['开始']), float(b['开']), float(b['高']), float(b['低']), float(b['收']), float(b['量']), False, {}))
72
+ return res
插件_行情/公共WS.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio, json, time
3
+ from typing import List, Dict, Any, Callable, Awaitable, Optional
4
+ import websockets
5
+
6
+ OKX公共WS地址 = "wss://ws.okx.com:8443/ws/v5/public"
7
+
8
+ class OKX公共WS:
9
+ """
10
+ - add: 添加订阅(channel='candle1m'/'trades'/'tickers', instId='BTC-USDT-SWAP')
11
+ - on: 注册回调(统一事件字典)
12
+ - run: 永久运行(自动重连/心跳/续订)
13
+ 事件格式:
14
+ {'类型': 'k线'|'成交'|'系统'|'确认'|'错误',
15
+ '频道': 'candle1m'|'trades'|'tickers',
16
+ '合约': 'BTC-USDT-SWAP',
17
+ '参数': {...}, '数据': [...], '时间': int?}
18
+ """
19
+ def __init__(self, 地址: str = OKX公共WS地址, 心跳秒: int = 20, 重连秒: int = 3):
20
+ self.地址 = 地址
21
+ self.心跳秒 = 心跳秒
22
+ self.重连秒 = 重连秒
23
+ self._订阅: List[Dict[str, Any]] = []
24
+ self._回调: List[Callable[[Dict[str, Any]], Awaitable[None] | None]] = []
25
+ self._ws = None
26
+ self._停 = False
27
+
28
+ def 添加订阅(self, 频道: str, 合约: str, bar: Optional[str] = None):
29
+ if 频道.startswith('candle') and bar and not 频道.endswith(bar):
30
+ 频道 = f'candle{bar}'
31
+ self._订阅.append({"channel": 频道, "instId": 合约})
32
+
33
+ def 注册回调(self, 函数: Callable[[Dict[str, Any]], Awaitable[None] | None]):
34
+ self._回调.append(函数)
35
+
36
+ async def _发(self, ws, msg: Dict[str, Any]):
37
+ await ws.send(json.dumps(msg))
38
+
39
+ async def _发心跳(self, ws):
40
+ while not self._停:
41
+ try:
42
+ await ws.ping()
43
+ except Exception:
44
+ return
45
+ await asyncio.sleep(self.心跳秒)
46
+
47
+ async def _续订(self, ws):
48
+ if not self._订阅:
49
+ return
50
+ 批 = 20
51
+ for i in range(0, len(self._订阅), 批):
52
+ args = self._订阅[i:i+批]
53
+ await self._发(ws, {"op": "subscribe", "args": args})
54
+ await asyncio.sleep(0.05)
55
+
56
+ async def _emit(self, 事件: Dict[str, Any]):
57
+ for cb in list(self._回调):
58
+ r = cb(事件)
59
+ if asyncio.iscoroutine(r):
60
+ await r
61
+
62
+ async def 永久运行(self):
63
+ while not self._停:
64
+ try:
65
+ async with websockets.connect(self.地址, ping_interval=None, close_timeout=3) as ws:
66
+ self._ws = ws
67
+ await self._emit({'类型':'系统','事件':'已连接'})
68
+ await self._续订(ws)
69
+ 心跳 = asyncio.create_task(self._发心跳(ws))
70
+ try:
71
+ async for 原 in ws:
72
+ try:
73
+ m = json.loads(原)
74
+ except Exception:
75
+ continue
76
+ if isinstance(m, dict) and 'event' in m:
77
+ ev = '确认' if m['event']=='subscribe' else '错误'
78
+ await self._emit({'类型':ev,'参数':m.get('arg'),'代码':m.get('code'),'信息':m.get('msg')})
79
+ continue
80
+ if 'arg' in m and 'data' in m:
81
+ arg = m['arg']; ch = arg.get('channel'); inst = arg.get('instId')
82
+ 类 = 'k线' if str(ch).startswith('candle') else ('成交' if ch=='trades' else '数据')
83
+ await self._emit({'类型':类,'频道':ch,'合约':inst,'参数':arg,'数据':m['data']})
84
+ finally:
85
+ 心跳.cancel()
86
+ except Exception as e:
87
+ await self._emit({'类型':'系统','事件':'断开','错误':str(e)})
88
+ await asyncio.sleep(self.重连秒)
89
+
90
+ def 停止(self):
91
+ self._停 = True
插件_行情/历史数据.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ from typing import List, Optional
3
+ import aiohttp, pandas as pd
4
+ from .类型定义 import 蜡烛
5
+
6
+ OKX_REST根 = "https://www.okx.com"
7
+
8
+ async def 获取OKX历史1分钟K(合约: str, 根数: int = 1000) -> List[蜡烛]:
9
+ url = f"{OKX_REST根}/api/v5/market/history-candles"
10
+ params = {"instId": 合约, "bar": "1m", "limit": str(根数)}
11
+ out: List[蜡烛] = []
12
+ async with aiohttp.ClientSession() as sess:
13
+ async with sess.get(url, params=params, timeout=20) as r:
14
+ r.raise_for_status()
15
+ js = await r.json()
16
+ for row in js.get('data', []):
17
+ ts = int(row[0]); o,h,l,c,vol = map(float, (row[1],row[2],row[3],row[4],row[5]))
18
+ out.append(蜡烛(合约,'1m',ts,o,h,l,c,vol,True,{}))
19
+ out.sort(key=lambda x: x.时间戳)
20
+ return out
21
+
22
+ def 读取CSV为1分钟K(路径: str, 合约: str,
23
+ 时间列='时间', 开列='开', 高列='高', 低列='低', 收列='收', 量列='量') -> List[蜡烛]:
24
+ df = pd.read_csv(路径)
25
+ if 时间列 in df.columns:
26
+ ts = pd.to_datetime(df[时间列], utc=True, errors='coerce').astype('int64')//10**6
27
+ df = df.assign(_ts=ts)
28
+ else:
29
+ for c in ['timestamp','Datetime','date']:
30
+ if c in df.columns:
31
+ ts = pd.to_datetime(df[c], utc=True, errors='coerce').astype('int64')//10**6
32
+ df = df.assign(_ts=ts); break
33
+ 需要 = [开列,高列,低列,收列]
34
+ for c in 需要:
35
+ if c not in df.columns:
36
+ raise ValueError(f"CSV 缺少列: {c}")
37
+ out=[]
38
+ for _,r in df.sort_values('_ts').iterrows():
39
+ out.append(蜡烛(合约,'1m',int(r['_ts']), float(r[开列]), float(r[高列]), float(r[低列]), float(r[收列]), float(r.get(量列,0.0)), True, {}))
40
+ return out
插件_行情/类型定义.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass, asdict
4
+ from typing import Optional, Dict, Any
5
+
6
+ @dataclass
7
+ class 蜡烛:
8
+ 合约: str # 例: "BTC-USDT-SWAP"
9
+ 周期: str # '1m','60m','120m','240m'
10
+ 时间戳: int # 毫秒
11
+ 开: float
12
+ 高: float
13
+ 低: float
14
+ 收: float
15
+ 量: float
16
+ 已收盘: bool
17
+ 附加: Optional[Dict[str, Any]] = None
18
+
19
+ def 到字典(self) -> Dict[str, Any]:
20
+ d = asdict(self)
21
+ d['time'] = d.pop('时间戳')
22
+ return d
23
+
24
+ @dataclass
25
+ class 成交:
26
+ 合约: str
27
+ 时间戳: int
28
+ 价: float
29
+ 量: float
30
+ 方向: str # 'buy'/'sell'(可能缺省)