hyperquant 1.48__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hyperquant might be problematic. Click here for more details.
- hyperquant/__init__.py +8 -0
- hyperquant/broker/auth.py +972 -0
- hyperquant/broker/bitget.py +311 -0
- hyperquant/broker/bitmart.py +720 -0
- hyperquant/broker/coinw.py +487 -0
- hyperquant/broker/deepcoin.py +651 -0
- hyperquant/broker/edgex.py +500 -0
- hyperquant/broker/hyperliquid.py +570 -0
- hyperquant/broker/lbank.py +661 -0
- hyperquant/broker/lib/edgex_sign.py +455 -0
- hyperquant/broker/lib/hpstore.py +252 -0
- hyperquant/broker/lib/hyper_types.py +48 -0
- hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
- hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
- hyperquant/broker/lib/util.py +22 -0
- hyperquant/broker/lighter.py +679 -0
- hyperquant/broker/models/apexpro.py +150 -0
- hyperquant/broker/models/bitget.py +359 -0
- hyperquant/broker/models/bitmart.py +635 -0
- hyperquant/broker/models/coinw.py +724 -0
- hyperquant/broker/models/deepcoin.py +809 -0
- hyperquant/broker/models/edgex.py +1053 -0
- hyperquant/broker/models/hyperliquid.py +284 -0
- hyperquant/broker/models/lbank.py +557 -0
- hyperquant/broker/models/lighter.py +868 -0
- hyperquant/broker/models/ourbit.py +1155 -0
- hyperquant/broker/models/polymarket.py +1071 -0
- hyperquant/broker/ourbit.py +550 -0
- hyperquant/broker/polymarket.py +2399 -0
- hyperquant/broker/ws.py +132 -0
- hyperquant/core.py +513 -0
- hyperquant/datavison/_util.py +18 -0
- hyperquant/datavison/binance.py +111 -0
- hyperquant/datavison/coinglass.py +237 -0
- hyperquant/datavison/okx.py +177 -0
- hyperquant/db.py +191 -0
- hyperquant/draw.py +1200 -0
- hyperquant/logkit.py +205 -0
- hyperquant/notikit.py +124 -0
- hyperquant-1.48.dist-info/METADATA +32 -0
- hyperquant-1.48.dist-info/RECORD +42 -0
- hyperquant-1.48.dist-info/WHEEL +4 -0
hyperquant/broker/ws.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pybotters
|
|
6
|
+
from aiohttp import WSMsgType
|
|
7
|
+
from pybotters.ws import ClientWebSocketResponse, logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Heartbeat:
|
|
11
|
+
@staticmethod
|
|
12
|
+
async def ourbit(ws: pybotters.ws.ClientWebSocketResponse):
|
|
13
|
+
while not ws.closed:
|
|
14
|
+
await ws.send_str('{"method":"ping"}')
|
|
15
|
+
await asyncio.sleep(10.0)
|
|
16
|
+
|
|
17
|
+
async def ourbit_spot(ws: pybotters.ws.ClientWebSocketResponse):
|
|
18
|
+
while not ws.closed:
|
|
19
|
+
await ws.send_str('{"method":"ping"}')
|
|
20
|
+
await asyncio.sleep(10.0)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
async def edgex(ws: pybotters.ws.ClientWebSocketResponse):
|
|
24
|
+
while not ws.closed:
|
|
25
|
+
now = str(int(time.time() * 1000))
|
|
26
|
+
await ws.send_json({"type": "ping", "time": now})
|
|
27
|
+
await asyncio.sleep(20.0)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
async def lbank(ws: ClientWebSocketResponse):
|
|
31
|
+
while not ws.closed:
|
|
32
|
+
await ws.send_str('ping')
|
|
33
|
+
await asyncio.sleep(6)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
async def coinw(ws: ClientWebSocketResponse):
|
|
37
|
+
while not ws.closed:
|
|
38
|
+
await ws.send_json({"event": "ping"})
|
|
39
|
+
await asyncio.sleep(3.0)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
async def deepcoin(ws: ClientWebSocketResponse):
|
|
43
|
+
while not ws.closed:
|
|
44
|
+
await ws.send_str("ping")
|
|
45
|
+
await asyncio.sleep(30)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
async def lighter(ws: ClientWebSocketResponse):
|
|
49
|
+
while not ws.closed:
|
|
50
|
+
await ws.send_json({"type":"ping"})
|
|
51
|
+
await asyncio.sleep(3)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
|
|
56
|
+
pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
|
|
57
|
+
pybotters.ws.HeartbeatHosts.items['quote.edgex.exchange'] = Heartbeat.edgex
|
|
58
|
+
pybotters.ws.HeartbeatHosts.items['uuws.rerrkvifj.com'] = Heartbeat.lbank
|
|
59
|
+
pybotters.ws.HeartbeatHosts.items['ws.futurescw.com'] = Heartbeat.coinw
|
|
60
|
+
pybotters.ws.HeartbeatHosts.items['stream.deepcoin.com'] = Heartbeat.deepcoin
|
|
61
|
+
pybotters.ws.HeartbeatHosts.items['mainnet.zklighter.elliot.ai'] = Heartbeat.lighter
|
|
62
|
+
# pybotters.ws.HeartbeatHosts.items['ws-subscriptions-clob.polymarket.com'] = Heartbeat.polymarket
|
|
63
|
+
|
|
64
|
+
class WssAuth:
|
|
65
|
+
@staticmethod
|
|
66
|
+
async def ourbit(ws: ClientWebSocketResponse):
|
|
67
|
+
key: str = ws._response._session.__dict__["_apis"][
|
|
68
|
+
pybotters.ws.AuthHosts.items[ws._response.url.host].name
|
|
69
|
+
][0]
|
|
70
|
+
await ws.send_json(
|
|
71
|
+
{
|
|
72
|
+
"method": "login",
|
|
73
|
+
"param": {
|
|
74
|
+
"token": key
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
async for msg in ws:
|
|
79
|
+
# {"channel":"rs.login","data":"success","ts":1756470267848}
|
|
80
|
+
data = msg.json()
|
|
81
|
+
if data.get("channel") == "rs.login":
|
|
82
|
+
if data.get("data") == "success":
|
|
83
|
+
break
|
|
84
|
+
else:
|
|
85
|
+
logger.warning(f"WebSocket login failed: {data}")
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
async def coinw(ws: ClientWebSocketResponse):
|
|
89
|
+
creds = ws._response._session.__dict__["_apis"].get(
|
|
90
|
+
pybotters.ws.AuthHosts.items[ws._response.url.host].name
|
|
91
|
+
)
|
|
92
|
+
if not creds:
|
|
93
|
+
raise RuntimeError("CoinW credentials are required for websocket login.")
|
|
94
|
+
if isinstance(creds, dict):
|
|
95
|
+
raise RuntimeError("CoinW credentials must be a sequence, not a dict.")
|
|
96
|
+
if len(creds) < 1:
|
|
97
|
+
raise RuntimeError("CoinW credentials are incomplete.")
|
|
98
|
+
|
|
99
|
+
api_key = creds[0]
|
|
100
|
+
secret = creds[1] if len(creds) > 1 else ""
|
|
101
|
+
|
|
102
|
+
await ws.send_json(
|
|
103
|
+
{
|
|
104
|
+
"event": "login",
|
|
105
|
+
"params": {
|
|
106
|
+
"api_key": api_key,
|
|
107
|
+
"passphrase": secret.decode(),
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async for msg in ws:
|
|
113
|
+
if msg.type != WSMsgType.TEXT:
|
|
114
|
+
continue
|
|
115
|
+
try:
|
|
116
|
+
data:dict = msg.json()
|
|
117
|
+
except Exception: # pragma: no cover - defensive
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
channel = data.get("channel")
|
|
121
|
+
event_type = data.get("type")
|
|
122
|
+
if channel == "login" or event_type == "login":
|
|
123
|
+
result = data.get("data", {}).get("result")
|
|
124
|
+
if result is not True:
|
|
125
|
+
raise RuntimeError(f"CoinW WebSocket login failed: {data}")
|
|
126
|
+
break
|
|
127
|
+
if data.get("event") == "pong":
|
|
128
|
+
# ignore heartbeat responses while waiting
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
pybotters.ws.AuthHosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", WssAuth.ourbit)
|
|
132
|
+
pybotters.ws.AuthHosts.items['ws.futurescw.com'] = pybotters.auth.Item("coinw", WssAuth.coinw)
|
hyperquant/core.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# %%
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from .draw import draw
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExchangeBase:
|
|
8
|
+
def __init__(self, initial_balance=10000, recorded=False):
|
|
9
|
+
self.initial_balance = initial_balance # 初始的资产
|
|
10
|
+
self.recorded = recorded # 是否记录历史
|
|
11
|
+
self.opt = {
|
|
12
|
+
'trades': [],
|
|
13
|
+
'history': [] # 集成 history 到 opt 中
|
|
14
|
+
}
|
|
15
|
+
self.account = {'USDT': {'realised_profit': 0, 'unrealised_profit': 0, 'total': initial_balance,
|
|
16
|
+
'fee': 0, 'leverage': 0, 'hold': 0, 'long': 0, 'short': 0}}
|
|
17
|
+
|
|
18
|
+
def record_history(self, time):
|
|
19
|
+
"""记录当前总资产和时间到 history 中"""
|
|
20
|
+
self.opt['history'].append({
|
|
21
|
+
'date': time,
|
|
22
|
+
'total': self.account['USDT']['total']
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
def __getitem__(self, symbol):
|
|
26
|
+
return self.account.get(symbol, None)
|
|
27
|
+
|
|
28
|
+
def __setitem__(self, symbol, value):
|
|
29
|
+
self.account[symbol] = value
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def activate_symbols(self):
|
|
33
|
+
return [symbol for symbol in self.trade_symbols if self.account[symbol]['amount'] != 0]
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def total(self):
|
|
37
|
+
return self.account['USDT']['total']
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def leverage(self):
|
|
41
|
+
return self.account['USDT']['leverage']
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def realised_profit(self):
|
|
45
|
+
return self.account['USDT']['realised_profit']
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def unrealised_profit(self):
|
|
49
|
+
return self.account['USDT']['unrealised_profit']
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def history(self):
|
|
53
|
+
if not self.recorded:
|
|
54
|
+
raise ValueError("History is only available in recorded mode.")
|
|
55
|
+
return self.opt['history']
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def available_margin(self):
|
|
59
|
+
return self.account['USDT']['total'] - self.account['USDT']['hold']
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def realised_profit(self):
|
|
63
|
+
return self.account['USDT']['realised_profit']
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def trades(self):
|
|
67
|
+
return self.opt['trades']
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def stats(self):
|
|
71
|
+
if not self.recorded:
|
|
72
|
+
raise ValueError("Stats are only available in recorded mode.")
|
|
73
|
+
|
|
74
|
+
if not self.opt['history']:
|
|
75
|
+
return {
|
|
76
|
+
'初始资产': f'{self.initial_balance:.2f} USDT',
|
|
77
|
+
'当前资产': f'{self.account["USDT"]["total"]:.2f} USDT',
|
|
78
|
+
'已实现利润': f'{self.account["USDT"]["realised_profit"]:.2f} USDT',
|
|
79
|
+
'未实现利润': f'{self.account["USDT"]["unrealised_profit"]:.2f} USDT',
|
|
80
|
+
'总手续费': f'{self.account["USDT"]["fee"]:.2f} USDT',
|
|
81
|
+
'杯杆率': f'{self.account["USDT"]["leverage"]:.2f}',
|
|
82
|
+
'活跃交易对数量': len(self.activate_symbols),
|
|
83
|
+
'持仓价值': f'{self.account["USDT"]["hold"]:.2f} USDT',
|
|
84
|
+
'多头持仓价值': f'{self.account["USDT"]["long"]:.2f} USDT',
|
|
85
|
+
'空头持仓价值': f'{self.account["USDT"]["short"]:.2f} USDT',
|
|
86
|
+
'总交易笔数': 0,
|
|
87
|
+
'胜率': '0.00%',
|
|
88
|
+
'年化收益率': '0.00%',
|
|
89
|
+
'最大回撤时间范围': 'N/A',
|
|
90
|
+
'最大回撤': '0.00%',
|
|
91
|
+
'夏普比率': '0.00'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# 创建一个账户历史的DataFrame
|
|
95
|
+
history_df = pd.DataFrame(self.opt['history'])
|
|
96
|
+
history_df = history_df.sort_values(by='date')
|
|
97
|
+
history_df = history_df.drop_duplicates(subset='date')
|
|
98
|
+
history_df = history_df.set_index('date')
|
|
99
|
+
|
|
100
|
+
# 计算累计收益
|
|
101
|
+
history_df['max2here'] = history_df['total'].expanding().max()
|
|
102
|
+
history_df['dd2here'] = history_df['total'] / history_df['max2here'] - 1
|
|
103
|
+
drwa_down_df = history_df.sort_values(by=['dd2here'])
|
|
104
|
+
drwa_down_df = drwa_down_df[drwa_down_df['dd2here'] < 0]
|
|
105
|
+
if drwa_down_df.empty:
|
|
106
|
+
start_date = np.nan
|
|
107
|
+
end_data = np.nan
|
|
108
|
+
max_draw_down = 0
|
|
109
|
+
else:
|
|
110
|
+
max_draw_down = drwa_down_df.iloc[0]['dd2here']
|
|
111
|
+
end_data = drwa_down_df.iloc[0].name
|
|
112
|
+
start_date = history_df[history_df.index <= end_data].sort_values(by='total', ascending=False).iloc[0].name
|
|
113
|
+
|
|
114
|
+
# 计算胜率
|
|
115
|
+
total_trades = len(self.opt['trades'])
|
|
116
|
+
if total_trades == 0:
|
|
117
|
+
win_rate = 0
|
|
118
|
+
else:
|
|
119
|
+
winning_trades = sum(1 for trade in self.opt['trades'] if trade['pos'] > 0)
|
|
120
|
+
losing_trades = sum(1 for trade in self.opt['trades'] if trade['pos'] < 0)
|
|
121
|
+
win_rate = winning_trades / (winning_trades + losing_trades) if (winning_trades + losing_trades) > 0 else 0
|
|
122
|
+
|
|
123
|
+
# 计算年化收益率
|
|
124
|
+
if len(history_df) < 2:
|
|
125
|
+
annual_return = 0
|
|
126
|
+
else:
|
|
127
|
+
start_date_for_return = history_df.index[0]
|
|
128
|
+
end_date_for_return = history_df.index[-1]
|
|
129
|
+
total_days = (end_date_for_return - start_date_for_return).days
|
|
130
|
+
if total_days > 0:
|
|
131
|
+
annual_return = ((history_df['total'].iloc[-1] / self.initial_balance) ** (365 / total_days) - 1)
|
|
132
|
+
else:
|
|
133
|
+
annual_return = 0
|
|
134
|
+
|
|
135
|
+
# 计算夏普比率
|
|
136
|
+
# 计算每日收益率
|
|
137
|
+
daily_history = history_df['total'].resample('D').ffill().dropna()
|
|
138
|
+
daily_returns = daily_history.pct_change().dropna()
|
|
139
|
+
if len(daily_returns) > 1:
|
|
140
|
+
risk_free_rate = 0.03 / 365
|
|
141
|
+
sharpe_ratio = (daily_returns.mean() - risk_free_rate) / daily_returns.std() * np.sqrt(365)
|
|
142
|
+
else:
|
|
143
|
+
sharpe_ratio = 0
|
|
144
|
+
|
|
145
|
+
stats = {
|
|
146
|
+
'初始资产': f'{self.initial_balance:.2f} USDT',
|
|
147
|
+
'当前资产': f'{self.account["USDT"]["total"]:.2f} USDT',
|
|
148
|
+
'已实现利润': f'{self.account["USDT"]["realised_profit"]:.2f} USDT',
|
|
149
|
+
'未实现利润': f'{self.account["USDT"]["unrealised_profit"]:.2f} USDT',
|
|
150
|
+
'总手续费': f'{self.account["USDT"]["fee"]:.2f} USDT',
|
|
151
|
+
'活跃交易对数量': len(self.activate_symbols),
|
|
152
|
+
'持仓价值': f'{self.account["USDT"]["hold"]:.2f} USDT',
|
|
153
|
+
'多头持仓价值': f'{self.account["USDT"]["long"]:.2f} USDT',
|
|
154
|
+
'空头持仓价值': f'{self.account["USDT"]["short"]:.2f} USDT',
|
|
155
|
+
'总交易笔数': total_trades,
|
|
156
|
+
'胜率': f'{win_rate:.2%}',
|
|
157
|
+
'年化收益率': f'{annual_return:.2%}',
|
|
158
|
+
'最大回撤时间范围': (start_date,end_data),
|
|
159
|
+
'最大回撤': f'{max_draw_down:.2%}',
|
|
160
|
+
'夏普比率': f'{sharpe_ratio:.2f}'
|
|
161
|
+
}
|
|
162
|
+
return stats
|
|
163
|
+
|
|
164
|
+
def draw(self, data_df: pd.DataFrame, title: str, indicators: list, show_kline=True, show_total=True, show_base=False):
|
|
165
|
+
"""
|
|
166
|
+
:param data_df: 数据 DataFrame
|
|
167
|
+
:param title: 图表标题
|
|
168
|
+
:param indicators: 画图指标 [[('指标名', '指标类型'), ('指标名', '指标类型')], [('指标名', '指标类型')]]
|
|
169
|
+
:param show_kline: 是否显示K线图
|
|
170
|
+
:param show_total: 是否显示总资产曲线
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
# 将 self.history 转换为 DataFrame
|
|
174
|
+
history_df = pd.DataFrame(self.opt['history'])
|
|
175
|
+
|
|
176
|
+
# 按照 'date' 分组,并保留每组的最后一条记录
|
|
177
|
+
history_df = history_df.sort_values('date').groupby('date', as_index=False).last()
|
|
178
|
+
|
|
179
|
+
# 按照 'date' 将 history_df 和 data_df 合并
|
|
180
|
+
data_df = pd.merge(data_df, history_df, on='date', how='left')
|
|
181
|
+
|
|
182
|
+
# 使用前向填充处理 'total' 列的缺失值
|
|
183
|
+
data_df['total'] = data_df['total'].ffill()
|
|
184
|
+
|
|
185
|
+
data_dict = []
|
|
186
|
+
if show_kline:
|
|
187
|
+
# 如果signal列存在,将signal列的值赋值给signal列
|
|
188
|
+
opt = {
|
|
189
|
+
"series_name": "K",
|
|
190
|
+
"draw_type": "Kline",
|
|
191
|
+
"col": ["open", "close", "low", "high"],
|
|
192
|
+
"height": 50,
|
|
193
|
+
}
|
|
194
|
+
if 'signal' in data_df.columns:
|
|
195
|
+
opt['trade_single'] = 'signal'
|
|
196
|
+
data_dict.append(opt)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if indicators:
|
|
200
|
+
for ind in indicators:
|
|
201
|
+
ind_data = {}
|
|
202
|
+
for i, indicator in enumerate(ind):
|
|
203
|
+
if i == 0:
|
|
204
|
+
ind_data = {
|
|
205
|
+
"series_name": indicator[0],
|
|
206
|
+
"draw_type": indicator[1],
|
|
207
|
+
"height": 0,
|
|
208
|
+
}
|
|
209
|
+
else:
|
|
210
|
+
if 'sub_chart' not in ind_data:
|
|
211
|
+
ind_data['sub_chart'] = []
|
|
212
|
+
ind_data['sub_chart'].append(
|
|
213
|
+
{"series_name": indicator[0], "draw_type": indicator[1]}
|
|
214
|
+
)
|
|
215
|
+
data_dict.append(ind_data)
|
|
216
|
+
|
|
217
|
+
if show_total:
|
|
218
|
+
# 绘制基准收益曲线
|
|
219
|
+
total_dict = {
|
|
220
|
+
"series_name": "total",
|
|
221
|
+
"draw_type": "Line",
|
|
222
|
+
"height": 0,
|
|
223
|
+
}
|
|
224
|
+
if show_base:
|
|
225
|
+
data_df.loc[:, "base"] = (data_df["close"] / data_df["close"].iloc[0]) * self.initial_balance
|
|
226
|
+
total_dict['sub_chart'] = [
|
|
227
|
+
{"series_name": "base", "draw_type": "Line"},
|
|
228
|
+
]
|
|
229
|
+
data_dict.append(total_dict)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
sub_width = 40 / (len(data_dict) - 1)
|
|
233
|
+
for d in data_dict:
|
|
234
|
+
if d['draw_type'] != "Kline":
|
|
235
|
+
d['height'] = sub_width
|
|
236
|
+
|
|
237
|
+
draw(
|
|
238
|
+
data_df,
|
|
239
|
+
data_dict=data_dict,
|
|
240
|
+
date_col="date",
|
|
241
|
+
date_formate="%Y-%m-%d %H:%M:%S",
|
|
242
|
+
title=title,
|
|
243
|
+
height_type="%",
|
|
244
|
+
auto_play_space="""
|
|
245
|
+
function auto_play_space(xi){
|
|
246
|
+
return 200;
|
|
247
|
+
}""",
|
|
248
|
+
show=True,
|
|
249
|
+
display_js="""
|
|
250
|
+
// 设置 dataZoom 为最大范围
|
|
251
|
+
window.onload = function() {
|
|
252
|
+
var isSettingZoom = false;
|
|
253
|
+
|
|
254
|
+
// 获取 x 轴的数据
|
|
255
|
+
var xData = chart_option.xAxis[0].data;
|
|
256
|
+
if (xData.length > 0) {
|
|
257
|
+
var startValue = xData[0];
|
|
258
|
+
var endValue = xData[xData.length - 1];
|
|
259
|
+
isSettingZoom = true;
|
|
260
|
+
chart_ins.dispatchAction({
|
|
261
|
+
type: 'dataZoom',
|
|
262
|
+
startValue: startValue,
|
|
263
|
+
endValue: endValue
|
|
264
|
+
});
|
|
265
|
+
isSettingZoom = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
""",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
class Exchange(ExchangeBase):
|
|
272
|
+
def __init__(self, trade_symbols:list=[], fee=0.0002, initial_balance=10000, recorded=False):
|
|
273
|
+
super().__init__(initial_balance=initial_balance, recorded=recorded)
|
|
274
|
+
self.fee = fee
|
|
275
|
+
self.trade_symbols:list = trade_symbols
|
|
276
|
+
self.id_gen = 0
|
|
277
|
+
self.account['USDT'].update({
|
|
278
|
+
'hold': 0,
|
|
279
|
+
'long': 0,
|
|
280
|
+
'short': 0
|
|
281
|
+
})
|
|
282
|
+
for symbol in trade_symbols:
|
|
283
|
+
self.account[symbol] = self._act_template
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def _act_template(self):
|
|
287
|
+
return {'amount': 0, 'hold_price': 0, 'value': 0, 'price': 0,
|
|
288
|
+
'realised_profit': 0, 'unrealised_profit': 0, 'fee': 0}.copy()
|
|
289
|
+
|
|
290
|
+
def Trade(self, symbol, direction, price, amount, **kwargs):
|
|
291
|
+
if self.recorded and 'time' not in kwargs:
|
|
292
|
+
raise ValueError("Time parameter is required in recorded mode.")
|
|
293
|
+
|
|
294
|
+
time = kwargs.get('time', pd.Timestamp.now())
|
|
295
|
+
|
|
296
|
+
self.id_gen += 1
|
|
297
|
+
tid = len(self.trades) if self.recorded else self.id_gen
|
|
298
|
+
|
|
299
|
+
trade = {
|
|
300
|
+
'symbol': symbol,
|
|
301
|
+
'exchange': "local",
|
|
302
|
+
'orderid': tid,
|
|
303
|
+
'tradeid': tid,
|
|
304
|
+
'direction': direction,
|
|
305
|
+
'price': price,
|
|
306
|
+
'volume': abs(amount),
|
|
307
|
+
'datetime': time,
|
|
308
|
+
'gateway_name': "local",
|
|
309
|
+
'pos': 0 # 初始化盈亏
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if symbol not in self.trade_symbols:
|
|
313
|
+
self.trade_symbols.append(symbol)
|
|
314
|
+
self.account[symbol] = self._act_template
|
|
315
|
+
|
|
316
|
+
cover_amount = 0 if direction * self.account[symbol]['amount'] >= 0 else min(abs(self.account[symbol]['amount']), amount)
|
|
317
|
+
open_amount = amount - cover_amount
|
|
318
|
+
|
|
319
|
+
if cover_amount > 0 and np.isnan(price):
|
|
320
|
+
print(f'{symbol} 可能已经下架, 清仓')
|
|
321
|
+
price = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
|
|
322
|
+
else:
|
|
323
|
+
if np.isnan(price) or np.isnan(amount):
|
|
324
|
+
print(f'{symbol} 价格或者数量为nan, 交易忽略')
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# 扣除手续费
|
|
328
|
+
self.account['USDT']['realised_profit'] -= price * amount * self.fee
|
|
329
|
+
self.account['USDT']['fee'] += price * amount * self.fee
|
|
330
|
+
self.account[symbol]['fee'] += price * amount * self.fee
|
|
331
|
+
|
|
332
|
+
if cover_amount > 0: # 先平仓
|
|
333
|
+
profit = -direction * (price - self.account[symbol]['hold_price']) * cover_amount
|
|
334
|
+
self.account['USDT']['realised_profit'] += profit # 利润
|
|
335
|
+
self.account[symbol]['realised_profit'] += profit
|
|
336
|
+
self.account[symbol]['amount'] -= -direction * cover_amount
|
|
337
|
+
trade['pos'] = profit # 记录盈亏
|
|
338
|
+
|
|
339
|
+
trade['pos_rate'] = -direction * (price / self.account[symbol]['hold_price'] - 1) if self.account[symbol]['hold_price'] != 0 else 0
|
|
340
|
+
|
|
341
|
+
self.account[symbol]['hold_price'] = 0 if self.account[symbol]['amount'] == 0 else self.account[symbol]['hold_price']
|
|
342
|
+
|
|
343
|
+
if open_amount > 0:
|
|
344
|
+
total_cost = self.account[symbol]['hold_price'] * direction * self.account[symbol]['amount'] + price * open_amount
|
|
345
|
+
total_amount = direction * self.account[symbol]['amount'] + open_amount
|
|
346
|
+
|
|
347
|
+
self.account[symbol]['hold_price'] = total_cost / total_amount
|
|
348
|
+
self.account[symbol]['amount'] += direction * open_amount
|
|
349
|
+
|
|
350
|
+
if kwargs:
|
|
351
|
+
self.opt.update(kwargs)
|
|
352
|
+
self.account[symbol].update(kwargs)
|
|
353
|
+
|
|
354
|
+
# 记录账户总资产到 history
|
|
355
|
+
if self.recorded:
|
|
356
|
+
self.opt['trades'].append(trade)
|
|
357
|
+
self.record_history(time)
|
|
358
|
+
|
|
359
|
+
# 自动更新账户状态
|
|
360
|
+
self.Update({symbol: price}, time=time)
|
|
361
|
+
|
|
362
|
+
return trade
|
|
363
|
+
|
|
364
|
+
def Buy(self, symbol, price, amount, **kwargs):
|
|
365
|
+
return self.Trade(symbol, 1, price, amount, **kwargs)
|
|
366
|
+
|
|
367
|
+
def Sell(self, symbol, price, amount, **kwargs):
|
|
368
|
+
return self.Trade(symbol, -1, price, amount, **kwargs)
|
|
369
|
+
|
|
370
|
+
def CloseAll(self, price, symbols=None, **kwargs):
|
|
371
|
+
if symbols is None:
|
|
372
|
+
symbols = self.trade_symbols
|
|
373
|
+
trades = []
|
|
374
|
+
symbols = [s for s in symbols if s in self.account and self.account[s]['amount'] != 0]
|
|
375
|
+
for symbol in symbols:
|
|
376
|
+
if symbol not in price or np.isnan(price[symbol]):
|
|
377
|
+
print(f'{symbol} 可能已经下架')
|
|
378
|
+
price[symbol] = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
|
|
379
|
+
if np.isnan(price[symbol]):
|
|
380
|
+
price[symbol] = self.account[symbol]['price'] if self.account[symbol]['price'] != 0 else self.account[symbol]['hold_price']
|
|
381
|
+
|
|
382
|
+
direction = -np.sign(self.account[symbol]['amount'])
|
|
383
|
+
trade = self.Trade(symbol, direction, price[symbol], abs(self.account[symbol]['amount']), **kwargs)
|
|
384
|
+
trades.append(trade)
|
|
385
|
+
return trades
|
|
386
|
+
|
|
387
|
+
def _recalc_aggregates(self):
|
|
388
|
+
"""基于 self.account 中已保存的各 symbol 状态,重算聚合字段。"""
|
|
389
|
+
usdt = self.account['USDT']
|
|
390
|
+
usdt['unrealised_profit'] = 0
|
|
391
|
+
usdt['hold'] = 0
|
|
392
|
+
usdt['long'] = 0
|
|
393
|
+
usdt['short'] = 0
|
|
394
|
+
|
|
395
|
+
for symbol in self.trade_symbols:
|
|
396
|
+
if symbol not in self.account:
|
|
397
|
+
continue
|
|
398
|
+
sym = self.account[symbol]
|
|
399
|
+
px = sym.get('price', 0)
|
|
400
|
+
amt = sym.get('amount', 0)
|
|
401
|
+
hp = sym.get('hold_price', 0)
|
|
402
|
+
|
|
403
|
+
# 仅当价格有效时计入聚合
|
|
404
|
+
if px is not None and not np.isnan(px) and px != 0:
|
|
405
|
+
sym['unrealised_profit'] = (px - hp) * amt
|
|
406
|
+
sym['value'] = amt * px
|
|
407
|
+
|
|
408
|
+
if amt > 0:
|
|
409
|
+
usdt['long'] += sym['value']
|
|
410
|
+
elif amt < 0:
|
|
411
|
+
usdt['short'] += sym['value']
|
|
412
|
+
|
|
413
|
+
usdt['hold'] += abs(sym['value'])
|
|
414
|
+
usdt['unrealised_profit'] += sym['unrealised_profit']
|
|
415
|
+
|
|
416
|
+
usdt['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + usdt['unrealised_profit'], 6)
|
|
417
|
+
usdt['leverage'] = round(usdt['hold'] / usdt['total'] if usdt['total'] != 0 else 0.0, 3)
|
|
418
|
+
|
|
419
|
+
def Update(self, close_price=None, symbols=None, partial=True, **kwargs):
|
|
420
|
+
"""
|
|
421
|
+
更新账户状态。
|
|
422
|
+
- partial=True:只更新给定 symbols 的逐符号状态,然后对所有符号做一次聚合重算(推荐)。
|
|
423
|
+
- partial=False:与原逻辑兼容;当提供一部分 symbol 时,也会聚合重算,不会清空未提供符号的信息。
|
|
424
|
+
|
|
425
|
+
支持三种入参形式:
|
|
426
|
+
1) close_price 为 dict/Series:symbols 自动取其键/索引
|
|
427
|
+
2) close_price 为标量 + symbols 为单个字符串
|
|
428
|
+
3) 显式传 symbols=list[...],close_price 为 dict/Series(从中取价)
|
|
429
|
+
如果既不传 close_price 也不传 symbols,则只做一次聚合重算(例如你先前已经手动修改了某些 symbol 的 price)。
|
|
430
|
+
"""
|
|
431
|
+
if self.recorded and 'time' not in kwargs:
|
|
432
|
+
raise ValueError("Time parameter is required in recorded mode.")
|
|
433
|
+
|
|
434
|
+
time = kwargs.get('time', pd.Timestamp.now())
|
|
435
|
+
|
|
436
|
+
# 解析 symbols & 价格获取器
|
|
437
|
+
if symbols is None:
|
|
438
|
+
if isinstance(close_price, dict):
|
|
439
|
+
symbols = list(close_price.keys())
|
|
440
|
+
elif isinstance(close_price, pd.Series):
|
|
441
|
+
symbols = list(close_price.index)
|
|
442
|
+
else:
|
|
443
|
+
symbols = []
|
|
444
|
+
elif isinstance(symbols, str):
|
|
445
|
+
symbols = [symbols]
|
|
446
|
+
|
|
447
|
+
def get_px(sym):
|
|
448
|
+
if isinstance(close_price, (int, float, np.floating)) and len(symbols) == 1:
|
|
449
|
+
return float(close_price)
|
|
450
|
+
if isinstance(close_price, dict):
|
|
451
|
+
return close_price.get(sym, np.nan)
|
|
452
|
+
if isinstance(close_price, pd.Series):
|
|
453
|
+
return close_price.get(sym, np.nan)
|
|
454
|
+
return np.nan
|
|
455
|
+
|
|
456
|
+
# 仅更新传入的 symbols(部分更新,不动其它符号已保存信息)
|
|
457
|
+
for sym in symbols:
|
|
458
|
+
if sym not in self.trade_symbols or sym not in self.account:
|
|
459
|
+
# 未登记的交易对直接跳过(或可选择自动登记,但此处保持严格)
|
|
460
|
+
continue
|
|
461
|
+
px = get_px(sym)
|
|
462
|
+
if px is None or np.isnan(px):
|
|
463
|
+
# 价格无效则不覆盖旧价格
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
self.account[sym]['price'] = float(px)
|
|
467
|
+
amt = self.account[sym]['amount']
|
|
468
|
+
self.account[sym]['value'] = amt * float(px)
|
|
469
|
+
# 不在这里算 unrealised_profit,聚合阶段统一算
|
|
470
|
+
|
|
471
|
+
# 无论 partial 与否,最后都用“账户中保存的所有 symbol 当前状态”做一次聚合重算
|
|
472
|
+
self._recalc_aggregates()
|
|
473
|
+
|
|
474
|
+
# 记录账户总资产到 history
|
|
475
|
+
if self.recorded:
|
|
476
|
+
self.record_history(time)
|
|
477
|
+
|
|
478
|
+
# e = Exchange([])
|
|
479
|
+
# e.Sell('DOGEUSDT', 0.3, 3)
|
|
480
|
+
# print(e.account)
|
|
481
|
+
|
|
482
|
+
def gen_back_time(start_date, end_date, train_period_days, test_period_days):
|
|
483
|
+
# 将输入的日期字符串转换为时间戳
|
|
484
|
+
start_date = pd.to_datetime(start_date)
|
|
485
|
+
end_date = pd.to_datetime(end_date)
|
|
486
|
+
|
|
487
|
+
# 定义训练和测试周期
|
|
488
|
+
train_period = pd.Timedelta(days=train_period_days)
|
|
489
|
+
test_period = pd.Timedelta(days=test_period_days)
|
|
490
|
+
|
|
491
|
+
# 存储训练和测试日期区间
|
|
492
|
+
train_date = []
|
|
493
|
+
test_date = []
|
|
494
|
+
|
|
495
|
+
# 确定训练和测试的时间区间
|
|
496
|
+
current_date = start_date
|
|
497
|
+
while current_date + test_period <= end_date:
|
|
498
|
+
tsd_start = current_date
|
|
499
|
+
tsd_end = tsd_start + test_period
|
|
500
|
+
trd_end = tsd_start
|
|
501
|
+
trd_start = trd_end - train_period
|
|
502
|
+
|
|
503
|
+
train_date.append((pd.Timestamp(trd_start.date()), pd.Timestamp(trd_end.date())))
|
|
504
|
+
test_date.append((pd.Timestamp(tsd_start.date()), pd.Timestamp(tsd_end.date())))
|
|
505
|
+
|
|
506
|
+
# 移动到下一个测试周期的开始
|
|
507
|
+
current_date = tsd_end
|
|
508
|
+
|
|
509
|
+
# 将其转换为DataFrame
|
|
510
|
+
train_date = pd.DataFrame(train_date, columns=['x_start', 'x_end'])
|
|
511
|
+
test_date = pd.DataFrame(test_date, columns=['y_start', 'y_end'])
|
|
512
|
+
back_df = pd.concat([train_date, test_date], axis=1)
|
|
513
|
+
return back_df
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _to_milliseconds( t):
|
|
5
|
+
"""
|
|
6
|
+
支持毫秒时间戳或datetime/date类型,返回毫秒时间戳
|
|
7
|
+
"""
|
|
8
|
+
if t is None:
|
|
9
|
+
return None
|
|
10
|
+
if isinstance(t, int):
|
|
11
|
+
return t
|
|
12
|
+
if isinstance(t, float):
|
|
13
|
+
return int(t * 1000)
|
|
14
|
+
if isinstance(t, datetime):
|
|
15
|
+
return int(t.timestamp() * 1000)
|
|
16
|
+
if isinstance(t, date):
|
|
17
|
+
return int(datetime.combine(t, datetime.min.time()).timestamp() * 1000)
|
|
18
|
+
raise ValueError(f"不支持的时间类型: {type(t)}")
|