Compare commits

...

29 Commits

Author SHA1 Message Date
ddrwode
d1deb97843 加入一个回测, 2026-03-06 16:40:04 +08:00
ddrwode
b2515bb7ce 加入一个回测, 2026-03-06 10:36:18 +08:00
ddrwode
c473c738a3 加入一个回测, 2026-03-05 18:29:05 +08:00
ddrwode
79f4e03d6a 加入一个回测, 2026-03-05 17:46:34 +08:00
ddrwode
6b1a707e3f 加入一个回测, 2026-03-05 15:51:31 +08:00
ddrwode
3a6678089c 加入一个回测, 2026-03-05 15:44:55 +08:00
ddrwode
5530a008b3 加入一个回测, 2026-03-05 12:56:09 +08:00
ddrwode
01b6a0fdcb 加入一个回测, 2026-03-05 12:51:12 +08:00
ddrwode
b74449989b 加入一个回测, 2026-03-04 18:02:27 +08:00
ddrwode
89bc5a7a00 加入一个回测, 2026-03-04 18:02:16 +08:00
ddrwode
4b5a66d588 加入一个回测, 2026-03-04 18:02:02 +08:00
ddrwode
de22d1f3ae 加入一个回测, 2026-03-04 16:48:24 +08:00
ddrwode
2a98d431a9 第一版策略 2026-03-03 17:20:42 +08:00
27942
79bf079548 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	bb_trade_log_20260228.txt
2026-03-02 10:56:08 +08:00
27942
aec9fc7830 haha 2026-03-02 10:55:39 +08:00
ddrwode
9a383143e0 第一版策略 2026-02-28 17:35:21 +08:00
ddrwode
1d68fcd925 第一版策略 2026-02-28 16:48:40 +08:00
ddrwode
0ec23b28eb 哈哈 2026-02-28 16:43:38 +08:00
ddrwode
088f94c5c4 第一版策略 2026-02-28 13:39:20 +08:00
ddrwode
302086b7d5 第一版策略 2026-02-28 13:27:54 +08:00
ddrwode
fe87f49734 第一版策略 2026-02-28 13:21:58 +08:00
27942
c45017dcf4 haha 2026-02-28 13:10:47 +08:00
ddrwode
2e0a2bc74f 第一版策略 2026-02-27 16:10:56 +08:00
ddrwode
85212a462d 第一版策略 2026-02-26 19:05:17 +08:00
ddrwode
cf499863a3 第一版策略 2026-02-26 16:34:30 +08:00
27942
0edf741849 haha 2026-02-26 01:29:18 +08:00
27942
905ce34aa7 haha 2026-02-25 06:21:49 +08:00
27942
c8fb43a700 haha 2026-02-25 02:14:31 +08:00
27942
2712ec598d haha 2026-02-25 02:14:25 +08:00
51 changed files with 836923 additions and 5735 deletions

2710
1.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>ETHUSDT 2026-03-02 K线 + 布林带</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #111;
color: #eee;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
#chart {
width: 100%;
height: 100%;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
async function main() {
// 从同目录加载由 Python 导出的 JSON 数据
const resp = await fetch("bb_chart_2026_03_02_data.json");
const raw = await resp.json();
const categoryData = [];
const klineData = [];
const upper = [];
const mid = [];
const lower = [];
for (const k of raw) {
const d = new Date(k.timestamp);
const label = `${d.getHours().toString().padStart(2, "0")}:${d
.getMinutes()
.toString()
.padStart(2, "0")}`;
categoryData.push(label);
klineData.push([k.open, k.close, k.low, k.high]);
upper.push(k.bb_upper);
mid.push(k.bb_mid);
lower.push(k.bb_lower);
}
const chartDom = document.getElementById("chart");
const chart = echarts.init(chartDom, null, { renderer: "canvas" });
const option = {
backgroundColor: "#111",
tooltip: {
trigger: "axis",
axisPointer: { type: "cross" },
},
axisPointer: {
link: [{ xAxisIndex: "all" }],
},
grid: {
left: "3%",
right: "3%",
top: "6%",
bottom: "5%",
containLabel: true,
},
xAxis: {
type: "category",
data: categoryData,
scale: true,
boundaryGap: true,
axisLine: { lineStyle: { color: "#888" } },
axisLabel: { color: "#ccc" },
},
yAxis: {
scale: true,
axisLine: { lineStyle: { color: "#888" } },
splitLine: { lineStyle: { color: "#333" } },
axisLabel: { color: "#ccc" },
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
type: "slider",
start: 0,
end: 100,
height: 20,
},
],
series: [
{
name: "K线",
type: "candlestick",
data: klineData,
itemStyle: {
color: "#26a69a",
color0: "#ef5350",
borderColor: "#26a69a",
borderColor0: "#ef5350",
},
},
{
name: "BB上轨",
type: "line",
data: upper,
symbol: "none",
lineStyle: { color: "#ff9800", width: 1 },
},
{
name: "BB中轨",
type: "line",
data: mid,
symbol: "none",
lineStyle: { color: "#fff", width: 1, type: "dashed" },
},
{
name: "BB下轨",
type: "line",
data: lower,
symbol: "none",
lineStyle: { color: "#ff9800", width: 1 },
},
],
};
chart.setOption(option);
window.addEventListener("resize", () => chart.resize());
}
main().catch((e) => {
console.error(e);
alert("加载数据失败,请确认 bb_chart_2026_03_02_data.json 已生成并与本 HTML 同目录。");
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
178.5,305.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,305.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,306.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,306.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,307.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,307.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,308.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,308.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,309.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,309.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,310.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,310.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,311.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,311.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,312.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,312.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,313.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,313.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,314.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,314.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,315.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,315.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,316.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,316.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,317.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,317.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,318.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,318.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,319.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,319.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,320.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,320.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,321.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,321.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,322.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,322.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,323.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,323.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,324.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,324.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,325.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,325.5,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
178.5,326.0,5.2791790570783465e+19,2.6395895285391733e+19,33680,56.24109263657957,21.175874581988747,4.8783963655773225e+19
1 178.5 305.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
2 178.5 305.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
3 178.5 306.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
4 178.5 306.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
5 178.5 307.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
6 178.5 307.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
7 178.5 308.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
8 178.5 308.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
9 178.5 309.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
10 178.5 309.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
11 178.5 310.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
12 178.5 310.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
13 178.5 311.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
14 178.5 311.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
15 178.5 312.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
16 178.5 312.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
17 178.5 313.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
18 178.5 313.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
19 178.5 314.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
20 178.5 314.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
21 178.5 315.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
22 178.5 315.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
23 178.5 316.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
24 178.5 316.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
25 178.5 317.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
26 178.5 317.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
27 178.5 318.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
28 178.5 318.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
29 178.5 319.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
30 178.5 319.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
31 178.5 320.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
32 178.5 320.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
33 178.5 321.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
34 178.5 321.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
35 178.5 322.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
36 178.5 322.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
37 178.5 323.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
38 178.5 323.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
39 178.5 324.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
40 178.5 324.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
41 178.5 325.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
42 178.5 325.5 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19
43 178.5 326.0 5.2791790570783465e+19 2.6395895285391733e+19 33680 56.24109263657957 21.175874581988747 4.8783963655773225e+19

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
timestamp,action,price,size,margin,fee,capital,reason,pnl
2026-03-01 01:35,开short,1970.895742,0.02536917551471376,1.0,0.0024999999999999996,98.9975,触上轨开空,
2026-03-01 01:50,平仓100%,2022.33,0.02536917551471376,,0.0025652422359335528,98.690090039093,止损,-1.3048447186710703
2026-03-01 03:50,开long,2019.4038,0.02443545219611179,0.9869009003909299,0.0024672522509773245,97.70072188645109,触下轨开多,
2026-03-01 03:55,加long,2018.383596,0.04840542802670058,1.9540144377290218,0.004885036094322554,95.74182241262774,触下轨加多,
2026-03-01 04:00,平仓50%,2028.348,0.03642044011140619,,0.0036936663429545246,97.55902982143648,触中轨平50%-1m(04:02)回踩中轨,0.35044340609171903
2026-03-01 04:05,平仓100%,2018.7258371425564,0.03642044011140619,,0.003676144172649939,99.0258113463238,回开仓价全平,0.0
2026-03-01 04:15,开long,1999.6626196004338,0.02476062971215385,0.9902581134632381,0.0024756452836580947,98.03307758757691,触下轨开多,
2026-03-01 04:35,平仓50%,2014.6299999999999,0.012380314856076925,,0.0012470876859249124,98.71226043853991,触中轨平50%-1m(04:39)回踩中轨,0.18530088191730254
2026-03-01 05:10,平仓100%,2017.8250206638015,0.012380314856076925,,0.0012490654540143892,99.43099667352436,延迟反转-同K回调确认-平多,0.22485624370683877
2026-03-01 05:10,开short,2017.4214556596687,0.02464308991920873,0.9943099667352436,0.0024857749168381085,98.43420093187228,延迟反转-同K回调确认-开空,
2026-03-01 05:15,平仓50%,2014.157,0.012321544959604365,,0.0012408763015600921,98.9703381761176,触中轨平50%-1m(05:16)反抽中轨,0.04022313717924384
2026-03-01 05:20,平仓100%,2017.4214556596687,0.012321544959604365,,0.0012428874584190543,99.4662502720268,回开仓价全平,0.0
2026-03-01 05:20,开short,2020.9165042134857,0.0246091934190863,0.994662502720268,0.0024866562568006696,98.46910111304973,触上轨开空,
2026-03-01 05:35,平仓50%,2016.4740000000002,0.01230459670954315,,0.0012405949672639653,99.01985499216998,触中轨平50%-1m(05:38)反抽中轨,0.05466322272738604
2026-03-01 06:10,平仓100%,1993.0582785593572,0.01230459670954315,,0.0012261889168144599,99.8587442863308,延迟反转-同K反弹确认-平空,0.34278423171750066
2026-03-01 06:10,开long,1993.456890215069,0.02504662748828175,0.998587442863308,0.00249646860715827,98.85766037486034,延迟反转-同K反弹确认-开多,
2026-03-01 07:00,平仓50%,1996.611,0.012523313744140875,,0.0012502092989001429,99.39520379341326,触中轨平50%-1m(07:01)回踩中轨,0.03949990642015626
2026-03-01 07:05,平仓100%,1993.456890215069,0.012523313744140875,,0.001248234303579135,99.89324928054134,回开仓价全平,0.0
2026-03-01 07:10,开short,2002.49942,0.024942141876011462,0.9989324928054134,0.002497331232013533,98.89181945650391,触上轨开空,
2026-03-01 07:25,平仓50%,1997.67,0.012471070938005731,,0.0012456542140362953,99.450268088102,触中轨平50%-1m(07:26)反抽中轨,0.06022803940942398
2026-03-01 07:35,平仓100%,2002.49942,0.012471070938005731,,0.0012486656160067666,99.94848566888871,回开仓价全平,0.0
2026-03-01 08:50,开long,1983.9752370436452,0.02518894485241248,0.9994848566888871,0.0024987121417222175,98.9465021000581,触下轨开多,
2026-03-01 09:00,加long,1984.6668539999998,0.049855471662982744,1.978930042001162,0.004947325105002905,96.96262473295194,触下轨加多,
2026-03-01 09:15,平仓50%,1992.7069999999999,0.037522208257697615,,0.003738538352528591,98.75848822726503,触中轨平50%-1m(09:18)回踩中轨,0.31039458332059994
2026-03-01 09:30,平仓100%,1984.4347101286562,0.037522208257697615,,0.003723018623362561,100.24397265798669,回开仓价全平,0.0
2026-03-01 09:35,开long,1977.5454880734026,0.025345554188907188,1.0024397265798668,0.0025060993164496663,99.23902683209037,触下轨开多,
2026-03-01 10:15,平仓50%,1979.625,0.012672777094453594,,0.0012543673177803846,99.76534551917355,触中轨平50%-1m(10:17)回踩中轨,0.02635319111102702
2026-03-01 10:15,平仓100%,1977.5454880734026,0.012672777094453594,,0.0012530496582248331,100.26531233280527,回开仓价全平,0.0
2026-03-01 10:25,开short,1983.593202,0.02527365798383223,1.0026531233280527,0.0025066328083201313,99.26015257666889,触上轨开空,
2026-03-01 10:35,加short,1987.0210368714647,0.049954253495449914,1.9852030515333778,0.004963007628833443,97.26998651750668,触上轨加空,
2026-03-01 10:40,平仓50%,1980.9359999999997,0.037613955739641075,,0.0037255419513530803,98.94575440746019,触中轨平50%-1m(10:41)反抽中轨,0.18556534447413794
2026-03-01 11:00,平仓100%,1985.8694174198158,0.037613955739641075,,0.0037348202185767876,100.43594767467232,回开仓价全平,0.0
2026-03-01 13:20,开long,1971.0811861410878,0.02547737464617128,1.0043594767467232,0.0025108986918668075,99.42907729923373,触下轨开多,
2026-03-01 13:25,平仓50%,1977.221,0.01273868732308564,,0.0012593600043819353,100.00821084657333,触中轨平50%-1m(13:27)回踩中轨,0.07821316897063098
2026-03-01 13:50,平仓100%,2012.7953226137352,0.01273868732308564,,0.0012820185130072825,101.04049190791126,延迟反转-同K回调确认-平多,0.5313833414775777
2026-03-01 13:50,开short,2012.3927635492125,0.025104565504824317,1.0104049190791127,0.002526012297697781,100.02756097653445,延迟反转-同K回调确认-开空,
2026-03-01 14:25,平仓50%,2004.753,0.012552282752412158,,0.0012582113252373262,100.62740169698006,触中轨平50%-1m(14:28)反抽中轨,0.09589647223128844
2026-03-01 14:40,平仓100%,1994.9544227315625,0.012552282752412158,,0.0012520615996150872,101.35024307959607,延迟反转-同K反弹确认-平空,0.21889098467607288
2026-03-01 14:40,开long,1995.3534136161088,0.025396564435150013,1.0135024307959608,0.0025337560769899017,100.33420689272312,延迟反转-同K反弹确认-开多,
2026-03-01 15:00,平仓50%,2004.636,0.012698282217575007,,0.0012727716835755343,100.95755823804919,触中轨平50%-1m(15:03)回踩中轨,0.11787290161166897
2026-03-01 15:20,平仓100%,2012.645228772288,0.012698282217575007,,0.0012778568559403158,101.68260794549853,延迟反转-同K回调确认-平多,0.21957634890730374
2026-03-01 15:20,开short,2012.2426997265336,0.02526599002180933,1.0168260794549853,0.002542065198637463,100.6632398008449,延迟反转-同K回调确认-开空,
2026-03-01 15:50,平仓50%,2008.9260000000002,0.012632995010904665,,0.0012689376067638331,101.2122837540636,触中轨平50%-1m(15:52)反抽中轨,0.04189985109796585
2026-03-01 15:50,平仓100%,2012.2426997265336,0.012632995010904665,,0.0012710325993187314,101.71942576119179,回开仓价全平,0.0
2026-03-01 15:55,开long,1992.6786162417027,0.025523289338307855,1.017194257611918,0.0025429856440297945,100.69968851793584,触下轨开多,
2026-03-01 16:00,加long,1991.1565538891698,0.050573466120103445,2.013993770358717,0.005034984425896792,98.68065976315123,触下轨加多,
2026-03-01 16:25,平仓50%,2002.4759999999999,0.03804837772920565,,0.00380954816208344,100.60370677137739,触中轨平50%-1m(16:26)回踩中轨,0.41126254240293536
2026-03-01 16:45,平仓100%,1967.62,0.03804837772920565,,0.00374323744937698,101.20060583618708,止损,-0.9149517117262566
2026-03-01 16:50,开long,1965.2229659999998,0.02574786871185666,1.0120060583618709,0.0025300151459046764,100.18606976267931,触下轨开多,
2026-03-01 17:35,平仓50%,1967.5260000000003,0.01287393435592833,,0.001266490028379112,100.72045541036735,触中轨平50%-1m(17:36)回踩中轨,0.029649108535477225
2026-03-01 17:45,平仓100%,1965.2229659999998,0.01287393435592833,,0.0012650075729523382,101.22519343197533,回开仓价全平,0.0
2026-03-01 18:00,开short,1982.393442,0.025531055361505614,1.0122519343197534,0.002530629835799383,100.21041086781977,触上轨开空,
2026-03-01 18:05,加short,1991.233970955805,0.05032578407635248,2.0042082173563953,0.005010520543390987,98.20119212991999,触上轨加空,
2026-03-01 18:30,平仓50%,1977.5280000000002,0.037928419718929046,,0.0037502255994967156,100.11266378212795,触中轨平50%-1m(18:33)反抽中轨,0.4069918019693819
2026-03-01 18:55,平仓100%,1972.9829470185493,0.037928419718929046,,0.0037416062656404534,102.196530730795,延迟反转-同K反弹确认-平空,0.5793784790946218
2026-03-01 18:55,开long,1973.377543607953,0.025893811111265538,1.02196530730795,0.0025549132682698744,101.17201051021878,延迟反转-同K反弹确认-开多,
2026-03-01 19:05,加long,1968.973716,0.051383118874614164,2.0234402102043756,0.0050586005255109385,99.1435116994889,触下轨加多,
2026-03-01 19:40,平仓100%,1946.27,0.0772769299858797,,0.007520088526180904,100.31289177647686,止损,-1.8685053519981858
2026-03-01 20:10,开long,1933.526628,0.025940395731771856,1.0031289177647686,0.002507822294411921,99.30725503641769,触下轨开多,
2026-03-01 20:35,平仓100%,1911.05,0.025940395731771856,,0.00247866966316013,99.72485265948347,止损,-0.5830526250358269
2026-03-01 22:10,开short,1943.1708021370632,0.025660341476366342,0.9972485265948348,0.002493121316487086,98.72511101157215,触上轨开空,
2026-03-01 22:25,平仓50%,1933.2939999999999,0.012830170738183171,,0.0012402246053552545,99.34921610802999,触中轨平50%-1m(22:28)反抽中轨,0.12672105776577522
2026-03-01 22:25,平仓100%,1943.1708021370632,0.012830170738183171,,0.001246560658243543,99.84659381066916,回开仓价全平,0.0
2026-03-02 00:10,开short,1947.900342,0.025629286996313172,0.9984659381066916,0.0024961648452667285,98.8456317077172,触上轨开空,
2026-03-02 00:15,平仓50%,1940.6370000000002,0.012814643498156586,,0.001243428565716605,99.43669838654002,触中轨平50%-1m(00:17)反抽中轨,0.0930771383351843
2026-03-02 00:20,平仓100%,1947.900342,0.012814643498156586,,0.0012480824226333643,99.93468327317073,回开仓价全平,0.0
2026-03-02 00:40,开short,1961.187684,0.02547810290888272,0.9993468327317073,0.002498367081829268,98.9328380733572,触上轨开空,
2026-03-02 00:45,加short,1959.7479720000001,0.050482429111735394,1.9786567614671442,0.004946641903667859,96.9492346699864,触上轨加空,
2026-03-02 01:10,平仓100%,1988.21,0.07596053202061812,,0.007551274468435656,97.79437733094622,止损,-2.1253096587705884
2026-03-02 02:05,开long,1949.6071946200707,0.02508053355588993,0.9779437733094622,0.002444859433273655,96.81398869820349,触下轨开多,
2026-03-02 02:10,加long,1940.8287934851146,0.049882807295102104,1.9362797739640698,0.004840699434910173,94.8728682248045,触下轨加多,
2026-03-02 02:30,平仓50%,1953.676,0.037481670425496015,,0.0036613519975100665,96.69776991480681,触中轨平50%-1m(02:34)回踩中轨,0.371451268363048
2026-03-02 03:10,平仓100%,1966.7001436494575,0.037481670425496015,,0.0036857603305022308,99.0108138563194,延迟反转-同K回调确认-平多,0.859617928206332
2026-03-02 03:10,开short,1966.3068036207276,0.025176847700979926,0.990108138563194,0.0024752703464079847,98.0182304474098,延迟反转-同K回调确认-开空,
2026-03-02 03:55,平仓50%,1971.86,0.012588423850489963,,0.0012411304726913566,98.44213739647142,触中轨平50%-1m(03:57)反抽中轨,-0.0699059897472862
2026-03-02 03:55,平仓100%,1966.3068036207276,0.012588423850489963,,0.0012376351732039923,98.93595383057982,回开仓价全平,0.0
2026-03-02 04:15,开long,1969.413804,0.025118122364541886,0.9893595383057983,0.002473398845764495,97.94412089342826,触下轨开多,
2026-03-02 04:20,平仓50%,1971.578,0.012559061182270943,,0.001238058436380969,98.46474287411921,触中轨平50%-1m(04:22)回踩中轨,0.02718026997442538
2026-03-02 04:20,平仓100%,1969.413804,0.012559061182270943,,0.0012366994228822474,98.95818594384923,回开仓价全平,0.0
2026-03-02 05:05,开long,1964.19276,0.025190548493786637,0.9895818594384923,0.0024739546485962305,97.96613012976213,触下轨开多,
2026-03-02 05:15,平仓50%,1970.2719999999997,0.012595274246893318,,0.0012408058090487492,98.53624994868501,触中轨平50%-1m(05:17)回踩中轨,0.07656969501268121
2026-03-02 05:20,平仓100%,1964.19276,0.012595274246893318,,0.0012369773242981153,99.02980390107996,回开仓价全平,0.0
2026-03-02 05:40,开short,1979.524016,0.025013539391451352,0.9902980390107996,0.0024757450975269983,98.03703011697164,触上轨开空,
2026-03-02 07:20,平仓50%,1937.3039999999996,0.012506769695725676,,0.0012114707479304063,99.05900368239097,触中轨平50%-1m(07:24)反抽中轨,0.5280360166618587
2026-03-02 08:10,平仓100%,1928.936186384675,0.012506769695725676,,0.0012062380320432252,100.1856367982698,延迟反转-同K反弹确认-平空,0.6326903344054814
2026-03-02 08:10,开long,1929.3219736219519,0.025963949555342865,1.001856367982698,0.0025046409199567447,99.18127578936715,延迟反转-同K反弹确认-开多,
2026-03-02 08:15,加long,1930.6160459999999,0.051372864114984755,1.983625515787343,0.004959063789468357,97.19269120979034,触下轨加多,
2026-03-02 08:50,平仓100%,1948.7595882760102,0.07733681367032762,,0.007535542858338308,101.60740052896617,延迟反转-同K回调确认-平多,1.4367629782641276
2026-03-02 08:50,开short,1948.369836358355,0.026074977818091708,1.0160740052896617,0.0025401850132241535,100.58878633866328,延迟反转-同K回调确认-开空,
2026-03-02 09:40,平仓50%,1955.359,0.013037488909045854,,0.0012746485637851492,101.00442754928288,触中轨平50%-1m(09:43)反抽中轨,-0.09112114346145109
2026-03-02 09:40,平仓100%,1948.369836358355,0.013037488909045854,,0.0012700925066120767,101.5111944594211,回开仓价全平,0.0
2026-03-02 11:05,开long,1939.03773,0.02617566251777399,1.0151119445942112,0.0025377798614855274,100.49354473496541,触下轨开多,
2026-03-02 12:00,平仓50%,1943.6330000000003,0.013087831258886995,,0.0012718970366602154,101.05997092857488,触中轨平50%-1m(12:02)回踩中轨,0.06014211834902898
2026-03-02 12:20,平仓100%,1939.03773,0.013087831258886995,,0.0012688899307427637,101.56625801094124,回开仓价全平,0.0
2026-03-02 12:20,开long,1936.8878482318667,0.026218931081543613,1.0156625801094123,0.0025391564502735306,100.54805627438155,触下轨开多,
2026-03-02 12:35,加long,1930.6260479999999,0.05208054474274946,2.010961125487631,0.005027402813719077,98.5320677460802,触下轨加多,
2026-03-02 12:50,平仓50%,1939.3809999999999,0.039149737912146536,,0.003796312893089832,100.30224850785619,触中轨平50%-1m(12:51)回踩中轨,0.26066522187056274
2026-03-02 13:05,平仓100%,1932.7228399260932,0.039149737912146536,,0.0037832796319963034,101.81177708102271,回开仓价全平,0.0
2026-03-02 13:05,开long,1930.4860199999998,0.026369467591643766,1.0181177708102271,0.0025452944270255673,100.79111401578545,触下轨开多,
2026-03-02 13:15,平仓50%,1935.7879999999998,0.013184733795821883,,0.001276142473257322,101.36880195360808,触中轨平50%-1m(13:16)回踩中轨,0.06990519489077116
2026-03-02 13:25,平仓100%,1930.4860199999998,0.013184733795821883,,0.0012726472135127836,101.87658819179968,回开仓价全平,0.0
2026-03-02 14:25,开long,1922.6183440503523,0.02649423077311791,1.0187658819179968,0.002546914704794992,100.85527539517689,触下轨开多,
2026-03-02 14:30,平仓100%,1950.3106548105818,0.02649423077311791,,0.0025835990283911126,102.60514414998892,延迟反转-同K回调确认-平多,0.733686471922416
2026-03-02 14:30,开short,1949.9205926796196,0.02631008271187774,1.0260514414998891,0.0025651286037497224,101.57652757988528,延迟反转-同K回调确认-开空,
2026-03-02 14:45,平仓100%,1977.81,0.02631008271187774,,0.002601817234418945,101.8662045907663,止损,-0.7337726133844545
2026-03-02 17:40,开long,2025.7860592669192,0.025142389573859815,1.018662045907663,0.002546655114769157,100.84499588974387,触下轨开多,
2026-03-02 18:15,平仓50%,2037.0170000000003,0.012571194786929907,,0.00128038687456438,101.49423286941916,触中轨平50%-1m(18:15)回踩中轨,0.1411863435960275
2026-03-02 18:40,加long,2027.0190691607734,0.05007068478711442,2.0298846573883833,0.005074711643470957,99.45927350038731,触下轨加多,
2026-03-02 18:45,平仓50%,2033.3019999999997,0.03132093978702216,,0.0031842464755415857,100.93023459635924,触中轨平50%-1m(18:47)回踩中轨,0.20453750227635803
2026-03-02 18:50,平仓100%,2026.7716243577877,0.03132093978702216,,0.003174019600427768,102.19666841692992,回开仓价全平,0.0
2026-03-02 18:50,开long,2028.895698,0.02518529378264026,1.0219666841692994,0.002554916710423248,101.1721468160502,触下轨开多,
2026-03-02 18:55,加long,2027.7254639999999,0.049894400702781824,2.023442936321004,0.005058607340802509,99.1436452723884,触下轨加多,
2026-03-02 19:05,平仓50%,2031.3199999999997,0.03753984724271104,,0.003812772125053188,100.78273929931468,触中轨平50%-1m(19:05)回踩中轨,0.1202019888061939
2026-03-02 19:05,平仓100%,2028.1180160380236,0.03753984724271104,,0.003806762025612878,102.30163734753422,回开仓价全平,0.0
2026-03-02 19:05,开long,2026.35519,0.025242770332760428,1.0230163734753424,0.0025575409336883554,101.27606343312519,触下轨开多,
2026-03-02 19:10,加long,2023.6443782356102,0.05004637401825834,2.0255212686625037,0.005063803171656258,99.24547836129103,触下轨加多,
2026-03-02 19:15,平仓50%,2030.7600000000002,0.037644572175509386,,0.0038223545695568717,100.99957516548169,触中轨平50%-1m(19:18)回踩中轨,0.23365033769128954
2026-03-02 19:35,平仓100%,2038.953388071054,0.037644572175509386,,0.003837776398987009,103.06209313644565,延迟反转-同K回调确认-平多,0.5420869262940294
2026-03-02 19:35,开short,2038.5455973934397,0.025278338946213584,1.0306209313644565,0.002576552328411141,102.02889565275278,延迟反转-同K回调确认-开空,
2026-03-02 19:45,平仓50%,2031.934,0.012639169473106792,,0.0012840979092083886,102.62648712046943,触中轨平50%-1m(19:45)反抽中轨,0.08356509994363615
2026-03-02 19:50,平仓100%,2038.5455973934397,0.012639169473106792,,0.0012882761642055705,103.14050930998745,回开仓价全平,0.0
2026-03-02 19:50,开short,2043.2612660000002,0.025239187720692457,1.0314050930998746,0.0025785127327496863,102.10652570415482,触上轨开空,
2026-03-02 19:55,加short,2042.021514,0.05000266892592338,2.0421305140830963,0.00510532628520774,100.05928986378652,触上轨加空,
2026-03-02 20:05,平仓50%,2037.4699999999998,0.03762092832330792,,0.003832575641544508,101.77910244042056,触中轨平50%-1m(20:09)反抽中轨,0.18687734868409472
2026-03-02 20:05,平仓100%,2042.4373773884072,0.03762092832330792,,0.003841919508978713,103.31202832450306,回开仓价全平,0.0
2026-03-02 20:50,开short,2045.370844,0.02525508482424204,1.0331202832450306,0.002582800708112576,102.27632524054992,触上轨开空,
2026-03-02 20:55,平仓50%,2038.2310000000002,0.01262754241212102,,0.0012868924199099917,102.88175717267845,触中轨平50%-1m(20:59)反抽中轨,0.09015868292592541
2026-03-02 21:45,平仓100%,2045.370844,0.01262754241212102,,0.001291400354056288,103.3970259139469,回开仓价全平,0.0
2026-03-02 22:05,开short,2047.470424,0.025249943711506063,1.033970259139469,0.002584925647848672,102.36047072915959,触上轨开空,
2026-03-02 22:15,平仓50%,2043.7830000000001,0.012624971855753031,,0.0012901351427133249,102.92271934780683,触中轨平50%-1m(22:15)反抽中轨,0.04655362422022782
2026-03-02 22:25,平仓100%,2047.470424,0.012624971855753031,,0.001292462823924336,103.43841201455264,回开仓价全平,0.0
2026-03-02 22:30,开short,2051.5995980000002,0.025209210441303816,1.0343841201455264,0.0025859603003638154,102.40144193410674,触上轨开空,
2026-03-02 22:50,平仓50%,2049.2699999999995,0.012604605220651908,,0.001291511967026266,102.94670614532531,触中轨平50%-1m(22:54)反抽中轨,0.029363663112829255
2026-03-02 22:50,平仓100%,2051.5995980000002,0.012604605220651908,,0.0012929801501819077,103.46260522524788,回开仓价全平,0.0
2026-03-03 00:30,开long,2020.584036,0.02560215348183813,1.0346260522524788,0.0025865651306311967,102.42539260786478,触下轨开多,
2026-03-03 00:45,平仓50%,2029.438,0.012801076740919065,,0.001298949578946865,103.05474695703741,触中轨平50%-1m(00:47)回踩中轨,0.11334027262533643
2026-03-03 01:30,加long,2025.1515544797203,0.05088742456290541,2.061094939140748,0.005152737347851869,100.98849928054881,触下轨加多,
2026-03-03 02:15,平仓100%,2002.4,0.06368850130382447,,0.0063764927505389046,102.16998750049402,止损,-1.390543252571244
2026-03-03 02:45,开short,2019.206078,0.025299544363914604,1.0216998750049402,0.00255424968751235,101.14573337580157,触上轨开空,
2026-03-03 03:10,平仓50%,2012.1740000000002,0.012649772181957302,,0.0012726771345228875,101.74426482083527,触中轨平50%-1m(03:13)反抽中轨,0.0889541846657505
2026-03-03 04:35,平仓100%,2003.7637516585983,0.012649772181957302,,0.0012673577482472664,102.44918931076766,延迟反转-同K反弹确认-平空,0.1953419101781693
2026-03-03 04:35,开long,2004.16450440893,0.025559076883507146,1.0244918931076767,0.0025612297327691916,101.42213618792722,延迟反转-同K反弹确认-开多,
2026-03-03 04:40,加long,2002.570434,0.05064597702331163,2.0284427237585443,0.0050711068093963595,99.38862235735928,触下轨加多,
2026-03-03 05:05,平仓50%,2003.5459999999998,0.03810252695340939,,0.0038170082733697775,100.92807270325906,触中轨平50%-1m(05:09)回踩中轨,0.016800045740044088
2026-03-03 05:05,平仓100%,2003.1050831610567,0.03810252695340939,,0.003816168271082775,102.45072384342109,回开仓价全平,0.0
2026-03-03 05:15,开short,2013.5872020000002,0.02543985275176105,1.0245072384342109,0.0025612680960855265,101.42365533689079,触上轨开空,
2026-03-03 05:20,平仓50%,2003.975,0.012719926375880526,,0.0012745207229552585,102.05690093713504,触中轨平50%-1m(05:24)反抽中轨,0.12226650175009475
2026-03-03 06:35,加short,1993.821156,0.0511865874379031,2.041138018742701,0.005102845046856751,100.01066007334548,触上轨加空,
2026-03-03 06:40,平仓50%,1992.6649999999997,0.03195325690689181,,0.0031836068337185775,101.44682656011526,触中轨平50%-1m(06:41)反抽中轨,0.1626542746235937
2026-03-03 06:45,平仓100%,1988.3703092642115,0.03195325690689181,,0.003176745365897763,103.02022926476914,延迟反转-同K反弹确认-平空,0.2998836310398825
2026-03-03 06:45,开long,1988.7679833260643,0.025900514823372103,1.0302022926476915,0.002575505731619228,101.98745146638983,延迟反转-同K反弹确认-开多,
2026-03-03 06:50,平仓50%,1992.611,0.012950257411686051,,0.0012902412685678573,102.55103042660998,触中轨平50%-1m(06:51)回踩中轨,0.04976805516487063
2026-03-03 06:55,平仓100%,1999.2593560774471,0.012950257411686051,,0.0012945461646962318,103.20070300450149,延迟反转-同K回调确认-平多,0.1358659777323571
2026-03-03 06:55,开short,1998.8595042062318,0.025814896641643555,1.032007030045015,0.002580017575112537,102.16611595688137,延迟反转-同K回调确认-开空,
2026-03-03 07:05,平仓50%,1993.9159999999997,0.012907448320821777,,0.0012868183863029834,102.74464067858328,触中轨平50%-1m(07:08)反抽中轨,0.0638080250657058
2026-03-03 07:05,平仓100%,1998.8595042062318,0.012907448320821777,,0.0012900087875562685,103.25935418481824,回开仓价全平,0.0
1 timestamp action price size margin fee capital reason pnl
2 2026-03-01 01:35 开short 1970.895742 0.02536917551471376 1.0 0.0024999999999999996 98.9975 触上轨开空
3 2026-03-01 01:50 平仓100% 2022.33 0.02536917551471376 0.0025652422359335528 98.690090039093 止损 -1.3048447186710703
4 2026-03-01 03:50 开long 2019.4038 0.02443545219611179 0.9869009003909299 0.0024672522509773245 97.70072188645109 触下轨开多
5 2026-03-01 03:55 加long 2018.383596 0.04840542802670058 1.9540144377290218 0.004885036094322554 95.74182241262774 触下轨加多
6 2026-03-01 04:00 平仓50% 2028.348 0.03642044011140619 0.0036936663429545246 97.55902982143648 触中轨平50%-1m(04:02)回踩中轨 0.35044340609171903
7 2026-03-01 04:05 平仓100% 2018.7258371425564 0.03642044011140619 0.003676144172649939 99.0258113463238 回开仓价全平 0.0
8 2026-03-01 04:15 开long 1999.6626196004338 0.02476062971215385 0.9902581134632381 0.0024756452836580947 98.03307758757691 触下轨开多
9 2026-03-01 04:35 平仓50% 2014.6299999999999 0.012380314856076925 0.0012470876859249124 98.71226043853991 触中轨平50%-1m(04:39)回踩中轨 0.18530088191730254
10 2026-03-01 05:10 平仓100% 2017.8250206638015 0.012380314856076925 0.0012490654540143892 99.43099667352436 延迟反转-同K回调确认-平多 0.22485624370683877
11 2026-03-01 05:10 开short 2017.4214556596687 0.02464308991920873 0.9943099667352436 0.0024857749168381085 98.43420093187228 延迟反转-同K回调确认-开空
12 2026-03-01 05:15 平仓50% 2014.157 0.012321544959604365 0.0012408763015600921 98.9703381761176 触中轨平50%-1m(05:16)反抽中轨 0.04022313717924384
13 2026-03-01 05:20 平仓100% 2017.4214556596687 0.012321544959604365 0.0012428874584190543 99.4662502720268 回开仓价全平 0.0
14 2026-03-01 05:20 开short 2020.9165042134857 0.0246091934190863 0.994662502720268 0.0024866562568006696 98.46910111304973 触上轨开空
15 2026-03-01 05:35 平仓50% 2016.4740000000002 0.01230459670954315 0.0012405949672639653 99.01985499216998 触中轨平50%-1m(05:38)反抽中轨 0.05466322272738604
16 2026-03-01 06:10 平仓100% 1993.0582785593572 0.01230459670954315 0.0012261889168144599 99.8587442863308 延迟反转-同K反弹确认-平空 0.34278423171750066
17 2026-03-01 06:10 开long 1993.456890215069 0.02504662748828175 0.998587442863308 0.00249646860715827 98.85766037486034 延迟反转-同K反弹确认-开多
18 2026-03-01 07:00 平仓50% 1996.611 0.012523313744140875 0.0012502092989001429 99.39520379341326 触中轨平50%-1m(07:01)回踩中轨 0.03949990642015626
19 2026-03-01 07:05 平仓100% 1993.456890215069 0.012523313744140875 0.001248234303579135 99.89324928054134 回开仓价全平 0.0
20 2026-03-01 07:10 开short 2002.49942 0.024942141876011462 0.9989324928054134 0.002497331232013533 98.89181945650391 触上轨开空
21 2026-03-01 07:25 平仓50% 1997.67 0.012471070938005731 0.0012456542140362953 99.450268088102 触中轨平50%-1m(07:26)反抽中轨 0.06022803940942398
22 2026-03-01 07:35 平仓100% 2002.49942 0.012471070938005731 0.0012486656160067666 99.94848566888871 回开仓价全平 0.0
23 2026-03-01 08:50 开long 1983.9752370436452 0.02518894485241248 0.9994848566888871 0.0024987121417222175 98.9465021000581 触下轨开多
24 2026-03-01 09:00 加long 1984.6668539999998 0.049855471662982744 1.978930042001162 0.004947325105002905 96.96262473295194 触下轨加多
25 2026-03-01 09:15 平仓50% 1992.7069999999999 0.037522208257697615 0.003738538352528591 98.75848822726503 触中轨平50%-1m(09:18)回踩中轨 0.31039458332059994
26 2026-03-01 09:30 平仓100% 1984.4347101286562 0.037522208257697615 0.003723018623362561 100.24397265798669 回开仓价全平 0.0
27 2026-03-01 09:35 开long 1977.5454880734026 0.025345554188907188 1.0024397265798668 0.0025060993164496663 99.23902683209037 触下轨开多
28 2026-03-01 10:15 平仓50% 1979.625 0.012672777094453594 0.0012543673177803846 99.76534551917355 触中轨平50%-1m(10:17)回踩中轨 0.02635319111102702
29 2026-03-01 10:15 平仓100% 1977.5454880734026 0.012672777094453594 0.0012530496582248331 100.26531233280527 回开仓价全平 0.0
30 2026-03-01 10:25 开short 1983.593202 0.02527365798383223 1.0026531233280527 0.0025066328083201313 99.26015257666889 触上轨开空
31 2026-03-01 10:35 加short 1987.0210368714647 0.049954253495449914 1.9852030515333778 0.004963007628833443 97.26998651750668 触上轨加空
32 2026-03-01 10:40 平仓50% 1980.9359999999997 0.037613955739641075 0.0037255419513530803 98.94575440746019 触中轨平50%-1m(10:41)反抽中轨 0.18556534447413794
33 2026-03-01 11:00 平仓100% 1985.8694174198158 0.037613955739641075 0.0037348202185767876 100.43594767467232 回开仓价全平 0.0
34 2026-03-01 13:20 开long 1971.0811861410878 0.02547737464617128 1.0043594767467232 0.0025108986918668075 99.42907729923373 触下轨开多
35 2026-03-01 13:25 平仓50% 1977.221 0.01273868732308564 0.0012593600043819353 100.00821084657333 触中轨平50%-1m(13:27)回踩中轨 0.07821316897063098
36 2026-03-01 13:50 平仓100% 2012.7953226137352 0.01273868732308564 0.0012820185130072825 101.04049190791126 延迟反转-同K回调确认-平多 0.5313833414775777
37 2026-03-01 13:50 开short 2012.3927635492125 0.025104565504824317 1.0104049190791127 0.002526012297697781 100.02756097653445 延迟反转-同K回调确认-开空
38 2026-03-01 14:25 平仓50% 2004.753 0.012552282752412158 0.0012582113252373262 100.62740169698006 触中轨平50%-1m(14:28)反抽中轨 0.09589647223128844
39 2026-03-01 14:40 平仓100% 1994.9544227315625 0.012552282752412158 0.0012520615996150872 101.35024307959607 延迟反转-同K反弹确认-平空 0.21889098467607288
40 2026-03-01 14:40 开long 1995.3534136161088 0.025396564435150013 1.0135024307959608 0.0025337560769899017 100.33420689272312 延迟反转-同K反弹确认-开多
41 2026-03-01 15:00 平仓50% 2004.636 0.012698282217575007 0.0012727716835755343 100.95755823804919 触中轨平50%-1m(15:03)回踩中轨 0.11787290161166897
42 2026-03-01 15:20 平仓100% 2012.645228772288 0.012698282217575007 0.0012778568559403158 101.68260794549853 延迟反转-同K回调确认-平多 0.21957634890730374
43 2026-03-01 15:20 开short 2012.2426997265336 0.02526599002180933 1.0168260794549853 0.002542065198637463 100.6632398008449 延迟反转-同K回调确认-开空
44 2026-03-01 15:50 平仓50% 2008.9260000000002 0.012632995010904665 0.0012689376067638331 101.2122837540636 触中轨平50%-1m(15:52)反抽中轨 0.04189985109796585
45 2026-03-01 15:50 平仓100% 2012.2426997265336 0.012632995010904665 0.0012710325993187314 101.71942576119179 回开仓价全平 0.0
46 2026-03-01 15:55 开long 1992.6786162417027 0.025523289338307855 1.017194257611918 0.0025429856440297945 100.69968851793584 触下轨开多
47 2026-03-01 16:00 加long 1991.1565538891698 0.050573466120103445 2.013993770358717 0.005034984425896792 98.68065976315123 触下轨加多
48 2026-03-01 16:25 平仓50% 2002.4759999999999 0.03804837772920565 0.00380954816208344 100.60370677137739 触中轨平50%-1m(16:26)回踩中轨 0.41126254240293536
49 2026-03-01 16:45 平仓100% 1967.62 0.03804837772920565 0.00374323744937698 101.20060583618708 止损 -0.9149517117262566
50 2026-03-01 16:50 开long 1965.2229659999998 0.02574786871185666 1.0120060583618709 0.0025300151459046764 100.18606976267931 触下轨开多
51 2026-03-01 17:35 平仓50% 1967.5260000000003 0.01287393435592833 0.001266490028379112 100.72045541036735 触中轨平50%-1m(17:36)回踩中轨 0.029649108535477225
52 2026-03-01 17:45 平仓100% 1965.2229659999998 0.01287393435592833 0.0012650075729523382 101.22519343197533 回开仓价全平 0.0
53 2026-03-01 18:00 开short 1982.393442 0.025531055361505614 1.0122519343197534 0.002530629835799383 100.21041086781977 触上轨开空
54 2026-03-01 18:05 加short 1991.233970955805 0.05032578407635248 2.0042082173563953 0.005010520543390987 98.20119212991999 触上轨加空
55 2026-03-01 18:30 平仓50% 1977.5280000000002 0.037928419718929046 0.0037502255994967156 100.11266378212795 触中轨平50%-1m(18:33)反抽中轨 0.4069918019693819
56 2026-03-01 18:55 平仓100% 1972.9829470185493 0.037928419718929046 0.0037416062656404534 102.196530730795 延迟反转-同K反弹确认-平空 0.5793784790946218
57 2026-03-01 18:55 开long 1973.377543607953 0.025893811111265538 1.02196530730795 0.0025549132682698744 101.17201051021878 延迟反转-同K反弹确认-开多
58 2026-03-01 19:05 加long 1968.973716 0.051383118874614164 2.0234402102043756 0.0050586005255109385 99.1435116994889 触下轨加多
59 2026-03-01 19:40 平仓100% 1946.27 0.0772769299858797 0.007520088526180904 100.31289177647686 止损 -1.8685053519981858
60 2026-03-01 20:10 开long 1933.526628 0.025940395731771856 1.0031289177647686 0.002507822294411921 99.30725503641769 触下轨开多
61 2026-03-01 20:35 平仓100% 1911.05 0.025940395731771856 0.00247866966316013 99.72485265948347 止损 -0.5830526250358269
62 2026-03-01 22:10 开short 1943.1708021370632 0.025660341476366342 0.9972485265948348 0.002493121316487086 98.72511101157215 触上轨开空
63 2026-03-01 22:25 平仓50% 1933.2939999999999 0.012830170738183171 0.0012402246053552545 99.34921610802999 触中轨平50%-1m(22:28)反抽中轨 0.12672105776577522
64 2026-03-01 22:25 平仓100% 1943.1708021370632 0.012830170738183171 0.001246560658243543 99.84659381066916 回开仓价全平 0.0
65 2026-03-02 00:10 开short 1947.900342 0.025629286996313172 0.9984659381066916 0.0024961648452667285 98.8456317077172 触上轨开空
66 2026-03-02 00:15 平仓50% 1940.6370000000002 0.012814643498156586 0.001243428565716605 99.43669838654002 触中轨平50%-1m(00:17)反抽中轨 0.0930771383351843
67 2026-03-02 00:20 平仓100% 1947.900342 0.012814643498156586 0.0012480824226333643 99.93468327317073 回开仓价全平 0.0
68 2026-03-02 00:40 开short 1961.187684 0.02547810290888272 0.9993468327317073 0.002498367081829268 98.9328380733572 触上轨开空
69 2026-03-02 00:45 加short 1959.7479720000001 0.050482429111735394 1.9786567614671442 0.004946641903667859 96.9492346699864 触上轨加空
70 2026-03-02 01:10 平仓100% 1988.21 0.07596053202061812 0.007551274468435656 97.79437733094622 止损 -2.1253096587705884
71 2026-03-02 02:05 开long 1949.6071946200707 0.02508053355588993 0.9779437733094622 0.002444859433273655 96.81398869820349 触下轨开多
72 2026-03-02 02:10 加long 1940.8287934851146 0.049882807295102104 1.9362797739640698 0.004840699434910173 94.8728682248045 触下轨加多
73 2026-03-02 02:30 平仓50% 1953.676 0.037481670425496015 0.0036613519975100665 96.69776991480681 触中轨平50%-1m(02:34)回踩中轨 0.371451268363048
74 2026-03-02 03:10 平仓100% 1966.7001436494575 0.037481670425496015 0.0036857603305022308 99.0108138563194 延迟反转-同K回调确认-平多 0.859617928206332
75 2026-03-02 03:10 开short 1966.3068036207276 0.025176847700979926 0.990108138563194 0.0024752703464079847 98.0182304474098 延迟反转-同K回调确认-开空
76 2026-03-02 03:55 平仓50% 1971.86 0.012588423850489963 0.0012411304726913566 98.44213739647142 触中轨平50%-1m(03:57)反抽中轨 -0.0699059897472862
77 2026-03-02 03:55 平仓100% 1966.3068036207276 0.012588423850489963 0.0012376351732039923 98.93595383057982 回开仓价全平 0.0
78 2026-03-02 04:15 开long 1969.413804 0.025118122364541886 0.9893595383057983 0.002473398845764495 97.94412089342826 触下轨开多
79 2026-03-02 04:20 平仓50% 1971.578 0.012559061182270943 0.001238058436380969 98.46474287411921 触中轨平50%-1m(04:22)回踩中轨 0.02718026997442538
80 2026-03-02 04:20 平仓100% 1969.413804 0.012559061182270943 0.0012366994228822474 98.95818594384923 回开仓价全平 0.0
81 2026-03-02 05:05 开long 1964.19276 0.025190548493786637 0.9895818594384923 0.0024739546485962305 97.96613012976213 触下轨开多
82 2026-03-02 05:15 平仓50% 1970.2719999999997 0.012595274246893318 0.0012408058090487492 98.53624994868501 触中轨平50%-1m(05:17)回踩中轨 0.07656969501268121
83 2026-03-02 05:20 平仓100% 1964.19276 0.012595274246893318 0.0012369773242981153 99.02980390107996 回开仓价全平 0.0
84 2026-03-02 05:40 开short 1979.524016 0.025013539391451352 0.9902980390107996 0.0024757450975269983 98.03703011697164 触上轨开空
85 2026-03-02 07:20 平仓50% 1937.3039999999996 0.012506769695725676 0.0012114707479304063 99.05900368239097 触中轨平50%-1m(07:24)反抽中轨 0.5280360166618587
86 2026-03-02 08:10 平仓100% 1928.936186384675 0.012506769695725676 0.0012062380320432252 100.1856367982698 延迟反转-同K反弹确认-平空 0.6326903344054814
87 2026-03-02 08:10 开long 1929.3219736219519 0.025963949555342865 1.001856367982698 0.0025046409199567447 99.18127578936715 延迟反转-同K反弹确认-开多
88 2026-03-02 08:15 加long 1930.6160459999999 0.051372864114984755 1.983625515787343 0.004959063789468357 97.19269120979034 触下轨加多
89 2026-03-02 08:50 平仓100% 1948.7595882760102 0.07733681367032762 0.007535542858338308 101.60740052896617 延迟反转-同K回调确认-平多 1.4367629782641276
90 2026-03-02 08:50 开short 1948.369836358355 0.026074977818091708 1.0160740052896617 0.0025401850132241535 100.58878633866328 延迟反转-同K回调确认-开空
91 2026-03-02 09:40 平仓50% 1955.359 0.013037488909045854 0.0012746485637851492 101.00442754928288 触中轨平50%-1m(09:43)反抽中轨 -0.09112114346145109
92 2026-03-02 09:40 平仓100% 1948.369836358355 0.013037488909045854 0.0012700925066120767 101.5111944594211 回开仓价全平 0.0
93 2026-03-02 11:05 开long 1939.03773 0.02617566251777399 1.0151119445942112 0.0025377798614855274 100.49354473496541 触下轨开多
94 2026-03-02 12:00 平仓50% 1943.6330000000003 0.013087831258886995 0.0012718970366602154 101.05997092857488 触中轨平50%-1m(12:02)回踩中轨 0.06014211834902898
95 2026-03-02 12:20 平仓100% 1939.03773 0.013087831258886995 0.0012688899307427637 101.56625801094124 回开仓价全平 0.0
96 2026-03-02 12:20 开long 1936.8878482318667 0.026218931081543613 1.0156625801094123 0.0025391564502735306 100.54805627438155 触下轨开多
97 2026-03-02 12:35 加long 1930.6260479999999 0.05208054474274946 2.010961125487631 0.005027402813719077 98.5320677460802 触下轨加多
98 2026-03-02 12:50 平仓50% 1939.3809999999999 0.039149737912146536 0.003796312893089832 100.30224850785619 触中轨平50%-1m(12:51)回踩中轨 0.26066522187056274
99 2026-03-02 13:05 平仓100% 1932.7228399260932 0.039149737912146536 0.0037832796319963034 101.81177708102271 回开仓价全平 0.0
100 2026-03-02 13:05 开long 1930.4860199999998 0.026369467591643766 1.0181177708102271 0.0025452944270255673 100.79111401578545 触下轨开多
101 2026-03-02 13:15 平仓50% 1935.7879999999998 0.013184733795821883 0.001276142473257322 101.36880195360808 触中轨平50%-1m(13:16)回踩中轨 0.06990519489077116
102 2026-03-02 13:25 平仓100% 1930.4860199999998 0.013184733795821883 0.0012726472135127836 101.87658819179968 回开仓价全平 0.0
103 2026-03-02 14:25 开long 1922.6183440503523 0.02649423077311791 1.0187658819179968 0.002546914704794992 100.85527539517689 触下轨开多
104 2026-03-02 14:30 平仓100% 1950.3106548105818 0.02649423077311791 0.0025835990283911126 102.60514414998892 延迟反转-同K回调确认-平多 0.733686471922416
105 2026-03-02 14:30 开short 1949.9205926796196 0.02631008271187774 1.0260514414998891 0.0025651286037497224 101.57652757988528 延迟反转-同K回调确认-开空
106 2026-03-02 14:45 平仓100% 1977.81 0.02631008271187774 0.002601817234418945 101.8662045907663 止损 -0.7337726133844545
107 2026-03-02 17:40 开long 2025.7860592669192 0.025142389573859815 1.018662045907663 0.002546655114769157 100.84499588974387 触下轨开多
108 2026-03-02 18:15 平仓50% 2037.0170000000003 0.012571194786929907 0.00128038687456438 101.49423286941916 触中轨平50%-1m(18:15)回踩中轨 0.1411863435960275
109 2026-03-02 18:40 加long 2027.0190691607734 0.05007068478711442 2.0298846573883833 0.005074711643470957 99.45927350038731 触下轨加多
110 2026-03-02 18:45 平仓50% 2033.3019999999997 0.03132093978702216 0.0031842464755415857 100.93023459635924 触中轨平50%-1m(18:47)回踩中轨 0.20453750227635803
111 2026-03-02 18:50 平仓100% 2026.7716243577877 0.03132093978702216 0.003174019600427768 102.19666841692992 回开仓价全平 0.0
112 2026-03-02 18:50 开long 2028.895698 0.02518529378264026 1.0219666841692994 0.002554916710423248 101.1721468160502 触下轨开多
113 2026-03-02 18:55 加long 2027.7254639999999 0.049894400702781824 2.023442936321004 0.005058607340802509 99.1436452723884 触下轨加多
114 2026-03-02 19:05 平仓50% 2031.3199999999997 0.03753984724271104 0.003812772125053188 100.78273929931468 触中轨平50%-1m(19:05)回踩中轨 0.1202019888061939
115 2026-03-02 19:05 平仓100% 2028.1180160380236 0.03753984724271104 0.003806762025612878 102.30163734753422 回开仓价全平 0.0
116 2026-03-02 19:05 开long 2026.35519 0.025242770332760428 1.0230163734753424 0.0025575409336883554 101.27606343312519 触下轨开多
117 2026-03-02 19:10 加long 2023.6443782356102 0.05004637401825834 2.0255212686625037 0.005063803171656258 99.24547836129103 触下轨加多
118 2026-03-02 19:15 平仓50% 2030.7600000000002 0.037644572175509386 0.0038223545695568717 100.99957516548169 触中轨平50%-1m(19:18)回踩中轨 0.23365033769128954
119 2026-03-02 19:35 平仓100% 2038.953388071054 0.037644572175509386 0.003837776398987009 103.06209313644565 延迟反转-同K回调确认-平多 0.5420869262940294
120 2026-03-02 19:35 开short 2038.5455973934397 0.025278338946213584 1.0306209313644565 0.002576552328411141 102.02889565275278 延迟反转-同K回调确认-开空
121 2026-03-02 19:45 平仓50% 2031.934 0.012639169473106792 0.0012840979092083886 102.62648712046943 触中轨平50%-1m(19:45)反抽中轨 0.08356509994363615
122 2026-03-02 19:50 平仓100% 2038.5455973934397 0.012639169473106792 0.0012882761642055705 103.14050930998745 回开仓价全平 0.0
123 2026-03-02 19:50 开short 2043.2612660000002 0.025239187720692457 1.0314050930998746 0.0025785127327496863 102.10652570415482 触上轨开空
124 2026-03-02 19:55 加short 2042.021514 0.05000266892592338 2.0421305140830963 0.00510532628520774 100.05928986378652 触上轨加空
125 2026-03-02 20:05 平仓50% 2037.4699999999998 0.03762092832330792 0.003832575641544508 101.77910244042056 触中轨平50%-1m(20:09)反抽中轨 0.18687734868409472
126 2026-03-02 20:05 平仓100% 2042.4373773884072 0.03762092832330792 0.003841919508978713 103.31202832450306 回开仓价全平 0.0
127 2026-03-02 20:50 开short 2045.370844 0.02525508482424204 1.0331202832450306 0.002582800708112576 102.27632524054992 触上轨开空
128 2026-03-02 20:55 平仓50% 2038.2310000000002 0.01262754241212102 0.0012868924199099917 102.88175717267845 触中轨平50%-1m(20:59)反抽中轨 0.09015868292592541
129 2026-03-02 21:45 平仓100% 2045.370844 0.01262754241212102 0.001291400354056288 103.3970259139469 回开仓价全平 0.0
130 2026-03-02 22:05 开short 2047.470424 0.025249943711506063 1.033970259139469 0.002584925647848672 102.36047072915959 触上轨开空
131 2026-03-02 22:15 平仓50% 2043.7830000000001 0.012624971855753031 0.0012901351427133249 102.92271934780683 触中轨平50%-1m(22:15)反抽中轨 0.04655362422022782
132 2026-03-02 22:25 平仓100% 2047.470424 0.012624971855753031 0.001292462823924336 103.43841201455264 回开仓价全平 0.0
133 2026-03-02 22:30 开short 2051.5995980000002 0.025209210441303816 1.0343841201455264 0.0025859603003638154 102.40144193410674 触上轨开空
134 2026-03-02 22:50 平仓50% 2049.2699999999995 0.012604605220651908 0.001291511967026266 102.94670614532531 触中轨平50%-1m(22:54)反抽中轨 0.029363663112829255
135 2026-03-02 22:50 平仓100% 2051.5995980000002 0.012604605220651908 0.0012929801501819077 103.46260522524788 回开仓价全平 0.0
136 2026-03-03 00:30 开long 2020.584036 0.02560215348183813 1.0346260522524788 0.0025865651306311967 102.42539260786478 触下轨开多
137 2026-03-03 00:45 平仓50% 2029.438 0.012801076740919065 0.001298949578946865 103.05474695703741 触中轨平50%-1m(00:47)回踩中轨 0.11334027262533643
138 2026-03-03 01:30 加long 2025.1515544797203 0.05088742456290541 2.061094939140748 0.005152737347851869 100.98849928054881 触下轨加多
139 2026-03-03 02:15 平仓100% 2002.4 0.06368850130382447 0.0063764927505389046 102.16998750049402 止损 -1.390543252571244
140 2026-03-03 02:45 开short 2019.206078 0.025299544363914604 1.0216998750049402 0.00255424968751235 101.14573337580157 触上轨开空
141 2026-03-03 03:10 平仓50% 2012.1740000000002 0.012649772181957302 0.0012726771345228875 101.74426482083527 触中轨平50%-1m(03:13)反抽中轨 0.0889541846657505
142 2026-03-03 04:35 平仓100% 2003.7637516585983 0.012649772181957302 0.0012673577482472664 102.44918931076766 延迟反转-同K反弹确认-平空 0.1953419101781693
143 2026-03-03 04:35 开long 2004.16450440893 0.025559076883507146 1.0244918931076767 0.0025612297327691916 101.42213618792722 延迟反转-同K反弹确认-开多
144 2026-03-03 04:40 加long 2002.570434 0.05064597702331163 2.0284427237585443 0.0050711068093963595 99.38862235735928 触下轨加多
145 2026-03-03 05:05 平仓50% 2003.5459999999998 0.03810252695340939 0.0038170082733697775 100.92807270325906 触中轨平50%-1m(05:09)回踩中轨 0.016800045740044088
146 2026-03-03 05:05 平仓100% 2003.1050831610567 0.03810252695340939 0.003816168271082775 102.45072384342109 回开仓价全平 0.0
147 2026-03-03 05:15 开short 2013.5872020000002 0.02543985275176105 1.0245072384342109 0.0025612680960855265 101.42365533689079 触上轨开空
148 2026-03-03 05:20 平仓50% 2003.975 0.012719926375880526 0.0012745207229552585 102.05690093713504 触中轨平50%-1m(05:24)反抽中轨 0.12226650175009475
149 2026-03-03 06:35 加short 1993.821156 0.0511865874379031 2.041138018742701 0.005102845046856751 100.01066007334548 触上轨加空
150 2026-03-03 06:40 平仓50% 1992.6649999999997 0.03195325690689181 0.0031836068337185775 101.44682656011526 触中轨平50%-1m(06:41)反抽中轨 0.1626542746235937
151 2026-03-03 06:45 平仓100% 1988.3703092642115 0.03195325690689181 0.003176745365897763 103.02022926476914 延迟反转-同K反弹确认-平空 0.2998836310398825
152 2026-03-03 06:45 开long 1988.7679833260643 0.025900514823372103 1.0302022926476915 0.002575505731619228 101.98745146638983 延迟反转-同K反弹确认-开多
153 2026-03-03 06:50 平仓50% 1992.611 0.012950257411686051 0.0012902412685678573 102.55103042660998 触中轨平50%-1m(06:51)回踩中轨 0.04976805516487063
154 2026-03-03 06:55 平仓100% 1999.2593560774471 0.012950257411686051 0.0012945461646962318 103.20070300450149 延迟反转-同K回调确认-平多 0.1358659777323571
155 2026-03-03 06:55 开short 1998.8595042062318 0.025814896641643555 1.032007030045015 0.002580017575112537 102.16611595688137 延迟反转-同K回调确认-开空
156 2026-03-03 07:05 平仓50% 1993.9159999999997 0.012907448320821777 0.0012868183863029834 102.74464067858328 触中轨平50%-1m(07:08)反抽中轨 0.0638080250657058
157 2026-03-03 07:05 平仓100% 1998.8595042062318 0.012907448320821777 0.0012900087875562685 103.25935418481824 回开仓价全平 0.0

View File

@@ -0,0 +1,580 @@
"""
布林带延迟反转策略回测 - 2025年多周期测试
测试1分钟和15分钟周期的收益对比
策略规则:
1. BB(10, 2.5)
2. 空仓触上轨开空,触下轨开多
3. 同向加仓最多1次保证金1%
4. 延迟反转:触轨不立刻平仓,记录价格,回调到该价再平仓+反向开仓
5. 中轨平半+回开仓价全平
回测参数:
- 本金: 100U
- 杠杆: 100x
- 逐仓模式
- 开仓保证金: 1%
- 手续费: 0.05% (万五)
- 返佣: 90%次日早上8点到账
"""
import pandas as pd
from pathlib import Path
from peewee import *
from loguru import logger
import numpy as np
# 数据库配置
DB_PATH = Path(__file__).parent / 'models' / 'database.db'
db = SqliteDatabase(str(DB_PATH))
class BitMartETH1M(Model):
"""1分钟K线模型"""
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
class Meta:
database = db
table_name = 'bitmart_eth_1m'
class BitMartETH3M(Model):
"""3分钟K线模型"""
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
class Meta:
database = db
table_name = 'bitmart_eth_3m'
class BitMartETH5M(Model):
"""5分钟K线模型"""
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
class Meta:
database = db
table_name = 'bitmart_eth_5m'
class BitMartETH15M(Model):
"""15分钟K线模型"""
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
class Meta:
database = db
table_name = 'bitmart_eth_15m'
class BBDelayReversalBacktest:
"""布林带延迟反转策略回测"""
def __init__(self, timeframe='1m'):
# 策略参数
self.timeframe = timeframe
self.bb_period = 10
self.bb_std = 2.5
self.initial_capital = 100 # 初始本金100U
self.leverage = 100 # 100倍杠杆
self.fee_rate = 0.0005 # 万五手续费
self.rebate_rate = 0.9 # 90%返佣
self.margin_ratio = 0.01 # 开仓保证金1%
self.rebate_credit_hour = 8 # 次日早上8点返佣到账
# 账户状态
self.capital = self.initial_capital
self.position = 0 # 持仓量(正=多,负=空)
self.position_count = 0 # 持仓次数0=空仓1=首次2=加仓)
self.entry_price = 0 # 开仓均价
self.total_margin = 0 # 总保证金
# 延迟反转状态
self.delay_reverse_price = None
self.delay_reverse_type = None
self.delay_reverse_kline_index = None
# 中轨平仓记录
self.mid_closed_half = False
# 交易记录
self.trades = []
self.pending_rebates = []
self.total_rebate_credited = 0.0
def calculate_bollinger_bands(self, df):
"""计算布林带整体右移1根避免使用当前K收盘价"""
df['sma'] = df['close'].rolling(window=self.bb_period).mean()
df['std'] = df['close'].rolling(window=self.bb_period).std()
df['upper'] = (df['sma'] + self.bb_std * df['std']).shift(1)
df['lower'] = (df['sma'] - self.bb_std * df['std']).shift(1)
df['middle'] = df['sma'].shift(1)
return df
def schedule_rebate(self, fee, timestamp):
"""登记返佣到账时间次日08:00上海时间"""
rebate = fee * self.rebate_rate
if rebate <= 0:
return
trade_utc = pd.Timestamp(timestamp, tz='UTC')
trade_local = trade_utc.tz_convert('Asia/Shanghai')
credit_local = (trade_local + pd.Timedelta(days=1)).normalize() + pd.Timedelta(hours=self.rebate_credit_hour)
credit_utc = credit_local.tz_convert('UTC').tz_localize(None)
self.pending_rebates.append({
'credit_time': credit_utc,
'amount': rebate,
'trade_time': trade_utc.tz_localize(None),
})
def apply_pending_rebates(self, current_time):
"""处理当前时刻前应到账的返佣"""
if not self.pending_rebates:
return
remaining = []
for item in self.pending_rebates:
if item['credit_time'] <= current_time:
amount = item['amount']
self.capital += amount
self.total_rebate_credited += amount
else:
remaining.append(item)
self.pending_rebates = remaining
def clear_delay_reversal(self):
"""清理延迟反转状态"""
self.delay_reverse_price = None
self.delay_reverse_type = None
self.delay_reverse_kline_index = None
def mark_delay_reversal(self, reverse_type, trigger_price, kline_index):
"""记录延迟反转触发信息"""
self.delay_reverse_type = reverse_type
self.delay_reverse_price = trigger_price
self.delay_reverse_kline_index = kline_index
def check_delay_reversal_signal(self, i, row, prev_row):
"""检查延迟反转是否成立"""
if self.position == 0 or self.delay_reverse_price is None or self.delay_reverse_kline_index is None:
return None
offset = i - self.delay_reverse_kline_index
if offset <= 0:
return None
high = row['high']
low = row['low']
if self.delay_reverse_type == 'long_to_short':
# 多转空:回调确认
if offset == 1 and low <= self.delay_reverse_price:
return 'short', self.delay_reverse_price, "次K回调确认"
if offset >= 2 and prev_row is not None:
prev_upper = prev_row['upper']
prev_touch_upper = pd.notna(prev_upper) and prev_row['high'] >= prev_upper
if prev_touch_upper:
if low <= prev_upper:
return 'short', prev_upper, "上一根触上轨后回调确认"
else:
prev_body_low = min(prev_row['open'], prev_row['close'])
if low <= prev_body_low:
return 'short', prev_body_low, "跌破上一根实体确认"
elif self.delay_reverse_type == 'short_to_long':
# 空转多:反弹确认
if offset == 1 and high >= self.delay_reverse_price:
return 'long', self.delay_reverse_price, "次K反弹确认"
if offset >= 2 and prev_row is not None:
prev_lower = prev_row['lower']
prev_touch_lower = pd.notna(prev_lower) and prev_row['low'] <= prev_lower
if prev_touch_lower:
if high >= prev_lower:
return 'long', prev_lower, "上一根触下轨后反弹确认"
else:
prev_body_high = max(prev_row['open'], prev_row['close'])
if high >= prev_body_high:
return 'long', prev_body_high, "突破上一根实体确认"
return None
def open_position(self, price, direction, timestamp, reason):
"""开仓或加仓"""
if self.position_count not in (0, 1):
return False
if self.position_count == 1:
current_direction = 'long' if self.position > 0 else 'short'
if direction != current_direction:
return False
margin = self.capital * self.margin_ratio
if margin <= 0:
return False
position_size = margin * self.leverage / price
fee = position_size * price * self.fee_rate
required = margin + fee
if self.capital < required:
return False
self.capital -= required
self.schedule_rebate(fee, timestamp)
if self.position_count == 0:
self.position = position_size if direction == 'long' else -position_size
self.entry_price = price
self.total_margin = margin
self.position_count = 1
else:
old_size = abs(self.position)
new_size = old_size + position_size
old_value = old_size * self.entry_price
new_value = position_size * price
self.entry_price = (old_value + new_value) / new_size
self.position = new_size if direction == 'long' else -new_size
self.total_margin += margin
self.position_count = 2
self.mid_closed_half = False
self.trades.append({
'timestamp': timestamp,
'action': f'{direction}' if self.position_count == 1 else f'{direction}',
'price': price,
'size': position_size,
'margin': margin,
'fee': fee,
'capital': self.capital,
'reason': reason
})
return True
def close_position(self, price, ratio, timestamp, reason):
"""平仓"""
if self.position == 0:
return False
ratio = min(max(ratio, 0.0), 1.0)
if ratio == 0:
return False
close_size = abs(self.position) * ratio
if self.position > 0:
pnl = close_size * (price - self.entry_price)
else:
pnl = close_size * (self.entry_price - price)
fee = close_size * price * self.fee_rate
released_margin = self.total_margin * ratio
self.capital += released_margin + pnl - fee
self.schedule_rebate(fee, timestamp)
if ratio >= 0.999:
self.position = 0
self.position_count = 0
self.total_margin = 0
self.entry_price = 0
self.mid_closed_half = False
self.clear_delay_reversal()
else:
self.position *= (1 - ratio)
self.total_margin *= (1 - ratio)
self.trades.append({
'timestamp': timestamp,
'action': f'平仓{int(ratio*100)}%',
'price': price,
'size': close_size,
'pnl': pnl,
'fee': fee,
'capital': self.capital,
'reason': reason
})
return True
def run_backtest(self, start_date, end_date):
"""运行回测"""
# 重置状态
self.capital = self.initial_capital
self.position = 0
self.position_count = 0
self.entry_price = 0
self.total_margin = 0
self.mid_closed_half = False
self.trades = []
self.pending_rebates = []
self.total_rebate_credited = 0.0
self.clear_delay_reversal()
logger.info(f"{'='*80}")
logger.info(f"开始回测: {start_date} ~ {end_date} | 周期: {self.timeframe}")
logger.info(f"初始资金: {self.initial_capital}U | 杠杆: {self.leverage}x | BB({self.bb_period}, {self.bb_std})")
logger.info(f"{'='*80}")
# 从数据库加载数据
start_dt = pd.Timestamp(start_date)
end_dt = pd.Timestamp(end_date)
if isinstance(end_date, str) and len(end_date) <= 10:
end_dt = end_dt + pd.Timedelta(days=1) - pd.Timedelta(milliseconds=1)
start_ts = int(start_dt.timestamp() * 1000)
end_ts = int(end_dt.timestamp() * 1000)
# 根据周期选择数据表
model_mapping = {
'1m': BitMartETH1M,
'3m': BitMartETH3M,
'5m': BitMartETH5M,
'15m': BitMartETH15M,
}
Model = model_mapping.get(self.timeframe)
if Model is None:
logger.error(f"不支持的周期: {self.timeframe}")
return None
query = Model.select().where(
(Model.id >= start_ts) & (Model.id <= end_ts)
).order_by(Model.id)
data = []
for row in query:
data.append({
'timestamp': row.id,
'open': row.open,
'high': row.high,
'low': row.low,
'close': row.close
})
if not data:
logger.error("没有找到数据!")
return None
df = pd.DataFrame(data)
df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
logger.info(f"加载数据: {len(df)} 根K线")
logger.info(f"时间范围: {df['datetime'].min()} ~ {df['datetime'].max()}")
# 计算布林带
df = self.calculate_bollinger_bands(df)
if len(df) <= self.bb_period + 1:
logger.error("数据不足,无法执行回测")
return None
# 逐根K线回测
for i in range(self.bb_period, len(df)):
row = df.iloc[i]
prev_row = df.iloc[i-1] if i > 0 else None
signal_dt = row['datetime']
signal_ts = signal_dt.strftime('%Y-%m-%d %H:%M')
high = row['high']
low = row['low']
close = row['close']
upper = row['upper']
lower = row['lower']
middle = row['middle']
# 处理返佣到账
self.apply_pending_rebates(signal_dt)
if pd.isna(upper) or pd.isna(lower) or pd.isna(middle):
continue
# 检查延迟反转确认
if self.delay_reverse_price is not None:
reversal_signal = self.check_delay_reversal_signal(i, row, prev_row)
if reversal_signal is not None and self.position != 0:
new_direction, reversal_price, reason = reversal_signal
self.close_position(reversal_price, 1.0, signal_ts, f"{reason}-平仓")
self.open_position(reversal_price, new_direction, signal_ts, f"{reason}-开仓")
self.mid_closed_half = False
self.clear_delay_reversal()
continue
# 中轨平仓逻辑
if self.position != 0:
if self.position > 0:
# 回到开仓价全平+反手
if self.mid_closed_half and low <= self.entry_price:
self.close_position(close, 1.0, signal_ts, "回开仓价全平")
self.open_position(close, 'short', signal_ts, "回开仓价反手开空")
self.mid_closed_half = False
continue
# 触中轨平半
if not self.mid_closed_half and low <= middle <= high:
self.close_position(close, 0.5, signal_ts, "触中轨平50%")
self.mid_closed_half = True
continue
else: # 空仓
# 回到开仓价全平+反手
if self.mid_closed_half and high >= self.entry_price:
self.close_position(close, 1.0, signal_ts, "回开仓价全平")
self.open_position(close, 'long', signal_ts, "回开仓价反手开多")
self.mid_closed_half = False
continue
# 触中轨平半
if not self.mid_closed_half and low <= middle <= high:
self.close_position(close, 0.5, signal_ts, "触中轨平50%")
self.mid_closed_half = True
continue
# 开仓与加仓逻辑
if self.position == 0:
self.clear_delay_reversal()
# 触上轨开空
if high >= upper:
self.open_position(upper, 'short', signal_ts, "触上轨开空")
# 触下轨开多
elif low <= lower:
self.open_position(lower, 'long', signal_ts, "触下轨开多")
continue
# 延迟反转触发
if self.position > 0 and high >= upper:
self.mark_delay_reversal('long_to_short', upper, i)
continue
elif self.position < 0 and low <= lower:
self.mark_delay_reversal('short_to_long', lower, i)
continue
# 加仓
if self.delay_reverse_price is None and self.position_count == 1:
if self.position > 0 and low <= lower:
self.open_position(lower, 'long', signal_ts, "触下轨加多")
elif self.position < 0 and high >= upper:
self.open_position(upper, 'short', signal_ts, "触上轨加空")
# 回测末尾处理返佣
self.apply_pending_rebates(df.iloc[-1]['datetime'])
# 最后平仓
if self.position != 0:
final_price = df.iloc[-1]['close']
final_time = df.iloc[-1]['datetime'].strftime('%Y-%m-%d %H:%M')
self.close_position(final_price, 1.0, final_time, "回测结束平仓")
# 生成报告
return self.generate_report(df)
def generate_report(self, df):
"""生成回测报告"""
logger.info(f"\n{'='*80}")
logger.info(f"回测报告 - {self.timeframe}")
logger.info(f"{'='*80}")
# 基本统计
total_trades = len([t for t in self.trades if '' in t['action']])
win_trades = len([t for t in self.trades if '' in t['action'] and t.get('pnl', 0) > 0])
loss_trades = len([t for t in self.trades if '' in t['action'] and t.get('pnl', 0) < 0])
total_pnl = sum([t.get('pnl', 0) for t in self.trades])
total_fee = sum([t.get('fee', 0) for t in self.trades])
pending_rebate = sum([x['amount'] for x in self.pending_rebates])
realized_net_fee = total_fee - self.total_rebate_credited
final_capital = self.capital
roi = (final_capital - self.initial_capital) / self.initial_capital * 100
logger.info(f"初始资金: {self.initial_capital:.2f}U")
logger.info(f"最终资金: {final_capital:.2f}U")
logger.info(f"总盈亏: {total_pnl:.2f}U")
logger.info(f"总手续费: {total_fee:.2f}U")
logger.info(f"返佣已到账: {self.total_rebate_credited:.2f}U")
logger.info(f"返佣待到账: {pending_rebate:.2f}U")
logger.info(f"已实现净手续费: {realized_net_fee:.2f}U")
logger.info(f"净收益: {final_capital - self.initial_capital:.2f}U")
logger.info(f"收益率: {roi:.2f}%")
logger.info(f"总交易次数: {total_trades}")
logger.info(f"盈利次数: {win_trades}")
logger.info(f"亏损次数: {loss_trades}")
if win_trades + loss_trades > 0:
logger.info(f"胜率: {win_trades/(win_trades+loss_trades)*100:.2f}%")
# 保存交易记录
trades_df = pd.DataFrame(self.trades)
output_dir = Path(__file__).parent / 'backtest_outputs' / 'trades'
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f'bb_backtest_2025_{self.timeframe}_trades.csv'
trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
logger.info(f"\n交易记录已保存到: {output_file}")
return {
'timeframe': self.timeframe,
'initial_capital': self.initial_capital,
'final_capital': final_capital,
'total_pnl': total_pnl,
'total_fee': total_fee,
'total_rebate_credited': self.total_rebate_credited,
'pending_rebate': pending_rebate,
'realized_net_fee': realized_net_fee,
'roi': roi,
'total_trades': total_trades,
'win_trades': win_trades,
'loss_trades': loss_trades,
'win_rate': win_trades/(win_trades+loss_trades)*100 if (win_trades+loss_trades) > 0 else 0,
'trades_file': str(output_file)
}
if __name__ == '__main__':
# 连接数据库
db.connect(reuse_if_open=True)
try:
results = []
for tf in ['1m', '3m', '5m', '15m']:
logger.info("\n" + "="*80)
logger.info(f"测试 {tf} 周期")
logger.info("="*80)
backtest = BBDelayReversalBacktest(timeframe=tf)
result = backtest.run_backtest('2025-01-01', '2025-12-31')
if result:
results.append(result)
# 对比总结
if len(results) > 0:
logger.info("\n" + "="*80)
logger.info("回测对比总结")
logger.info("="*80)
for result in results:
logger.info(f"\n{result['timeframe']}周期】")
logger.info(f" 收益率: {result['roi']:.2f}%")
logger.info(f" 最终资金: {result['final_capital']:.2f}U")
logger.info(f" 总交易次数: {result['total_trades']}")
logger.info(f" 胜率: {result['win_rate']:.2f}%")
logger.info(f" 净手续费: {result['realized_net_fee']:.2f}U")
finally:
db.close()

754
bb_backtest_march_2026.py Normal file
View File

@@ -0,0 +1,754 @@
"""
布林带延迟反转策略回测 - 2026年3月
策略规则:
1. 5分钟K线BB(10, 2.5)
2. 空仓触上轨开空,触下轨开多
3. 同向加仓最多1次保证金递增(1%->2%)
4. 延迟反转:触轨不立刻平仓,记录价格,回调到该价再平仓+反向开仓
5. 中轨平半+回开仓价全平
6. 止损亏损达保证金50%
"""
import pandas as pd
from pathlib import Path
from peewee import *
from loguru import logger
# 数据库配置
DB_PATH = Path(__file__).parent / 'models' / 'database.db'
db = SqliteDatabase(str(DB_PATH))
class BitMartETH5M(Model):
"""5分钟K线模型"""
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
class Meta:
database = db
table_name = 'bitmart_eth_5m'
class BitMartETH1M(Model):
"""1分钟K线模型"""
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
class Meta:
database = db
table_name = 'bitmart_eth_1m'
class BollingerBandBacktest:
"""布林带延迟反转策略回测"""
def __init__(self):
# 策略参数
self.bb_period = 10
self.bb_std = 2.5
self.initial_capital = 100 # 初始本金100U
self.leverage = 50 # 50倍杠杆
self.fee_rate = 0.0005 # 万五手续费
self.rebate_rate = 0.9 # 90%返佣
self.margin_ratio_1 = 0.01 # 首次开仓保证金比例1%
self.margin_ratio_2 = 0.01 # 加仓保证金比例1%
self.stop_loss_ratio = 0.5 # 止损比例50%
self.entry_slippage = 0.0002 # 开仓滑点2bps
self.rebate_credit_hour = 8 # 次日早上8点返佣到账上海时间
# 账户状态
self.capital = self.initial_capital
self.position = 0 # 持仓量(正=多,负=空)
self.position_count = 0 # 持仓次数0=空仓1=首次2=加仓)
self.entry_price = 0 # 开仓均价
self.total_margin = 0 # 总保证金
# 延迟反转状态
self.delay_reverse_price = None # 记录触碰轨道时的轨道价格
self.delay_reverse_type = None # 'long_to_short' 或 'short_to_long'
self.delay_reverse_kline_index = None # 触发延迟反转的K线索引
# 中轨平仓记录
self.mid_closed_half = False # 是否已平50%
# 1分钟K线缓存key=5分钟起始时间戳, value=该5分钟内的1m列表
self.one_minute_by_5m = {}
# 交易记录
self.trades = []
self.daily_pnl = []
self.pending_rebates = [] # 待到账返佣队列
self.total_rebate_credited = 0.0
self.current_run_label = "default"
def calculate_bollinger_bands(self, df):
"""计算布林带整体右移1根避免使用当前K收盘价"""
df['sma'] = df['close'].rolling(window=self.bb_period).mean()
df['std'] = df['close'].rolling(window=self.bb_period).std()
df['upper'] = (df['sma'] + self.bb_std * df['std']).shift(1)
df['lower'] = (df['sma'] - self.bb_std * df['std']).shift(1)
df['middle'] = df['sma'].shift(1)
return df
def get_rebate_amount(self, fee):
"""计算返佣金额"""
return fee * self.rebate_rate
def schedule_rebate(self, fee, timestamp):
"""登记返佣到账时间次日08:00上海时间"""
rebate = self.get_rebate_amount(fee)
if rebate <= 0:
return
trade_utc = pd.Timestamp(timestamp, tz='UTC')
trade_local = trade_utc.tz_convert('Asia/Shanghai')
credit_local = (trade_local + pd.Timedelta(days=1)).normalize() + pd.Timedelta(hours=self.rebate_credit_hour)
credit_utc = credit_local.tz_convert('UTC').tz_localize(None)
self.pending_rebates.append({
'credit_time': credit_utc,
'amount': rebate,
'trade_time': trade_utc.tz_localize(None),
})
def apply_pending_rebates(self, current_time):
"""处理当前时刻前应到账的返佣"""
if not self.pending_rebates:
return
remaining = []
for item in self.pending_rebates:
if item['credit_time'] <= current_time:
amount = item['amount']
self.capital += amount
self.total_rebate_credited += amount
self.trades.append({
'timestamp': current_time.strftime('%Y-%m-%d %H:%M'),
'action': '返佣到账',
'price': None,
'size': None,
'rebate': amount,
'capital': self.capital,
'reason': f"次日08:00返佣到账({item['trade_time'].strftime('%Y-%m-%d %H:%M')}手续费)"
})
logger.info(
f"[{current_time.strftime('%Y-%m-%d %H:%M')}] 返佣到账: {amount:.4f}U | 可用资金: {self.capital:.4f}U"
)
else:
remaining.append(item)
self.pending_rebates = remaining
def apply_entry_slippage(self, price, direction):
"""按方向施加不利滑点"""
if direction == 'long':
return price * (1 + self.entry_slippage)
return price * (1 - self.entry_slippage)
def load_one_minute_cache(self, start_ts, end_ts):
"""加载并缓存1分钟K线用于5分钟内走势确认"""
query = BitMartETH1M.select().where(
(BitMartETH1M.id >= start_ts) & (BitMartETH1M.id <= end_ts)
).order_by(BitMartETH1M.id)
bucket = {}
for row in query:
five_min_start = int(row.id - (row.id % 300000))
bucket.setdefault(five_min_start, []).append({
'timestamp': int(row.id),
'open': float(row.open) if row.open is not None else None,
'high': float(row.high) if row.high is not None else None,
'low': float(row.low) if row.low is not None else None,
'close': float(row.close) if row.close is not None else None,
})
self.one_minute_by_5m = bucket
total_rows = sum(len(v) for v in bucket.values())
logger.info(f"加载1分钟数据: {total_rows} 根 | 5分钟桶: {len(bucket)}")
def should_close_half_by_1m_trend(self, five_min_ts, middle_price, side):
"""
1分钟顺序确认中轨平半
- 多仓:先到中轨上方,再回踩中轨
- 空仓:先到中轨下方,再反抽中轨
"""
minute_rows = self.one_minute_by_5m.get(five_min_ts, [])
if not minute_rows:
return False, None
seen_above_middle = False
seen_below_middle = False
for minute in minute_rows:
m_open = minute['open']
m_high = minute['high']
m_low = minute['low']
m_close = minute['close']
if None in (m_open, m_high, m_low, m_close):
continue
minute_time = pd.to_datetime(minute['timestamp'], unit='ms').strftime('%H:%M')
if side == 'long':
# 当前1m开在中轨上方且回踩到中轨视为有效回踩
if m_open >= middle_price and m_low <= middle_price:
return True, f"1m({minute_time})回踩中轨"
# 已经到过中轨上方后,再次触及中轨
if seen_above_middle and m_low <= middle_price:
return True, f"1m({minute_time})回踩中轨"
if m_high >= middle_price:
seen_above_middle = True
else: # short
# 当前1m开在中轨下方且反抽到中轨视为有效反抽
if m_open <= middle_price and m_high >= middle_price:
return True, f"1m({minute_time})反抽中轨"
# 已经到过中轨下方后,再次触及中轨
if seen_below_middle and m_high >= middle_price:
return True, f"1m({minute_time})反抽中轨"
if m_low <= middle_price:
seen_below_middle = True
return False, None
def get_touch_entry_price(self, five_min_ts, direction, touch_price):
"""
触轨开仓的保守成交价:
- 在该5分钟内找到首个触轨1m
- 用该1m收盘价作为基准
- 为避免“过于理想”,不允许优于触轨价,再叠加不利滑点
"""
minute_rows = self.one_minute_by_5m.get(five_min_ts, [])
if not minute_rows:
return self.apply_entry_slippage(touch_price, direction)
trigger_close = None
for minute in minute_rows:
m_high = minute['high']
m_low = minute['low']
m_close = minute['close']
if None in (m_high, m_low, m_close):
continue
if direction == 'short' and m_high >= touch_price:
trigger_close = m_close
break
if direction == 'long' and m_low <= touch_price:
trigger_close = m_close
break
if trigger_close is None:
base_price = touch_price
elif direction == 'long':
base_price = max(touch_price, trigger_close)
else:
base_price = min(touch_price, trigger_close)
return self.apply_entry_slippage(base_price, direction)
def clear_delay_reversal(self):
"""清理延迟反转状态"""
self.delay_reverse_price = None
self.delay_reverse_type = None
self.delay_reverse_kline_index = None
def mark_delay_reversal(self, reverse_type, trigger_price, kline_index, timestamp):
"""记录延迟反转触发信息"""
self.delay_reverse_type = reverse_type
self.delay_reverse_price = trigger_price
self.delay_reverse_kline_index = kline_index
if reverse_type == 'long_to_short':
logger.info(f"[{timestamp}] 多仓触上轨 @ {trigger_price:.2f},进入延迟反转")
else:
logger.info(f"[{timestamp}] 空仓触下轨 @ {trigger_price:.2f},进入延迟反转")
def reverse_position(self, price, new_direction, timestamp, reason):
"""全平后反向开仓"""
if self.position == 0:
self.clear_delay_reversal()
return False
close_side = "" if self.position > 0 else ""
open_side = "" if new_direction == "long" else ""
self.close_position(price, 1.0, timestamp, f"{reason}-平{close_side}")
open_price = self.apply_entry_slippage(price, new_direction)
self.open_position(open_price, new_direction, timestamp, f"{reason}-开{open_side}")
self.mid_closed_half = False
self.clear_delay_reversal()
return True
def check_delay_reversal_signal(self, i, row, prev_row):
"""检查延迟反转是否在当前收盘K成立仅返回信号不直接执行"""
if self.position == 0 or self.delay_reverse_price is None or self.delay_reverse_kline_index is None:
return None
offset = i - self.delay_reverse_kline_index
# 禁止同K确认最早次K确认
if offset <= 0:
return None
high = row['high']
low = row['low']
if self.delay_reverse_type == 'long_to_short':
# 情况1触上轨后次K回调到记录上轨价
if offset == 1 and low <= self.delay_reverse_price:
return 'short', "延迟反转-次K回调确认", self.delay_reverse_price
# 情况2持续等待动态追踪上一根K线条件
if offset >= 2 and prev_row is not None:
prev_upper = prev_row['upper']
prev_touch_upper = pd.notna(prev_upper) and prev_row['high'] >= prev_upper
if prev_touch_upper:
if low <= prev_upper:
return 'short', "延迟反转-上一根触上轨后回调确认", prev_upper
else:
prev_body_low = min(prev_row['open'], prev_row['close'])
if low <= prev_body_low:
return 'short', "延迟反转-跌破上一根实体确认", prev_body_low
elif self.delay_reverse_type == 'short_to_long':
# 情况1触下轨后次K反弹到记录下轨价
if offset == 1 and high >= self.delay_reverse_price:
return 'long', "延迟反转-次K反弹确认", self.delay_reverse_price
# 情况2持续等待动态追踪上一根K线条件
if offset >= 2 and prev_row is not None:
prev_lower = prev_row['lower']
prev_touch_lower = pd.notna(prev_lower) and prev_row['low'] <= prev_lower
if prev_touch_lower:
if high >= prev_lower:
return 'long', "延迟反转-上一根触下轨后反弹确认", prev_lower
else:
prev_body_high = max(prev_row['open'], prev_row['close'])
if high >= prev_body_high:
return 'long', "延迟反转-突破上一根实体确认", prev_body_high
return None
def open_position(self, price, direction, timestamp, reason):
"""开仓或加仓"""
if self.position_count not in (0, 1):
return False
if self.position_count == 1:
current_direction = 'long' if self.position > 0 else 'short'
if direction != current_direction:
logger.warning("加仓方向不一致,跳过")
return False
margin_ratio = self.margin_ratio_1 if self.position_count == 0 else self.margin_ratio_2
margin = self.capital * margin_ratio
if margin <= 0:
logger.warning(f"[{timestamp}] 资金不足,无法开仓 | 可用资金: {self.capital:.4f}U")
return False
position_size = margin * self.leverage / price
fee = position_size * price * self.fee_rate
required = margin + fee
if self.capital < required:
logger.warning(
f"[{timestamp}] 可用资金不足,无法开仓 | 需要: {required:.4f}U | 可用: {self.capital:.4f}U"
)
return False
# 冻结保证金并扣除手续费
self.capital -= required
self.schedule_rebate(fee, timestamp)
if self.position_count == 0:
self.position = position_size if direction == 'long' else -position_size
self.entry_price = price
self.total_margin = margin
self.position_count = 1
action = f'{direction}'
else:
old_size = abs(self.position)
new_size = old_size + position_size
old_value = old_size * self.entry_price
new_value = position_size * price
self.entry_price = (old_value + new_value) / new_size
self.position = new_size if direction == 'long' else -new_size
self.total_margin += margin
self.position_count = 2
action = f'{direction}'
self.mid_closed_half = False
self.trades.append({
'timestamp': timestamp,
'action': action,
'price': price,
'size': position_size,
'margin': margin,
'fee': fee,
'rebate': self.get_rebate_amount(fee),
'capital': self.capital,
'reason': reason
})
logger.info(
f"[{timestamp}] {action} @ {price:.2f} | 仓位: {position_size:.4f} | "
f"保证金: {margin:.4f}U | 手续费: {fee:.4f}U | 返佣待到账: {self.get_rebate_amount(fee):.4f}U | "
f"可用资金: {self.capital:.4f}U | {reason}"
)
return True
def close_position(self, price, ratio, timestamp, reason):
"""平仓"""
if self.position == 0:
return False
ratio = min(max(ratio, 0.0), 1.0)
if ratio == 0:
return False
close_size = abs(self.position) * ratio
if self.position > 0:
pnl = close_size * (price - self.entry_price)
else:
pnl = close_size * (self.entry_price - price)
fee = close_size * price * self.fee_rate
released_margin = self.total_margin * ratio
self.capital += released_margin + pnl - fee
self.schedule_rebate(fee, timestamp)
if ratio >= 0.999:
self.position = 0
self.position_count = 0
self.total_margin = 0
self.entry_price = 0
self.mid_closed_half = False
self.clear_delay_reversal()
else:
self.position *= (1 - ratio)
self.total_margin *= (1 - ratio)
if abs(self.position) < 1e-12:
self.position = 0
self.position_count = 0
self.total_margin = 0
self.entry_price = 0
self.mid_closed_half = False
self.clear_delay_reversal()
self.trades.append({
'timestamp': timestamp,
'action': f'平仓{int(ratio*100)}%',
'price': price,
'size': close_size,
'pnl': pnl,
'fee': fee,
'rebate': self.get_rebate_amount(fee),
'capital': self.capital,
'reason': reason
})
logger.info(f"[{timestamp}] 平仓{int(ratio*100)}% @ {price:.2f} | "
f"盈亏: {pnl:.4f}U | 手续费: {fee:.4f}U | 返佣待到账: {self.get_rebate_amount(fee):.4f}U | "
f"可用资金: {self.capital:.4f}U | {reason}")
return True
def check_stop_loss(self, high, low):
"""检查当前收盘K是否触发止损信号"""
if self.position == 0:
return False
stop_price = low if self.position > 0 else high
if self.position > 0:
unrealized_pnl = abs(self.position) * (stop_price - self.entry_price)
else:
unrealized_pnl = abs(self.position) * (self.entry_price - stop_price)
return unrealized_pnl <= -self.total_margin * self.stop_loss_ratio
def run_backtest(self, start_date, end_date):
"""运行回测开仓当K触发平仓/止损仍按下一根K开盘执行"""
# 重置状态,支持同一实例重复回测
self.capital = self.initial_capital
self.position = 0
self.position_count = 0
self.entry_price = 0
self.total_margin = 0
self.mid_closed_half = False
self.trades = []
self.daily_pnl = []
self.pending_rebates = []
self.total_rebate_credited = 0.0
self.clear_delay_reversal()
logger.info(f"{'='*80}")
logger.info(f"开始回测: {start_date} ~ {end_date}")
logger.info(f"初始资金: {self.initial_capital}U | 杠杆: {self.leverage}x | BB({self.bb_period}, {self.bb_std})")
logger.info(f"{'='*80}")
# 从数据库加载数据
start_dt = pd.Timestamp(start_date)
end_dt = pd.Timestamp(end_date)
if isinstance(end_date, str) and len(end_date) <= 10:
end_dt = end_dt + pd.Timedelta(days=1) - pd.Timedelta(milliseconds=1)
self.current_run_label = f"{start_dt.strftime('%Y%m%d')}_{end_dt.strftime('%Y%m%d')}"
start_ts = int(start_dt.timestamp() * 1000)
end_ts = int(end_dt.timestamp() * 1000)
query = BitMartETH5M.select().where(
(BitMartETH5M.id >= start_ts) & (BitMartETH5M.id <= end_ts)
).order_by(BitMartETH5M.id)
data = []
for row in query:
data.append({
'timestamp': row.id,
'open': row.open,
'high': row.high,
'low': row.low,
'close': row.close
})
if not data:
logger.error("没有找到数据!")
return None
df = pd.DataFrame(data)
df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
logger.info(f"加载数据: {len(df)} 根K线")
logger.info(f"时间范围: {df['datetime'].min()} ~ {df['datetime'].max()}")
# 计算布林带
df = self.calculate_bollinger_bands(df)
if len(df) <= self.bb_period + 1:
logger.error("数据不足,无法执行回测")
return None
# 逐根K线回测开仓当K触发平仓/止损下一K开盘执行
for i in range(self.bb_period, len(df) - 1):
row = df.iloc[i]
prev_row = df.iloc[i-1] if i > 0 else None
next_row = df.iloc[i + 1]
signal_dt = row['datetime']
signal_ts = signal_dt.strftime('%Y-%m-%d %H:%M')
five_min_ts = int(row['timestamp'])
execute_ts = next_row['datetime'].strftime('%Y-%m-%d %H:%M')
next_open = float(next_row['open']) if pd.notna(next_row['open']) else None
high = row['high']
low = row['low']
upper = row['upper']
lower = row['lower']
middle = row['middle']
# 先处理当前收盘时刻返佣到账
self.apply_pending_rebates(signal_dt)
if pd.isna(upper) or pd.isna(lower) or pd.isna(middle):
continue
if next_open is None:
continue
# 检查止损收盘确认下一K开盘平仓
if self.check_stop_loss(high, low):
logger.warning(f"[{signal_ts}] 触发止损信号下一K开盘执行")
self.close_position(next_open, 1.0, execute_ts, f"止损-收盘确认({signal_ts})")
continue
# 已处于延迟反转状态时,先检查确认逻辑
if self.delay_reverse_price is not None:
reversal_signal = self.check_delay_reversal_signal(i, row, prev_row)
if reversal_signal is not None and self.position != 0:
new_direction, reason, reversal_price = reversal_signal
if reversal_price is None or pd.isna(reversal_price):
reversal_price = float(row['close'])
close_side = "" if self.position > 0 else ""
open_side = "" if new_direction == 'long' else ""
self.close_position(
reversal_price,
1.0,
signal_ts,
f"{reason}-当K确认({signal_ts})-平{close_side}"
)
open_price = self.apply_entry_slippage(reversal_price, new_direction)
self.open_position(
open_price,
new_direction,
signal_ts,
f"{reason}-当K确认({signal_ts})-开{open_side}"
)
self.mid_closed_half = False
self.clear_delay_reversal()
continue
# === 中轨平仓逻辑 ===
if self.position != 0:
had_mid_closed_half = self.mid_closed_half
if self.position > 0: # 多仓
# 回到开仓价全平+反手(仅在此前已平半的前提下触发)
if had_mid_closed_half and low <= self.entry_price:
self.close_position(next_open, 1.0, execute_ts, f"回开仓价全平-收盘确认({signal_ts})")
entry_price = self.apply_entry_slippage(next_open, 'short')
self.open_position(entry_price, 'short', execute_ts, f"回开仓价反手开空-收盘确认({signal_ts})")
self.mid_closed_half = False
continue
# 触中轨平半(收盘确认)
if not had_mid_closed_half and low <= middle <= high:
self.close_position(next_open, 0.5, execute_ts, f"触中轨平50%-收盘确认({signal_ts})")
self.mid_closed_half = True
continue
else: # 空仓
# 回到开仓价全平+反手(仅在此前已平半的前提下触发)
if had_mid_closed_half and high >= self.entry_price:
self.close_position(next_open, 1.0, execute_ts, f"回开仓价全平-收盘确认({signal_ts})")
entry_price = self.apply_entry_slippage(next_open, 'long')
self.open_position(entry_price, 'long', execute_ts, f"回开仓价反手开多-收盘确认({signal_ts})")
self.mid_closed_half = False
continue
# 触中轨平半(收盘确认)
if not had_mid_closed_half and low <= middle <= high:
self.close_position(next_open, 0.5, execute_ts, f"触中轨平50%-收盘确认({signal_ts})")
self.mid_closed_half = True
continue
# === 开仓与加仓逻辑 ===
if self.position == 0: # 空仓
self.clear_delay_reversal()
# 触上轨开空
if high >= upper:
entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper)
self.open_position(entry_price, 'short', signal_ts, f"触上轨开空-当K触发({signal_ts})")
# 触下轨开多
elif low <= lower:
entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower)
self.open_position(entry_price, 'long', signal_ts, f"触下轨开多-当K触发({signal_ts})")
continue
# 有持仓:先检查是否触发延迟反转(核心)
if self.position > 0 and high >= upper:
self.mark_delay_reversal('long_to_short', upper, i, signal_ts)
continue
elif self.position < 0 and low <= lower:
self.mark_delay_reversal('short_to_long', lower, i, signal_ts)
continue
# 进入延迟反转等待后,不再执行加仓
if self.delay_reverse_price is not None:
continue
# 同向加仓最多1次
if self.position_count == 1:
if self.position > 0 and low <= lower:
entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower)
self.open_position(entry_price, 'long', signal_ts, f"触下轨加多-当K触发({signal_ts})")
elif self.position < 0 and high >= upper:
entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper)
self.open_position(entry_price, 'short', signal_ts, f"触上轨加空-当K触发({signal_ts})")
# 回测末尾再处理一次返佣到账
self.apply_pending_rebates(df.iloc[-1]['datetime'])
# 最后平仓
if self.position != 0:
final_price = df.iloc[-1]['close']
final_time = df.iloc[-1]['datetime'].strftime('%Y-%m-%d %H:%M')
self.close_position(final_price, 1.0, final_time, "回测结束平仓")
# 生成报告
return self.generate_report(df)
def generate_report(self, df):
"""生成回测报告"""
logger.info(f"\n{'='*80}")
logger.info("回测报告")
logger.info(f"{'='*80}")
# 基本统计
total_trades = len([t for t in self.trades if '' in t['action']])
win_trades = len([t for t in self.trades if '' in t['action'] and t.get('pnl', 0) > 0])
loss_trades = len([t for t in self.trades if '' in t['action'] and t.get('pnl', 0) < 0])
total_pnl = sum([t.get('pnl', 0) for t in self.trades])
total_fee = sum([t.get('fee', 0) for t in self.trades])
total_rebate_expected = sum([t.get('rebate', 0) for t in self.trades if t.get('fee', 0) > 0])
pending_rebate = sum([x['amount'] for x in self.pending_rebates])
realized_net_fee = total_fee - self.total_rebate_credited
final_capital = self.capital
roi = (final_capital - self.initial_capital) / self.initial_capital * 100
logger.info(f"初始资金: {self.initial_capital:.2f}U")
logger.info(f"最终资金: {final_capital:.2f}U")
logger.info(f"总盈亏: {total_pnl:.2f}U")
logger.info(f"总手续费(开平全额): {total_fee:.2f}U")
logger.info(f"返佣应返总额: {total_rebate_expected:.2f}U")
logger.info(f"返佣已到账: {self.total_rebate_credited:.2f}U")
logger.info(f"返佣待到账: {pending_rebate:.2f}U")
logger.info(f"已实现净手续费: {realized_net_fee:.2f}U")
logger.info(f"净收益: {final_capital - self.initial_capital:.2f}U")
logger.info(f"收益率: {roi:.2f}%")
logger.info(f"总交易次数: {total_trades}")
logger.info(f"盈利次数: {win_trades}")
logger.info(f"亏损次数: {loss_trades}")
if win_trades + loss_trades > 0:
logger.info(f"胜率: {win_trades/(win_trades+loss_trades)*100:.2f}%")
# 保存交易记录
trades_df = pd.DataFrame(self.trades)
output_dir = Path(__file__).parent / 'backtest_outputs' / 'trades'
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f'bb_backtest_{self.current_run_label}_trades.csv'
trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
logger.info(f"\n交易记录已保存到: {output_file}")
return {
'initial_capital': self.initial_capital,
'final_capital': final_capital,
'total_pnl': total_pnl,
'total_fee': total_fee,
'total_rebate_expected': total_rebate_expected,
'total_rebate_credited': self.total_rebate_credited,
'pending_rebate': pending_rebate,
'realized_net_fee': realized_net_fee,
'roi': roi,
'total_trades': total_trades,
'win_trades': win_trades,
'loss_trades': loss_trades,
'trades_file': str(output_file),
'trades': self.trades
}
if __name__ == '__main__':
# 连接数据库
db.connect(reuse_if_open=True)
try:
# 创建回测实例
backtest = BollingerBandBacktest()
# 运行回测2026年全年
result = backtest.run_backtest('2026-01-01', '2026-12-31')
if result:
logger.success(f"\n回测完成!最终收益率: {result['roi']:.2f}%")
finally:
db.close()

626
bb_delay_reversal_trade.py Normal file
View File

@@ -0,0 +1,626 @@
"""
布林带均值回归策略 — 实盘交易 (D方案: 递增加仓)
BB(10, 2.5) | 5分钟K线 | ETH | 50x杠杆 逐仓 | 递增加仓+1%/次 max=3
逻辑:
- 价格触及上布林带 → 平多(如有) + 开空; 已持空则加仓
- 价格触及下布林带 → 平空(如有) + 开多; 已持多则加仓
- 始终持仓(多空翻转 + 同向加仓)
- 加仓比例: 开仓1%, 第1次加仓2%, 第2次3%, 第3次4%, 最多加仓3次
使用浏览器自动化进行开平仓有手续费返佣API仅用于查询数据
"""
import time
import numpy as np
from datetime import datetime, timezone
from pathlib import Path
from loguru import logger
from bitmart.api_contract import APIContract
from bit_tools import openBrowser
from DrissionPage import ChromiumPage, ChromiumOptions
# ---------------------------------------------------------------------------
# 配置
# ---------------------------------------------------------------------------
class BBDelayReversalConfig:
# API 凭证
API_KEY = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
SECRET_KEY = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
MEMO = "合约交易"
# 合约
CONTRACT_SYMBOL = "ETHUSDT"
TRADE_URL = "https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT"
# 浏览器
BIT_ID = "f2320f57e24c45529a009e1541e25961"
# 布林带参数
BB_PERIOD = 10 # 10根5分钟K线 = 50分钟回看
BB_STD = 2.5 # 标准差倍数
# 仓位管理
LEVERAGE = 50 # 杠杆倍数
OPEN_TYPE = "isolated" # 逐仓模式
MARGIN_PCT = 0.01 # 首次开仓用权益的1%作为保证金
# 递增加仓 (D方案)
PYRAMID_STEP = 0.01 # 每次加仓增加1%权益比例 (1%→2%→3%→4%)
PYRAMID_MAX = 3 # 最多加仓3次 (首次开仓不算)
# 风控
MAX_DAILY_LOSS = 50.0 # 日最大亏损(U),达到后停止交易
COOLDOWN_SECONDS = 30 # 两次交易之间最小间隔(秒)
# 运行
POLL_INTERVAL = 5 # 主循环轮询间隔(秒)
KLINE_STEP = 5 # K线周期(分钟)
KLINE_HOURS = 2 # 获取最近多少小时K线(需覆盖BB_PERIOD)
# ---------------------------------------------------------------------------
# 布林带计算
# ---------------------------------------------------------------------------
def calc_bollinger(closes: list, period: int, n_std: float):
"""计算布林带,返回 (mid, upper, lower) 或 None数据不足时"""
if len(closes) < period:
return None
arr = np.array(closes[-period:], dtype=float)
mid = arr.mean()
std = arr.std(ddof=0)
upper = mid + n_std * std
lower = mid - n_std * std
return mid, upper, lower
# ---------------------------------------------------------------------------
# 交易主类
# ---------------------------------------------------------------------------
class BBDelayReversalTrader:
def __init__(self, cfg: BBDelayReversalConfig = None, bit_id: str = None):
self.cfg = cfg or BBDelayReversalConfig()
if bit_id:
self.cfg.BIT_ID = bit_id
self.api = APIContract(
self.cfg.API_KEY, self.cfg.SECRET_KEY, self.cfg.MEMO,
timeout=(5, 15)
)
# 浏览器
self.page: ChromiumPage | None = None
self.page_start = True # 需要(重新)打开浏览器
self.last_page_open_time = 0.0 # 上次打开浏览器的时间
self.PAGE_REFRESH_INTERVAL = 1800 # 每30分钟关闭重开浏览器
# 持仓状态: -1=空, 0=无, 1=多
self.position = 0
self.open_avg_price = None
self.current_amount = None
# 加仓状态
self.pyramid_count = 0 # 当前已加仓次数 (0=仅首次开仓)
# 风控
self.daily_pnl = 0.0
self.daily_stopped = False
self.current_date = None
self.last_trade_time = 0.0
# 日志
self.log_dir = Path(__file__).resolve().parent
logger.add(
self.log_dir / "bb_delay_reversal_trade_{time:YYYY-MM-DD}.log",
rotation="1 day", retention="30 days",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
)
# ------------------------------------------------------------------
# API 封装
# ------------------------------------------------------------------
def get_klines(self) -> list | None:
"""获取最近N小时的5分钟K线返回 [{id, open, high, low, close}, ...]"""
try:
end_time = int(time.time())
start_time = end_time - 3600 * self.cfg.KLINE_HOURS
resp = self.api.get_kline(
contract_symbol=self.cfg.CONTRACT_SYMBOL,
step=self.cfg.KLINE_STEP,
start_time=start_time,
end_time=end_time
)[0]
if resp.get("code") != 1000:
logger.error(f"获取K线失败: {resp}")
return None
data = resp["data"]
klines = []
for k in data:
klines.append({
"id": int(k["timestamp"]),
"open": float(k["open_price"]),
"high": float(k["high_price"]),
"low": float(k["low_price"]),
"close": float(k["close_price"]),
})
klines.sort(key=lambda x: x["id"])
return klines
except Exception as e:
logger.error(f"获取K线异常: {e}")
return None
def get_current_price(self) -> float | None:
"""获取当前最新价格最近1分钟K线收盘价"""
try:
end_time = int(time.time())
resp = self.api.get_kline(
contract_symbol=self.cfg.CONTRACT_SYMBOL,
step=1,
start_time=end_time - 300,
end_time=end_time
)[0]
if resp.get("code") == 1000 and resp["data"]:
return float(resp["data"][-1]["close_price"])
return None
except Exception as e:
logger.error(f"获取价格异常: {e}")
return None
def get_balance(self) -> float | None:
"""获取合约账户可用余额"""
try:
resp = self.api.get_assets_detail()[0]
if resp.get("code") == 1000:
data = resp["data"]
if isinstance(data, dict):
return float(data.get("available_balance", 0))
elif isinstance(data, list):
for asset in data:
if asset.get("currency") == "USDT":
return float(asset.get("available_balance", 0))
return None
except Exception as e:
logger.error(f"查询余额异常: {e}")
return None
def get_position_status(self) -> bool:
"""查询当前持仓,更新 self.position / open_avg_price / current_amount"""
try:
resp = self.api.get_position(contract_symbol=self.cfg.CONTRACT_SYMBOL)[0]
if resp.get("code") != 1000:
logger.error(f"查询持仓失败: {resp}")
return False
positions = resp["data"]
if not positions:
self.position = 0
self.open_avg_price = None
self.current_amount = None
return True
pos = positions[0]
self.position = 1 if pos["position_type"] == 1 else -1
self.open_avg_price = float(pos["open_avg_price"])
self.current_amount = float(pos["current_amount"])
unrealized = float(pos.get("unrealized_value", 0))
logger.debug(f"持仓: dir={self.position} price={self.open_avg_price} "
f"amt={self.current_amount} upnl={unrealized:.2f}")
return True
except Exception as e:
logger.error(f"查询持仓异常: {e}")
return False
def set_leverage(self) -> bool:
"""设置杠杆和逐仓模式"""
try:
resp = self.api.post_submit_leverage(
contract_symbol=self.cfg.CONTRACT_SYMBOL,
leverage=str(self.cfg.LEVERAGE),
open_type=self.cfg.OPEN_TYPE
)[0]
if resp.get("code") == 1000:
logger.success(f"杠杆设置成功: {self.cfg.LEVERAGE}x {self.cfg.OPEN_TYPE}")
return True
else:
logger.error(f"杠杆设置失败: {resp}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
# ------------------------------------------------------------------
# 浏览器自动化
# ------------------------------------------------------------------
def open_browser(self) -> bool:
"""打开浏览器并进入交易页面"""
try:
bit_port = openBrowser(id=self.cfg.BIT_ID)
co = ChromiumOptions()
co.set_local_port(port=bit_port)
self.page = ChromiumPage(addr_or_opts=co)
self.last_page_open_time = time.time()
return True
except Exception as e:
logger.error(f"打开浏览器失败: {e}")
return False
def click_safe(self, xpath, sleep=0.5) -> bool:
"""安全点击元素"""
try:
ele = self.page.ele(xpath)
if not ele:
return False
ele.click(by_js=True)
time.sleep(sleep)
return True
except Exception as e:
logger.warning(f"点击失败 [{xpath}]: {e}")
return False
def browser_close_position(self) -> bool:
"""浏览器点击市价全平"""
logger.info("浏览器操作: 市价平仓")
return self.click_safe('x://span[normalize-space(text()) ="市价"]')
# ------------------------------------------------------------------
# 仓位操作
# ------------------------------------------------------------------
def calc_order_usdt(self, is_add: bool = False) -> float:
"""
计算开仓/加仓金额(U)
首次开仓: 余额 × MARGIN_PCT (1%)
加仓: 余额 × (MARGIN_PCT + PYRAMID_STEP × (pyramid_count+1))
例: 开仓1%, 第1次加仓2%, 第2次加仓3%, 第3次加仓4%
"""
balance = self.get_balance()
if balance is None or balance <= 0:
logger.warning(f"余额不足或查询失败: {balance}")
return 0
if is_add:
pct = self.cfg.MARGIN_PCT + self.cfg.PYRAMID_STEP * (self.pyramid_count + 1)
else:
pct = self.cfg.MARGIN_PCT
order_usdt = round(balance * pct, 2)
logger.info(f"仓位计算: 余额={balance:.2f} × {pct:.0%} = {order_usdt} U"
f" ({'加仓#' + str(self.pyramid_count+1) if is_add else '首次开仓'})")
return order_usdt
def verify_position(self, expected: int) -> bool:
"""验证持仓方向"""
if self.get_position_status():
if self.position == expected:
return True
logger.warning(f"持仓方向不符: 期望{expected}, 实际{self.position}")
return False
# ------------------------------------------------------------------
# 风控
# ------------------------------------------------------------------
def check_daily_reset(self):
"""每日重置(UTC+8 00:00 = UTC 16:00)"""
now = datetime.now(timezone.utc)
# 用UTC日期做简单日切
today = now.date()
if self.current_date != today:
if self.current_date is not None:
logger.info(f"日切: {self.current_date}{today}, 日PnL={self.daily_pnl:.2f}")
self.current_date = today
self.daily_pnl = 0.0
self.daily_stopped = False
def can_trade(self) -> bool:
"""检查是否可交易"""
if self.daily_stopped:
return False
now = time.time()
if now - self.last_trade_time < self.cfg.COOLDOWN_SECONDS:
remain = self.cfg.COOLDOWN_SECONDS - (now - self.last_trade_time)
logger.debug(f"交易冷却中,剩余 {remain:.0f}s")
return False
return True
# ------------------------------------------------------------------
# 日志
# ------------------------------------------------------------------
def write_trade_log(self, action: str, price: float, bb_upper: float,
bb_mid: float, bb_lower: float, reason: str):
"""写入交易日志文件"""
try:
date_str = datetime.now().strftime("%Y%m%d")
log_file = self.log_dir / f"bb_delay_reversal_trade_log_{date_str}.txt"
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
block = (
f"\n{'='*60}\n"
f"时间: {time_str}\n"
f"操作: {action}\n"
f"价格: {price:.2f}\n"
f"BB上轨: {bb_upper:.2f} | 中轨: {bb_mid:.2f} | 下轨: {bb_lower:.2f}\n"
f"原因: {reason}\n"
f"{'='*60}\n"
)
with open(log_file, "a", encoding="utf-8") as f:
f.write(block)
except Exception as e:
logger.warning(f"写入日志失败: {e}")
def login(self):
self.page.ele('x://input[@placeholder="邮箱"]').input("ddrwode@gmail.com")
self.page.ele('x://input[@placeholder="密码"]').input("040828cjj")
self.page.ele('x://*[@id="__layout"]/div/div[2]/div/div[2]/div/div/div[2]/div[1]/div[2]/div/div[1]/div[2]/form/div[3]/div/button').click()
# ------------------------------------------------------------------
# 主循环(浏览器流程与四分之一代码一致)
# ------------------------------------------------------------------
def run(self):
"""策略主循环"""
logger.info("=" * 60)
logger.info(f" BB策略启动(D方案): BB({self.cfg.BB_PERIOD},{self.cfg.BB_STD})")
logger.info(f" 合约: {self.cfg.CONTRACT_SYMBOL} | {self.cfg.LEVERAGE}x {self.cfg.OPEN_TYPE}")
logger.info(f" 首次开仓: 权益×{self.cfg.MARGIN_PCT:.0%} | 递增加仓: +{self.cfg.PYRAMID_STEP:.0%}/次 | 最多{self.cfg.PYRAMID_MAX}")
logger.info("=" * 60)
# 设置杠杆
if not self.set_leverage():
logger.error("杠杆设置失败,退出")
return
# 初始持仓同步
if not self.get_position_status():
logger.error("初始持仓查询失败,退出")
return
logger.info(f"初始持仓状态: {self.position}")
last_kline_id = None # 避免同一根K线重复触发
page_start = True # 需要打开浏览器
while True:
# ===== 浏览器管理 =====
# page_start时: 打开浏览器 → 导航 → 点市价 → 输入张数
if page_start:
for i in range(5):
if self.open_browser():
logger.info("浏览器打开成功")
break
else:
logger.error("打开浏览器失败!")
return
# self.login()
self.page.get(self.cfg.TRADE_URL)
time.sleep(2)
# 点击市价模式
self.click_safe('x://button[normalize-space(text()) ="市价"]')
time.sleep(0.5)
# 计算并预输入开仓金额(U)
current_price = self.get_current_price()
if current_price:
order_usdt = self.calc_order_usdt()
if order_usdt > 0:
self.page.ele('x://*[@id="size_0"]').input(vals=order_usdt, clear=True)
logger.info(f"预输入开仓金额: {order_usdt} U")
page_start = False
try:
# 每30分钟关闭浏览器重新打开
if time.time() - self.last_page_open_time >= self.PAGE_REFRESH_INTERVAL:
logger.info("浏览器已打开超过30分钟关闭刷新")
try:
self.page.close()
except Exception:
pass
self.page = None
page_start = True
time.sleep(3)
continue
self.check_daily_reset()
if self.daily_stopped:
logger.info(f"日亏损已达限制({self.daily_pnl:.2f}),等待日切")
time.sleep(60)
continue
# 1. 获取K线
klines = self.get_klines()
if not klines or len(klines) < self.cfg.BB_PERIOD:
logger.warning(f"K线数据不足({len(klines) if klines else 0}根),等待...")
time.sleep(self.cfg.POLL_INTERVAL)
continue
closed_klines = klines[:-1]
current_kline = klines[-1]
if len(closed_klines) < self.cfg.BB_PERIOD:
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 2. 计算布林带
closes = [k["close"] for k in closed_klines]
bb = calc_bollinger(closes, self.cfg.BB_PERIOD, self.cfg.BB_STD)
if bb is None:
time.sleep(self.cfg.POLL_INTERVAL)
continue
bb_mid, bb_upper, bb_lower = bb
# 3. 获取当前价格
current_price = self.get_current_price()
if current_price is None:
time.sleep(self.cfg.POLL_INTERVAL)
continue
cur_high = current_kline["high"]
cur_low = current_kline["low"]
# 容错: K线high/low + 当前实时价格,任一触及即算触碰
touched_upper = cur_high >= bb_upper or current_price >= bb_upper
touched_lower = cur_low <= bb_lower or current_price <= bb_lower
logger.info(
f"价格={current_price:.2f} | "
f"BB: {bb_lower:.2f} / {bb_mid:.2f} / {bb_upper:.2f} | "
f"H={cur_high:.2f} L={cur_low:.2f} | "
f"触上={touched_upper} 触下={touched_lower} | "
f"仓位={self.position}"
)
# 4. 同步持仓状态
if not self.get_position_status():
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 5. 信号判断
kline_id = current_kline["id"]
if kline_id == last_kline_id:
time.sleep(self.cfg.POLL_INTERVAL)
continue
if touched_upper and touched_lower:
logger.warning("同时触及上下轨,跳过")
time.sleep(self.cfg.POLL_INTERVAL)
continue
action = None
reason = ""
success = False
# ===== 触及上轨 → 开空 / 翻转为空 / 加仓空 =====
if touched_upper:
if not self.can_trade():
time.sleep(self.cfg.POLL_INTERVAL)
continue
reason = (f"价格最高{cur_high:.2f}触及上轨{bb_upper:.2f}"
f"BB({self.cfg.BB_PERIOD},{self.cfg.BB_STD})")
if self.position == 1:
action = "翻转: 平多→开空"
# 在当前页面点市价平仓
self.browser_close_position()
time.sleep(1)
# 等待确认平仓
for _ in range(10):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
if self.position != 0:
logger.warning(f"平仓后仍有持仓({self.position}),放弃开空")
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 翻转时重置加仓计数
self.pyramid_count = 0
# 平仓后在同一页面直接点卖出/做空
logger.info("平仓完成,直接开空")
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3)
if self.verify_position(-1):
success = True
elif self.position == 0:
action = "开空"
self.pyramid_count = 0
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3)
if self.verify_position(-1):
success = True
elif self.position == -1 and self.pyramid_count < self.cfg.PYRAMID_MAX:
# 已持空仓 + 再次触上轨 → 加仓做空
action = f"加仓空#{self.pyramid_count+1}"
reason += f" (加仓#{self.pyramid_count+1}/{self.cfg.PYRAMID_MAX})"
# 重新计算加仓金额并输入
add_usdt = self.calc_order_usdt(is_add=True)
if add_usdt > 0:
self.page.ele('x://*[@id="size_0"]').input(vals=add_usdt, clear=True)
time.sleep(0.5)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3)
if self.verify_position(-1):
self.pyramid_count += 1
success = True
else:
logger.info(f"已持空仓,加仓已达上限({self.pyramid_count}/{self.cfg.PYRAMID_MAX})")
# ===== 触及下轨 → 开多 / 翻转为多 / 加仓多 =====
elif touched_lower:
if not self.can_trade():
time.sleep(self.cfg.POLL_INTERVAL)
continue
reason = (f"价格最低{cur_low:.2f}触及下轨{bb_lower:.2f}"
f"BB({self.cfg.BB_PERIOD},{self.cfg.BB_STD})")
if self.position == -1:
action = "翻转: 平空→开多"
self.browser_close_position()
time.sleep(1)
for _ in range(10):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
if self.position != 0:
logger.warning(f"平仓后仍有持仓({self.position}),放弃开多")
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 翻转时重置加仓计数
self.pyramid_count = 0
logger.info("平仓完成,直接开多")
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3)
if self.verify_position(1):
success = True
elif self.position == 0:
action = "开多"
self.pyramid_count = 0
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3)
if self.verify_position(1):
success = True
elif self.position == 1 and self.pyramid_count < self.cfg.PYRAMID_MAX:
# 已持多仓 + 再次触下轨 → 加仓做多
action = f"加仓多#{self.pyramid_count+1}"
reason += f" (加仓#{self.pyramid_count+1}/{self.cfg.PYRAMID_MAX})"
# 重新计算加仓金额并输入
add_usdt = self.calc_order_usdt(is_add=True)
if add_usdt > 0:
self.page.ele('x://*[@id="size_0"]').input(vals=add_usdt, clear=True)
time.sleep(0.5)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3)
if self.verify_position(1):
self.pyramid_count += 1
success = True
else:
logger.info(f"已持多仓,加仓已达上限({self.pyramid_count}/{self.cfg.PYRAMID_MAX})")
# ===== 交易成功后处理 =====
if success and action:
last_kline_id = kline_id
self.last_trade_time = time.time()
self.write_trade_log(action, current_price,
bb_upper, bb_mid, bb_lower, reason)
logger.success(f"{action} 执行成功")
# 交易完成后关闭浏览器,下轮重新打开
page_start = True
try:
self.page.close()
except Exception:
pass
self.page = None
time.sleep(5)
time.sleep(self.cfg.POLL_INTERVAL)
except KeyboardInterrupt:
logger.info("用户中断,程序退出")
break
except Exception as e:
logger.error(f"主循环异常: {e}")
page_start = True
time.sleep(10)
# ---------------------------------------------------------------------------
# 入口
# ---------------------------------------------------------------------------
if __name__ == "__main__":
trader = BBDelayReversalTrader()
trader.run()

189789
bb_sweep_results.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,24 @@
"""
布林带均值回归策略 — 实盘交易
BB(10, 2.5) | 5分钟K线 | ETH | 50x杠杆 | 每单权益1%
布林带均值回归策略 — 实盘交易 (D方案: 递增加仓)
BB(10, 2.5) | 5分钟K线 | ETH | 50x杠杆 逐仓 | 递增加仓+1%/次 max=3
逻辑:
- 价格触及上布林带 → 平多(如有) + 开空
- 价格触及下布林带 → 平空(如有) + 开多
- 始终持仓(多空翻转)
- 价格触及上布林带 → 平多(如有) + 开空; 已持空则加仓
- 价格触及下布林带 → 平空(如有) + 开多; 已持多则加仓
- 始终持仓(多空翻转 + 同向加仓
- 加仓比例: 开仓1%, 第1次加仓2%, 第2次3%, 第3次4%, 最多加仓3次
使用 BitMart Futures API 进行开平仓
使用浏览器自动化进行开平仓有手续费返佣API仅用于查询数据
"""
import time
import uuid
import numpy as np
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from loguru import logger
from bitmart.api_contract import APIContract
from bit_tools import openBrowser
from DrissionPage import ChromiumPage, ChromiumOptions
# ---------------------------------------------------------------------------
@@ -30,6 +32,10 @@ class BBTradeConfig:
# 合约
CONTRACT_SYMBOL = "ETHUSDT"
TRADE_URL = "https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT"
# 浏览器
BIT_ID = "62f9107d0c674925972084e282df55b3"
# 布林带参数
BB_PERIOD = 10 # 10根5分钟K线 = 50分钟回看
@@ -37,8 +43,12 @@ class BBTradeConfig:
# 仓位管理
LEVERAGE = 50 # 杠杆倍数
OPEN_TYPE = "cross" # 仓模式
MARGIN_PCT = 0.01 # 每单用权益的1%作为保证金
OPEN_TYPE = "isolated" # 仓模式
MARGIN_PCT = 0.01 # 首次开仓用权益的1%作为保证金
# 递增加仓 (D方案)
PYRAMID_STEP = 0.01 # 每次加仓增加1%权益比例 (1%→2%→3%→4%)
PYRAMID_MAX = 3 # 最多加仓3次 (首次开仓不算)
# 风控
MAX_DAILY_LOSS = 50.0 # 日最大亏损(U),达到后停止交易
@@ -76,11 +86,20 @@ class BBTrader:
timeout=(5, 15)
)
# 浏览器
self.page: ChromiumPage | None = None
self.page_start = True # 需要(重新)打开浏览器
self.last_page_open_time = 0.0 # 上次打开浏览器的时间
self.PAGE_REFRESH_INTERVAL = 1800 # 每30分钟关闭重开浏览器
# 持仓状态: -1=空, 0=无, 1=多
self.position = 0
self.open_avg_price = None
self.current_amount = None
# 加仓状态
self.pyramid_count = 0 # 当前已加仓次数 (0=仅首次开仓)
# 风控
self.daily_pnl = 0.0
self.daily_stopped = False
@@ -188,7 +207,7 @@ class BBTrader:
return False
def set_leverage(self) -> bool:
"""设置杠杆和仓模式"""
"""设置杠杆和仓模式"""
try:
resp = self.api.post_submit_leverage(
contract_symbol=self.cfg.CONTRACT_SYMBOL,
@@ -205,133 +224,77 @@ class BBTrader:
logger.error(f"设置杠杆异常: {e}")
return False
def _gen_client_order_id(self) -> str:
return f"BB_{uuid.uuid4().hex[:12]}"
def submit_order(self, side: int, size: int) -> bool:
"""
提交市价单
side: 1=买入开多, 2=买入平空, 3=卖出平多, 4=卖出开空
size: 张数
"""
side_names = {1: "买入开多", 2: "买入平空", 3: "卖出平多", 4: "卖出开空"}
logger.info(f"下单: {side_names.get(side, side)} {size}")
# ------------------------------------------------------------------
# 浏览器自动化
# ------------------------------------------------------------------
def open_browser(self) -> bool:
"""打开浏览器并进入交易页面"""
try:
resp = self.api.post_submit_order(
contract_symbol=self.cfg.CONTRACT_SYMBOL,
client_order_id=self._gen_client_order_id(),
side=side,
mode=1, # GTC
type="market",
leverage=str(self.cfg.LEVERAGE),
open_type=self.cfg.OPEN_TYPE,
size=size,
)[0]
if resp.get("code") == 1000:
logger.success(f"下单成功: {side_names.get(side)} {size}张 resp={resp}")
return True
else:
logger.error(f"下单失败: {resp}")
return False
bit_port = openBrowser(id=self.cfg.BIT_ID)
co = ChromiumOptions()
co.set_local_port(port=bit_port)
self.page = ChromiumPage(addr_or_opts=co)
self.last_page_open_time = time.time()
return True
except Exception as e:
logger.error(f"下单异常: {e}")
logger.error(f"打开浏览器失败: {e}")
return False
def click_safe(self, xpath, sleep=0.5) -> bool:
"""安全点击元素"""
try:
ele = self.page.ele(xpath)
if not ele:
return False
ele.click(by_js=True)
time.sleep(sleep)
return True
except Exception as e:
logger.warning(f"点击失败 [{xpath}]: {e}")
return False
def browser_close_position(self) -> bool:
"""浏览器点击市价全平"""
logger.info("浏览器操作: 市价平仓")
return self.click_safe('x://span[normalize-space(text()) ="市价"]')
# ------------------------------------------------------------------
# 仓位操作
# ------------------------------------------------------------------
def calc_order_size(self, price: float) -> int:
def calc_order_usdt(self, is_add: bool = False) -> float:
"""
根据当前权益的1%计算开仓张数
BitMart ETH合约: 1张 = 0.01 ETH
保证金 = equity * margin_pct
名义价值 = margin * leverage
数量(ETH) = 名义价值 / price
张数 = 数量 / 0.01
计算开仓/加仓金额(U)
首次开仓: 余额 × MARGIN_PCT (1%)
加仓: 余额 × (MARGIN_PCT + PYRAMID_STEP × (pyramid_count+1))
例: 开仓1%, 第1次加仓2%, 第2次加仓3%, 第3次加仓4%
"""
balance = self.get_balance()
if balance is None or balance <= 0:
logger.warning(f"余额不足或查询失败: {balance}")
return 0
margin = balance * self.cfg.MARGIN_PCT
notional = margin * self.cfg.LEVERAGE
qty_eth = notional / price
size = max(1, int(qty_eth / 0.01)) # 1张=0.01ETH
logger.info(f"仓位计算: 余额={balance:.2f} 保证金={margin:.2f} "
f"名义={notional:.2f} 数量={qty_eth:.4f}ETH = {size}")
return size
def close_current_position(self) -> bool:
"""平掉当前持仓"""
if not self.get_position_status():
return False
if self.position == 0:
logger.info("无持仓,无需平仓")
return True
if self.position == 1:
# 平多: side=3
size = int(self.current_amount)
return self.submit_order(side=3, size=size)
if is_add:
pct = self.cfg.MARGIN_PCT + self.cfg.PYRAMID_STEP * (self.pyramid_count + 1)
else:
# 平空: side=2
size = int(self.current_amount)
return self.submit_order(side=2, size=size)
pct = self.cfg.MARGIN_PCT
order_usdt = round(balance * pct, 2)
logger.info(f"仓位计算: 余额={balance:.2f} × {pct:.0%} = {order_usdt} U"
f" ({'加仓#' + str(self.pyramid_count+1) if is_add else '首次开仓'})")
return order_usdt
def open_long(self, price: float) -> bool:
"""开多"""
size = self.calc_order_size(price)
if size <= 0:
return False
return self.submit_order(side=1, size=size)
def open_short(self, price: float) -> bool:
"""开空"""
size = self.calc_order_size(price)
if size <= 0:
return False
return self.submit_order(side=4, size=size)
def flip_to_long(self, price: float) -> bool:
"""平空 → 开多"""
logger.info("=== 翻转为多 ===")
if self.position == -1:
if not self.close_current_position():
logger.error("平空失败,放弃开多")
return False
time.sleep(2)
# 确认已无仓
for _ in range(5):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
if self.position != 0:
logger.warning(f"平仓后仍有持仓({self.position}),放弃开多")
return False
return self.open_long(price)
def flip_to_short(self, price: float) -> bool:
"""平多 → 开空"""
logger.info("=== 翻转为空 ===")
if self.position == 1:
if not self.close_current_position():
logger.error("平多失败,放弃开空")
return False
time.sleep(2)
for _ in range(5):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
if self.position != 0:
logger.warning(f"平仓后仍有持仓({self.position}),放弃开空")
return False
return self.open_short(price)
def verify_position(self, expected: int) -> bool:
"""验证持仓方向"""
if self.get_position_status():
if self.position == expected:
return True
logger.warning(f"持仓方向不符: 期望{expected}, 实际{self.position}")
return False
# ------------------------------------------------------------------
# 风控
# ------------------------------------------------------------------
def check_daily_reset(self):
"""每日重置(UTC+8 00:00 = UTC 16:00)"""
now = datetime.utcnow()
now = datetime.now(timezone.utc)
# 用UTC日期做简单日切
today = now.date()
if self.current_date != today:
@@ -376,15 +339,20 @@ class BBTrader:
except Exception as e:
logger.warning(f"写入日志失败: {e}")
def login(self):
self.page.ele('x://input[@placeholder="邮箱"]').input("ddrwode@gmail.com")
self.page.ele('x://input[@placeholder="密码"]').input("040828cjj")
self.page.ele('x://*[@id="__layout"]/div/div[2]/div/div[2]/div/div/div[2]/div[1]/div[2]/div/div[1]/div[2]/form/div[3]/div/button').click()
# ------------------------------------------------------------------
# 主循环
# 主循环(浏览器流程与四分之一代码一致)
# ------------------------------------------------------------------
def run(self):
"""策略主循环"""
logger.info("=" * 60)
logger.info(f" BB策略启动: BB({self.cfg.BB_PERIOD},{self.cfg.BB_STD})")
logger.info(f" BB策略启动(D方案): BB({self.cfg.BB_PERIOD},{self.cfg.BB_STD})")
logger.info(f" 合约: {self.cfg.CONTRACT_SYMBOL} | {self.cfg.LEVERAGE}x {self.cfg.OPEN_TYPE}")
logger.info(f" 每单: 权益×{self.cfg.MARGIN_PCT:.0%}")
logger.info(f" 首次开仓: 权益×{self.cfg.MARGIN_PCT:.0%} | 递增加仓: +{self.cfg.PYRAMID_STEP:.0%}/次 | 最多{self.cfg.PYRAMID_MAX}")
logger.info("=" * 60)
# 设置杠杆
@@ -399,9 +367,52 @@ class BBTrader:
logger.info(f"初始持仓状态: {self.position}")
last_kline_id = None # 避免同一根K线重复触发
page_start = True # 需要打开浏览器
while True:
# ===== 浏览器管理 =====
# page_start时: 打开浏览器 → 导航 → 点市价 → 输入张数
if page_start:
for i in range(5):
if self.open_browser():
logger.info("浏览器打开成功")
break
else:
logger.error("打开浏览器失败!")
return
# self.login()
self.page.get(self.cfg.TRADE_URL)
time.sleep(2)
# 点击市价模式
self.click_safe('x://button[normalize-space(text()) ="市价"]')
time.sleep(0.5)
# 计算并预输入开仓金额(U)
current_price = self.get_current_price()
if current_price:
order_usdt = self.calc_order_usdt()
if order_usdt > 0:
self.page.ele('x://*[@id="size_0"]').input(vals=order_usdt, clear=True)
logger.info(f"预输入开仓金额: {order_usdt} U")
page_start = False
try:
# 每30分钟关闭浏览器重新打开
if time.time() - self.last_page_open_time >= self.PAGE_REFRESH_INTERVAL:
logger.info("浏览器已打开超过30分钟关闭刷新")
try:
self.page.close()
except Exception:
pass
self.page = None
page_start = True
time.sleep(3)
continue
self.check_daily_reset()
if self.daily_stopped:
@@ -416,10 +427,8 @@ class BBTrader:
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 当前K线 = 最后一根未收盘信号用已收盘的K线
# 使用倒数第二根及之前的收盘价算BB已收盘的K线
closed_klines = klines[:-1] # 已收盘的K线
current_kline = klines[-1] # 当前未收盘K线
closed_klines = klines[:-1]
current_kline = klines[-1]
if len(closed_klines) < self.cfg.BB_PERIOD:
time.sleep(self.cfg.POLL_INTERVAL)
@@ -439,11 +448,11 @@ class BBTrader:
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 用当前K线的 high/low 判断是否触及布林带
cur_high = current_kline["high"]
cur_low = current_kline["low"]
touched_upper = cur_high >= bb_upper
touched_lower = cur_low <= bb_lower
# 容错: K线high/low + 当前实时价格,任一触及即算触碰
touched_upper = cur_high >= bb_upper or current_price >= bb_upper
touched_lower = cur_low <= bb_lower or current_price <= bb_lower
logger.info(
f"价格={current_price:.2f} | "
@@ -458,15 +467,12 @@ class BBTrader:
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 5. 信号判断 + 执行
# 同一根K线只触发一次
# 5. 信号判断
kline_id = current_kline["id"]
if kline_id == last_kline_id:
# 已在这根K线触发过不重复操作
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 同时触及上下轨(极端波动)→ 跳过
if touched_upper and touched_lower:
logger.warning("同时触及上下轨,跳过")
time.sleep(self.cfg.POLL_INTERVAL)
@@ -474,9 +480,10 @@ class BBTrader:
action = None
reason = ""
success = False
# ===== 触及上轨 → 开空 / 翻转为空 / 加仓空 =====
if touched_upper:
# 触及上轨 → 开空 / 翻转为空
if not self.can_trade():
time.sleep(self.cfg.POLL_INTERVAL)
continue
@@ -486,24 +493,52 @@ class BBTrader:
if self.position == 1:
action = "翻转: 平多→开空"
success = self.flip_to_short(current_price)
# 在当前页面点市价平仓
self.browser_close_position()
time.sleep(1)
# 等待确认平仓
for _ in range(10):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
if self.position != 0:
logger.warning(f"平仓后仍有持仓({self.position}),放弃开空")
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 翻转时重置加仓计数
self.pyramid_count = 0
# 平仓后在同一页面直接点卖出/做空
logger.info("平仓完成,直接开空")
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3)
if self.verify_position(-1):
success = True
elif self.position == 0:
action = "开空"
success = self.open_short(current_price)
self.pyramid_count = 0
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3)
if self.verify_position(-1):
success = True
elif self.position == -1 and self.pyramid_count < self.cfg.PYRAMID_MAX:
# 已持空仓 + 再次触上轨 → 加仓做空
action = f"加仓空#{self.pyramid_count+1}"
reason += f" (加仓#{self.pyramid_count+1}/{self.cfg.PYRAMID_MAX})"
# 重新计算加仓金额并输入
add_usdt = self.calc_order_usdt(is_add=True)
if add_usdt > 0:
self.page.ele('x://*[@id="size_0"]').input(vals=add_usdt, clear=True)
time.sleep(0.5)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
time.sleep(3)
if self.verify_position(-1):
self.pyramid_count += 1
success = True
else:
# 已经是空仓,不操作
logger.info("已持空仓,触上轨无需操作")
success = False
if success:
last_kline_id = kline_id
self.last_trade_time = time.time()
self.write_trade_log(action, current_price,
bb_upper, bb_mid, bb_lower, reason)
logger.success(f"{action} 执行成功")
logger.info(f"已持空仓,加仓已达上限({self.pyramid_count}/{self.cfg.PYRAMID_MAX})")
# ===== 触及下轨 → 开多 / 翻转为多 / 加仓多 =====
elif touched_lower:
# 触及下轨 → 开多 / 翻转为多
if not self.can_trade():
time.sleep(self.cfg.POLL_INTERVAL)
continue
@@ -513,20 +548,62 @@ class BBTrader:
if self.position == -1:
action = "翻转: 平空→开多"
success = self.flip_to_long(current_price)
self.browser_close_position()
time.sleep(1)
for _ in range(10):
if self.get_position_status() and self.position == 0:
break
time.sleep(1)
if self.position != 0:
logger.warning(f"平仓后仍有持仓({self.position}),放弃开多")
time.sleep(self.cfg.POLL_INTERVAL)
continue
# 翻转时重置加仓计数
self.pyramid_count = 0
logger.info("平仓完成,直接开多")
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3)
if self.verify_position(1):
success = True
elif self.position == 0:
action = "开多"
success = self.open_long(current_price)
self.pyramid_count = 0
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3)
if self.verify_position(1):
success = True
elif self.position == 1 and self.pyramid_count < self.cfg.PYRAMID_MAX:
# 已持多仓 + 再次触下轨 → 加仓做多
action = f"加仓多#{self.pyramid_count+1}"
reason += f" (加仓#{self.pyramid_count+1}/{self.cfg.PYRAMID_MAX})"
# 重新计算加仓金额并输入
add_usdt = self.calc_order_usdt(is_add=True)
if add_usdt > 0:
self.page.ele('x://*[@id="size_0"]').input(vals=add_usdt, clear=True)
time.sleep(0.5)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
time.sleep(3)
if self.verify_position(1):
self.pyramid_count += 1
success = True
else:
logger.info("已持多仓,触下轨无需操作")
success = False
logger.info(f"已持多仓,加仓已达上限({self.pyramid_count}/{self.cfg.PYRAMID_MAX})")
if success:
last_kline_id = kline_id
self.last_trade_time = time.time()
self.write_trade_log(action, current_price,
bb_upper, bb_mid, bb_lower, reason)
logger.success(f"{action} 执行成功")
# ===== 交易成功后处理 =====
if success and action:
last_kline_id = kline_id
self.last_trade_time = time.time()
self.write_trade_log(action, current_price,
bb_upper, bb_mid, bb_lower, reason)
logger.success(f"{action} 执行成功")
# 交易完成后关闭浏览器,下轮重新打开
page_start = True
try:
self.page.close()
except Exception:
pass
self.page = None
time.sleep(5)
time.sleep(self.cfg.POLL_INTERVAL)
@@ -535,6 +612,7 @@ class BBTrader:
break
except Exception as e:
logger.error(f"主循环异常: {e}")
page_start = True
time.sleep(10)

262
bit_tools.py Normal file
View File

@@ -0,0 +1,262 @@
import json
import random
import requests
from peewee import fn
from models.ips import Ips
from models.xstart import Xstart
from models.xtoken import XToken
url = "http://127.0.0.1:54345"
headers = {'Content-Type': 'application/json'}
tg_url = "https://web.telegram.org/a/"
def createBrowser(
groupId=None,
host=None,
port=None,
proxyUserName=None,
proxyPassword=None,
name='google',
proxyType="socks5"
): # 创建或者更新窗口,指纹参数 browserFingerPrint 如没有特定需求,只需要指定下内核即可,如果需要更详细的参数,请参考文档
json_data = {
"groupId": groupId, # 分组id
'name': name, # 窗口名称
'remark': '', # 备注
'proxyMethod': 1, # 代理方式 2自定义 3 提取IP
# 代理类型 ['noproxy', 'http', 'https', 'socks5', 'ssh']
'proxyType': proxyType,
'host': host, # 代理主机EE
'port': port, # 代理端口
'proxyUserName': proxyUserName, # 代理账号
'proxyPassword': proxyPassword, # 代理账号
"browserFingerPrint": { # 指纹对象
'coreVersion': '138' # 内核版本注意win7/win8/winserver 2012 已经不支持112及以上内核了无法打开
}
}
res = requests.post(f"{url}/browser/update",
data=json.dumps(json_data), headers=headers).json()
print(res)
browserId = res['data']['id']
return browserId
def updateBrowser(): # 更新窗口支持批量更新和按需更新ids 传入数组单独更新只传一个id即可只传入需要修改的字段即可比如修改备注具体字段请参考文档browserFingerPrint指纹对象不修改则无需传入
json_data = {'ids': ['93672cf112a044f08b653cab691216f0'],
'remark': '我是一个备注', 'browserFingerPrint': {}}
res = requests.post(f"{url}/browser/update/partial",
data=json.dumps(json_data), headers=headers).json()
print(res)
def openBrowser(id): # 直接指定ID打开窗口也可以使用 createBrowser 方法返回的ID
json_data = {"id": f'{id}', "args": [
# "--disable-application-cache",
# "--disable-cache",
# "--disable-gpu-shader-disk-cache",
# "--media-cache-size=1",
# "--disk-cache-size=1",
# "--incognito"
]}
res = requests.post(f"{url}/browser/open",
data=json.dumps(json_data), headers=headers).json()
print(res)
return res["data"]["http"].split(":")[1]
def closeBrowser(id): # 关闭窗口
json_data = {'id': f'{id}'}
res = requests.post(f"{url}/browser/close",
data=json.dumps(json_data), headers=headers)
return res.json()
def deleteBrowser(id): # 删除窗口
json_data = {'id': f'{id}'}
print(requests.post(f"{url}/browser/delete",
data=json.dumps(json_data), headers=headers).json())
def query_bit_browser(page, page_size):
data = {"page": page, "pageSize": page_size, 'sort': 'asc'}
res = requests.post(f'{url}/browser/list', data=json.dumps(data), headers=headers)
return res.json()["data"]["list"]
def update_proxy_Browser(
id,
host,
port,
proxyType="socks5",
proxyUserName="",
proxyPassword=""
):
json_data = {
"ids": [id],
# "ipCheckService": "ip123in",
"proxyMethod": 2,
"proxyType": proxyType,
"host": host,
"port": port,
"proxyUserName": proxyUserName,
"proxyPassword": proxyPassword
}
res = requests.post(f'{url}/browser/proxy/update', data=json.dumps(json_data), headers=headers)
print(res.json())
return res.json()
def get_group_lists_Browser():
json_data = {
"page": 0,
"pageSize": 100,
"all": True
}
res = requests.post(f'{url}/group/list', data=json.dumps(json_data), headers=headers)
return res.json()["data"]["list"]
def get_browser_lists_Browser(id, page=0):
json_data = {
"groupId": id,
"page": page,
"pageSize": 100
}
res = requests.post(f'{url}/browser/list', data=json.dumps(json_data), headers=headers)
return res.json()["data"]["list"]
def get_group_lists(): # 获取全部分组的信息
# url = "/group/list"
json_data = {
"page": 0,
"pageSize": 100,
"all": True
}
res = requests.post(f'{url}/group/list', data=json.dumps(json_data), headers=headers)
data = {}
for i in res.json()["data"]["list"]:
data[i["groupName"]] = i["id"]
return data
def group_add(groupName):
json_data = {
"groupName": groupName,
"sortNum": 0
}
res = requests.post(f'{url}/group/add', data=json.dumps(json_data), headers=headers)
return res.json()
def browser_detail(id):
json_data = {
"id": id
}
res = requests.post(f'{url}/browser/detail', data=json.dumps(json_data), headers=headers)
return res.json()
def group_update(groupId, browserIds):
# json_data = {
# "groupId": "41notc1202sr8gu5o6emb9ihaqbzbkic",
# "browserIds": ["af25e626167f4870b8f257e697bb4f05", "3baa6e990fee4e839c72722c8dc18019"]
# }
json_data = {
"groupId": groupId,
"browserIds": browserIds
}
res = requests.post(f'{url}/browser/group/update', data=json.dumps(json_data), headers=headers)
return res.json()
if __name__ == '__main__':
# for i in Xstart.select().where(
# Xstart.x_id.is_null()
# ):
# ips_info = Ips.select().where(Ips.start == 1, Ips.country == "法国").order_by(fn.Rand()).get()
#
# update_proxy_Browser(
# id=i.bit_id,
# host=ips_info.host,
# port=ips_info.port,
# proxyUserName=ips_info.username,
# proxyPassword=ips_info.password
# )
# fz_datas = get_group_lists()
# # fz_datas['推特']
#
# for i in range(10):
# for i in get_browser_lists_Browser(id=fz_datas['推特'], page=i):
# x_start_info = Xstart.get_or_none(
# Xstart.bit_id == i["id"]
# )
#
# if not x_start_info:
# deleteBrowser(id=i["id"])
#
# continue
#
# if x_start_info.start:
# continue
#
# deleteBrowser(id=i["id"])
#
# x_start_info.bit_id = None
# x_start_info.save()
# for i in Xstart.select():
# res = browser_detail(id=i.bit_id)
# print(res)
#
# if not res["success"]:
# i.bit_id = None
# i.save()
# print(browser_detail(id="532651f5330e4caa917e644f9b676b"))
# 批量修改代理
for i in Xstart.select().where(Xstart.start == 1):
update_proxy_Browser(id=i.bit_id, proxyType="http", host="127.0.0.1", port=random.randint(42000, 42089), )
# fz_datas = get_group_lists()
# print(fz_datas)
# bit_id_list = []
# for i in XToken.select().where(XToken.account_start == 2):
# sql_info = Xstart.get_or_none(
# Xstart.x_id == i.id
# )
#
# bit_id_list.append(sql_info.bit_id)
#
# print(len(bit_id_list))
# print(bit_id_list)
#
# print(group_update(fz_datas["西班牙语"], bit_id_list))

268
bitmart/框架.py Normal file
View File

@@ -0,0 +1,268 @@
import time
from tqdm import tqdm
from loguru import logger
from bit_tools import openBrowser
from DrissionPage import ChromiumPage
from DrissionPage import ChromiumOptions
from bitmart.api_contract import APIContract
class BitmartFuturesTransaction:
def __init__(self, bit_id):
self.page: ChromiumPage | None = None
self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
self.memo = "合约交易"
self.contract_symbol = "ETHUSDT"
self.contractAPI = APIContract(self.api_key, self.secret_key, self.memo, timeout=(5, 15))
self.start = 0 # 持仓状态: -1 空, 0 无, 1 多
self.direction = None
self.pbar = tqdm(total=30, desc="等待K线", ncols=80)
self.last_kline_time = None
self.leverage = "100" # 高杠杆(全仓模式下可开更大仓位)
self.open_type = "cross" # 全仓模式(你的“成本开仓”需求)
self.risk_percent = 0.01 # 每次开仓使用可用余额的 1%
self.open_avg_price = None # 开仓价格
self.current_amount = None # 持仓量
self.bit_id = bit_id
def get_klines(self):
"""获取最近3根30分钟K线step=30"""
try:
end_time = int(time.time())
# 获取足够多的条目确保有最新3根
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=30, # 30分钟
start_time=end_time - 3600 * 10, # 取最近10小时
end_time=end_time
)[0]["data"]
# 每根: [timestamp, open, high, low, close, volume]
formatted = []
for k in response:
formatted.append({
'id': int(k["timestamp"]),
'open': float(k["open_price"]),
'high': float(k["high_price"]),
'low': float(k["low_price"]),
'close': float(k["close_price"])
})
formatted.sort(key=lambda x: x['id'])
return formatted # 最近3根: kline_1 (最老), kline_2, kline_3 (最新)
except Exception as e:
logger.error(f"获取K线异常: {e}")
self.ding(error=True, msg="获取K线异常")
return None
def get_current_price(self):
"""获取当前最新价格,用于计算张数"""
try:
end_time = int(time.time())
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1, # 1分钟
start_time=end_time - 3600 * 3, # 取最近10小时
end_time=end_time
)[0]
if response['code'] == 1000:
return float(response['data'][-1]["close_price"])
return None
except Exception as e:
logger.error(f"获取价格异常: {e}")
return None
def get_available_balance(self):
"""获取合约账户可用USDT余额"""
try:
response = self.contractAPI.get_assets_detail()[0]
if response['code'] == 1000:
data = response['data']
if isinstance(data, dict):
return float(data.get('available_balance', 0))
elif isinstance(data, list):
for asset in data:
if asset.get('currency') == 'USDT':
return float(asset.get('available_balance', 0))
return None
except Exception as e:
logger.error(f"余额查询异常: {e}")
return None
# 获取当前持仓方向
def get_position_status(self):
"""获取当前持仓方向"""
try:
response = self.contractAPI.get_position(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
positions = response['data']
if not positions:
self.start = 0
return True
self.start = 1 if positions[0]['position_type'] == 1 else -1
self.open_avg_price = positions[0]['open_avg_price']
self.current_amount = positions[0]['current_amount']
self.position_cross = positions[0]["position_cross"]
return True
else:
return False
except Exception as e:
logger.error(f"持仓查询异常: {e}")
return False
# 设置杠杆和全仓
def set_leverage(self):
"""程序启动时设置全仓 + 高杠杆"""
try:
response = self.contractAPI.post_submit_leverage(
contract_symbol=self.contract_symbol,
leverage=self.leverage,
open_type=self.open_type
)[0]
if response['code'] == 1000:
logger.success(f"全仓模式 + {self.leverage}x 杠杆设置成功")
return True
else:
logger.error(f"杠杆设置失败: {response}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
def openBrowser(self):
"""打开 TGE 对应浏览器实例"""
try:
bit_port = openBrowser(id=self.bit_id)
co = ChromiumOptions()
co.set_local_port(port=bit_port)
self.page = ChromiumPage(addr_or_opts=co)
return True
except:
return False
def take_over_browser(self):
"""接管浏览器"""
try:
co = ChromiumOptions()
co.set_local_port(self.tge_port)
self.page = ChromiumPage(addr_or_opts=co)
self.page.set.window.max()
return True
except:
return False
def close_extra_tabs(self):
"""关闭多余 tab"""
try:
for idx, tab in enumerate(self.page.get_tabs()):
if idx > 0:
tab.close()
return True
except:
return False
def click_safe(self, xpath, sleep=0.5):
"""安全点击"""
try:
ele = self.page.ele(xpath)
if not ele:
return False
ele.scroll.to_see(center=True)
time.sleep(sleep)
ele.click()
return True
except:
return False
def 全平仓(self):
self.click_safe('x://span[normalize-space(text()) ="市价"]')
def 平一半多仓(self):
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]')
self.click_safe('x://span[normalize-space(text()) ="卖出/平多"]')
def 平一半空仓(self):
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]')
self.click_safe('x://span[normalize-space(text()) ="买入/平空"]')
def 开单(self, marketPriceLongOrder=0, limitPriceShortOrder=0, size=None, price=None):
"""
marketPriceLongOrder 市价最多或者做空1是做多-1是做空
limitPriceShortOrder 限价最多或者做空
"""
if marketPriceLongOrder == -1:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(size)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif marketPriceLongOrder == 1:
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(size)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
if limitPriceShortOrder == -1:
self.click_safe('x://button[normalize-space(text()) ="限价"]')
self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True)
time.sleep(1)
self.page.ele('x://*[@id="size_0"]').input(1)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif limitPriceShortOrder == 1:
self.click_safe('x://button[normalize-space(text()) ="限价"]')
self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True)
time.sleep(1)
self.page.ele('x://*[@id="size_0"]').input(1)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
def ding(self, text, error=False):
logger.info(text)
def action(self):
# 启动时设置全仓高杠杆
if not self.set_leverage():
logger.error("杠杆设置失败,程序继续运行但可能下单失败")
return
# 1. 打开浏览器
if not self.openBrowser():
self.ding("打开 TGE 失败!", error=True)
return
logger.info("TGE 端口获取成功")
self.get_klines()
# self.close_extra_tabs()
# self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT")
#
self.click_safe('x://button[normalize-space(text()) ="市价"]')
# self.click_safe('x://button[normalize-space(text()) ="限价"]')
#
# self.page.ele('x://*[@id="price_0"]').input(vals=3000, clear=True)
# self.page.ele('x://*[@id="size_0"]').input(1)
# self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
# self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
self.click_safe('x://button[normalize-space(text()) ="开仓"]')
self.click_safe('x://button[normalize-space(text()) ="平仓"]')
self.click_safe('x://span[normalize-space(text()) ="买入/平空"]')
self.click_safe('x://span[normalize-space(text()) ="卖出/平多"]')
self.click_safe('x://*[@id="futureTradeForm"]/div[5]/div[3]/div[3]/span[3]')
if __name__ == '__main__':
BitmartFuturesTransaction(bit_id="f2320f57e24c45529a009e1541e25961").action()

527
generate_backtest_chart.py Normal file
View File

@@ -0,0 +1,527 @@
"""
生成回测可视化图表
显示K线、布林带、开仓/平仓位置
"""
import pandas as pd
import json
from pathlib import Path
from peewee import *
import time
from loguru import logger
# 数据库配置
DB_PATH = Path(__file__).parent / 'models' / 'database.db'
db = SqliteDatabase(str(DB_PATH))
class BitMartETH5M(Model):
"""5分钟K线模型"""
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
class Meta:
database = db
table_name = 'bitmart_eth_5m'
def calculate_bollinger_bands(df, period=10, std_dev=2.5):
"""计算布林带右移1根与回测口径一致"""
df['sma'] = df['close'].rolling(window=period).mean()
df['std'] = df['close'].rolling(window=period).std()
df['bb_upper'] = (df['sma'] + std_dev * df['std']).shift(1)
df['bb_mid'] = df['sma'].shift(1)
df['bb_lower'] = (df['sma'] - std_dev * df['std']).shift(1)
return df
def generate_chart_data(start_date, end_date, trades_file):
"""生成图表数据"""
# 1. 加载K线数据
start_ts = int(pd.Timestamp(start_date).timestamp() * 1000)
end_ts = int(pd.Timestamp(end_date).timestamp() * 1000) + 86400000 # 加一天确保包含end_date当天的数据
query = BitMartETH5M.select().where(
(BitMartETH5M.id >= start_ts) & (BitMartETH5M.id <= end_ts)
).order_by(BitMartETH5M.id)
data = []
for row in query:
data.append({
'timestamp': row.id,
'open': row.open,
'high': row.high,
'low': row.low,
'close': row.close
})
df = pd.DataFrame(data)
df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
# 2. 计算布林带
df = calculate_bollinger_bands(df)
# 3. 加载交易记录
trades_df = pd.read_csv(trades_file)
trades_df['datetime'] = pd.to_datetime(trades_df['timestamp'])
# 4. 准备图表数据
chart_data = []
for idx, row in df.iterrows():
chart_data.append({
'timestamp': int(row['timestamp']),
'datetime': row['datetime'].strftime('%Y-%m-%d %H:%M'),
'open': float(row['open']) if pd.notna(row['open']) else None,
'high': float(row['high']) if pd.notna(row['high']) else None,
'low': float(row['low']) if pd.notna(row['low']) else None,
'close': float(row['close']) if pd.notna(row['close']) else None,
'bb_upper': float(row['bb_upper']) if pd.notna(row['bb_upper']) else None,
'bb_mid': float(row['bb_mid']) if pd.notna(row['bb_mid']) else None,
'bb_lower': float(row['bb_lower']) if pd.notna(row['bb_lower']) else None,
})
# 5. 准备交易标记数据
trades_markers = []
for idx, trade in trades_df.iterrows():
action = trade['action']
price = trade['price']
timestamp = trade['datetime']
reason = trade['reason']
# 找到对应的K线索引使用最近的K线
kline_idx = df[df['datetime'] == timestamp].index
if len(kline_idx) == 0:
# 如果找不到完全匹配的找最近的K线
time_diff = abs(df['datetime'] - timestamp)
min_diff = time_diff.min()
# 如果时间差超过10分钟跳过这个标记
if min_diff > pd.Timedelta(minutes=10):
print(f"跳过标记 {timestamp}找不到匹配的K线最小时间差: {min_diff}")
continue
kline_idx = time_diff.idxmin()
else:
kline_idx = kline_idx[0]
# 图上显示真实成交价,避免“总在最高/最低点成交”的错觉
kline = df.loc[kline_idx]
if pd.notna(price):
display_price = float(price)
else:
display_price = float(kline['close'])
marker = {
'timestamp': timestamp.strftime('%Y-%m-%d %H:%M'),
'price': display_price, # 使用K线实际价格
'action': action,
'reason': reason,
'index': int(kline_idx)
}
# 关键操作直接显示在图上,便于快速理解“为什么做这笔交易”
# 新增:开仓/加仓也展示标签
marker['show_reason_label'] = (
('延迟反转' in reason)
or ('止损' in reason)
or ('开long' in action)
or ('开short' in action)
or ('加long' in action)
or ('加short' in action)
)
if '止损' in reason:
marker['short_reason'] = '止损'
elif '延迟反转' in reason:
marker['short_reason'] = '延迟反转'
elif '触中轨平50%' in reason:
marker['short_reason'] = '中轨平半'
elif '回开仓价全平' in reason:
marker['short_reason'] = '回本全平'
elif '触上轨开空' in reason:
marker['short_reason'] = '上轨开空'
elif '触下轨开多' in reason:
marker['short_reason'] = '下轨开多'
elif '触上轨加空' in reason:
marker['short_reason'] = '上轨加空'
elif '触下轨加多' in reason:
marker['short_reason'] = '下轨加多'
else:
marker['short_reason'] = reason
# 分类标记
if '开long' in action or '加long' in action:
marker['type'] = 'open_long'
marker['color'] = '#00ff00'
marker['symbol'] = 'triangle'
elif '开short' in action or '加short' in action:
marker['type'] = 'open_short'
marker['color'] = '#ff0000'
marker['symbol'] = 'triangle'
elif '平仓' in action:
if '50%' in action:
marker['type'] = 'close_half'
marker['color'] = '#ffff00'
marker['symbol'] = 'diamond'
else:
marker['type'] = 'close_all'
marker['color'] = '#ff00ff'
marker['symbol'] = 'circle'
else:
marker['type'] = 'other'
marker['color'] = '#ffffff'
marker['symbol'] = 'circle'
trades_markers.append(marker)
return chart_data, trades_markers
def generate_html(chart_data, trades_markers, output_file):
"""生成HTML文件"""
html_content = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>布林带策略回测可视化 - 2026年3月</title>
<style>
html,
body {{
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #0a0e27;
color: #e0e6ed;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
}}
#chart {{
width: 100%;
height: 100%;
}}
.legend {{
position: fixed;
top: 20px;
right: 20px;
background: rgba(15, 23, 42, 0.95);
padding: 20px;
border-radius: 12px;
border: 1px solid #334155;
font-size: 13px;
line-height: 1.8;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
z-index: 1000;
}}
.legend-title {{
font-weight: 600;
margin-bottom: 12px;
color: #f1f5f9;
font-size: 14px;
letter-spacing: 0.5px;
}}
.legend-item {{
display: flex;
align-items: center;
margin: 8px 0;
}}
.legend-marker {{
width: 14px;
height: 14px;
margin-right: 10px;
border-radius: 2px;
}}
</style>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
</head>
<body>
<div id="chart"></div>
<div class="legend">
<div class="legend-title">📊 交易标记说明</div>
<div class="legend-item">
<div class="legend-marker" style="background: #00ff00;">▲</div>
<span>开多/加多</span>
</div>
<div class="legend-item">
<div class="legend-marker" style="background: #ff0000;">▼</div>
<span>开空/加空</span>
</div>
<div class="legend-item">
<div class="legend-marker" style="background: #ffff00;">◆</div>
<span>平仓50%</span>
</div>
<div class="legend-item">
<div class="legend-marker" style="background: #ff00ff;">●</div>
<span>平仓100%</span>
</div>
</div>
<script>
const chartData = {json.dumps(chart_data, ensure_ascii=False)};
const tradesMarkers = {json.dumps(trades_markers, ensure_ascii=False)};
function main() {{
const categoryData = [];
const klineData = [];
const upper = [];
const mid = [];
const lower = [];
for (const k of chartData) {{
const d = new Date(k.timestamp);
const label = `${{d.getMonth()+1}}/${{d.getDate()}} ${{d.getHours().toString().padStart(2, "0")}}:${{d
.getMinutes()
.toString()
.padStart(2, "0")}}`;
categoryData.push(label);
klineData.push([k.open, k.close, k.low, k.high]);
upper.push(k.bb_upper);
mid.push(k.bb_mid);
lower.push(k.bb_lower);
}}
// 准备交易标记数据
const openLongData = [];
const openShortData = [];
const closeHalfData = [];
const closeAllData = [];
for (const marker of tradesMarkers) {{
const point = {{
value: [marker.index, marker.price],
reason: marker.reason,
action: marker.action,
shortReason: marker.short_reason,
label: marker.show_reason_label
? {{
show: true,
formatter: marker.short_reason,
color: "#f8fafc",
backgroundColor: "rgba(15,23,42,0.85)",
borderColor: "#475569",
borderWidth: 1,
borderRadius: 4,
padding: [2, 4],
fontSize: 10,
}}
: {{ show: false }},
itemStyle: {{ color: marker.color }},
}};
if (marker.type === 'open_long') {{
openLongData.push(point);
}} else if (marker.type === 'open_short') {{
openShortData.push(point);
}} else if (marker.type === 'close_half') {{
closeHalfData.push(point);
}} else if (marker.type === 'close_all') {{
closeAllData.push(point);
}}
}}
const chartDom = document.getElementById("chart");
const chart = echarts.init(chartDom, null, {{ renderer: "canvas" }});
const option = {{
backgroundColor: "#0a0e27",
tooltip: {{
trigger: "axis",
axisPointer: {{ type: "cross" }},
backgroundColor: "rgba(15, 23, 42, 0.95)",
borderColor: "#334155",
textStyle: {{ color: "#e0e6ed" }},
}},
axisPointer: {{
link: [{{ xAxisIndex: "all" }}],
}},
grid: {{
left: "3%",
right: "200px",
top: "6%",
bottom: "8%",
containLabel: true,
}},
xAxis: {{
type: "category",
data: categoryData,
scale: true,
boundaryGap: true,
axisLine: {{ lineStyle: {{ color: "#475569" }} }},
axisLabel: {{
color: "#94a3b8",
rotate: 45,
fontSize: 11
}},
}},
yAxis: {{
scale: true,
axisLine: {{ lineStyle: {{ color: "#475569" }} }},
splitLine: {{ lineStyle: {{ color: "#1e293b" }} }},
axisLabel: {{ color: "#94a3b8" }},
}},
dataZoom: [
{{
type: "inside",
start: 0,
end: 100,
}},
{{
type: "slider",
start: 0,
end: 100,
height: 30,
backgroundColor: "#1e293b",
fillerColor: "rgba(100, 116, 139, 0.3)",
borderColor: "#334155",
handleStyle: {{
color: "#64748b",
borderColor: "#94a3b8"
}},
textStyle: {{ color: "#94a3b8" }},
}},
],
series: [
{{
name: "K线",
type: "candlestick",
data: klineData,
itemStyle: {{
color: "#10b981",
color0: "#ef4444",
borderColor: "#10b981",
borderColor0: "#ef4444",
}},
}},
{{
name: "BB上轨",
type: "line",
data: upper,
symbol: "none",
lineStyle: {{ color: "#f59e0b", width: 2 }},
z: 1,
}},
{{
name: "BB中轨",
type: "line",
data: mid,
symbol: "none",
lineStyle: {{ color: "#8b5cf6", width: 2, type: "dashed" }},
z: 1,
}},
{{
name: "BB下轨",
type: "line",
data: lower,
symbol: "none",
lineStyle: {{ color: "#f59e0b", width: 2 }},
z: 1,
}},
{{
name: "开多/加多",
type: "scatter",
symbol: "triangle",
symbolSize: 12,
symbolOffset: [0, 8], // 向下偏移,使三角形底部对齐价格
data: openLongData,
itemStyle: {{ color: "#00ff00" }},
z: 10,
tooltip: {{
formatter: (params) => {{
const marker = params.data;
return `开多/加多<br/>价格: ${{marker.value[1].toFixed(2)}}<br/>原因: ${{marker.reason}}`;
}}
}}
}},
{{
name: "开空/加空",
type: "scatter",
symbol: "triangle",
symbolSize: 12,
symbolRotate: 180,
symbolOffset: [0, -8], // 向上偏移,使三角形底部对齐价格
data: openShortData,
itemStyle: {{ color: "#ff0000" }},
z: 10,
tooltip: {{
formatter: (params) => {{
const marker = params.data;
return `开空/加空<br/>价格: ${{marker.value[1].toFixed(2)}}<br/>原因: ${{marker.reason}}`;
}}
}}
}},
{{
name: "平仓50%",
type: "scatter",
symbol: "diamond",
symbolSize: 10,
data: closeHalfData,
itemStyle: {{ color: "#ffff00" }},
z: 10,
tooltip: {{
formatter: (params) => {{
const marker = params.data;
return `平仓50%<br/>价格: ${{marker.value[1].toFixed(2)}}<br/>原因: ${{marker.reason}}`;
}}
}}
}},
{{
name: "平仓100%",
type: "scatter",
symbol: "circle",
symbolSize: 10,
data: closeAllData,
itemStyle: {{ color: "#ff00ff" }},
z: 10,
tooltip: {{
formatter: (params) => {{
const marker = params.data;
return `平仓100%<br/>价格: ${{marker.value[1].toFixed(2)}}<br/>原因: ${{marker.reason}}`;
}}
}}
}},
],
}};
chart.setOption(option);
window.addEventListener("resize", () => chart.resize());
}}
main();
</script>
</body>
</html>
"""
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ 图表已生成: {output_file}")
if __name__ == '__main__':
db.connect(reuse_if_open=True)
try:
print("正在生成回测可视化图表...")
output_dir = Path(__file__).parent / 'backtest_outputs' / 'charts'
output_dir.mkdir(parents=True, exist_ok=True)
# 生成图表数据
chart_data, trades_markers = generate_chart_data(
start_date='2026-03-01',
end_date='2026-03-03',
trades_file=str(Path(__file__).parent / 'backtest_outputs' / 'trades' / 'bb_backtest_march_2026_trades.csv')
)
print(f"📊 K线数据: {len(chart_data)}")
print(f"📍 交易标记: {len(trades_markers)}")
# 生成HTML
output_file = output_dir / 'bb_backtest_visualization.html'
generate_html(chart_data, trades_markers, str(output_file))
print(f"\n🎉 完成!请在浏览器中打开 {output_file} 查看图表")
finally:
db.close()

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +0,0 @@
bitmart-python-sdk-api
loguru
peewee
pymysql
numpy
pandas
scikit-learn
joblib
lightgbm>=3.0.0
optuna>=3.0.0
matplotlib

View File

@@ -1 +0,0 @@
from .bb_backtest import BBConfig, BBResult, BBTrade, run_bb_backtest

View File

@@ -1,156 +0,0 @@
"""
2023 年回测入口 - 用训练出的最优参数在 2023 全年数据上回测
"""
import json
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pandas as pd
import numpy as np
from strategy.data_loader import load_klines
from strategy.indicators import compute_all_indicators
from strategy.strategy_signal import (
generate_indicator_signals, compute_composite_score,
apply_htf_filter,
)
from strategy.backtest_engine import BacktestEngine
def main():
# 加载最佳参数
params_path = os.path.join(os.path.dirname(__file__), 'best_params_2020_2022.json')
if not os.path.exists(params_path):
print(f"错误: 找不到参数文件 {params_path}")
print("请先运行 train.py 进行训练")
return
with open(params_path, 'r') as f:
params = json.load(f)
print("=" * 70)
print("2023 年真实回测 (样本外)")
print("=" * 70)
# 加载数据 (多加载一些前置数据用于指标预热)
print("\n加载数据...")
df_5m = load_klines('5m', '2022-11-01', '2024-01-01')
df_1h = load_klines('1h', '2022-11-01', '2024-01-01')
print(f" 5m: {len(df_5m)} 条, 1h: {len(df_1h)}")
# 计算指标
print("计算指标...")
df_5m = compute_all_indicators(df_5m, params)
df_1h = compute_all_indicators(df_1h, params)
# 生成信号
print("生成信号...")
df_5m = generate_indicator_signals(df_5m, params)
df_1h = generate_indicator_signals(df_1h, params)
# 综合得分
score = compute_composite_score(df_5m, params)
score = apply_htf_filter(score, df_1h, params)
# 截取 2023 年数据
mask = (df_5m.index >= '2023-01-01') & (df_5m.index < '2024-01-01')
df_2023 = df_5m.loc[mask]
score_2023 = score.loc[mask]
print(f" 2023年数据: {len(df_2023)}")
# 回测
print("\n开始回测...")
engine = BacktestEngine(
initial_capital=1000.0,
margin_per_trade=25.0,
leverage=50,
fee_rate=0.0005,
rebate_ratio=0.70,
max_daily_drawdown=50.0,
min_hold_bars=1,
stop_loss_pct=params['stop_loss_pct'],
take_profit_pct=params['take_profit_pct'],
max_positions=int(params.get('max_positions', 3)),
)
result = engine.run(df_2023, score_2023, open_threshold=params['open_threshold'])
# ============================================================
# 输出结果
# ============================================================
print("\n" + "=" * 70)
print("2023 年回测结果")
print("=" * 70)
print(f" 初始资金: 1000.00 U")
print(f" 最终资金: {result['final_capital']:.2f} U")
print(f" 总收益: {result['total_pnl']:.2f} U")
print(f" 总手续费: {result['total_fee']:.2f} U")
print(f" 总返佣: {result['total_rebate']:.2f} U")
print(f" 交易次数: {result['num_trades']}")
print(f" 胜率: {result['win_rate']:.2%}")
print(f" 盈亏比: {result['profit_factor']:.2f}")
print(f" 日均收益: {result['avg_daily_pnl']:.2f} U")
print(f" 最大日回撤: {result['max_daily_dd']:.2f} U")
# 月度统计
daily_pnl = result['daily_pnl']
if daily_pnl:
df_daily = pd.DataFrame(list(daily_pnl.items()), columns=['date', 'pnl'])
df_daily['date'] = pd.to_datetime(df_daily['date'])
df_daily['month'] = df_daily['date'].dt.to_period('M')
monthly = df_daily.groupby('month')['pnl'].agg(['sum', 'count', 'mean', 'min'])
monthly.columns = ['月收益', '交易天数', '日均收益', '最大日亏损']
print("\n" + "-" * 70)
print("月度统计:")
print("-" * 70)
for idx, row in monthly.iterrows():
status = "" if row['月收益'] > 0 else ""
dd_status = "" if row['最大日亏损'] > -50 else "⚠️"
print(f" {idx} | 收益: {row['月收益']:>8.2f}U | "
f"日均: {row['日均收益']:>7.2f}U | "
f"最大日亏: {row['最大日亏损']:>7.2f}U {dd_status} | {status}")
# 日均收益是否达标
avg_daily = df_daily['pnl'].mean()
days_above_50 = (df_daily['pnl'] >= 50).sum()
days_below_neg50 = (df_daily['pnl'] < -50).sum()
print(f"\n 日均收益: {avg_daily:.2f}U {'✅ 达标' if avg_daily >= 50 else '❌ 未达标'}")
print(f" 日收益>=50U的天数: {days_above_50} / {len(df_daily)}")
print(f" 日回撤>50U的天数: {days_below_neg50} / {len(df_daily)}")
# 保存逐日 PnL
output_dir = os.path.dirname(__file__)
if daily_pnl:
df_daily_out = pd.DataFrame(list(daily_pnl.items()), columns=['date', 'pnl'])
df_daily_out['cumulative_pnl'] = df_daily_out['pnl'].cumsum()
daily_csv = os.path.join(output_dir, 'backtest_2023_daily_pnl.csv')
df_daily_out.to_csv(daily_csv, index=False)
print(f"\n逐日PnL已保存: {daily_csv}")
# 保存交易记录
if result['trades']:
trades_data = []
for t in result['trades']:
trades_data.append({
'entry_time': t.entry_time,
'exit_time': t.exit_time,
'direction': '' if t.direction == 1 else '',
'entry_price': t.entry_price,
'exit_price': t.exit_price,
'pnl': round(t.pnl, 4),
'fee': round(t.fee, 4),
'rebate': round(t.rebate, 4),
'holding_bars': t.holding_bars,
})
df_trades = pd.DataFrame(trades_data)
trades_csv = os.path.join(output_dir, 'backtest_2023_trades.csv')
df_trades.to_csv(trades_csv, index=False)
print(f"交易记录已保存: {trades_csv}")
print("\n" + "=" * 70)
if __name__ == '__main__':
main()

View File

@@ -1,366 +0,0 @@
date,pnl,cumulative_pnl
2023-01-01,-13.24030780391167,-13.24030780391167
2023-01-02,-6.446144625588336,-19.686452429500005
2023-01-03,1.604645378778668,-18.081807050721338
2023-01-04,56.24999999999943,38.168192949278094
2023-01-05,0.33406192478092844,38.50225487405902
2023-01-06,-4.28936549280675,34.21288938125227
2023-01-07,14.786004985419915,48.99889436667218
2023-01-08,74.57347500987392,123.5723693765461
2023-01-09,100.43932814185042,224.01169751839655
2023-01-10,-11.400950315324355,212.61074720307218
2023-01-11,119.73808656927638,332.34883377234854
2023-01-12,-46.87500000000008,285.4738337723485
2023-01-13,24.95301706493165,310.42685083728014
2023-01-14,36.87499999999923,347.3018508372794
2023-01-15,-42.164085212923396,305.137765624356
2023-01-16,-42.37550401931327,262.76226160504274
2023-01-17,-39.23788027501831,223.52438133002443
2023-01-18,10.968990345277774,234.4933716753022
2023-01-19,-17.519099732290336,216.97427194301187
2023-01-20,258.1249999999992,475.0992719430111
2023-01-21,-26.63800028548694,448.46127165752415
2023-01-22,-27.862031912052718,420.59923974547144
2023-01-23,-40.570948096946864,380.0282916485246
2023-01-24,153.52954075490175,533.5578324034263
2023-01-25,76.9770888682049,610.5349212716312
2023-01-26,-43.12500000000006,567.4099212716312
2023-01-27,4.574004296634158,571.9839255682654
2023-01-28,110.89323045990369,682.8771560281691
2023-01-29,65.39337129909306,748.2705273272621
2023-01-30,89.15573047359129,837.4262578008534
2023-01-31,-33.65661196432259,803.7696458365308
2023-02-01,-42.346269783529834,761.423376053001
2023-02-02,5.6249999999996945,767.0483760530007
2023-02-03,-38.015635992701625,729.032740060299
2023-02-04,-6.061609860875804,722.9711301994232
2023-02-05,91.32767729532108,814.2988074947443
2023-02-06,12.340770370556534,826.6395778653009
2023-02-07,-38.31565728321921,788.3239205820817
2023-02-08,-10.754771409427375,777.5691491726543
2023-02-09,180.00000000000034,957.5691491726546
2023-02-10,90.62500000000017,1048.1941491726548
2023-02-11,-40.23338038017373,1007.9607687924811
2023-02-12,50.843005130216255,1058.8037739226972
2023-02-13,-43.12499999999999,1015.6787739226972
2023-02-14,-45.000000000000014,970.6787739226972
2023-02-15,211.8749999999989,1182.553773922696
2023-02-16,123.74999999999923,1306.3037739226954
2023-02-17,-43.1250000000002,1263.1787739226952
2023-02-18,-30.38874055427764,1232.7900333684177
2023-02-19,2.5357914472640948,1235.3258248156817
2023-02-20,10.13433097511785,1245.4601557907995
2023-02-21,41.5960974135976,1287.056253204397
2023-02-22,39.9999999999999,1327.056253204397
2023-02-23,-13.478092269295054,1313.578160935102
2023-02-24,-33.61127571949922,1279.9668852156028
2023-02-25,56.87500000000026,1336.841885215603
2023-02-26,52.96295237320025,1389.8048375888034
2023-02-27,-21.012957685691507,1368.7918799031117
2023-02-28,-17.510525954953838,1351.2813539481579
2023-03-01,48.43861521106629,1399.719969159224
2023-03-02,5.370115987703678,1405.0900851469278
2023-03-03,114.81346298981391,1519.9035481367416
2023-03-04,-9.975555145718014,1509.9279929910235
2023-03-05,-40.070176144315376,1469.8578168467081
2023-03-06,-11.368260314393366,1458.4895565323147
2023-03-07,29.800169999101556,1488.2897265314164
2023-03-08,-41.066516251938125,1447.2232102794783
2023-03-09,178.12499999999966,1625.3482102794778
2023-03-10,69.54272408495805,1694.890934364436
2023-03-11,218.8441938252083,1913.7351281896442
2023-03-12,-43.124999999999986,1870.6101281896442
2023-03-13,31.874999999999282,1902.4851281896435
2023-03-14,71.24999999999882,1973.7351281896424
2023-03-15,-46.87499999999994,1926.8601281896424
2023-03-16,-47.19007322239124,1879.6700549672512
2023-03-17,249.37499999999858,2129.0450549672496
2023-03-18,-39.375000000000455,2089.670054967249
2023-03-19,-37.72054430463878,2051.9495106626105
2023-03-20,-48.125000000000256,2003.8245106626102
2023-03-21,-48.955919791756116,1954.8685908708542
2023-03-22,-41.34851470243514,1913.520076168419
2023-03-23,-40.53293708055611,1872.987139087863
2023-03-24,-40.62177198705648,1832.3653671008065
2023-03-25,-12.808008186129763,1819.5573589146768
2023-03-26,44.60456842447671,1864.1619273391534
2023-03-27,33.10077209532424,1897.2626994344776
2023-03-28,-40.54590870106257,1856.7167907334149
2023-03-29,32.68290319879213,1889.399693932207
2023-03-30,-42.18093929678437,1847.2187546354226
2023-03-31,-30.18435407486843,1817.034400560554
2023-04-01,-18.629080327821725,1798.4053202327323
2023-04-02,78.36987927566116,1876.7751995083934
2023-04-03,-14.310148754581672,1862.4650507538117
2023-04-04,67.49999999999953,1929.9650507538113
2023-04-05,40.981623971671745,1970.946674725483
2023-04-06,27.071471993660218,1998.0181467191433
2023-04-07,69.95707277743023,2067.9752194965736
2023-04-08,16.74780048539444,2084.723019981968
2023-04-09,47.28635900754121,2132.009378989509
2023-04-10,92.25687955666356,2224.2662585461726
2023-04-11,13.118453933996253,2237.3847124801687
2023-04-12,56.24999999999995,2293.6347124801687
2023-04-13,179.99999999999943,2473.634712480168
2023-04-14,-21.87500000000091,2451.7597124801673
2023-04-15,0.6060490696602718,2452.3657615498278
2023-04-16,69.62762541934998,2521.9933869691777
2023-04-17,65.86212708679903,2587.8555140559765
2023-04-18,11.498179506746062,2599.3536935627226
2023-04-19,223.1815368848387,2822.5352304475614
2023-04-20,-43.12499999999999,2779.4102304475614
2023-04-21,31.875000000000142,2811.2852304475614
2023-04-22,-29.84738767205142,2781.43784277551
2023-04-23,13.154733757123939,2794.5925765326338
2023-04-24,6.396324986843672,2800.9889015194776
2023-04-25,123.74011330692568,2924.7290148264033
2023-04-26,261.41402100210826,3186.1430358285115
2023-04-27,24.10664119340748,3210.249677021919
2023-04-28,-10.62612672957838,3199.6235502923405
2023-04-29,23.597281085864836,3223.2208313782053
2023-04-30,-44.289545693205795,3178.9312856849997
2023-05-01,54.37500000000002,3233.3062856849997
2023-05-02,-2.017144354733148,3231.2891413302664
2023-05-03,81.77174013147513,3313.0608814617417
2023-05-04,-25.04485318862535,3288.0160282731163
2023-05-05,171.61321250444536,3459.629240777562
2023-05-06,52.49999999999993,3512.129240777562
2023-05-07,-33.168505868798924,3478.960734908763
2023-05-08,8.124999999999917,3487.085734908763
2023-05-09,-12.584917211039063,3474.5008176977235
2023-05-10,52.019173246963206,3526.519990944687
2023-05-11,90.00000000000003,3616.519990944687
2023-05-12,126.17158084605737,3742.691571790744
2023-05-13,8.545669463922014,3751.237241254666
2023-05-14,1.8924095106008352,3753.129650765267
2023-05-15,10.943496294234567,3764.0731470595015
2023-05-16,-30.264218933238727,3733.808928126263
2023-05-17,30.026157944387133,3763.83508607065
2023-05-18,23.13394310654973,3786.9690291772
2023-05-19,15.149354042131993,3802.118383219332
2023-05-20,-16.41923661401725,3785.6991466053146
2023-05-21,7.846127328452241,3793.545273933767
2023-05-22,21.279945465818493,3814.8252193995854
2023-05-23,70.02437426214179,3884.8495936617273
2023-05-24,72.70217181804935,3957.551765479777
2023-05-25,-16.75708025521019,3940.794685224567
2023-05-26,18.74999999999976,3959.5446852245664
2023-05-27,39.20117578898695,3998.745861013553
2023-05-28,110.91041187532412,4109.6562728888775
2023-05-29,-32.49999999999996,4077.1562728888775
2023-05-30,10.449185839751497,4087.605458728629
2023-05-31,-10.136360635569764,4077.469098093059
2023-06-01,14.358833920817762,4091.8279320138768
2023-06-02,65.90793376617691,4157.735865780053
2023-06-03,2.796329514545665,4160.532195294599
2023-06-04,-6.651917557240656,4153.8802777373585
2023-06-05,168.7500000000002,4322.6302777373585
2023-06-06,-43.12500000000005,4279.5052777373585
2023-06-07,34.36109954050025,4313.866377277859
2023-06-08,-25.452505493132982,4288.413871784726
2023-06-09,7.08099194612123,4295.494863730848
2023-06-10,121.25000000000003,4416.744863730848
2023-06-11,-20.937848671894088,4395.807015058954
2023-06-12,-21.37821245611659,4374.428802602837
2023-06-13,-13.703676058378367,4360.725126544458
2023-06-14,166.27160175206745,4526.996728296526
2023-06-15,15.069476387286578,4542.066204683812
2023-06-16,106.96240346155682,4649.02860814537
2023-06-17,-11.250000000000341,4637.77860814537
2023-06-18,-10.120078428463955,4627.658529716906
2023-06-19,-38.97345934789618,4588.68507036901
2023-06-20,42.86461601305588,4631.549686382065
2023-06-21,187.49999999999937,4819.0496863820645
2023-06-22,31.80896255236462,4850.858648934429
2023-06-23,23.85220828852076,4874.71085722295
2023-06-24,-30.371879505034936,4844.338977717915
2023-06-25,12.45391150222293,4856.792889220138
2023-06-26,78.4973773289051,4935.290266549043
2023-06-27,-39.87042272482522,4895.419843824217
2023-06-28,88.12499999999999,4983.544843824217
2023-06-29,-40.51516243372697,4943.029681390491
2023-06-30,105.46931766571112,5048.498999056202
2023-07-01,-13.796315557439133,5034.702683498763
2023-07-02,-41.59344453195906,4993.109238966804
2023-07-03,28.89550853439942,5022.004747501203
2023-07-04,-5.3332824068334626,5016.6714650943695
2023-07-05,90.00000000000007,5106.6714650943695
2023-07-06,65.47404542743519,5172.145510521805
2023-07-07,-8.375646476725684,5163.769864045079
2023-07-08,2.0825098762478143,5165.852373921327
2023-07-09,-12.284261589217671,5153.568112332109
2023-07-10,4.7069840687455775,5158.275096400855
2023-07-11,29.744227743509978,5188.019324144365
2023-07-12,13.786259374056783,5201.805583518421
2023-07-13,179.07533869993108,5380.880922218353
2023-07-14,74.05844966531129,5454.939371883664
2023-07-15,-24.570748403829185,5430.368623479834
2023-07-16,6.803094664210427,5437.1717181440445
2023-07-17,27.410515235666534,5464.582233379711
2023-07-18,4.897911857902959,5469.480145237614
2023-07-19,6.308694855454021,5475.788840093068
2023-07-20,41.93544480774592,5517.7242849008135
2023-07-21,16.096325607887692,5533.820610508701
2023-07-22,-2.7025252562022395,5531.1180852524985
2023-07-23,-36.865989707872636,5494.252095544626
2023-07-24,86.80264475362813,5581.054740298255
2023-07-25,-24.16611590079934,5556.888624397456
2023-07-26,-12.803087110558725,5544.085537286897
2023-07-27,29.206711819629362,5573.2922491065265
2023-07-28,13.812242058805946,5587.104491165333
2023-07-29,11.977355224058611,5599.081846389391
2023-07-30,-5.150633707859324,5593.931212681532
2023-07-31,-31.586985170360826,5562.344227511171
2023-08-01,34.57747987841326,5596.921707389584
2023-08-02,19.53988833101348,5616.461595720598
2023-08-03,26.963711901014904,5643.425307621613
2023-08-04,0.4840259877677071,5643.90933360938
2023-08-05,8.851832296486007,5652.761165905866
2023-08-06,-4.2929518635524,5648.468214042313
2023-08-07,4.423504364914294,5652.891718407227
2023-08-08,79.37499999999964,5732.266718407227
2023-08-09,-1.0555745361586295,5731.211143871068
2023-08-10,-15.802261213704188,5715.408882657364
2023-08-11,-19.434997227631737,5695.973885429733
2023-08-12,-14.180480342977727,5681.793405086755
2023-08-13,-25.33078616920967,5656.462618917545
2023-08-14,-29.365151992789308,5627.097466924756
2023-08-15,-1.25,5625.847466924756
2023-08-16,90.00000000000014,5715.847466924756
2023-08-17,181.8750000000003,5897.722466924756
2023-08-18,20.625000000000128,5918.347466924756
2023-08-19,-27.59032679226934,5890.757140132487
2023-08-20,-23.87577257083145,5866.881367561656
2023-08-21,-22.59314579170043,5844.288221769955
2023-08-22,113.75000000000014,5958.038221769955
2023-08-23,8.70637062854804,5966.744592398503
2023-08-24,22.993336529216194,5989.73792892772
2023-08-25,-41.837123197460336,5947.900805730259
2023-08-26,-20.238929404490822,5927.661876325768
2023-08-27,-2.006488273327664,5925.655388052441
2023-08-28,14.024404963407886,5939.679793015848
2023-08-29,143.13137132947674,6082.811164345325
2023-08-30,-43.125000000000064,6039.686164345325
2023-08-31,31.6249079218707,6071.311072267195
2023-09-01,56.25000000000017,6127.561072267195
2023-09-02,-27.62863635821808,6099.932435908977
2023-09-03,-18.380622263138324,6081.551813645839
2023-09-04,-4.629639415293694,6076.922174230545
2023-09-05,24.14433818655366,6101.066512417099
2023-09-06,-46.72867445132057,6054.337837965779
2023-09-07,-3.6562754869909058,6050.681562478788
2023-09-08,42.254582050520796,6092.936144529309
2023-09-09,-30.94267544057865,6061.99346908873
2023-09-10,53.2912479279699,6115.284717016701
2023-09-11,146.25000000000028,6261.534717016701
2023-09-12,-18.31728723379048,6243.21742978291
2023-09-13,-2.141383984249483,6241.07604579866
2023-09-14,56.24999999999978,6297.32604579866
2023-09-15,-37.98094850138423,6259.345097297276
2023-09-16,-33.38082020481237,6225.964277092464
2023-09-17,-2.737757522168219,6223.226519570296
2023-09-18,28.781772740126208,6252.008292310422
2023-09-19,4.273043833846757,6256.281336144269
2023-09-20,24.242189180030334,6280.523525324299
2023-09-21,90.00000000000011,6370.523525324299
2023-09-22,-30.115416338642312,6340.408108985656
2023-09-23,-24.28225302122884,6316.1258559644275
2023-09-24,-42.98193078708244,6273.143925177345
2023-09-25,-5.561737200674414,6267.582187976671
2023-09-26,19.48204133300013,6287.064229309671
2023-09-27,56.24999999999976,6343.314229309671
2023-09-28,109.05931606292351,6452.373545372594
2023-09-29,-3.7500000000000906,6448.623545372594
2023-09-30,34.11899846667325,6482.7425438392675
2023-10-01,94.78119432198413,6577.5237381612515
2023-10-02,-43.125,6534.3987381612515
2023-10-03,-1.875,6532.5237381612515
2023-10-04,60.137202072927415,6592.660940234179
2023-10-05,-47.15508896159986,6545.505851272579
2023-10-06,-41.50509357550347,6504.000757697076
2023-10-07,-21.848785430736374,6482.151972266339
2023-10-08,-2.133334555954726,6480.018637710384
2023-10-09,120.94752554130996,6600.966163251694
2023-10-10,-30.55517668632468,6570.410986565369
2023-10-11,12.918899632245676,6583.3298861976145
2023-10-12,86.41945469513863,6669.749340892753
2023-10-13,-38.92743296812289,6630.82190792463
2023-10-14,-9.664398308242207,6621.157509616388
2023-10-15,0.32442084565507256,6621.481930462043
2023-10-16,17.666860566236,6639.148791028279
2023-10-17,71.96782243080129,6711.11661345908
2023-10-18,-26.10871664471985,6685.007896814361
2023-10-19,27.131717309356045,6712.1396141237165
2023-10-20,104.9999999999995,6817.139614123716
2023-10-21,95.53563465609497,6912.67524877981
2023-10-22,-39.07986275419109,6873.5953860256195
2023-10-23,159.37499999999892,7032.970386025619
2023-10-24,125.89084826115247,7158.861234286771
2023-10-25,-39.7457464430455,7119.1154878437255
2023-10-26,76.84599913941024,7195.961486983136
2023-10-27,-40.93958561285608,7155.02190137028
2023-10-28,-0.12582720298443117,7154.896074167295
2023-10-29,1.7653990903884869,7156.661473257684
2023-10-30,14.80106712479257,7171.462540382477
2023-10-31,-0.7859893532677678,7170.676551029209
2023-11-01,45.63294697011628,7216.309497999325
2023-11-02,66.38708795013648,7282.696585949461
2023-11-03,7.818102574679843,7290.514688524141
2023-11-04,78.74999999999962,7369.264688524141
2023-11-05,93.530612820923,7462.795301345064
2023-11-06,-8.841261133494985,7453.954040211569
2023-11-07,13.587699262662081,7467.541739474231
2023-11-08,-27.39298530178767,7440.148754172443
2023-11-09,233.65340882150477,7673.802162993948
2023-11-10,-43.1250000000002,7630.677162993948
2023-11-11,65.04467136509504,7695.721834359043
2023-11-12,-24.51389709666313,7671.20793726238
2023-11-13,6.381878610566607,7677.589815872947
2023-11-14,124.7302217309696,7802.320037603917
2023-11-15,-15.4428344502457,7786.877203153671
2023-11-16,22.788163027994152,7809.665366181665
2023-11-17,-40.58878257444296,7769.076583607222
2023-11-18,10.05941544996627,7779.135999057188
2023-11-19,77.18072310694912,7856.316722164137
2023-11-20,-33.719119790508586,7822.597602373628
2023-11-21,-39.39362250328517,7783.203979870344
2023-11-22,62.49999999999903,7845.703979870343
2023-11-23,-43.750000000000135,7801.953979870343
2023-11-24,43.12499999999979,7845.078979870343
2023-11-25,2.7372815485791326,7847.816261418921
2023-11-26,-19.434424922416646,7828.381836496505
2023-11-27,74.22705085378658,7902.608887350291
2023-11-28,-21.10900157778203,7881.4998857725095
2023-11-29,1.584413397259107,7883.084299169768
2023-11-30,-5.586662127245132,7877.497637042523
2023-12-01,68.65652016602549,7946.154157208548
2023-12-02,116.24999999999937,8062.404157208547
2023-12-03,48.229742583832405,8110.633899792379
2023-12-04,13.529045410407036,8124.162945202786
2023-12-05,93.37552564770242,8217.538470850488
2023-12-06,-39.37279199427863,8178.1656788562095
2023-12-07,160.37637314576938,8338.54205200198
2023-12-08,-32.94183697847613,8305.600215023504
2023-12-09,-22.342388616671474,8283.257826406832
2023-12-10,-29.00075720912898,8254.257069197703
2023-12-11,112.22981231135489,8366.486881509058
2023-12-12,-46.76135746492969,8319.725524044128
2023-12-13,102.21323939340017,8421.938763437529
2023-12-14,33.74999999999983,8455.688763437529
2023-12-15,63.75916511664332,8519.447928554173
2023-12-16,-32.300370998108704,8487.147557556063
2023-12-17,17.066032222984127,8504.213589779047
2023-12-18,168.68048765961714,8672.894077438665
2023-12-19,25.118359089859197,8698.012436528525
2023-12-20,-43.080721035673186,8654.931715492852
2023-12-21,-31.737797271451193,8623.193918221401
2023-12-22,47.49999999999983,8670.693918221401
2023-12-23,20.47855147595199,8691.172469697352
2023-12-24,108.453203178383,8799.625672875736
2023-12-25,-34.74504529363933,8764.880627582097
2023-12-26,104.99999999999984,8869.880627582097
2023-12-27,204.66554953165667,9074.546177113754
2023-12-28,-5.606769621811815,9068.939407491942
2023-12-29,220.95172689013637,9289.891134382078
2023-12-30,-15.105228436109885,9274.785905945968
2023-12-31,-0.005881281931285898,9274.780024664036
1 date pnl cumulative_pnl
2 2023-01-01 -13.24030780391167 -13.24030780391167
3 2023-01-02 -6.446144625588336 -19.686452429500005
4 2023-01-03 1.604645378778668 -18.081807050721338
5 2023-01-04 56.24999999999943 38.168192949278094
6 2023-01-05 0.33406192478092844 38.50225487405902
7 2023-01-06 -4.28936549280675 34.21288938125227
8 2023-01-07 14.786004985419915 48.99889436667218
9 2023-01-08 74.57347500987392 123.5723693765461
10 2023-01-09 100.43932814185042 224.01169751839655
11 2023-01-10 -11.400950315324355 212.61074720307218
12 2023-01-11 119.73808656927638 332.34883377234854
13 2023-01-12 -46.87500000000008 285.4738337723485
14 2023-01-13 24.95301706493165 310.42685083728014
15 2023-01-14 36.87499999999923 347.3018508372794
16 2023-01-15 -42.164085212923396 305.137765624356
17 2023-01-16 -42.37550401931327 262.76226160504274
18 2023-01-17 -39.23788027501831 223.52438133002443
19 2023-01-18 10.968990345277774 234.4933716753022
20 2023-01-19 -17.519099732290336 216.97427194301187
21 2023-01-20 258.1249999999992 475.0992719430111
22 2023-01-21 -26.63800028548694 448.46127165752415
23 2023-01-22 -27.862031912052718 420.59923974547144
24 2023-01-23 -40.570948096946864 380.0282916485246
25 2023-01-24 153.52954075490175 533.5578324034263
26 2023-01-25 76.9770888682049 610.5349212716312
27 2023-01-26 -43.12500000000006 567.4099212716312
28 2023-01-27 4.574004296634158 571.9839255682654
29 2023-01-28 110.89323045990369 682.8771560281691
30 2023-01-29 65.39337129909306 748.2705273272621
31 2023-01-30 89.15573047359129 837.4262578008534
32 2023-01-31 -33.65661196432259 803.7696458365308
33 2023-02-01 -42.346269783529834 761.423376053001
34 2023-02-02 5.6249999999996945 767.0483760530007
35 2023-02-03 -38.015635992701625 729.032740060299
36 2023-02-04 -6.061609860875804 722.9711301994232
37 2023-02-05 91.32767729532108 814.2988074947443
38 2023-02-06 12.340770370556534 826.6395778653009
39 2023-02-07 -38.31565728321921 788.3239205820817
40 2023-02-08 -10.754771409427375 777.5691491726543
41 2023-02-09 180.00000000000034 957.5691491726546
42 2023-02-10 90.62500000000017 1048.1941491726548
43 2023-02-11 -40.23338038017373 1007.9607687924811
44 2023-02-12 50.843005130216255 1058.8037739226972
45 2023-02-13 -43.12499999999999 1015.6787739226972
46 2023-02-14 -45.000000000000014 970.6787739226972
47 2023-02-15 211.8749999999989 1182.553773922696
48 2023-02-16 123.74999999999923 1306.3037739226954
49 2023-02-17 -43.1250000000002 1263.1787739226952
50 2023-02-18 -30.38874055427764 1232.7900333684177
51 2023-02-19 2.5357914472640948 1235.3258248156817
52 2023-02-20 10.13433097511785 1245.4601557907995
53 2023-02-21 41.5960974135976 1287.056253204397
54 2023-02-22 39.9999999999999 1327.056253204397
55 2023-02-23 -13.478092269295054 1313.578160935102
56 2023-02-24 -33.61127571949922 1279.9668852156028
57 2023-02-25 56.87500000000026 1336.841885215603
58 2023-02-26 52.96295237320025 1389.8048375888034
59 2023-02-27 -21.012957685691507 1368.7918799031117
60 2023-02-28 -17.510525954953838 1351.2813539481579
61 2023-03-01 48.43861521106629 1399.719969159224
62 2023-03-02 5.370115987703678 1405.0900851469278
63 2023-03-03 114.81346298981391 1519.9035481367416
64 2023-03-04 -9.975555145718014 1509.9279929910235
65 2023-03-05 -40.070176144315376 1469.8578168467081
66 2023-03-06 -11.368260314393366 1458.4895565323147
67 2023-03-07 29.800169999101556 1488.2897265314164
68 2023-03-08 -41.066516251938125 1447.2232102794783
69 2023-03-09 178.12499999999966 1625.3482102794778
70 2023-03-10 69.54272408495805 1694.890934364436
71 2023-03-11 218.8441938252083 1913.7351281896442
72 2023-03-12 -43.124999999999986 1870.6101281896442
73 2023-03-13 31.874999999999282 1902.4851281896435
74 2023-03-14 71.24999999999882 1973.7351281896424
75 2023-03-15 -46.87499999999994 1926.8601281896424
76 2023-03-16 -47.19007322239124 1879.6700549672512
77 2023-03-17 249.37499999999858 2129.0450549672496
78 2023-03-18 -39.375000000000455 2089.670054967249
79 2023-03-19 -37.72054430463878 2051.9495106626105
80 2023-03-20 -48.125000000000256 2003.8245106626102
81 2023-03-21 -48.955919791756116 1954.8685908708542
82 2023-03-22 -41.34851470243514 1913.520076168419
83 2023-03-23 -40.53293708055611 1872.987139087863
84 2023-03-24 -40.62177198705648 1832.3653671008065
85 2023-03-25 -12.808008186129763 1819.5573589146768
86 2023-03-26 44.60456842447671 1864.1619273391534
87 2023-03-27 33.10077209532424 1897.2626994344776
88 2023-03-28 -40.54590870106257 1856.7167907334149
89 2023-03-29 32.68290319879213 1889.399693932207
90 2023-03-30 -42.18093929678437 1847.2187546354226
91 2023-03-31 -30.18435407486843 1817.034400560554
92 2023-04-01 -18.629080327821725 1798.4053202327323
93 2023-04-02 78.36987927566116 1876.7751995083934
94 2023-04-03 -14.310148754581672 1862.4650507538117
95 2023-04-04 67.49999999999953 1929.9650507538113
96 2023-04-05 40.981623971671745 1970.946674725483
97 2023-04-06 27.071471993660218 1998.0181467191433
98 2023-04-07 69.95707277743023 2067.9752194965736
99 2023-04-08 16.74780048539444 2084.723019981968
100 2023-04-09 47.28635900754121 2132.009378989509
101 2023-04-10 92.25687955666356 2224.2662585461726
102 2023-04-11 13.118453933996253 2237.3847124801687
103 2023-04-12 56.24999999999995 2293.6347124801687
104 2023-04-13 179.99999999999943 2473.634712480168
105 2023-04-14 -21.87500000000091 2451.7597124801673
106 2023-04-15 0.6060490696602718 2452.3657615498278
107 2023-04-16 69.62762541934998 2521.9933869691777
108 2023-04-17 65.86212708679903 2587.8555140559765
109 2023-04-18 11.498179506746062 2599.3536935627226
110 2023-04-19 223.1815368848387 2822.5352304475614
111 2023-04-20 -43.12499999999999 2779.4102304475614
112 2023-04-21 31.875000000000142 2811.2852304475614
113 2023-04-22 -29.84738767205142 2781.43784277551
114 2023-04-23 13.154733757123939 2794.5925765326338
115 2023-04-24 6.396324986843672 2800.9889015194776
116 2023-04-25 123.74011330692568 2924.7290148264033
117 2023-04-26 261.41402100210826 3186.1430358285115
118 2023-04-27 24.10664119340748 3210.249677021919
119 2023-04-28 -10.62612672957838 3199.6235502923405
120 2023-04-29 23.597281085864836 3223.2208313782053
121 2023-04-30 -44.289545693205795 3178.9312856849997
122 2023-05-01 54.37500000000002 3233.3062856849997
123 2023-05-02 -2.017144354733148 3231.2891413302664
124 2023-05-03 81.77174013147513 3313.0608814617417
125 2023-05-04 -25.04485318862535 3288.0160282731163
126 2023-05-05 171.61321250444536 3459.629240777562
127 2023-05-06 52.49999999999993 3512.129240777562
128 2023-05-07 -33.168505868798924 3478.960734908763
129 2023-05-08 8.124999999999917 3487.085734908763
130 2023-05-09 -12.584917211039063 3474.5008176977235
131 2023-05-10 52.019173246963206 3526.519990944687
132 2023-05-11 90.00000000000003 3616.519990944687
133 2023-05-12 126.17158084605737 3742.691571790744
134 2023-05-13 8.545669463922014 3751.237241254666
135 2023-05-14 1.8924095106008352 3753.129650765267
136 2023-05-15 10.943496294234567 3764.0731470595015
137 2023-05-16 -30.264218933238727 3733.808928126263
138 2023-05-17 30.026157944387133 3763.83508607065
139 2023-05-18 23.13394310654973 3786.9690291772
140 2023-05-19 15.149354042131993 3802.118383219332
141 2023-05-20 -16.41923661401725 3785.6991466053146
142 2023-05-21 7.846127328452241 3793.545273933767
143 2023-05-22 21.279945465818493 3814.8252193995854
144 2023-05-23 70.02437426214179 3884.8495936617273
145 2023-05-24 72.70217181804935 3957.551765479777
146 2023-05-25 -16.75708025521019 3940.794685224567
147 2023-05-26 18.74999999999976 3959.5446852245664
148 2023-05-27 39.20117578898695 3998.745861013553
149 2023-05-28 110.91041187532412 4109.6562728888775
150 2023-05-29 -32.49999999999996 4077.1562728888775
151 2023-05-30 10.449185839751497 4087.605458728629
152 2023-05-31 -10.136360635569764 4077.469098093059
153 2023-06-01 14.358833920817762 4091.8279320138768
154 2023-06-02 65.90793376617691 4157.735865780053
155 2023-06-03 2.796329514545665 4160.532195294599
156 2023-06-04 -6.651917557240656 4153.8802777373585
157 2023-06-05 168.7500000000002 4322.6302777373585
158 2023-06-06 -43.12500000000005 4279.5052777373585
159 2023-06-07 34.36109954050025 4313.866377277859
160 2023-06-08 -25.452505493132982 4288.413871784726
161 2023-06-09 7.08099194612123 4295.494863730848
162 2023-06-10 121.25000000000003 4416.744863730848
163 2023-06-11 -20.937848671894088 4395.807015058954
164 2023-06-12 -21.37821245611659 4374.428802602837
165 2023-06-13 -13.703676058378367 4360.725126544458
166 2023-06-14 166.27160175206745 4526.996728296526
167 2023-06-15 15.069476387286578 4542.066204683812
168 2023-06-16 106.96240346155682 4649.02860814537
169 2023-06-17 -11.250000000000341 4637.77860814537
170 2023-06-18 -10.120078428463955 4627.658529716906
171 2023-06-19 -38.97345934789618 4588.68507036901
172 2023-06-20 42.86461601305588 4631.549686382065
173 2023-06-21 187.49999999999937 4819.0496863820645
174 2023-06-22 31.80896255236462 4850.858648934429
175 2023-06-23 23.85220828852076 4874.71085722295
176 2023-06-24 -30.371879505034936 4844.338977717915
177 2023-06-25 12.45391150222293 4856.792889220138
178 2023-06-26 78.4973773289051 4935.290266549043
179 2023-06-27 -39.87042272482522 4895.419843824217
180 2023-06-28 88.12499999999999 4983.544843824217
181 2023-06-29 -40.51516243372697 4943.029681390491
182 2023-06-30 105.46931766571112 5048.498999056202
183 2023-07-01 -13.796315557439133 5034.702683498763
184 2023-07-02 -41.59344453195906 4993.109238966804
185 2023-07-03 28.89550853439942 5022.004747501203
186 2023-07-04 -5.3332824068334626 5016.6714650943695
187 2023-07-05 90.00000000000007 5106.6714650943695
188 2023-07-06 65.47404542743519 5172.145510521805
189 2023-07-07 -8.375646476725684 5163.769864045079
190 2023-07-08 2.0825098762478143 5165.852373921327
191 2023-07-09 -12.284261589217671 5153.568112332109
192 2023-07-10 4.7069840687455775 5158.275096400855
193 2023-07-11 29.744227743509978 5188.019324144365
194 2023-07-12 13.786259374056783 5201.805583518421
195 2023-07-13 179.07533869993108 5380.880922218353
196 2023-07-14 74.05844966531129 5454.939371883664
197 2023-07-15 -24.570748403829185 5430.368623479834
198 2023-07-16 6.803094664210427 5437.1717181440445
199 2023-07-17 27.410515235666534 5464.582233379711
200 2023-07-18 4.897911857902959 5469.480145237614
201 2023-07-19 6.308694855454021 5475.788840093068
202 2023-07-20 41.93544480774592 5517.7242849008135
203 2023-07-21 16.096325607887692 5533.820610508701
204 2023-07-22 -2.7025252562022395 5531.1180852524985
205 2023-07-23 -36.865989707872636 5494.252095544626
206 2023-07-24 86.80264475362813 5581.054740298255
207 2023-07-25 -24.16611590079934 5556.888624397456
208 2023-07-26 -12.803087110558725 5544.085537286897
209 2023-07-27 29.206711819629362 5573.2922491065265
210 2023-07-28 13.812242058805946 5587.104491165333
211 2023-07-29 11.977355224058611 5599.081846389391
212 2023-07-30 -5.150633707859324 5593.931212681532
213 2023-07-31 -31.586985170360826 5562.344227511171
214 2023-08-01 34.57747987841326 5596.921707389584
215 2023-08-02 19.53988833101348 5616.461595720598
216 2023-08-03 26.963711901014904 5643.425307621613
217 2023-08-04 0.4840259877677071 5643.90933360938
218 2023-08-05 8.851832296486007 5652.761165905866
219 2023-08-06 -4.2929518635524 5648.468214042313
220 2023-08-07 4.423504364914294 5652.891718407227
221 2023-08-08 79.37499999999964 5732.266718407227
222 2023-08-09 -1.0555745361586295 5731.211143871068
223 2023-08-10 -15.802261213704188 5715.408882657364
224 2023-08-11 -19.434997227631737 5695.973885429733
225 2023-08-12 -14.180480342977727 5681.793405086755
226 2023-08-13 -25.33078616920967 5656.462618917545
227 2023-08-14 -29.365151992789308 5627.097466924756
228 2023-08-15 -1.25 5625.847466924756
229 2023-08-16 90.00000000000014 5715.847466924756
230 2023-08-17 181.8750000000003 5897.722466924756
231 2023-08-18 20.625000000000128 5918.347466924756
232 2023-08-19 -27.59032679226934 5890.757140132487
233 2023-08-20 -23.87577257083145 5866.881367561656
234 2023-08-21 -22.59314579170043 5844.288221769955
235 2023-08-22 113.75000000000014 5958.038221769955
236 2023-08-23 8.70637062854804 5966.744592398503
237 2023-08-24 22.993336529216194 5989.73792892772
238 2023-08-25 -41.837123197460336 5947.900805730259
239 2023-08-26 -20.238929404490822 5927.661876325768
240 2023-08-27 -2.006488273327664 5925.655388052441
241 2023-08-28 14.024404963407886 5939.679793015848
242 2023-08-29 143.13137132947674 6082.811164345325
243 2023-08-30 -43.125000000000064 6039.686164345325
244 2023-08-31 31.6249079218707 6071.311072267195
245 2023-09-01 56.25000000000017 6127.561072267195
246 2023-09-02 -27.62863635821808 6099.932435908977
247 2023-09-03 -18.380622263138324 6081.551813645839
248 2023-09-04 -4.629639415293694 6076.922174230545
249 2023-09-05 24.14433818655366 6101.066512417099
250 2023-09-06 -46.72867445132057 6054.337837965779
251 2023-09-07 -3.6562754869909058 6050.681562478788
252 2023-09-08 42.254582050520796 6092.936144529309
253 2023-09-09 -30.94267544057865 6061.99346908873
254 2023-09-10 53.2912479279699 6115.284717016701
255 2023-09-11 146.25000000000028 6261.534717016701
256 2023-09-12 -18.31728723379048 6243.21742978291
257 2023-09-13 -2.141383984249483 6241.07604579866
258 2023-09-14 56.24999999999978 6297.32604579866
259 2023-09-15 -37.98094850138423 6259.345097297276
260 2023-09-16 -33.38082020481237 6225.964277092464
261 2023-09-17 -2.737757522168219 6223.226519570296
262 2023-09-18 28.781772740126208 6252.008292310422
263 2023-09-19 4.273043833846757 6256.281336144269
264 2023-09-20 24.242189180030334 6280.523525324299
265 2023-09-21 90.00000000000011 6370.523525324299
266 2023-09-22 -30.115416338642312 6340.408108985656
267 2023-09-23 -24.28225302122884 6316.1258559644275
268 2023-09-24 -42.98193078708244 6273.143925177345
269 2023-09-25 -5.561737200674414 6267.582187976671
270 2023-09-26 19.48204133300013 6287.064229309671
271 2023-09-27 56.24999999999976 6343.314229309671
272 2023-09-28 109.05931606292351 6452.373545372594
273 2023-09-29 -3.7500000000000906 6448.623545372594
274 2023-09-30 34.11899846667325 6482.7425438392675
275 2023-10-01 94.78119432198413 6577.5237381612515
276 2023-10-02 -43.125 6534.3987381612515
277 2023-10-03 -1.875 6532.5237381612515
278 2023-10-04 60.137202072927415 6592.660940234179
279 2023-10-05 -47.15508896159986 6545.505851272579
280 2023-10-06 -41.50509357550347 6504.000757697076
281 2023-10-07 -21.848785430736374 6482.151972266339
282 2023-10-08 -2.133334555954726 6480.018637710384
283 2023-10-09 120.94752554130996 6600.966163251694
284 2023-10-10 -30.55517668632468 6570.410986565369
285 2023-10-11 12.918899632245676 6583.3298861976145
286 2023-10-12 86.41945469513863 6669.749340892753
287 2023-10-13 -38.92743296812289 6630.82190792463
288 2023-10-14 -9.664398308242207 6621.157509616388
289 2023-10-15 0.32442084565507256 6621.481930462043
290 2023-10-16 17.666860566236 6639.148791028279
291 2023-10-17 71.96782243080129 6711.11661345908
292 2023-10-18 -26.10871664471985 6685.007896814361
293 2023-10-19 27.131717309356045 6712.1396141237165
294 2023-10-20 104.9999999999995 6817.139614123716
295 2023-10-21 95.53563465609497 6912.67524877981
296 2023-10-22 -39.07986275419109 6873.5953860256195
297 2023-10-23 159.37499999999892 7032.970386025619
298 2023-10-24 125.89084826115247 7158.861234286771
299 2023-10-25 -39.7457464430455 7119.1154878437255
300 2023-10-26 76.84599913941024 7195.961486983136
301 2023-10-27 -40.93958561285608 7155.02190137028
302 2023-10-28 -0.12582720298443117 7154.896074167295
303 2023-10-29 1.7653990903884869 7156.661473257684
304 2023-10-30 14.80106712479257 7171.462540382477
305 2023-10-31 -0.7859893532677678 7170.676551029209
306 2023-11-01 45.63294697011628 7216.309497999325
307 2023-11-02 66.38708795013648 7282.696585949461
308 2023-11-03 7.818102574679843 7290.514688524141
309 2023-11-04 78.74999999999962 7369.264688524141
310 2023-11-05 93.530612820923 7462.795301345064
311 2023-11-06 -8.841261133494985 7453.954040211569
312 2023-11-07 13.587699262662081 7467.541739474231
313 2023-11-08 -27.39298530178767 7440.148754172443
314 2023-11-09 233.65340882150477 7673.802162993948
315 2023-11-10 -43.1250000000002 7630.677162993948
316 2023-11-11 65.04467136509504 7695.721834359043
317 2023-11-12 -24.51389709666313 7671.20793726238
318 2023-11-13 6.381878610566607 7677.589815872947
319 2023-11-14 124.7302217309696 7802.320037603917
320 2023-11-15 -15.4428344502457 7786.877203153671
321 2023-11-16 22.788163027994152 7809.665366181665
322 2023-11-17 -40.58878257444296 7769.076583607222
323 2023-11-18 10.05941544996627 7779.135999057188
324 2023-11-19 77.18072310694912 7856.316722164137
325 2023-11-20 -33.719119790508586 7822.597602373628
326 2023-11-21 -39.39362250328517 7783.203979870344
327 2023-11-22 62.49999999999903 7845.703979870343
328 2023-11-23 -43.750000000000135 7801.953979870343
329 2023-11-24 43.12499999999979 7845.078979870343
330 2023-11-25 2.7372815485791326 7847.816261418921
331 2023-11-26 -19.434424922416646 7828.381836496505
332 2023-11-27 74.22705085378658 7902.608887350291
333 2023-11-28 -21.10900157778203 7881.4998857725095
334 2023-11-29 1.584413397259107 7883.084299169768
335 2023-11-30 -5.586662127245132 7877.497637042523
336 2023-12-01 68.65652016602549 7946.154157208548
337 2023-12-02 116.24999999999937 8062.404157208547
338 2023-12-03 48.229742583832405 8110.633899792379
339 2023-12-04 13.529045410407036 8124.162945202786
340 2023-12-05 93.37552564770242 8217.538470850488
341 2023-12-06 -39.37279199427863 8178.1656788562095
342 2023-12-07 160.37637314576938 8338.54205200198
343 2023-12-08 -32.94183697847613 8305.600215023504
344 2023-12-09 -22.342388616671474 8283.257826406832
345 2023-12-10 -29.00075720912898 8254.257069197703
346 2023-12-11 112.22981231135489 8366.486881509058
347 2023-12-12 -46.76135746492969 8319.725524044128
348 2023-12-13 102.21323939340017 8421.938763437529
349 2023-12-14 33.74999999999983 8455.688763437529
350 2023-12-15 63.75916511664332 8519.447928554173
351 2023-12-16 -32.300370998108704 8487.147557556063
352 2023-12-17 17.066032222984127 8504.213589779047
353 2023-12-18 168.68048765961714 8672.894077438665
354 2023-12-19 25.118359089859197 8698.012436528525
355 2023-12-20 -43.080721035673186 8654.931715492852
356 2023-12-21 -31.737797271451193 8623.193918221401
357 2023-12-22 47.49999999999983 8670.693918221401
358 2023-12-23 20.47855147595199 8691.172469697352
359 2023-12-24 108.453203178383 8799.625672875736
360 2023-12-25 -34.74504529363933 8764.880627582097
361 2023-12-26 104.99999999999984 8869.880627582097
362 2023-12-27 204.66554953165667 9074.546177113754
363 2023-12-28 -5.606769621811815 9068.939407491942
364 2023-12-29 220.95172689013637 9289.891134382078
365 2023-12-30 -15.105228436109885 9274.785905945968
366 2023-12-31 -0.005881281931285898 9274.780024664036

File diff suppressed because it is too large Load Diff

View File

@@ -1,298 +0,0 @@
"""
回测引擎 - 完整模拟手续费、返佣延迟到账、每日回撤限制、持仓时间约束
支持同时持有多单并发,严格控制每日最大回撤
"""
import datetime
import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Trade:
entry_time: pd.Timestamp
exit_time: Optional[pd.Timestamp] = None
direction: int = 0
entry_price: float = 0.0
exit_price: float = 0.0
pnl: float = 0.0
fee: float = 0.0
rebate: float = 0.0
holding_bars: int = 0
@dataclass
class OpenPosition:
direction: int = 0
entry_price: float = 0.0
entry_time: pd.Timestamp = None
hold_bars: int = 0
class BacktestEngine:
def __init__(
self,
initial_capital: float = 1000.0,
margin_per_trade: float = 25.0,
leverage: int = 50,
fee_rate: float = 0.0005,
rebate_ratio: float = 0.70,
max_daily_drawdown: float = 50.0,
min_hold_bars: int = 1,
stop_loss_pct: float = 0.005,
take_profit_pct: float = 0.01,
max_positions: int = 3,
):
self.initial_capital = initial_capital
self.margin = margin_per_trade
self.leverage = leverage
self.notional = margin_per_trade * leverage
self.fee_rate = fee_rate
self.rebate_ratio = rebate_ratio
self.max_daily_dd = max_daily_drawdown
self.min_hold_bars = min_hold_bars
self.sl_pct = stop_loss_pct
self.tp_pct = take_profit_pct
self.max_positions = max_positions
def _close_position(self, pos, exit_price, t, today, trades, pending_rebates):
"""平仓一个持仓,返回 net_pnl"""
qty = self.notional / pos.entry_price
if pos.direction == 1:
raw_pnl = qty * (exit_price - pos.entry_price)
else:
raw_pnl = qty * (pos.entry_price - exit_price)
close_fee = self.notional * self.fee_rate
net_pnl = raw_pnl - close_fee
total_fee = self.notional * self.fee_rate * 2
rebate = total_fee * self.rebate_ratio
rebate_date = today + datetime.timedelta(days=1)
pending_rebates.append((rebate_date, rebate))
trades.append(Trade(
entry_time=pos.entry_time, exit_time=t,
direction=pos.direction, entry_price=pos.entry_price,
exit_price=exit_price, pnl=net_pnl, fee=total_fee,
rebate=rebate, holding_bars=pos.hold_bars,
))
return net_pnl
def _worst_unrealized(self, positions, h, lo):
"""计算所有持仓在本K线内的最坏浮动亏损用 high/low"""
worst = 0.0
for pos in positions:
qty = self.notional / pos.entry_price
if pos.direction == 1:
# 多单最坏情况: 价格跌到 low
worst += qty * (lo - pos.entry_price)
else:
# 空单最坏情况: 价格涨到 high
worst += qty * (pos.entry_price - h)
return worst
def run(self, df: pd.DataFrame, score: pd.Series, open_threshold: float = 0.3) -> dict:
capital = self.initial_capital
trades: List[Trade] = []
daily_pnl = {}
pending_rebates = []
positions: List[OpenPosition] = []
used_margin = 0.0
current_date = None
day_pnl = 0.0
day_stopped = False
close_arr = df['close'].values
high_arr = df['high'].values
low_arr = df['low'].values
times = df.index
scores = score.values
for i in range(len(df)):
t = times[i]
c = close_arr[i]
h = high_arr[i]
lo = low_arr[i]
s = scores[i]
today = t.date()
# --- 日切换 ---
if today != current_date:
if current_date is not None:
daily_pnl[current_date] = day_pnl
current_date = today
day_pnl = 0.0
day_stopped = False
arrived = []
remaining = []
for rd, ra in pending_rebates:
if today >= rd:
arrived.append(ra)
else:
remaining.append((rd, ra))
if arrived:
capital += sum(arrived)
pending_rebates = remaining
if day_stopped:
for pos in positions:
pos.hold_bars += 1
continue
# --- 正常止损止盈逻辑 ---
closed_indices = []
for pi, pos in enumerate(positions):
pos.hold_bars += 1
qty = self.notional / pos.entry_price
if pos.direction == 1:
sl_price = pos.entry_price * (1 - self.sl_pct)
tp_price = pos.entry_price * (1 + self.tp_pct)
hit_sl = lo <= sl_price
hit_tp = h >= tp_price
else:
sl_price = pos.entry_price * (1 + self.sl_pct)
tp_price = pos.entry_price * (1 - self.tp_pct)
hit_sl = h >= sl_price
hit_tp = lo <= tp_price
should_close = False
exit_price = c
# 止损始终生效(不受持仓时间限制)
if hit_sl:
should_close = True
exit_price = sl_price
elif pos.hold_bars >= self.min_hold_bars:
# 止盈和信号反转需要满足最小持仓时间
if hit_tp:
should_close = True
exit_price = tp_price
elif (pos.direction == 1 and s < -open_threshold) or \
(pos.direction == -1 and s > open_threshold):
should_close = True
exit_price = c
if should_close:
net = self._close_position(pos, exit_price, t, today, trades, pending_rebates)
capital += net
used_margin -= self.margin
day_pnl += net
closed_indices.append(pi)
# 每笔平仓后立即检查日回撤
if day_pnl < -self.max_daily_dd:
# 熔断剩余持仓
for pj, pos2 in enumerate(positions):
if pj not in closed_indices:
pos2.hold_bars += 1
net2 = self._close_position(pos2, c, t, today, trades, pending_rebates)
capital += net2
used_margin -= self.margin
day_pnl += net2
closed_indices.append(pj)
day_stopped = True
break
for pi in sorted(set(closed_indices), reverse=True):
positions.pop(pi)
if day_stopped:
continue
# --- 开仓 ---
if len(positions) < self.max_positions:
if np.isnan(s):
continue
# 开仓前检查: 当前所有持仓 + 新仓同时止损的最大亏损
n_after = len(positions) + 1
worst_total_sl = n_after * (self.notional * self.sl_pct + self.notional * self.fee_rate * 2)
if day_pnl - worst_total_sl < -self.max_daily_dd:
continue # 风险敞口太大
open_fee = self.notional * self.fee_rate
if capital - used_margin < self.margin + open_fee:
continue
new_dir = 0
if s > open_threshold:
new_dir = 1
elif s < -open_threshold:
new_dir = -1
if new_dir != 0:
positions.append(OpenPosition(
direction=new_dir, entry_price=c,
entry_time=t, hold_bars=0,
))
capital -= open_fee
used_margin += self.margin
day_pnl -= open_fee
# 最后一天
if current_date is not None:
daily_pnl[current_date] = day_pnl
# 强制平仓
if positions and len(df) > 0:
last_close = close_arr[-1]
for pos in positions:
qty = self.notional / pos.entry_price
if pos.direction == 1:
raw_pnl = qty * (last_close - pos.entry_price)
else:
raw_pnl = qty * (pos.entry_price - last_close)
fee = self.notional * self.fee_rate
net_pnl = raw_pnl - fee
capital += net_pnl
trades.append(Trade(
entry_time=pos.entry_time, exit_time=times[-1],
direction=pos.direction, entry_price=pos.entry_price,
exit_price=last_close, pnl=net_pnl,
fee=self.notional * self.fee_rate * 2,
rebate=0, holding_bars=pos.hold_bars,
))
remaining_rebate = sum(amt for _, amt in pending_rebates)
capital += remaining_rebate
return self._build_result(trades, daily_pnl, capital)
def _build_result(self, trades, daily_pnl, final_capital):
if not trades:
return {
'total_pnl': 0, 'final_capital': final_capital,
'num_trades': 0, 'win_rate': 0, 'avg_pnl': 0,
'max_daily_dd': 0, 'avg_daily_pnl': 0,
'profit_factor': 0, 'trades': [], 'daily_pnl': daily_pnl,
'total_fee': 0, 'total_rebate': 0,
}
pnls = [t.pnl for t in trades]
wins = [p for p in pnls if p > 0]
losses = [p for p in pnls if p <= 0]
daily_vals = list(daily_pnl.values())
total_fee = sum(t.fee for t in trades)
total_rebate = sum(t.rebate for t in trades)
gross_profit = sum(wins) if wins else 0
gross_loss = abs(sum(losses)) if losses else 1e-10
return {
'total_pnl': sum(pnls) + total_rebate,
'final_capital': final_capital,
'num_trades': len(trades),
'win_rate': len(wins) / len(trades) if trades else 0,
'avg_pnl': np.mean(pnls),
'max_daily_dd': min(daily_vals) if daily_vals else 0,
'avg_daily_pnl': np.mean(daily_vals) if daily_vals else 0,
'profit_factor': gross_profit / gross_loss,
'total_fee': total_fee,
'total_rebate': total_rebate,
'trades': trades,
'daily_pnl': daily_pnl,
}

View File

@@ -1,315 +0,0 @@
"""Bollinger Band mean-reversion strategy backtest.
Logic:
- Price touches upper BB → close any long, open short
- Price touches lower BB → close any short, open long
- Always in position (flip between long and short)
Uses 5-minute OHLC data from the database.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import List, Optional
import numpy as np
import pandas as pd
from .data_loader import KlineSource, load_klines
from .indicators import bollinger
# ---------------------------------------------------------------------------
# Config & result types
# ---------------------------------------------------------------------------
@dataclass
class BBConfig:
# Bollinger Band parameters
bb_period: int = 20 # SMA window
bb_std: float = 2.0 # standard deviation multiplier
# Position sizing
margin_per_trade: float = 80.0
leverage: float = 100.0
initial_capital: float = 1000.0
# Risk management
max_daily_loss: float = 150.0 # stop trading after this daily loss
stop_loss_pct: float = 0.0 # 0 = disabled; e.g. 0.02 = 2% SL from entry
# Dynamic sizing: if > 0, margin = equity * margin_pct (overrides margin_per_trade)
margin_pct: float = 0.0 # e.g. 0.01 = 1% of equity per trade
# Fee structure (taker)
fee_rate: float = 0.0006 # 0.06%
rebate_rate: float = 0.0 # instant maker rebate (if any)
# Delayed rebate: rebate_pct of daily fees returned next day at rebate_hour UTC
rebate_pct: float = 0.0 # e.g. 0.70 = 70% rebate
rebate_hour_utc: int = 0 # hour in UTC when rebate arrives (0 = 8am UTC+8)
@dataclass
class BBTrade:
side: str # "long" or "short"
entry_price: float
exit_price: float
entry_time: object # pd.Timestamp
exit_time: object
margin: float
leverage: float
qty: float
gross_pnl: float
fee: float
net_pnl: float
@dataclass
class BBResult:
equity_curve: pd.DataFrame # columns: equity, balance, price, position
trades: List[BBTrade]
daily_stats: pd.DataFrame # daily equity + pnl
total_fee: float
total_rebate: float
config: BBConfig
# ---------------------------------------------------------------------------
# Backtest engine
# ---------------------------------------------------------------------------
def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult:
"""Run Bollinger Band mean-reversion backtest on 5m OHLC data."""
close = df["close"].astype(float)
high = df["high"].astype(float)
low = df["low"].astype(float)
n = len(df)
# Compute Bollinger Bands
bb_mid, bb_upper, bb_lower, bb_width = bollinger(close, cfg.bb_period, cfg.bb_std)
# Convert to numpy for speed
arr_close = close.values
arr_high = high.values
arr_low = low.values
arr_upper = bb_upper.values
arr_lower = bb_lower.values
ts_index = df.index
# State
balance = cfg.initial_capital
position = 0 # +1 = long, -1 = short, 0 = flat
entry_price = 0.0
entry_time = None
entry_margin = 0.0
entry_qty = 0.0
trades: List[BBTrade] = []
total_fee = 0.0
total_rebate = 0.0
# Daily tracking
day_pnl = 0.0
day_stopped = False
current_day = None
# Delayed rebate tracking
pending_rebate = 0.0 # fees from previous day to be rebated
today_fees = 0.0 # fees accumulated today
rebate_applied_today = False
# Output arrays
out_equity = np.full(n, np.nan)
out_balance = np.full(n, np.nan)
out_position = np.zeros(n)
def unrealised(price):
if position == 0:
return 0.0
if position == 1:
return entry_qty * (price - entry_price)
else:
return entry_qty * (entry_price - price)
def close_position(exit_price, exit_idx):
nonlocal balance, position, entry_price, entry_time, entry_margin, entry_qty
nonlocal total_fee, total_rebate, day_pnl, today_fees
if position == 0:
return
if position == 1:
gross = entry_qty * (exit_price - entry_price)
else:
gross = entry_qty * (entry_price - exit_price)
exit_notional = entry_qty * exit_price
fee = exit_notional * cfg.fee_rate
rebate = exit_notional * cfg.rebate_rate # instant rebate only
net = gross - fee + rebate
trades.append(BBTrade(
side="long" if position == 1 else "short",
entry_price=entry_price,
exit_price=exit_price,
entry_time=entry_time,
exit_time=ts_index[exit_idx],
margin=entry_margin,
leverage=cfg.leverage,
qty=entry_qty,
gross_pnl=gross,
fee=fee,
net_pnl=net,
))
balance += net
total_fee += fee
total_rebate += rebate
today_fees += fee
day_pnl += net
position = 0
entry_price = 0.0
entry_time = None
entry_margin = 0.0
entry_qty = 0.0
def open_position(side, price, idx):
nonlocal position, entry_price, entry_time, entry_margin, entry_qty
nonlocal balance, total_fee, day_pnl, today_fees
if cfg.margin_pct > 0:
equity = balance + unrealised(price) if position != 0 else balance
margin = equity * cfg.margin_pct
else:
margin = cfg.margin_per_trade
margin = min(margin, balance * 0.95)
if margin <= 0:
return
notional = margin * cfg.leverage
qty = notional / price
fee = notional * cfg.fee_rate
balance -= fee
total_fee += fee
today_fees += fee
day_pnl -= fee
position = 1 if side == "long" else -1
entry_price = price
entry_time = ts_index[idx]
entry_margin = margin
entry_qty = qty
# Main loop
for i in range(n):
# Daily reset + delayed rebate
bar_day = ts_index[i].date() if hasattr(ts_index[i], 'date') else None
bar_hour = ts_index[i].hour if hasattr(ts_index[i], 'hour') else 0
if bar_day is not None and bar_day != current_day:
# New day: move today's fees to pending, reset
if cfg.rebate_pct > 0:
pending_rebate = today_fees * cfg.rebate_pct
today_fees = 0.0
rebate_applied_today = False
day_pnl = 0.0
day_stopped = False
current_day = bar_day
# Apply delayed rebate at specified hour
if cfg.rebate_pct > 0 and not rebate_applied_today and bar_hour >= cfg.rebate_hour_utc and pending_rebate > 0:
balance += pending_rebate
total_rebate += pending_rebate
pending_rebate = 0.0
rebate_applied_today = True
# Skip if BB not ready
if np.isnan(arr_upper[i]) or np.isnan(arr_lower[i]):
out_equity[i] = balance + unrealised(arr_close[i])
out_balance[i] = balance
out_position[i] = position
continue
# Daily loss check
if day_stopped:
out_equity[i] = balance + unrealised(arr_close[i])
out_balance[i] = balance
out_position[i] = position
continue
cur_equity = balance + unrealised(arr_close[i])
if day_pnl + unrealised(arr_close[i]) <= -cfg.max_daily_loss:
close_position(arr_close[i], i)
day_stopped = True
out_equity[i] = balance
out_balance[i] = balance
out_position[i] = 0
continue
# Stop loss check
if position != 0 and cfg.stop_loss_pct > 0:
if position == 1 and arr_low[i] <= entry_price * (1 - cfg.stop_loss_pct):
sl_price = entry_price * (1 - cfg.stop_loss_pct)
close_position(sl_price, i)
elif position == -1 and arr_high[i] >= entry_price * (1 + cfg.stop_loss_pct):
sl_price = entry_price * (1 + cfg.stop_loss_pct)
close_position(sl_price, i)
# Signal detection: use high/low to check if price touched BB
touched_upper = arr_high[i] >= arr_upper[i]
touched_lower = arr_low[i] <= arr_lower[i]
if touched_upper and touched_lower:
# Both touched in same bar (wide bar) — skip, too volatile
pass
elif touched_upper:
# Price touched upper BB → go short
if position == 1:
# Close long at upper BB price
close_position(arr_upper[i], i)
if position != -1:
# Open short
open_position("short", arr_upper[i], i)
elif touched_lower:
# Price touched lower BB → go long
if position == -1:
# Close short at lower BB price
close_position(arr_lower[i], i)
if position != 1:
# Open long
open_position("long", arr_lower[i], i)
# Record equity
out_equity[i] = balance + unrealised(arr_close[i])
out_balance[i] = balance
out_position[i] = position
# Force close at end
if position != 0:
close_position(arr_close[n - 1], n - 1)
out_equity[n - 1] = balance
out_balance[n - 1] = balance
out_position[n - 1] = 0
# Build equity DataFrame
eq_df = pd.DataFrame({
"equity": out_equity,
"balance": out_balance,
"price": arr_close,
"position": out_position,
}, index=ts_index)
# Daily stats
daily_eq = eq_df["equity"].resample("1D").last().dropna().to_frame("equity")
daily_eq["pnl"] = daily_eq["equity"].diff().fillna(0.0)
return BBResult(
equity_curve=eq_df,
trades=trades,
daily_stats=daily_eq,
total_fee=total_fee,
total_rebate=total_rebate,
config=cfg,
)

View File

@@ -1,53 +0,0 @@
{
"bb_period": 36,
"bb_std": 3.3000000000000003,
"kc_period": 24,
"kc_mult": 1.3,
"dc_period": 41,
"ema_fast": 3,
"ema_slow": 15,
"macd_fast": 9,
"macd_slow": 34,
"macd_signal": 15,
"adx_period": 16,
"st_period": 5,
"st_mult": 1.4,
"rsi_period": 7,
"stoch_k": 18,
"stoch_d": 6,
"stoch_smooth": 3,
"cci_period": 12,
"wr_period": 9,
"wma_period": 47,
"bb_oversold": -0.19999999999999998,
"bb_overbought": 1.3,
"kc_oversold": 0.2,
"kc_overbought": 0.75,
"dc_oversold": 0.05,
"dc_overbought": 0.75,
"adx_threshold": 15.0,
"rsi_overbought": 70.0,
"rsi_oversold": 18.0,
"stoch_overbought": 89.0,
"stoch_oversold": 10.0,
"cci_overbought": 80.0,
"cci_oversold": -140.0,
"wr_overbought": -28.0,
"wr_oversold": -90.0,
"w_bb": 0.15000000000000002,
"w_kc": 0.4,
"w_dc": 0.0,
"w_ema": 0.8500000000000001,
"w_macd": 0.35000000000000003,
"w_adx": 0.0,
"w_st": 0.15000000000000002,
"w_rsi": 0.4,
"w_stoch": 0.15000000000000002,
"w_cci": 0.1,
"w_wr": 0.0,
"w_wma": 0.4,
"open_threshold": 0.22,
"max_positions": 3,
"take_profit_pct": 0.024999999999999998,
"stop_loss_pct": 0.008
}

View File

@@ -1,69 +0,0 @@
"""
数据加载模块 - 从 SQLite 加载多周期K线数据为 DataFrame
"""
import pandas as pd
from peewee import SqliteDatabase
from pathlib import Path
DB_PATH = Path(__file__).parent.parent / 'models' / 'database.db'
# 周期 -> 表名
PERIOD_MAP = {
'1m': 'bitmart_eth_1m',
'3m': 'bitmart_eth_3m',
'5m': 'bitmart_eth_5m',
'15m': 'bitmart_eth_15m',
'30m': 'bitmart_eth_30m',
'1h': 'bitmart_eth_1h',
}
def load_klines(period: str, start_date: str, end_date: str) -> pd.DataFrame:
"""
加载指定周期、指定日期范围的K线数据
:param period: '1m','3m','5m','15m','30m','1h'
:param start_date: 'YYYY-MM-DD'
:param end_date: 'YYYY-MM-DD' (不包含该日)
:return: DataFrame with columns: datetime, open, high, low, close
"""
table = PERIOD_MAP.get(period)
if not table:
raise ValueError(f"不支持的周期: {period}, 可选: {list(PERIOD_MAP.keys())}")
start_ts = int(pd.Timestamp(start_date).timestamp() * 1000)
end_ts = int(pd.Timestamp(end_date).timestamp() * 1000)
db = SqliteDatabase(str(DB_PATH))
db.connect()
cursor = db.execute_sql(
f'SELECT id, open, high, low, close FROM [{table}] '
f'WHERE id >= ? AND id < ? ORDER BY id',
(start_ts, end_ts)
)
rows = cursor.fetchall()
db.close()
df = pd.DataFrame(rows, columns=['timestamp_ms', 'open', 'high', 'low', 'close'])
df['datetime'] = pd.to_datetime(df['timestamp_ms'], unit='ms')
df.set_index('datetime', inplace=True)
df.drop(columns=['timestamp_ms'], inplace=True)
df = df.astype(float)
return df
def load_multi_period(periods: list, start_date: str, end_date: str) -> dict:
"""
加载多个周期的数据
:return: {period: DataFrame}
"""
result = {}
for p in periods:
result[p] = load_klines(p, start_date, end_date)
print(f" 加载 {p}: {len(result[p])} 条 ({start_date} ~ {end_date})")
return result
if __name__ == '__main__':
data = load_multi_period(['5m', '15m', '1h'], '2020-01-01', '2024-01-01')
for k, v in data.items():
print(f"{k}: {v.shape}, {v.index[0]} ~ {v.index[-1]}")

View File

@@ -1,104 +0,0 @@
from __future__ import annotations
import numpy as np
import pandas as pd
def ema(s: pd.Series, span: int) -> pd.Series:
return s.ewm(span=span, adjust=False).mean()
def rsi(close: pd.Series, period: int) -> pd.Series:
delta = close.diff()
up = delta.clip(lower=0.0)
down = (-delta).clip(lower=0.0)
roll_up = up.ewm(alpha=1 / period, adjust=False).mean()
roll_down = down.ewm(alpha=1 / period, adjust=False).mean()
rs = roll_up / roll_down.replace(0.0, np.nan)
return 100.0 - (100.0 / (1.0 + rs))
def atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int) -> pd.Series:
prev_close = close.shift(1)
tr = pd.concat(
[
(high - low).abs(),
(high - prev_close).abs(),
(low - prev_close).abs(),
],
axis=1,
).max(axis=1)
return tr.ewm(alpha=1 / period, adjust=False).mean()
def bollinger(close: pd.Series, window: int, n_std: float):
mid = close.rolling(window=window, min_periods=window).mean()
std = close.rolling(window=window, min_periods=window).std(ddof=0)
upper = mid + n_std * std
lower = mid - n_std * std
width = (upper - lower) / mid
return mid, upper, lower, width
def macd(close: pd.Series, fast: int, slow: int, signal: int):
fast_ema = ema(close, fast)
slow_ema = ema(close, slow)
line = fast_ema - slow_ema
sig = ema(line, signal)
hist = line - sig
return line, sig, hist
def stochastic(high: pd.Series, low: pd.Series, close: pd.Series,
k_period: int = 14, d_period: int = 3):
"""Stochastic Oscillator (%K and %D)."""
lowest = low.rolling(window=k_period, min_periods=k_period).min()
highest = high.rolling(window=k_period, min_periods=k_period).max()
denom = highest - lowest
k = 100.0 * (close - lowest) / denom.replace(0.0, np.nan)
d = k.rolling(window=d_period, min_periods=d_period).mean()
return k, d
def cci(high: pd.Series, low: pd.Series, close: pd.Series,
period: int = 20) -> pd.Series:
"""Commodity Channel Index."""
tp = (high + low + close) / 3.0
sma = tp.rolling(window=period, min_periods=period).mean()
mad = tp.rolling(window=period, min_periods=period).apply(
lambda x: np.mean(np.abs(x - np.mean(x))), raw=True
)
return (tp - sma) / (0.015 * mad.replace(0.0, np.nan))
def adx(high: pd.Series, low: pd.Series, close: pd.Series,
period: int = 14) -> pd.Series:
"""Average Directional Index (returns ADX line only)."""
up_move = high.diff()
down_move = -low.diff()
plus_dm = pd.Series(np.where((up_move > down_move) & (up_move > 0), up_move, 0.0),
index=high.index)
minus_dm = pd.Series(np.where((down_move > up_move) & (down_move > 0), down_move, 0.0),
index=high.index)
atr_val = atr(high, low, close, period)
plus_di = 100.0 * plus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr_val.replace(0.0, np.nan)
minus_di = 100.0 * minus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr_val.replace(0.0, np.nan)
dx = 100.0 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0.0, np.nan)
adx_line = dx.ewm(alpha=1 / period, adjust=False).mean()
return adx_line
def keltner_channel(high: pd.Series, low: pd.Series, close: pd.Series,
ema_period: int = 20, atr_period: int = 14, atr_mult: float = 1.5):
"""Keltner Channel (mid, upper, lower)."""
mid = ema(close, ema_period)
atr_val = atr(high, low, close, atr_period)
upper = mid + atr_mult * atr_val
lower = mid - atr_mult * atr_val
return mid, upper, lower

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -1,199 +0,0 @@
"""Run Bollinger Band mean-reversion backtest on ETH 2023+2024.
Preloads data once, then sweeps parameters in-memory for speed.
"""
import sys, time
sys.stdout.reconfigure(line_buffering=True)
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parents[1]))
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path
from collections import defaultdict
from strategy.bb_backtest import BBConfig, run_bb_backtest
from strategy.data_loader import KlineSource, load_klines
from datetime import datetime, timezone
root = Path(__file__).resolve().parents[1]
src = KlineSource(db_path=root / "models" / "database.db", table_name="bitmart_eth_5m")
out_dir = root / "strategy" / "results"
out_dir.mkdir(parents=True, exist_ok=True)
t0 = time.time()
# Preload data once
print("Loading data...")
df_23 = load_klines(src, datetime(2023,1,1,tzinfo=timezone.utc),
datetime(2023,12,31,23,59,tzinfo=timezone.utc))
df_24 = load_klines(src, datetime(2024,1,1,tzinfo=timezone.utc),
datetime(2024,12,31,23,59,tzinfo=timezone.utc))
data = {2023: df_23, 2024: df_24}
print(f"Loaded: 2023={len(df_23)} bars, 2024={len(df_24)} bars ({time.time()-t0:.1f}s)")
# ================================================================
# Sweep
# ================================================================
print("\n" + "=" * 120)
print(" Bollinger Band Mean-Reversion — ETH 5min | 1000U capital")
print(" touch upper BB -> short, touch lower BB -> long (flip)")
print("=" * 120)
results = []
def test(label, cfg):
"""Run on both years, print summary, store results."""
row = {"label": label, "cfg": cfg}
for year in [2023, 2024]:
r = run_bb_backtest(data[year], cfg)
d = r.daily_stats
pnl = d["pnl"].astype(float)
eq = d["equity"].astype(float)
dd = float((eq - eq.cummax()).min())
final = float(eq.iloc[-1])
nt = len(r.trades)
wr = sum(1 for t in r.trades if t.net_pnl > 0) / max(nt, 1) * 100
nf = r.total_fee - r.total_rebate
row[f"a{year}"] = float(pnl.mean())
row[f"d{year}"] = dd
row[f"r{year}"] = r
row[f"n{year}"] = nt
row[f"w{year}"] = wr
row[f"f{year}"] = nf
row[f"eq{year}"] = final
mn = min(row["a2023"], row["a2024"])
avg = (row["a2023"] + row["a2024"]) / 2
mark = " <<<" if mn >= 20 else (" **" if mn >= 10 else "")
print(f" {label:52s} 23:{row['a2023']:+6.1f} 24:{row['a2024']:+6.1f} "
f"avg:{avg:+5.1f} n23:{row['n2023']:3d} n24:{row['n2024']:3d} "
f"dd:{min(row['d2023'],row['d2024']):+7.0f}{mark}")
row["mn"] = mn; row["avg"] = avg
results.append(row)
# [1] BB period
print("\n[1] Period sweep")
for p in [10, 15, 20, 30, 40]:
test(f"BB({p},2.0) 80u 100x", BBConfig(bb_period=p, bb_std=2.0, margin_per_trade=80, leverage=100))
# [2] BB std
print("\n[2] Std sweep")
for s in [1.5, 1.8, 2.0, 2.5, 3.0]:
test(f"BB(20,{s}) 80u 100x", BBConfig(bb_period=20, bb_std=s, margin_per_trade=80, leverage=100))
# [3] Margin
print("\n[3] Margin sweep")
for m in [40, 60, 80, 100, 120]:
test(f"BB(20,2.0) {m}u 100x", BBConfig(bb_period=20, bb_std=2.0, margin_per_trade=m, leverage=100))
# [4] SL
print("\n[4] Stop-loss sweep")
for sl in [0.0, 0.01, 0.02, 0.03, 0.05]:
test(f"BB(20,2.0) 80u SL={sl:.0%}", BBConfig(bb_period=20, bb_std=2.0, margin_per_trade=80, leverage=100, stop_loss_pct=sl))
# [5] MDL
print("\n[5] Max daily loss")
for mdl in [50, 100, 150, 200]:
test(f"BB(20,2.0) 80u mdl={mdl}", BBConfig(bb_period=20, bb_std=2.0, margin_per_trade=80, leverage=100, max_daily_loss=mdl))
# [6] Combined fine-tune
print("\n[6] Fine-tune")
for p in [15, 20, 30]:
for s in [1.5, 2.0, 2.5]:
for m in [80, 100]:
test(f"BB({p},{s}) {m}u mdl=150",
BBConfig(bb_period=p, bb_std=s, margin_per_trade=m, leverage=100, max_daily_loss=150))
# ================================================================
# Ranking
# ================================================================
results.sort(key=lambda x: x["mn"], reverse=True)
print(f"\n{'='*120}")
print(f" TOP 10 — ranked by min(daily_avg_2023, daily_avg_2024)")
print(f"{'='*120}")
for i, r in enumerate(results[:10]):
print(f" {i+1:2d}. {r['label']:50s} 23:{r['a2023']:+6.1f} 24:{r['a2024']:+6.1f} "
f"min:{r['mn']:+6.1f} dd:{min(r['d2023'],r['d2024']):+7.0f} "
f"wr23:{r['w2023']:.0f}% wr24:{r['w2024']:.0f}%")
# ================================================================
# Detailed report for best
# ================================================================
best = results[0]
print(f"\n{'#'*70}")
print(f" BEST: {best['label']}")
print(f"{'#'*70}")
for year in [2023, 2024]:
r = best[f"r{year}"]
cfg = best["cfg"]
d = r.daily_stats
pnl = d["pnl"].astype(float)
eq = d["equity"].astype(float)
dd = (eq - eq.cummax()).min()
final = float(eq.iloc[-1])
nt = len(r.trades)
wr = sum(1 for t in r.trades if t.net_pnl > 0) / max(nt, 1)
nf = r.total_fee - r.total_rebate
loss_streak = max_ls = 0
for v in pnl.values:
if v < 0: loss_streak += 1; max_ls = max(max_ls, loss_streak)
else: loss_streak = 0
print(f"\n --- {year} ---")
print(f" Final equity : {final:,.2f} U ({final-cfg.initial_capital:+,.2f}, "
f"{(final-cfg.initial_capital)/cfg.initial_capital*100:+.1f}%)")
print(f" Max drawdown : {dd:,.2f} U")
print(f" Avg daily PnL : {pnl.mean():+,.2f} U")
print(f" Median daily PnL : {pnl.median():+,.2f} U")
print(f" Best/worst day : {pnl.max():+,.2f} / {pnl.min():+,.2f}")
print(f" Profitable days : {(pnl>0).sum()}/{len(pnl)} ({(pnl>0).mean():.1%})")
print(f" Days >= 20U : {(pnl>=20).sum()}")
print(f" Max loss streak : {max_ls} days")
print(f" Trades : {nt} (win rate {wr:.1%})")
print(f" Net fees : {nf:,.0f} U")
sharpe = pnl.mean() / max(pnl.std(), 1e-10) * np.sqrt(365)
print(f" Sharpe (annual) : {sharpe:.2f}")
# ================================================================
# Chart
# ================================================================
fig, axes = plt.subplots(3, 2, figsize=(18, 12),
gridspec_kw={"height_ratios": [3, 1.5, 1]})
for col, year in enumerate([2023, 2024]):
r = best[f"r{year}"]
cfg = best["cfg"]
d = r.daily_stats
eq = d["equity"].astype(float)
pnl = d["pnl"].astype(float)
dd = eq - eq.cummax()
axes[0, col].plot(eq.index, eq.values, linewidth=1.2, color="#1f77b4")
axes[0, col].axhline(cfg.initial_capital, color="gray", ls="--", lw=0.5)
axes[0, col].set_title(f"BB Strategy Equity — {year}\n"
f"BB({cfg.bb_period},{cfg.bb_std}) {cfg.margin_per_trade}u {cfg.leverage:.0f}x",
fontsize=11)
axes[0, col].set_ylabel("Equity (U)")
axes[0, col].grid(True, alpha=0.3)
colors = ["#2ca02c" if v >= 0 else "#d62728" for v in pnl.values]
axes[1, col].bar(pnl.index, pnl.values, color=colors, width=0.8)
axes[1, col].axhline(20, color="orange", ls="--", lw=1, label="20U target")
axes[1, col].axhline(0, color="gray", lw=0.5)
axes[1, col].set_ylabel("Daily PnL (U)")
axes[1, col].legend(fontsize=8)
axes[1, col].grid(True, alpha=0.3)
axes[2, col].fill_between(dd.index, dd.values, 0, color="#d62728", alpha=0.4)
axes[2, col].set_ylabel("Drawdown (U)")
axes[2, col].grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig(out_dir / "bb_strategy_report.png", dpi=150)
plt.close(fig)
print(f"\nChart: {out_dir / 'bb_strategy_report.png'}")
print(f"Total time: {time.time()-t0:.0f}s")

View File

@@ -1,125 +0,0 @@
"""
信号生成模块 - 多指标加权投票 + 多时间框架过滤
"""
import numpy as np
import pandas as pd
def generate_indicator_signals(df: pd.DataFrame, params: dict) -> pd.DataFrame:
"""
根据指标值生成每个指标的独立信号 (+1 做多 / -1 做空 / 0 中性)
df 必须已经包含 compute_all_indicators 计算出的列
"""
out = df.copy()
# --- 布林带 %B ---
out['sig_bb'] = 0
out.loc[out['bb_pct'] < params.get('bb_oversold', 0.0), 'sig_bb'] = 1
out.loc[out['bb_pct'] > params.get('bb_overbought', 1.0), 'sig_bb'] = -1
# --- 肯特纳通道 ---
out['sig_kc'] = 0
out.loc[out['kc_pct'] < params.get('kc_oversold', 0.0), 'sig_kc'] = 1
out.loc[out['kc_pct'] > params.get('kc_overbought', 1.0), 'sig_kc'] = -1
# --- 唐奇安通道 ---
out['sig_dc'] = 0
out.loc[out['dc_pct'] < params.get('dc_oversold', 0.2), 'sig_dc'] = 1
out.loc[out['dc_pct'] > params.get('dc_overbought', 0.8), 'sig_dc'] = -1
# --- EMA 交叉 ---
out['sig_ema'] = 0
out.loc[out['ema_diff'] > 0, 'sig_ema'] = 1
out.loc[out['ema_diff'] < 0, 'sig_ema'] = -1
# --- MACD ---
out['sig_macd'] = 0
out.loc[out['macd_hist'] > 0, 'sig_macd'] = 1
out.loc[out['macd_hist'] < 0, 'sig_macd'] = -1
# --- ADX + DI ---
adx_thresh = params.get('adx_threshold', 25)
out['sig_adx'] = 0
out.loc[(out['adx'] > adx_thresh) & (out['di_diff'] > 0), 'sig_adx'] = 1
out.loc[(out['adx'] > adx_thresh) & (out['di_diff'] < 0), 'sig_adx'] = -1
# --- Supertrend ---
out['sig_st'] = out['st_dir']
# --- RSI ---
rsi_ob = params.get('rsi_overbought', 70)
rsi_os = params.get('rsi_oversold', 30)
out['sig_rsi'] = 0
out.loc[out['rsi'] < rsi_os, 'sig_rsi'] = 1
out.loc[out['rsi'] > rsi_ob, 'sig_rsi'] = -1
# --- Stochastic ---
stoch_ob = params.get('stoch_overbought', 80)
stoch_os = params.get('stoch_oversold', 20)
out['sig_stoch'] = 0
out.loc[(out['stoch_k'] < stoch_os) & (out['stoch_k'] > out['stoch_d']), 'sig_stoch'] = 1
out.loc[(out['stoch_k'] > stoch_ob) & (out['stoch_k'] < out['stoch_d']), 'sig_stoch'] = -1
# --- CCI ---
cci_ob = params.get('cci_overbought', 100)
cci_os = params.get('cci_oversold', -100)
out['sig_cci'] = 0
out.loc[out['cci'] < cci_os, 'sig_cci'] = 1
out.loc[out['cci'] > cci_ob, 'sig_cci'] = -1
# --- Williams %R ---
wr_ob = params.get('wr_overbought', -20)
wr_os = params.get('wr_oversold', -80)
out['sig_wr'] = 0
out.loc[out['wr'] < wr_os, 'sig_wr'] = 1
out.loc[out['wr'] > wr_ob, 'sig_wr'] = -1
# --- WMA ---
out['sig_wma'] = 0
out.loc[out['wma_diff'] > 0, 'sig_wma'] = 1
out.loc[out['wma_diff'] < 0, 'sig_wma'] = -1
return out
SIGNAL_COLS = [
'sig_bb', 'sig_kc', 'sig_dc', 'sig_ema', 'sig_macd',
'sig_adx', 'sig_st', 'sig_rsi', 'sig_stoch', 'sig_cci',
'sig_wr', 'sig_wma',
]
WEIGHT_KEYS = [
'w_bb', 'w_kc', 'w_dc', 'w_ema', 'w_macd',
'w_adx', 'w_st', 'w_rsi', 'w_stoch', 'w_cci',
'w_wr', 'w_wma',
]
def compute_composite_score(df: pd.DataFrame, params: dict) -> pd.Series:
"""
加权投票计算综合得分 (-1 ~ +1)
"""
weights = np.array([params.get(k, 1.0) for k in WEIGHT_KEYS])
total_w = weights.sum()
if total_w == 0:
total_w = 1.0
signals = df[SIGNAL_COLS].values # (N, 12)
score = (signals * weights).sum(axis=1) / total_w
return pd.Series(score, index=df.index, name='score')
def apply_htf_filter(score: pd.Series, htf_df: pd.DataFrame, params: dict) -> pd.Series:
"""
用高时间框架如1h的趋势方向过滤信号
htf_df 需要包含 'ema_diff'
只允许与大趋势同向的信号通过
"""
# 将 htf 的 ema_diff reindex 到主时间框架
htf_trend = htf_df['ema_diff'].reindex(score.index, method='ffill')
filtered = score.copy()
# 大趋势向上时,屏蔽做空信号
filtered.loc[(htf_trend > 0) & (filtered < 0)] = 0
# 大趋势向下时,屏蔽做多信号
filtered.loc[(htf_trend < 0) & (filtered > 0)] = 0
return filtered

View File

@@ -1,226 +0,0 @@
"""
Optuna 训练入口 - 在 2020-2022 数据上搜索最优参数
"""
import json
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import optuna
from optuna.samplers import TPESampler
import numpy as np
from strategy.data_loader import load_klines
from strategy.indicators import compute_all_indicators
from strategy.strategy_signal import (
generate_indicator_signals, compute_composite_score,
apply_htf_filter, WEIGHT_KEYS,
)
from strategy.backtest_engine import BacktestEngine
# ============================================================
# 全局加载数据 (只加载一次)
# ============================================================
print("正在加载 2020-2022 训练数据...")
DF_5M = load_klines('5m', '2020-01-01', '2023-01-01')
DF_1H = load_klines('1h', '2020-01-01', '2023-01-01')
print(f" 5m: {len(DF_5M)} 条, 1h: {len(DF_1H)}")
print("数据加载完成。\n")
def build_params(trial: optuna.Trial) -> dict:
"""从 Optuna trial 构建完整参数字典"""
p = {}
# --- 指标参数 ---
p['bb_period'] = trial.suggest_int('bb_period', 10, 50)
p['bb_std'] = trial.suggest_float('bb_std', 1.0, 3.5, step=0.1)
p['kc_period'] = trial.suggest_int('kc_period', 10, 50)
p['kc_mult'] = trial.suggest_float('kc_mult', 0.5, 3.0, step=0.1)
p['dc_period'] = trial.suggest_int('dc_period', 10, 50)
p['ema_fast'] = trial.suggest_int('ema_fast', 3, 20)
p['ema_slow'] = trial.suggest_int('ema_slow', 15, 60)
p['macd_fast'] = trial.suggest_int('macd_fast', 6, 20)
p['macd_slow'] = trial.suggest_int('macd_slow', 18, 40)
p['macd_signal'] = trial.suggest_int('macd_signal', 5, 15)
p['adx_period'] = trial.suggest_int('adx_period', 7, 30)
p['st_period'] = trial.suggest_int('st_period', 5, 20)
p['st_mult'] = trial.suggest_float('st_mult', 1.0, 5.0, step=0.1)
p['rsi_period'] = trial.suggest_int('rsi_period', 7, 28)
p['stoch_k'] = trial.suggest_int('stoch_k', 5, 21)
p['stoch_d'] = trial.suggest_int('stoch_d', 2, 7)
p['stoch_smooth'] = trial.suggest_int('stoch_smooth', 2, 7)
p['cci_period'] = trial.suggest_int('cci_period', 10, 40)
p['wr_period'] = trial.suggest_int('wr_period', 7, 28)
p['wma_period'] = trial.suggest_int('wma_period', 10, 50)
# --- 信号阈值参数 ---
p['bb_oversold'] = trial.suggest_float('bb_oversold', -0.3, 0.3, step=0.05)
p['bb_overbought'] = trial.suggest_float('bb_overbought', 0.7, 1.3, step=0.05)
p['kc_oversold'] = trial.suggest_float('kc_oversold', -0.3, 0.3, step=0.05)
p['kc_overbought'] = trial.suggest_float('kc_overbought', 0.7, 1.3, step=0.05)
p['dc_oversold'] = trial.suggest_float('dc_oversold', 0.0, 0.3, step=0.05)
p['dc_overbought'] = trial.suggest_float('dc_overbought', 0.7, 1.0, step=0.05)
p['adx_threshold'] = trial.suggest_float('adx_threshold', 15, 35, step=1)
p['rsi_overbought'] = trial.suggest_float('rsi_overbought', 60, 85, step=1)
p['rsi_oversold'] = trial.suggest_float('rsi_oversold', 15, 40, step=1)
p['stoch_overbought'] = trial.suggest_float('stoch_overbought', 70, 90, step=1)
p['stoch_oversold'] = trial.suggest_float('stoch_oversold', 10, 30, step=1)
p['cci_overbought'] = trial.suggest_float('cci_overbought', 80, 200, step=5)
p['cci_oversold'] = trial.suggest_float('cci_oversold', -200, -80, step=5)
p['wr_overbought'] = trial.suggest_float('wr_overbought', -30, -10, step=1)
p['wr_oversold'] = trial.suggest_float('wr_oversold', -90, -70, step=1)
# --- 权重 ---
for wk in WEIGHT_KEYS:
p[wk] = trial.suggest_float(wk, 0.0, 1.0, step=0.05)
# --- 回测参数 ---
p['open_threshold'] = trial.suggest_float('open_threshold', 0.1, 0.6, step=0.02)
p['max_positions'] = trial.suggest_int('max_positions', 1, 3)
p['take_profit_pct'] = trial.suggest_float('take_profit_pct', 0.003, 0.025, step=0.001)
# 止损约束: N单同时止损 + 手续费 <= 50U
# N * 1250 * sl_pct + N * 1.25 <= 50
# sl_pct <= (50 - N*1.25) / (N*1250)
n = p['max_positions']
max_sl = (50.0 - n * 1.25) / (n * 1250.0)
max_sl = round(max(max_sl, 0.002), 3) # 至少 0.2%
p['stop_loss_pct'] = trial.suggest_float('stop_loss_pct', 0.002, max_sl, step=0.001)
return p
def objective(trial: optuna.Trial) -> float:
params = build_params(trial)
# 确保 ema_slow > ema_fast, macd_slow > macd_fast
if params['ema_slow'] <= params['ema_fast']:
return -1e6
if params['macd_slow'] <= params['macd_fast']:
return -1e6
try:
# 计算指标
df_5m = compute_all_indicators(DF_5M, params)
df_1h = compute_all_indicators(DF_1H, params)
# 生成信号
df_5m = generate_indicator_signals(df_5m, params)
df_1h = generate_indicator_signals(df_1h, params)
# 综合得分
score = compute_composite_score(df_5m, params)
# 高时间框架过滤
score = apply_htf_filter(score, df_1h, params)
# 回测
engine = BacktestEngine(
initial_capital=1000.0,
margin_per_trade=25.0,
leverage=50,
fee_rate=0.0005,
rebate_ratio=0.70,
max_daily_drawdown=50.0,
min_hold_bars=1,
stop_loss_pct=params['stop_loss_pct'],
take_profit_pct=params['take_profit_pct'],
max_positions=params['max_positions'],
)
result = engine.run(df_5m, score, open_threshold=params['open_threshold'])
num_trades = result['num_trades']
if num_trades < 50:
return -1e6 # 交易次数太少,不可靠
total_pnl = result['total_pnl']
max_dd = result['max_daily_dd'] # 负数 (引擎已保证 >= -50)
avg_daily = result['avg_daily_pnl']
# 引擎内部已经有每日 50U 回撤熔断,这里不再硬约束
# 目标: 最大化总收益
score_val = total_pnl
# 奖励日均收益高的方案
if avg_daily >= 50:
score_val *= 1.3
elif avg_daily >= 30:
score_val *= 1.15
trial.set_user_attr('total_pnl', total_pnl)
trial.set_user_attr('num_trades', num_trades)
trial.set_user_attr('win_rate', result['win_rate'])
trial.set_user_attr('max_daily_dd', max_dd)
trial.set_user_attr('avg_daily_pnl', avg_daily)
trial.set_user_attr('profit_factor', result['profit_factor'])
return score_val
except Exception as e:
print(f"Trial {trial.number} 异常: {e}")
return -1e6
def main():
study = optuna.create_study(
direction='maximize',
sampler=TPESampler(seed=42, n_startup_trials=30),
study_name='eth_strategy_v1',
)
# 设置日志级别
optuna.logging.set_verbosity(optuna.logging.WARNING)
n_trials = 1000
print(f"开始 Optuna 优化, 共 {n_trials} 次试验 (多单并发版)...")
print("=" * 60)
def callback(study, trial):
if trial.number % 10 == 0:
best = study.best_trial
print(f"[Trial {trial.number:>4d}] "
f"当前值={trial.value:.2f} | "
f"最佳值={best.value:.2f} | "
f"PnL={best.user_attrs.get('total_pnl', 0):.1f}U | "
f"胜率={best.user_attrs.get('win_rate', 0):.1%} | "
f"日均={best.user_attrs.get('avg_daily_pnl', 0):.1f}U | "
f"最大日回撤={best.user_attrs.get('max_daily_dd', 0):.1f}U")
study.optimize(objective, n_trials=n_trials, callbacks=[callback], show_progress_bar=True)
# 输出最佳结果
best = study.best_trial
print("\n" + "=" * 60)
print("训练完成!最佳参数:")
print("=" * 60)
print(f" 目标值: {best.value:.4f}")
print(f" 总收益: {best.user_attrs.get('total_pnl', 0):.2f}U")
print(f" 交易次数: {best.user_attrs.get('num_trades', 0)}")
print(f" 胜率: {best.user_attrs.get('win_rate', 0):.2%}")
print(f" 日均收益: {best.user_attrs.get('avg_daily_pnl', 0):.2f}U")
print(f" 最大日回撤: {best.user_attrs.get('max_daily_dd', 0):.2f}U")
print(f" 盈亏比: {best.user_attrs.get('profit_factor', 0):.2f}")
# 保存最佳参数
output_path = os.path.join(os.path.dirname(__file__), 'best_params_2020_2022.json')
with open(output_path, 'w') as f:
json.dump(best.params, f, indent=2, ensure_ascii=False)
print(f"\n最佳参数已保存到: {output_path}")
if __name__ == '__main__':
main()

38
test.py Normal file
View File

@@ -0,0 +1,38 @@
import subprocess
import time
from wxautox4 import WeChat
def make_wechat_call(contact_name, retry_times=3):
"""
可靠的微信拨电话方法
Args:
contact_name: 联系人名称
retry_times: 重试次数
"""
for attempt in range(retry_times):
try:
# 打开微信应用
subprocess.Popen(['/Applications/WeChat.app/Contents/MacOS/WeChat'])
time.sleep(2) # 等待微信启动
# 初始化
wx = WeChat()
time.sleep(1)
# 切换聊天
wx.ChatWith(contact_name)
time.sleep(1)
print(f"✓ 已切换到 {contact_name} 聊天")
return True
except Exception as e:
print(f"⚠ 第 {attempt+1} 次尝试失败: {e}")
if attempt < retry_times - 1:
time.sleep(1)
return False
# 使用
make_wechat_call('Rainbow')

1
token.json Normal file
View File

@@ -0,0 +1 @@
{"token": "ya29.a0ATkoCc7EYaXMumpShCwmfiq5pqSbgMB_XkUuWWUFnYRs8fvfPt6xYaU3r8rrdiFWHpQ_07R2FN_ML4X-4Q9eoHp5Dj6R0xgL1A9xFONvaJhlEP_ndce3Uht6LpeIUhfoGw2fHU9ZlvKcVNyCfcGsTqsR8YXWoe49MmU3k-DiHQgIDrxZ0c7Kyfb0FltZ9bUwXJg8ILIaCgYKAYUSARMSFQHGX2MiD7gOuz1kuvvNQABqx39YNw0206", "refresh_token": "1//0gVVN8c-x-T1MCgYIARAAGBASNwF-L9IrNPuDnHu-3vZoJvPChjsyjrUwtlSX7qmY_xnwFUr0FPf-t7Tm53f3vSo4OZOaJWSqA0s", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "823839778551-mgovppr13aoil1r69upj7uiv84ej0sih.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/gmail.readonly"], "universe_domain": "googleapis.com", "account": "", "expiry": "2026-02-28T19:35:00Z"}

View File

@@ -0,0 +1,686 @@
import time
from tqdm import tqdm
from loguru import logger
from bit_tools import openBrowser
from DrissionPage import ChromiumPage
from DrissionPage import ChromiumOptions
from bitmart.api_contract import APIContract
class BitmartFuturesTransaction:
def __init__(self, bit_id):
self.page: ChromiumPage | None = None
self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
self.memo = "合约交易"
self.contract_symbol = "ETHUSDT"
self.contractAPI = APIContract(self.api_key, self.secret_key, self.memo, timeout=(5, 15))
self.start = 0 # 持仓状态: -1 空, 0 无, 1 多
self.pbar = tqdm(total=30, desc="等待K线", ncols=80) # 可选:用于长时间等待时展示进度
self.last_kline_time = None # 上一次处理的K线时间戳用于判断是否是新K线
# 反手频率控制
self.reverse_cooldown_seconds = 1.5 * 60 # 反手冷却时间(秒)
self.reverse_min_move_pct = 0.05 # 反手最小价差过滤(百分比)
self.last_reverse_time = None # 上次反手时间
# 开仓频率控制
self.open_cooldown_seconds = 60 # 开仓冷却时间(秒),两次开仓至少间隔此时长
self.last_open_time = None # 上次开仓时间
self.last_open_kline_id = None # 上次开仓所在 K 线 id同一根 K 线只允许开仓一次
self.leverage = "100" # 高杠杆(全仓模式下可开更大仓位)
self.open_type = "cross" # 全仓模式
self.risk_percent = 0 # 未使用;若启用则可为每次开仓占可用余额的百分比
self.take_profit_usd = 5 # 仓位盈利达到此金额(美元)时平仓止盈
self.stop_loss_usd = -3 # 固定止损:亏损达到 3 美元平仓
self.trailing_activation_usd = 2 # 盈利达到此金额后启动移动止损
self.trailing_distance_usd = 1.5 # 从最高盈利回撤此金额则平仓
self.max_unrealized_pnl_seen = None # 持仓期间见过的最大盈利(用于移动止损)
self.open_avg_price = None # 开仓价格
self.current_amount = None # 持仓量
self.bit_id = bit_id
self.default_order_size = 25 # 开仓/反手张数,统一在此修改
# 策略相关变量
self.prev_kline = None # 上一根K线
self.current_kline = None # 当前K线
self.prev_entity = None # 上一根K线实体大小
self.current_open = None # 当前K线开盘价
def get_klines(self):
"""获取最近2根K线当前K线和上一根K线"""
try:
end_time = int(time.time())
# 获取足够多的条目确保有最新的K线
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=5, # 5分钟
start_time=end_time - 3600 * 3, # 取最近3小时
end_time=end_time
)[0]["data"]
# 每根: [timestamp, open, high, low, close, volume]
formatted = []
for k in response:
formatted.append({
'id': int(k["timestamp"]),
'open': float(k["open_price"]),
'high': float(k["high_price"]),
'low': float(k["low_price"]),
'close': float(k["close_price"])
})
formatted.sort(key=lambda x: x['id'])
# 返回最近2根K线倒数第二根上一根和最后一根当前
if len(formatted) >= 2:
return formatted[-2], formatted[-1]
return None, None
except Exception as e:
logger.error(f"获取K线异常: {e}")
self.ding(text="获取K线异常", error=True)
return None, None
def get_current_price(self):
"""获取当前最新价格"""
try:
end_time = int(time.time())
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1, # 1分钟
start_time=end_time - 3600 * 1, # 取最近1小时
end_time=end_time
)[0]
if response['code'] == 1000:
return float(response['data'][-1]["close_price"])
return None
except Exception as e:
logger.error(f"获取价格异常: {e}")
return None
def get_available_balance(self):
"""获取合约账户可用USDT余额"""
try:
response = self.contractAPI.get_assets_detail()[0]
if response['code'] == 1000:
data = response['data']
if isinstance(data, dict):
return float(data.get('available_balance', 0))
elif isinstance(data, list):
for asset in data:
if asset.get('currency') == 'USDT':
return float(asset.get('available_balance', 0))
return None
except Exception as e:
logger.error(f"余额查询异常: {e}")
return None
def get_position_status(self):
"""获取当前持仓方向"""
try:
response = self.contractAPI.get_position(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
positions = response['data']
if not positions:
self.start = 0
self.open_avg_price = None
self.current_amount = None
self.unrealized_pnl = None
return True
pos = positions[0]
self.start = 1 if pos['position_type'] == 1 else -1
self.open_avg_price = float(pos['open_avg_price'])
self.current_amount = float(pos['current_amount'])
self.position_cross = pos["position_cross"]
# 直接从API获取未实现盈亏Bitmart返回的是 unrealized_value 字段)
self.unrealized_pnl = float(pos.get('unrealized_value', 0))
logger.debug(f"持仓详情: 方向={self.start}, 开仓均价={self.open_avg_price}, "
f"持仓量={self.current_amount}, 未实现盈亏={self.unrealized_pnl:.2f}")
return True
else:
return False
except Exception as e:
logger.error(f"持仓查询异常: {e}")
return False
def get_unrealized_pnl_usd(self):
"""
获取当前持仓未实现盈亏美元直接使用API返回值
"""
if self.start == 0 or self.unrealized_pnl is None:
return None
return self.unrealized_pnl
def set_leverage(self):
"""程序启动时设置全仓 + 高杠杆"""
try:
response = self.contractAPI.post_submit_leverage(
contract_symbol=self.contract_symbol,
leverage=self.leverage,
open_type=self.open_type
)[0]
if response['code'] == 1000:
logger.success(f"全仓模式 + {self.leverage}x 杠杆设置成功")
return True
else:
logger.error(f"杠杆设置失败: {response}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
def openBrowser(self):
"""打开 TGE 对应浏览器实例"""
try:
bit_port = openBrowser(id=self.bit_id)
co = ChromiumOptions()
co.set_local_port(port=bit_port)
self.page = ChromiumPage(addr_or_opts=co)
return True
except:
return False
def click_safe(self, xpath, sleep=0.5):
"""安全点击"""
try:
ele = self.page.ele(xpath)
if not ele:
return False
# ele.scroll.to_see(center=True)
# time.sleep(sleep)
ele.click(by_js=True)
return True
except:
return False
def 平仓(self):
"""平仓操作"""
self.click_safe('x://span[normalize-space(text()) ="市价"]')
def 开单(self, marketPriceLongOrder=0, limitPriceShortOrder=0, size=None, price=None):
"""
marketPriceLongOrder 市价做多或者做空1是做多-1是做空
limitPriceShortOrder 限价做多或者做空
"""
if marketPriceLongOrder == -1:
# self.click_safe('x://button[normalize-space(text()) ="市价"]')
# self.page.ele('x://*[@id="size_0"]').input(vals=size, clear=True)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif marketPriceLongOrder == 1:
# self.click_safe('x://button[normalize-space(text()) ="市价"]')
# self.page.ele('x://*[@id="size_0"]').input(vals=size, clear=True)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
if limitPriceShortOrder == -1:
self.click_safe('x://button[normalize-space(text()) ="限价"]')
self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True)
time.sleep(1)
self.page.ele('x://*[@id="size_0"]').input(1)
self.click_safe('x://span[normalize-space(text()) ="卖出/做空"]')
elif limitPriceShortOrder == 1:
self.click_safe('x://button[normalize-space(text()) ="限价"]')
self.page.ele('x://*[@id="price_0"]').input(vals=price, clear=True)
time.sleep(1)
self.page.ele('x://*[@id="size_0"]').input(1)
self.click_safe('x://span[normalize-space(text()) ="买入/做多"]')
def ding(self, text, error=False):
"""日志通知"""
if error:
logger.error(text)
else:
logger.info(text)
def calculate_entity(self, kline):
"""计算K线实体大小绝对值"""
return abs(kline['close'] - kline['open'])
def calculate_upper_shadow(self, kline):
"""计算上阴线(上影线)涨幅百分比"""
# 上阴线 = (最高价 - max(开盘价, 收盘价)) / max(开盘价, 收盘价)
body_top = max(kline['open'], kline['close'])
if body_top == 0:
return 0
return (kline['high'] - body_top) / body_top * 100
def calculate_lower_shadow(self, kline):
"""计算下阴线(下影线)跌幅百分比"""
# 下阴线 = (min(开盘价, 收盘价) - 最低价) / min(开盘价, 收盘价)
body_bottom = min(kline['open'], kline['close'])
if body_bottom == 0:
return 0
return (body_bottom - kline['low']) / body_bottom * 100
def get_entity_edge(self, kline):
"""获取K线实体边收盘价或开盘价取决于是阳线还是阴线"""
# 阳线(收盘>开盘):实体上边=收盘价,实体下边=开盘价
# 阴线(收盘<开盘):实体上边=开盘价,实体下边=收盘价
return {
'upper': max(kline['open'], kline['close']), # 实体上边
'lower': min(kline['open'], kline['close']) # 实体下边
}
def check_signal(self, current_price, prev_kline, current_kline):
"""
检查交易信号
返回: ('long', trigger_price) / ('short', trigger_price) / None
"""
# 计算上一根K线实体
prev_entity = self.calculate_entity(prev_kline)
# 实体过小不交易(实体 < 0.1
if prev_entity < 0.1:
logger.info(f"上一根K线实体过小: {prev_entity:.4f},跳过信号检测")
return None
# 获取上一根K线的实体上下边
prev_entity_edge = self.get_entity_edge(prev_kline)
prev_entity_upper = prev_entity_edge['upper'] # 实体上边
prev_entity_lower = prev_entity_edge['lower'] # 实体下边
# 优化:以下两种情况以当前这根的开盘价作为计算基准
# 1) 上一根阳线 且 当前开盘价 > 上一根收盘价(跳空高开)
# 2) 上一根阴线 且 当前开盘价 < 上一根收盘价(跳空低开)
prev_is_bullish_for_calc = prev_kline['close'] > prev_kline['open']
prev_is_bearish_for_calc = prev_kline['close'] < prev_kline['open']
current_open_above_prev_close = current_kline['open'] > prev_kline['close']
current_open_below_prev_close = current_kline['open'] < prev_kline['close']
use_current_open_as_base = (prev_is_bullish_for_calc and current_open_above_prev_close) or (prev_is_bearish_for_calc and current_open_below_prev_close)
if use_current_open_as_base:
# 以当前K线开盘价为基准计算跳空时用当前开盘价参与计算
calc_lower = current_kline['open']
calc_upper = current_kline['open'] # 同一基准,上下四分之一对称
long_trigger = calc_lower + prev_entity / 4
short_trigger = calc_upper - prev_entity / 4
long_breakout = calc_upper + prev_entity / 4
short_breakout = calc_lower - prev_entity / 4
else:
# 原有计算方式
long_trigger = prev_entity_lower + prev_entity / 4 # 做多触发价 = 实体下边 + 实体/4下四分之一处
short_trigger = prev_entity_upper - prev_entity / 4 # 做空触发价 = 实体上边 - 实体/4上四分之一处
long_breakout = prev_entity_upper + prev_entity / 4 # 做多突破价 = 实体上边 + 实体/4
short_breakout = prev_entity_lower - prev_entity / 4 # 做空突破价 = 实体下边 - 实体/4
# 上一根阴线 + 当前阳线做多形态不按上一根K线上三分之一做空
prev_is_bearish = prev_kline['close'] < prev_kline['open']
current_is_bullish = current_kline['close'] > current_kline['open']
skip_short_by_upper_third = prev_is_bearish and current_is_bullish
# 上一根阳线 + 当前阴线做空形态不按上一根K线下三分之一做多
prev_is_bullish = prev_kline['close'] > prev_kline['open']
current_is_bearish = current_kline['close'] < current_kline['open']
skip_long_by_lower_third = prev_is_bullish and current_is_bearish
if use_current_open_as_base:
if prev_is_bullish_for_calc and current_open_above_prev_close:
logger.info(f"上一根阳线且当前开盘价({current_kline['open']:.2f})>上一根收盘价({prev_kline['close']:.2f}),以当前开盘价为基准计算")
else:
logger.info(f"上一根阴线且当前开盘价({current_kline['open']:.2f})<上一根收盘价({prev_kline['close']:.2f}),以当前开盘价为基准计算")
logger.info(f"当前价格: {current_price:.2f}, 上一根实体: {prev_entity:.4f}")
logger.info(f"上一根实体上边: {prev_entity_upper:.2f}, 下边: {prev_entity_lower:.2f}")
logger.info(f"做多触发价(下1/4): {long_trigger:.2f}, 做空触发价(上1/4): {short_trigger:.2f}")
logger.info(f"突破做多价(上1/4外): {long_breakout:.2f}, 突破做空价(下1/4外): {short_breakout:.2f}")
if skip_short_by_upper_third:
logger.info("上一根阴线+当前阳线(做多形态),不按上四分之一做空")
if skip_long_by_lower_third:
logger.info("上一根阳线+当前阴线(做空形态),不按下四分之一做多")
# 无持仓时检查开仓信号
if self.start == 0:
if current_price >= long_breakout and not skip_long_by_lower_third:
logger.info(f"触发做多信号!价格 {current_price:.2f} >= 突破价(上1/4外) {long_breakout:.2f}")
return ('long', long_breakout)
elif current_price <= short_breakout and not skip_short_by_upper_third:
logger.info(f"触发做空信号!价格 {current_price:.2f} <= 突破价(下1/4外) {short_breakout:.2f}")
return ('short', short_breakout)
# 持仓时检查反手信号
elif self.start == 1: # 持多仓
# 反手条件1: 价格跌到上一根K线的上三分之一处做空触发价上一根阴线+当前阳线做多时跳过
if current_price <= short_trigger and not skip_short_by_upper_third:
logger.info(f"持多反手做空!价格 {current_price:.2f} <= 触发价(上1/4) {short_trigger:.2f}")
return ('reverse_short', short_trigger)
# 反手条件2: 上一根K线上阴线涨幅>0.01%,当前跌到上一根实体下边
upper_shadow_pct = self.calculate_upper_shadow(prev_kline)
if upper_shadow_pct > 0.01 and current_price <= prev_entity_lower:
logger.info(f"持多反手做空!上阴线涨幅 {upper_shadow_pct:.4f}% > 0.01%"
f"价格 {current_price:.2f} <= 实体下边 {prev_entity_lower:.2f}")
return ('reverse_short', prev_entity_lower)
elif self.start == -1: # 持空仓
# 反手条件1: 价格涨到上一根K线的下三分之一处做多触发价上一根阳线+当前阴线做空时跳过
if current_price >= long_trigger and not skip_long_by_lower_third:
logger.info(f"持空反手做多!价格 {current_price:.2f} >= 触发价(下1/4) {long_trigger:.2f}")
return ('reverse_long', long_trigger)
# 反手条件2: 上一根K线下阴线跌幅>0.01%,当前涨到上一根实体上边
lower_shadow_pct = self.calculate_lower_shadow(prev_kline)
if lower_shadow_pct > 0.01 and current_price >= prev_entity_upper:
logger.info(f"持空反手做多!下阴线跌幅 {lower_shadow_pct:.4f}% > 0.01%"
f"价格 {current_price:.2f} >= 实体上边 {prev_entity_upper:.2f}")
return ('reverse_long', prev_entity_upper)
return None
def can_open(self, current_kline_id):
"""开仓前过滤:同一根 K 线只开一次 + 开仓冷却时间。仅用于 long/short 新开仓。"""
now = time.time()
if self.last_open_kline_id is not None and self.last_open_kline_id == current_kline_id:
logger.info(f"开仓频率控制:本 K 线({current_kline_id})已开过仓,跳过")
return False
if self.last_open_time is not None and now - self.last_open_time < self.open_cooldown_seconds:
remain = self.open_cooldown_seconds - (now - self.last_open_time)
logger.info(f"开仓冷却中,剩余 {remain:.0f}")
return False
return True
def can_reverse(self, current_price, trigger_price):
"""反手前过滤:冷却时间 + 最小价差"""
now = time.time()
if self.last_reverse_time and now - self.last_reverse_time < self.reverse_cooldown_seconds:
remain = self.reverse_cooldown_seconds - (now - self.last_reverse_time)
logger.info(f"反手冷却中,剩余 {remain:.0f}")
return False
if trigger_price and trigger_price > 0:
move_pct = abs(current_price - trigger_price) / trigger_price * 100
if move_pct < self.reverse_min_move_pct:
logger.info(f"反手价差不足: {move_pct:.4f}% < {self.reverse_min_move_pct}%")
return False
return True
def verify_no_position(self, max_retries=5, retry_interval=3):
"""
验证当前无持仓
返回: True 表示无持仓可以开仓False 表示有持仓不能开仓
"""
for i in range(max_retries):
if self.get_position_status():
if self.start == 0:
logger.info(f"确认无持仓,可以开仓")
return True
else:
logger.warning(
f"仍有持仓 (方向: {self.start}),等待 {retry_interval} 秒后重试 ({i + 1}/{max_retries})")
time.sleep(retry_interval)
else:
logger.warning(f"查询持仓状态失败,等待 {retry_interval} 秒后重试 ({i + 1}/{max_retries})")
time.sleep(retry_interval)
logger.error(f"经过 {max_retries} 次重试仍有持仓或查询失败,放弃开仓")
return False
def verify_position_direction(self, expected_direction):
"""
验证当前持仓方向是否与预期一致
expected_direction: 1 多仓, -1 空仓
返回: True 表示持仓方向正确False 表示不正确
"""
if self.get_position_status():
if self.start == expected_direction:
logger.info(f"持仓方向验证成功: {self.start}")
return True
else:
logger.warning(f"持仓方向不符: 期望 {expected_direction}, 实际 {self.start}")
return False
else:
logger.error("查询持仓状态失败")
return False
def execute_trade(self, signal, size=None):
"""执行交易。size 不传或为 None 时使用 default_order_size。"""
signal_type, trigger_price = signal
size = self.default_order_size if size is None else size
if signal_type == 'long':
# 开多前先确认无持仓
logger.info(f"准备开多,触发价: {trigger_price:.2f}")
if not self.get_position_status():
logger.error("开仓前查询持仓状态失败,放弃开仓")
return False
if self.start != 0:
logger.warning(f"开多前发现已有持仓 (方向: {self.start}),放弃开仓避免双向持仓")
return False
logger.info(f"确认无持仓,执行开多")
self.开单(marketPriceLongOrder=1, size=size)
time.sleep(3) # 等待订单执行
# 验证开仓是否成功
if self.verify_position_direction(1):
self.max_unrealized_pnl_seen = None # 新仓位重置移动止损记录
self.last_open_time = time.time()
self.last_open_kline_id = getattr(self, "_current_kline_id_for_open", None)
logger.success("开多成功")
return True
else:
logger.error("开多后持仓验证失败")
return False
elif signal_type == 'short':
# 开空前先确认无持仓
logger.info(f"准备开空,触发价: {trigger_price:.2f}")
if not self.get_position_status():
logger.error("开仓前查询持仓状态失败,放弃开仓")
return False
if self.start != 0:
logger.warning(f"开空前发现已有持仓 (方向: {self.start}),放弃开仓避免双向持仓")
return False
logger.info(f"确认无持仓,执行开空")
self.开单(marketPriceLongOrder=-1, size=size)
time.sleep(3) # 等待订单执行
# 验证开仓是否成功
if self.verify_position_direction(-1):
self.max_unrealized_pnl_seen = None # 新仓位重置移动止损记录
self.last_open_time = time.time()
self.last_open_kline_id = getattr(self, "_current_kline_id_for_open", None)
logger.success("开空成功")
return True
else:
logger.error("开空后持仓验证失败")
return False
elif signal_type == 'reverse_long':
# 平空 + 开多(反手做多):先平仓,确认无仓后再开多,避免双向持仓
logger.info(f"执行反手做多,触发价: {trigger_price:.2f}")
self.平仓()
time.sleep(1) # 给交易所处理平仓的时间
# 轮询确认已无持仓再开多(最多等约 10 秒)
for _ in range(10):
if self.get_position_status() and self.start == 0:
break
time.sleep(1)
if self.start != 0:
logger.warning("反手做多:平仓后仍有持仓,放弃本次开多")
return False
logger.info("已确认无持仓,执行开多")
self.开单(marketPriceLongOrder=1, size=size)
time.sleep(3)
if self.verify_position_direction(1):
self.max_unrealized_pnl_seen = None
logger.success("反手做多成功")
self.last_reverse_time = time.time()
time.sleep(20)
return True
else:
logger.error("反手做多后持仓验证失败")
return False
elif signal_type == 'reverse_short':
# 平多 + 开空(反手做空):先平仓,确认无仓后再开空
logger.info(f"执行反手做空,触发价: {trigger_price:.2f}")
self.平仓()
time.sleep(1)
for _ in range(10):
if self.get_position_status() and self.start == 0:
break
time.sleep(1)
if self.start != 0:
logger.warning("反手做空:平仓后仍有持仓,放弃本次开空")
return False
logger.info("已确认无持仓,执行开空")
self.开单(marketPriceLongOrder=-1, size=size)
time.sleep(3)
if self.verify_position_direction(-1):
self.max_unrealized_pnl_seen = None
logger.success("反手做空成功")
self.last_reverse_time = time.time()
time.sleep(20)
return True
else:
logger.error("反手做空后持仓验证失败")
return False
return False
def action(self):
"""主循环"""
logger.info("开始运行四分之一策略交易...")
# 启动时设置全仓高杠杆
if not self.set_leverage():
logger.error("杠杆设置失败,程序继续运行但可能下单失败")
return
page_start = True
while True:
if page_start:
# 打开浏览器
for i in range(5):
if self.openBrowser():
logger.info("浏览器打开成功")
break
else:
self.ding("打开浏览器失败!", error=True)
return
# 进入交易页面
self.page.get("https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT")
self.click_safe('x://button[normalize-space(text()) ="市价"]')
self.page.ele('x://*[@id="size_0"]').input(vals=25, clear=True)
page_start = False
try:
# 1. 获取K线数据当前K线和上一根K线
prev_kline, current_kline = self.get_klines()
if not prev_kline or not current_kline:
logger.warning("获取K线失败等待重试...")
time.sleep(5)
continue
# 记录进入新的K线
current_kline_time = current_kline['id']
if self.last_kline_time != current_kline_time:
self.last_kline_time = current_kline_time
logger.info(f"进入新K线: {current_kline_time}")
# 2. 获取当前价格
current_price = self.get_current_price()
if not current_price:
logger.warning("获取价格失败,等待重试...")
time.sleep(2)
continue
# 3. 每次循环都通过SDK获取真实持仓状态避免状态不同步导致双向持仓
if not self.get_position_status():
logger.warning("获取持仓状态失败,等待重试...")
time.sleep(2)
continue
logger.debug(f"当前持仓状态: {self.start} (0=无, 1=多, -1=空)")
# 3.5 止损/止盈/移动止损
if self.start != 0:
pnl_usd = self.get_unrealized_pnl_usd()
if pnl_usd is not None:
# 固定止损:亏损达到 3 美元平仓
if pnl_usd <= self.stop_loss_usd:
logger.info(f"仓位亏损 {pnl_usd:.2f} 美元 <= 止损 {self.stop_loss_usd} 美元,执行止损平仓")
self.平仓()
self.max_unrealized_pnl_seen = None
time.sleep(3)
continue
# 更新持仓期间最大盈利(用于移动止损)
if self.max_unrealized_pnl_seen is None:
self.max_unrealized_pnl_seen = pnl_usd
else:
self.max_unrealized_pnl_seen = max(self.max_unrealized_pnl_seen, pnl_usd)
# 移动止损:盈利曾达到 activation 后,从最高盈利回撤 trailing_distance 则平仓
if self.max_unrealized_pnl_seen >= self.trailing_activation_usd:
if pnl_usd < self.max_unrealized_pnl_seen - self.trailing_distance_usd:
logger.info(f"移动止损:当前盈利 {pnl_usd:.2f} 从最高 {self.max_unrealized_pnl_seen:.2f} 回撤 >= {self.trailing_distance_usd} 美元,平仓")
self.平仓()
self.max_unrealized_pnl_seen = None
time.sleep(3)
continue
# 止盈:盈利达到 take_profit_usd 平仓
if pnl_usd >= self.take_profit_usd:
logger.info(f"仓位盈利 {pnl_usd:.2f} 美元 >= {self.take_profit_usd} 美元,执行止盈平仓")
self.平仓()
self.max_unrealized_pnl_seen = None
time.sleep(3)
continue
# 4. 检查信号
signal = self.check_signal(current_price, prev_kline, current_kline)
# 5. 反手过滤:冷却时间 + 最小价差
if signal and signal[0].startswith('reverse_'):
if not self.can_reverse(current_price, signal[1]):
signal = None
# 5.5 开仓频率过滤:同一根 K 线只开一次 + 开仓冷却
if signal and signal[0] in ('long', 'short'):
if not self.can_open(current_kline_time):
signal = None
else:
self._current_kline_id_for_open = current_kline_time # 供 execute_trade 成功后记录
# 6. 有信号则执行交易
if signal:
trade_success = self.execute_trade(signal)
if trade_success:
logger.success(f"交易执行完成: {signal[0]}, 当前持仓状态: {self.start}")
page_start = True
else:
logger.warning(f"交易执行失败或被阻止: {signal[0]}")
# 短暂等待后继续循环同一根K线遇到信号就操作
time.sleep(0.1)
if page_start:
self.page.close()
time.sleep(5)
except KeyboardInterrupt:
logger.info("用户中断,程序退出")
break
except Exception as e:
logger.error(f"主循环异常: {e}")
time.sleep(5)
if __name__ == '__main__':
BitmartFuturesTransaction(bit_id="f2320f57e24c45529a009e1541e25961").action()

540
抓取币安K线.py Normal file
View File

@@ -0,0 +1,540 @@
"""
币安 永续合约 ETHUSDT 多周期K线 + 秒级数据抓取
- K线: 1m、3m、5m、15m、30m、1h
- 秒级: 通过 aggTrades 逐笔成交聚合,每秒一条 (时间, 价格 OHLC)
支持断点续传
"""
import time
import datetime
from pathlib import Path
from loguru import logger
from peewee import *
try:
import requests
except ImportError:
requests = None
DB_PATH = Path(__file__).parent / 'models' / 'database.db'
db = SqliteDatabase(str(DB_PATH))
KLINE_CONFIGS = {
1: '1m',
3: '3m',
5: '5m',
15: '15m',
30: '30m',
60: '1h',
}
BINANCE_INTERVALS = {
1: '1m',
3: '3m',
5: '5m',
15: '15m',
30: '30m',
60: '1h',
}
BINANCE_BASE = 'https://fapi.binance.com'
class BinanceETHTrades(Model):
"""逐笔成交aggTrades 原始数据)"""
id = BigIntegerField(primary_key=True)
timestamp = BigIntegerField(index=True)
price = FloatField()
volume = FloatField()
side = IntegerField() # 1=买, 0=卖
class Meta:
database = db
table_name = 'binance_eth_trades'
class BinanceETHSecond(Model):
"""秒级数据每秒一条id=时间戳(毫秒取整到秒), close=该秒收盘价"""
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
volume = FloatField(null=True)
trade_count = IntegerField(null=True)
class Meta:
database = db
table_name = 'binance_eth_1s'
def create_binance_kline_model(step: int):
"""动态创建币安K线模型"""
suffix = KLINE_CONFIGS.get(step, f'{step}m')
tbl_name = f'binance_eth_{suffix}'
attrs = {
'id': BigIntegerField(primary_key=True),
'open': FloatField(null=True),
'high': FloatField(null=True),
'low': FloatField(null=True),
'close': FloatField(null=True),
}
meta_attrs = {'database': db, 'table_name': tbl_name}
Meta = type('Meta', (), meta_attrs)
attrs['Meta'] = Meta
name = f'BinanceETH{suffix.replace("m", "m").replace("h", "h")}'
return type(name, (Model,), attrs)
class BinanceKlineCollector:
"""币安永续合约 K线抓取器"""
def __init__(self, symbol: str = "ETHUSDT", proxy: str = None, verify_ssl: bool = True):
"""
:param symbol: 合约代码
:param proxy: 代理地址,如 'http://127.0.0.1:7890',也可通过环境变量 BINANCE_PROXY 设置
:param verify_ssl: 是否校验 SSL 证书,网络异常时可设为 False
"""
self.symbol = symbol
self.proxy = proxy or __import__('os').environ.get('BINANCE_PROXY', '').strip() or None
self.verify_ssl = verify_ssl
self._session = requests.Session() if requests else None
self.models = {}
self._init_database()
def _init_database(self):
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
db.connect(reuse_if_open=True)
for step in KLINE_CONFIGS.keys():
model = create_binance_kline_model(step)
self.models[step] = model
db.create_tables([model], safe=True)
logger.info(f"初始化表: {model._meta.table_name}")
db.create_tables([BinanceETHTrades, BinanceETHSecond], safe=True)
logger.info("初始化表: binance_eth_trades, binance_eth_1s")
def get_db_time_range(self, step: int):
model = self.models.get(step)
if not model:
return None, None
try:
earliest = model.select(fn.MIN(model.id)).scalar()
latest = model.select(fn.MAX(model.id)).scalar()
return earliest, latest
except Exception as e:
logger.error(f"查询数据库异常: {e}")
return None, None
def get_klines(self, step: int, start_time: int, end_time: int, max_retries: int = 3):
"""
从币安 API 获取K线
:param step: 周期(分钟)
:param start_time: 开始时间戳(秒)
:param end_time: 结束时间戳(秒)
:return: [{'id', 'open', 'high', 'low', 'close'}, ...]
"""
if not requests:
logger.error("需要安装 requests: pip install requests")
return []
interval = BINANCE_INTERVALS.get(step, f'{step}m')
start_ms = int(start_time) * 1000
end_ms = int(end_time) * 1000
url = f"{BINANCE_BASE}/fapi/v1/klines"
kwargs = {'timeout': 30, 'verify': self.verify_ssl}
if self.proxy:
kwargs['proxies'] = {'http': self.proxy, 'https': self.proxy}
sess = self._session
for attempt in range(max_retries):
try:
params = {
'symbol': self.symbol,
'interval': interval,
'startTime': start_ms,
'endTime': end_ms,
'limit': 1500,
}
r = (sess or requests).get(url, params=params, **kwargs)
r.raise_for_status()
data = r.json()
formatted = []
for k in data:
# [open_time, open, high, low, close, volume, ...]
formatted.append({
'id': k[0],
'open': float(k[1]),
'high': float(k[2]),
'low': float(k[3]),
'close': float(k[4]),
})
formatted.sort(key=lambda x: x['id'])
return formatted
except Exception as e:
delay = 3 + attempt * 2
logger.warning(f"获取K线异常 (尝试 {attempt+1}/{max_retries}): {e}{delay}s 后重试")
if attempt < max_retries - 1:
time.sleep(delay)
return []
def save_klines(self, step: int, klines: list):
model = self.models.get(step)
if not model:
return 0
new_count = 0
for kline in klines:
try:
_, created = model.get_or_create(
id=kline['id'],
defaults={
'open': kline['open'],
'high': kline['high'],
'low': kline['low'],
'close': kline['close'],
}
)
if created:
new_count += 1
except Exception as e:
logger.error(f"保存失败 {kline['id']}: {e}")
return new_count
def get_batch_seconds(self, step: int):
if step == 1:
return 3600 * 4
elif step == 3:
return 3600 * 8
elif step == 5:
return 3600 * 12
elif step == 15:
return 3600 * 24
elif step == 30:
return 3600 * 48
else:
return 3600 * 72
def collect_period_range(self, step: int, target_start: int, target_end: int):
suffix = KLINE_CONFIGS.get(step, f'{step}m')
batch_seconds = self.get_batch_seconds(step)
db_earliest, db_latest = self.get_db_time_range(step)
if db_earliest and db_latest:
db_earliest_sec = db_earliest // 1000
db_latest_sec = db_latest // 1000
logger.info(f"[{suffix}] 数据库已有: "
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(db_earliest_sec))} ~ "
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(db_latest_sec))}")
else:
db_earliest_sec = db_latest_sec = None
total_saved = 0
# 向前抓取历史
backward_end = db_earliest_sec if db_earliest_sec else target_end
if backward_end > target_start:
logger.info(f"[{suffix}] === 向前抓取历史 ===")
total_backward = backward_end - target_start
current_end = backward_end
fail_count = 0
while current_end > target_start and fail_count < 5:
current_start = max(current_end - batch_seconds, target_start)
klines = self.get_klines(step, current_start, current_end)
if klines:
saved = self.save_klines(step, klines)
total_saved += saved
progress = (backward_end - current_end) / total_backward * 100 if total_backward > 0 else 0
logger.info(f"[{suffix}] ← {time.strftime('%Y-%m-%d %H:%M', time.localtime(current_start))} ~ "
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(current_end))} | "
f"获取 {len(klines)} 新增 {saved} | 进度 {progress:.1f}%")
fail_count = 0
else:
fail_count += 1
current_end = current_start
time.sleep(0.2)
# 向后抓取最新
forward_start = db_latest_sec if db_latest_sec else target_start
if forward_start < target_end:
logger.info(f"[{suffix}] === 向后抓取最新 ===")
current_start = forward_start
fail_count = 0
while current_start < target_end and fail_count < 3:
current_end = min(current_start + batch_seconds, target_end)
klines = self.get_klines(step, current_start, current_end)
if klines:
saved = self.save_klines(step, klines)
total_saved += saved
logger.info(f"[{suffix}] → {time.strftime('%Y-%m-%d %H:%M', time.localtime(current_start))} ~ "
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(current_end))} | "
f"获取 {len(klines)} 新增 {saved}")
fail_count = 0
else:
fail_count += 1
current_start = current_end
time.sleep(0.2)
final_earliest, final_latest = self.get_db_time_range(step)
if final_earliest and final_latest:
logger.success(f"[{suffix}] 完成!本次新增 {total_saved} | "
f"{time.strftime('%Y-%m-%d', time.localtime(final_earliest//1000))} ~ "
f"{time.strftime('%Y-%m-%d', time.localtime(final_latest//1000))}")
return total_saved
def collect_from_date(self, start_date: str, periods: list = None):
if periods is None:
periods = list(KLINE_CONFIGS.keys())
start_dt = datetime.datetime.strptime(start_date, '%Y-%m-%d')
target_start = int(start_dt.timestamp())
target_end = int(time.time())
logger.info(f"币安 ETHUSDT 永续 | {start_date} ~ 当前 | 周期: {[KLINE_CONFIGS[p] for p in periods]}")
results = {}
for step in periods:
saved = self.collect_period_range(step, target_start, target_end)
results[KLINE_CONFIGS[step]] = saved
time.sleep(0.5)
logger.info("抓取完成: " + ", ".join(f"{k}: {v}" for k, v in results.items()))
return results
# ==================== 秒级数据 ====================
def get_agg_trades(self, start_ms: int, end_ms: int, max_retries: int = 3):
"""
获取逐笔成交 (aggTrades),时间范围需 <= 1 小时
:return: [{'id', 'timestamp', 'price', 'volume', 'side'}, ...]
"""
if not requests:
return []
url = f"{BINANCE_BASE}/fapi/v1/aggTrades"
kwargs = {'timeout': 30, 'verify': self.verify_ssl}
if self.proxy:
kwargs['proxies'] = {'http': self.proxy, 'https': self.proxy}
sess = self._session
for attempt in range(max_retries):
try:
params = {
'symbol': self.symbol,
'startTime': start_ms,
'endTime': end_ms,
'limit': 1000,
}
r = (sess or requests).get(url, params=params, **kwargs)
r.raise_for_status()
data = r.json()
out = []
for t in data:
out.append({
'id': t['a'],
'timestamp': t['T'],
'price': float(t['p']),
'volume': float(t['q']),
'side': 0 if t.get('m', False) else 1,
})
return out
except Exception as e:
delay = 3 + attempt * 2
logger.warning(f"获取 aggTrades 异常 ({attempt+1}/{max_retries}): {e}{delay}s 后重试")
if attempt < max_retries - 1:
time.sleep(delay)
return []
def save_trades(self, trades: list):
n = 0
for t in trades:
try:
_, created = BinanceETHTrades.get_or_create(
id=t['id'],
defaults={'timestamp': t['timestamp'], 'price': t['price'],
'volume': t['volume'], 'side': t['side']}
)
if created:
n += 1
except Exception:
pass
return n
def get_second_db_range(self):
try:
lo = BinanceETHSecond.select(fn.MIN(BinanceETHSecond.id)).scalar()
hi = BinanceETHSecond.select(fn.MAX(BinanceETHSecond.id)).scalar()
return lo, hi
except Exception:
return None, None
def aggregate_trades_to_seconds(self, start_ms: int = None, end_ms: int = None):
"""将 binance_eth_trades 聚合为秒级 K 线,写入 binance_eth_1s"""
q = BinanceETHTrades.select().order_by(BinanceETHTrades.timestamp)
if start_ms:
q = q.where(BinanceETHTrades.timestamp >= start_ms)
if end_ms:
q = q.where(BinanceETHTrades.timestamp <= end_ms)
sec_data = {}
for t in q:
sec_ts = (t.timestamp // 1000) * 1000
if sec_ts not in sec_data:
sec_data[sec_ts] = {
'open': t.price, 'high': t.price, 'low': t.price, 'close': t.price,
'volume': t.volume, 'trade_count': 1
}
else:
sec_data[sec_ts]['high'] = max(sec_data[sec_ts]['high'], t.price)
sec_data[sec_ts]['low'] = min(sec_data[sec_ts]['low'], t.price)
sec_data[sec_ts]['close'] = t.price
sec_data[sec_ts]['volume'] += t.volume
sec_data[sec_ts]['trade_count'] += 1
n = 0
for ts, d in sec_data.items():
try:
BinanceETHSecond.insert(
id=ts, open=d['open'], high=d['high'], low=d['low'], close=d['close'],
volume=d['volume'], trade_count=d['trade_count']
).on_conflict(
conflict_target=[BinanceETHSecond.id],
update={BinanceETHSecond.open: d['open'], BinanceETHSecond.high: d['high'],
BinanceETHSecond.low: d['low'], BinanceETHSecond.close: d['close'],
BinanceETHSecond.volume: d['volume'],
BinanceETHSecond.trade_count: d['trade_count']}
).execute()
n += 1
except Exception as e:
logger.error(f"保存秒级失败 {ts}: {e}")
logger.info(f"聚合: {len(sec_data)} 条秒级数据")
return n
def collect_second_data(self, start_date: str, end_date: str = None):
"""
抓取秒级数据:逐小时拉 aggTrades聚合为秒级后入库
:param start_date: 'YYYY-MM-DD'
:param end_date: 'YYYY-MM-DD' 或 None(当前)
"""
start_dt = datetime.datetime.strptime(start_date, '%Y-%m-%d')
start_ts = int(start_dt.timestamp())
end_ts = int(time.time()) if end_date is None else int(
datetime.datetime.strptime(end_date, '%Y-%m-%d').timestamp())
start_ms = start_ts * 1000
end_ms = end_ts * 1000
# 每次最多 1 小时
chunk_ms = 3600 * 1000
lo, hi = self.get_second_db_range()
if lo and hi:
# 向后补
fetch_start = hi + 1000
else:
fetch_start = start_ms
total_trades = 0
total_saved = 0
current = fetch_start
while current < end_ms:
chunk_end = min(current + chunk_ms, end_ms)
trades = self.get_agg_trades(current, chunk_end)
if trades:
saved = self.save_trades(trades)
total_trades += len(trades)
total_saved += saved
ts_str = datetime.datetime.fromtimestamp(current/1000).strftime('%Y-%m-%d %H:%M')
logger.info(f"[1s] {ts_str} | 成交 {len(trades)} 条, 新增 {saved}")
current = chunk_end
time.sleep(0.15)
if total_trades > 0:
self.aggregate_trades_to_seconds(start_ms=fetch_start, end_ms=end_ms)
logger.success(f"秒级抓取完成: 成交 {total_trades}, 新增 {total_saved}")
return total_saved
def collect_trades_realtime(self, duration_seconds: int = 3600, interval: float = 0.5):
"""实时采集逐笔成交(秒级数据源)"""
url = f"{BINANCE_BASE}/fapi/v1/aggTrades"
kwargs = {'timeout': 10, 'verify': self.verify_ssl}
if self.proxy:
kwargs['proxies'] = {'http': self.proxy, 'https': self.proxy}
sess = self._session
start = time.time()
end = start + duration_seconds
total = 0
last_id = None
while time.time() < end:
try:
params = {'symbol': self.symbol, 'limit': 1000}
if last_id:
params['fromId'] = last_id + 1
r = (sess or requests).get(url, params=params, **kwargs)
data = r.json()
if data:
trades = [{'id': t['a'], 'timestamp': t['T'], 'price': float(t['p']),
'volume': float(t['q']), 'side': 0 if t.get('m') else 1} for t in data]
saved = self.save_trades(trades)
total += saved
last_id = data[-1]['a']
if saved > 0 and total % 500 == 0:
logger.info(f"[实时] 累计新增 {total} | 价格 {trades[-1]['price']:.2f}")
except Exception as e:
logger.warning(f"实时采集异常: {e}")
time.sleep(interval)
self.aggregate_trades_to_seconds()
logger.success(f"实时采集完成: 新增 {total}")
return total
def get_stats(self):
logger.info("币安 K线 数据库统计:")
for step, model in self.models.items():
try:
count = model.select().count()
earliest, latest = self.get_db_time_range(step)
if earliest and latest:
s = f" {KLINE_CONFIGS[step]:>4}: {count:>8} 条 | " \
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(earliest//1000))} ~ " \
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(latest//1000))}"
else:
s = f" {KLINE_CONFIGS[step]:>4}: {count:>8}"
logger.info(s)
except Exception as e:
logger.error(f" {KLINE_CONFIGS[step]}: {e}")
try:
tc = BinanceETHTrades.select().count()
sc = BinanceETHSecond.select().count()
if sc > 0:
lo = BinanceETHSecond.select(fn.MIN(BinanceETHSecond.id)).scalar()
hi = BinanceETHSecond.select(fn.MAX(BinanceETHSecond.id)).scalar()
logger.info(f" trades: {tc:>8} | 1s: {sc:>8} | "
f"{datetime.datetime.fromtimestamp(lo/1000).strftime('%Y-%m-%d %H:%M:%S')} ~ "
f"{datetime.datetime.fromtimestamp(hi/1000).strftime('%Y-%m-%d %H:%M:%S')}")
else:
logger.info(f" trades: {tc:>8} | 1s: {sc:>8}")
except Exception as e:
logger.error(f" trades/1s: {e}")
def close(self):
if not db.is_closed():
db.close()
if __name__ == '__main__':
import os
# 代理示例: export BINANCE_PROXY='http://127.0.0.1:7890'
# 或: collector = BinanceKlineCollector(proxy='http://127.0.0.1:7890', verify_ssl=False)
collector = BinanceKlineCollector(
symbol="ETHUSDT",
proxy=os.environ.get('BINANCE_PROXY') or None,
verify_ssl=os.environ.get('BINANCE_VERIFY_SSL', '1') != '0',
)
try:
collector.get_stats()
# 可选: K线
# collector.collect_from_date(start_date='2020-01-01', periods=[1, 3, 5, 15, 30, 60])
# 秒级数据:历史按天抓取(从数据库最新继续)
collector.collect_second_data(start_date='2025-01-01')
# 或实时采集一段时间
# collector.collect_trades_realtime(duration_seconds=3600, interval=0.5)
collector.get_stats()
finally:
collector.close()

408
抓取币安K线_ccxt.py Normal file
View File

@@ -0,0 +1,408 @@
"""
基于 ccxt 的币安永续 ETHUSDT K线 + 秒级数据抓取
- K线: 1m、3m、5m、15m、30m、1h
- 秒级: fetch_trades 逐笔成交聚合
支持代理,与 抓取币安K线.py 共用同一数据库表
"""
import time
import datetime
import os
from pathlib import Path
from loguru import logger
from peewee import *
try:
import ccxt
except ImportError:
ccxt = None
DB_PATH = Path(__file__).parent / 'models' / 'database.db'
db = SqliteDatabase(str(DB_PATH))
KLINE_CONFIGS = {1: '1m', 3: '3m', 5: '5m', 15: '15m', 30: '30m', 60: '1h'}
CCXT_INTERVALS = {1: '1m', 3: '3m', 5: '5m', 15: '15m', 30: '30m', 60: '1h'}
class BinanceETHTrades(Model):
id = BigIntegerField(primary_key=True)
timestamp = BigIntegerField(index=True)
price = FloatField()
volume = FloatField()
side = IntegerField()
class Meta:
database = db
table_name = 'binance_eth_trades'
class BinanceETHSecond(Model):
id = BigIntegerField(primary_key=True)
open = FloatField(null=True)
high = FloatField(null=True)
low = FloatField(null=True)
close = FloatField(null=True)
volume = FloatField(null=True)
trade_count = IntegerField(null=True)
class Meta:
database = db
table_name = 'binance_eth_1s'
def create_kline_model(step: int):
suffix = KLINE_CONFIGS.get(step, f'{step}m')
tbl_name = f'binance_eth_{suffix}'
attrs = {
'id': BigIntegerField(primary_key=True),
'open': FloatField(null=True),
'high': FloatField(null=True),
'low': FloatField(null=True),
'close': FloatField(null=True),
}
meta = type('Meta', (), {'database': db, 'table_name': tbl_name})
attrs['Meta'] = meta
return type(f'BinanceETH{suffix}', (Model,), attrs)
class BinanceCCXTCollector:
"""基于 ccxt 的币安永续 K 线抓取"""
def __init__(self, symbol: str = "ETHUSDT", proxy: str = None):
self.symbol = symbol
self.ccxt_symbol = "ETH/USDT:USDT"
self.proxy = proxy or os.environ.get('BINANCE_PROXY', '').strip() or None
self.models = {}
self.exchange = None
self._init_exchange()
self._init_database()
def _init_exchange(self):
if not ccxt:
raise ImportError("请安装 ccxt: pip install ccxt")
options = {'defaultType': 'future', 'adjustForTimeDifference': True}
config = {'timeout': 30000, 'enableRateLimit': True}
if self.proxy:
config['proxy'] = self.proxy
self.exchange = ccxt.binanceusdm(config=config)
self.exchange.options.update(options)
logger.info(f"ccxt 交易所: binanceusdm" + (f" 代理: {self.proxy}" if self.proxy else ""))
def _init_database(self):
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
db.connect(reuse_if_open=True)
for step in KLINE_CONFIGS.keys():
model = create_kline_model(step)
self.models[step] = model
db.create_tables([model], safe=True)
db.create_tables([BinanceETHTrades, BinanceETHSecond], safe=True)
logger.info("数据库表已就绪")
def get_db_time_range(self, step: int):
model = self.models.get(step)
if not model:
return None, None
try:
lo = model.select(fn.MIN(model.id)).scalar()
hi = model.select(fn.MAX(model.id)).scalar()
return lo, hi
except Exception:
return None, None
def fetch_klines(self, step: int, start_sec: int, end_sec: int, max_retries: int = 5):
"""ccxt fetch_ohlcv按批次拉取"""
interval = CCXT_INTERVALS.get(step, f'{step}m')
since_ms = int(start_sec) * 1000
end_ms = int(end_sec) * 1000
out = []
while since_ms < end_ms:
for attempt in range(max_retries):
try:
ohlcv = self.exchange.fetch_ohlcv(
self.ccxt_symbol,
interval,
since=since_ms,
limit=1500,
)
if not ohlcv:
break
for row in ohlcv:
ts, o, h, l, c = int(row[0]), row[1], row[2], row[3], row[4]
if ts >= end_ms:
break
out.append({'id': ts, 'open': float(o), 'high': float(h),
'low': float(l), 'close': float(c)})
since_ms = int(ohlcv[-1][0]) + 1
time.sleep(0.15)
break
except Exception as e:
delay = 2 + attempt * 2
logger.warning(f"fetch_ohlcv 异常 ({attempt+1}/{max_retries}): {e}{delay}s 后重试")
if attempt < max_retries - 1:
time.sleep(delay)
else:
return []
return out
def save_klines(self, step: int, klines: list):
model = self.models.get(step)
if not model:
return 0
n = 0
for k in klines:
try:
_, created = model.get_or_create(
id=k['id'],
defaults={'open': k['open'], 'high': k['high'], 'low': k['low'], 'close': k['close']}
)
if created:
n += 1
except Exception as e:
logger.error(f"保存失败 {k['id']}: {e}")
return n
def get_batch_seconds(self, step: int):
return {1: 3600*4, 3: 3600*8, 5: 3600*12, 15: 3600*24, 30: 3600*48}.get(step, 3600*72)
def collect_period_range(self, step: int, target_start: int, target_end: int):
suffix = KLINE_CONFIGS.get(step, f'{step}m')
batch = self.get_batch_seconds(step)
db_lo, db_hi = self.get_db_time_range(step)
db_lo_sec = db_lo // 1000 if db_lo else None
db_hi_sec = db_hi // 1000 if db_hi else None
total = 0
backward_end = db_lo_sec if db_lo_sec else target_end
if backward_end > target_start:
logger.info(f"[{suffix}] 向前抓取历史")
current = backward_end
fail = 0
while current > target_start and fail < 5:
start = max(current - batch, target_start)
klines = self.fetch_klines(step, start, current)
if klines:
saved = self.save_klines(step, klines)
total += saved
logger.info(f"[{suffix}] ← {time.strftime('%Y-%m-%d %H:%M', time.localtime(start))} ~ "
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(current))} | "
f"获取 {len(klines)} 新增 {saved}")
fail = 0
else:
fail += 1
current = start
time.sleep(0.3)
forward_start = db_hi_sec if db_hi_sec else target_start
if forward_start < target_end:
logger.info(f"[{suffix}] 向后抓取最新")
current = forward_start
fail = 0
while current < target_end and fail < 3:
end = min(current + batch, target_end)
klines = self.fetch_klines(step, current, end)
if klines:
saved = self.save_klines(step, klines)
total += saved
logger.info(f"[{suffix}] → {time.strftime('%Y-%m-%d %H:%M', time.localtime(current))} ~ "
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(end))} | "
f"获取 {len(klines)} 新增 {saved}")
fail = 0
else:
fail += 1
current = end
time.sleep(0.3)
lo, hi = self.get_db_time_range(step)
if lo and hi:
logger.success(f"[{suffix}] 完成 新增 {total} | "
f"{time.strftime('%Y-%m-%d', time.localtime(lo//1000))} ~ "
f"{time.strftime('%Y-%m-%d', time.localtime(hi//1000))}")
return total
def collect_from_date(self, start_date: str, periods: list = None):
if periods is None:
periods = list(KLINE_CONFIGS.keys())
start_ts = int(datetime.datetime.strptime(start_date, '%Y-%m-%d').timestamp())
end_ts = int(time.time())
logger.info(f"币安 ETHUSDT 永续(ccxt) | {start_date} ~ 当前 | 周期: {[KLINE_CONFIGS[p] for p in periods]}")
results = {}
for step in periods:
results[KLINE_CONFIGS[step]] = self.collect_period_range(step, start_ts, end_ts)
time.sleep(0.5)
return results
# ==================== 秒级 ====================
def fetch_trades(self, since_ms: int = None, limit: int = 1000, max_retries: int = 5):
for attempt in range(max_retries):
try:
trades = self.exchange.fetch_trades(
self.ccxt_symbol,
since=since_ms,
limit=limit,
)
out = []
for t in trades:
out.append({
'id': t['id'],
'timestamp': t['timestamp'],
'price': float(t['price']),
'volume': float(t['amount']),
'side': 1 if t.get('side') == 'buy' else 0,
})
return out
except Exception as e:
delay = 2 + attempt * 2
logger.warning(f"fetch_trades 异常 ({attempt+1}/{max_retries}): {e}{delay}s 后重试")
if attempt < max_retries - 1:
time.sleep(delay)
return []
def save_trades(self, trades: list):
n = 0
for t in trades:
try:
_, created = BinanceETHTrades.get_or_create(
id=t['id'],
defaults={'timestamp': t['timestamp'], 'price': t['price'],
'volume': t['volume'], 'side': t['side']}
)
if created:
n += 1
except Exception:
pass
return n
def aggregate_to_seconds(self, start_ms: int = None, end_ms: int = None):
q = BinanceETHTrades.select().order_by(BinanceETHTrades.timestamp)
if start_ms:
q = q.where(BinanceETHTrades.timestamp >= start_ms)
if end_ms:
q = q.where(BinanceETHTrades.timestamp <= end_ms)
sec_data = {}
for t in q:
ts = (t.timestamp // 1000) * 1000
if ts not in sec_data:
sec_data[ts] = {
'open': t.price, 'high': t.price, 'low': t.price, 'close': t.price,
'volume': t.volume, 'trade_count': 1,
}
else:
sec_data[ts]['high'] = max(sec_data[ts]['high'], t.price)
sec_data[ts]['low'] = min(sec_data[ts]['low'], t.price)
sec_data[ts]['close'] = t.price
sec_data[ts]['volume'] += t.volume
sec_data[ts]['trade_count'] += 1
for ts, d in sec_data.items():
try:
BinanceETHSecond.insert(
id=ts, open=d['open'], high=d['high'], low=d['low'], close=d['close'],
volume=d['volume'], trade_count=d['trade_count']
).on_conflict(
conflict_target=[BinanceETHSecond.id],
update={
BinanceETHSecond.open: d['open'], BinanceETHSecond.high: d['high'],
BinanceETHSecond.low: d['low'], BinanceETHSecond.close: d['close'],
BinanceETHSecond.volume: d['volume'],
BinanceETHSecond.trade_count: d['trade_count'],
}
).execute()
except Exception as e:
logger.error(f"保存秒级失败 {ts}: {e}")
logger.info(f"聚合 {len(sec_data)} 条秒级数据")
def collect_second_data(self, start_date: str, end_date: str = None):
start_ts = int(datetime.datetime.strptime(start_date, '%Y-%m-%d').timestamp())
end_ts = int(time.time()) if end_date is None else int(
datetime.datetime.strptime(end_date, '%Y-%m-%d').timestamp())
try:
hi = BinanceETHSecond.select(fn.MAX(BinanceETHSecond.id)).scalar()
fetch_start = (hi + 1000) if hi else start_ts * 1000
except Exception:
fetch_start = start_ts * 1000
total_saved = 0
since = fetch_start
while since < end_ts * 1000:
trades = self.fetch_trades(since_ms=since, limit=1000)
if trades:
saved = self.save_trades(trades)
total_saved += saved
since = trades[-1]['timestamp'] + 1
logger.info(f"[1s] {datetime.datetime.fromtimestamp(since/1000).strftime('%Y-%m-%d %H:%M:%S')} | "
f"成交 {len(trades)} 新增 {saved}")
else:
since += 3600 * 1000
time.sleep(0.2)
if total_saved > 0:
self.aggregate_to_seconds(start_ms=fetch_start, end_ms=end_ts*1000)
logger.success(f"秒级抓取完成 新增 {total_saved}")
return total_saved
def collect_trades_realtime(self, duration_seconds: int = 3600, interval: float = 0.5):
end = time.time() + duration_seconds
total = 0
since = None
while time.time() < end:
try:
trades = self.fetch_trades(since_ms=since, limit=1000)
if trades:
total += self.save_trades(trades)
since = trades[-1]['timestamp'] + 1
except Exception as e:
logger.warning(f"实时采集异常: {e}")
time.sleep(interval)
self.aggregate_to_seconds()
logger.success(f"实时采集完成 新增 {total}")
return total
def get_stats(self):
logger.info("币安(ccxt) 数据库统计:")
for step, model in self.models.items():
try:
c = model.select().count()
lo, hi = self.get_db_time_range(step)
s = f" {KLINE_CONFIGS[step]:>4}: {c:>8}"
if lo and hi:
s += f" | {time.strftime('%Y-%m-%d %H:%M', time.localtime(lo//1000))} ~ " \
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(hi//1000))}"
logger.info(s)
except Exception as e:
logger.error(f" {KLINE_CONFIGS[step]}: {e}")
try:
tc = BinanceETHTrades.select().count()
sc = BinanceETHSecond.select().count()
if sc > 0:
lo = BinanceETHSecond.select(fn.MIN(BinanceETHSecond.id)).scalar()
hi = BinanceETHSecond.select(fn.MAX(BinanceETHSecond.id)).scalar()
logger.info(f" trades: {tc:>8} | 1s: {sc:>8} | "
f"{datetime.datetime.fromtimestamp(lo/1000).strftime('%Y-%m-%d %H:%M:%S')} ~ "
f"{datetime.datetime.fromtimestamp(hi/1000).strftime('%Y-%m-%d %H:%M:%S')}")
else:
logger.info(f" trades: {tc:>8} | 1s: {sc:>8}")
except Exception as e:
logger.error(f" trades/1s: {e}")
def close(self):
if not db.is_closed():
db.close()
if __name__ == '__main__':
proxy = os.environ.get('BINANCE_PROXY') or None
collector = BinanceCCXTCollector(symbol="ETHUSDT", proxy=proxy)
try:
collector.get_stats()
# K线不指定 periods 则抓取全部1m、3m、5m、15m、30m、1h
collector.collect_from_date(start_date='2025-01-01')
# 秒级
collector.collect_second_data(start_date='2025-01-01')
collector.get_stats()
finally:
collector.close()