Upload 4 files
Browse files- 插件_行情/K线聚合器.py +72 -0
- 插件_行情/公共WS.py +91 -0
- 插件_行情/历史数据.py +40 -0
- 插件_行情/类型定义.py +30 -0
插件_行情/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'(可能缺省)
|