fewfef
This commit is contained in:
16
1111
16
1111
@@ -12,3 +12,19 @@ websea
|
||||
|
||||
|
||||
|
||||
3. 更多交易信号
|
||||
包住形态(原策略)
|
||||
锤子线和上吊线
|
||||
早晨之星和黄昏之星
|
||||
乌云盖顶和刺透形态
|
||||
三只乌鸦和三白兵
|
||||
移动平均线金叉死叉
|
||||
RSI超买超卖
|
||||
布林带突破
|
||||
|
||||
4. 风险管理
|
||||
止损止盈:可配置的百分比止损止盈
|
||||
时间止损:持仓超过24小时自动平仓
|
||||
凯利公式改进版:仓位管理
|
||||
最大回撤控制
|
||||
手续费考虑:0.05%交易手续费
|
||||
BIN
bitmart/database.db
Normal file
BIN
bitmart/database.db
Normal file
Binary file not shown.
36
bitmart/eth_trades_2025_12.csv
Normal file
36
bitmart/eth_trades_2025_12.csv
Normal file
@@ -0,0 +1,36 @@
|
||||
open_datetime,close_datetime,direction,open_price,close_price,size,pnl_amount,pnl_pct,duration_minutes,open_reason,close_reason
|
||||
2025-12-01 08:23:00,2025-12-01 20:49:00,short,2890.34,2803.6298,0.1,8.67102,2.9999999999999996,746.0,乌云盖顶形态,止盈触发
|
||||
2025-12-02 00:47:00,2025-12-02 06:01:00,short,2743.72,2798.5944,0.1,-5.487440000000015,-2.0000000000000058,314.0,乌云盖顶形态,止损触发
|
||||
2025-12-02 07:10:00,2025-12-02 22:07:00,short,2800.39,2856.3977999999997,0.1,-5.600779999999986,-1.9999999999999951,897.0,乌云盖顶形态,止损触发
|
||||
2025-12-02 23:34:00,2025-12-03 12:27:00,long,2968.42,3057.4726,0.1,8.90526,2.9999999999999996,773.0,刺透形态,止盈触发
|
||||
2025-12-03 14:51:00,2025-12-03 22:41:00,short,3061.9,3123.138,0.1,-6.123799999999983,-1.9999999999999944,470.0,乌云盖顶形态,止损触发
|
||||
2025-12-03 23:44:00,2025-12-04 06:01:00,long,3080.1,3172.503,0.1,9.240300000000024,3.000000000000008,377.0,刺透形态,止盈触发
|
||||
2025-12-04 06:22:00,2025-12-05 03:13:00,long,3162.67,3099.4166,0.1,-6.325340000000007,-2.0000000000000018,1251.0,刺透形态,止损触发
|
||||
2025-12-05 03:46:00,2025-12-05 10:35:00,short,3117.16,3179.5032,0.1,-6.234320000000025,-2.000000000000008,409.0,乌云盖顶形态,止损触发
|
||||
2025-12-05 10:39:00,2025-12-05 21:43:00,short,3183.49,3087.9853,0.1,9.550469999999995,2.9999999999999987,664.0,乌云盖顶形态,止盈触发
|
||||
2025-12-05 22:03:00,2025-12-06 00:34:00,long,3102.98,3040.9204,0.1,-6.205960000000005,-2.0000000000000013,151.0,刺透形态,止损触发
|
||||
2025-12-06 01:15:00,2025-12-07 22:25:00,long,3016.18,2955.8563999999997,0.1,-6.032360000000017,-2.0000000000000058,2710.0,刺透形态,止损触发
|
||||
2025-12-08 01:45:00,2025-12-08 06:05:00,long,3113.29,3051.0242,0.1,-6.2265800000000135,-2.000000000000004,260.0,刺透形态,止损触发
|
||||
2025-12-08 06:42:00,2025-12-08 16:55:00,long,3049.43,3140.9129,0.1,9.148289999999998,2.999999999999999,613.0,刺透形态,止盈触发
|
||||
2025-12-08 17:53:00,2025-12-09 23:45:00,short,3154.12,3217.2024,0.1,-6.308240000000024,-2.000000000000007,1792.0,乌云盖顶形态,止损触发
|
||||
2025-12-10 01:37:00,2025-12-10 05:48:00,long,3368.1,3300.738,0.1,-6.736200000000008,-2.0000000000000027,251.0,刺透形态,止损触发
|
||||
2025-12-10 06:28:00,2025-12-11 01:21:00,short,3312.95,3379.209,0.1,-6.6259000000000015,-2.0000000000000004,1133.0,乌云盖顶形态,止损触发
|
||||
2025-12-11 02:25:00,2025-12-11 08:34:00,long,3363.14,3295.8772,0.1,-6.726279999999998,-1.9999999999999993,369.0,刺透形态,止损触发
|
||||
2025-12-11 09:36:00,2025-12-11 11:12:00,short,3266.05,3168.0685000000003,0.1,9.798149999999987,2.9999999999999956,96.0,乌云盖顶形态,止盈触发
|
||||
2025-12-11 13:39:00,2025-12-12 05:33:00,short,3205.6,3269.712,0.1,-6.411200000000008,-2.0000000000000027,954.0,乌云盖顶形态,止损触发
|
||||
2025-12-12 06:40:00,2025-12-12 23:31:00,short,3243.5,3146.1949999999997,0.1,9.73050000000003,3.000000000000009,1011.0,乌云盖顶形态,止盈触发
|
||||
2025-12-12 23:47:00,2025-12-13 00:21:00,long,3106.89,3044.7522,0.1,-6.213779999999997,-1.9999999999999991,34.0,刺透形态,止损触发
|
||||
2025-12-13 01:19:00,2025-12-15 07:32:00,long,3091.99,3030.1501999999996,0.1,-6.1839800000000205,-2.0000000000000067,3253.0,刺透形态,止损触发
|
||||
2025-12-15 07:54:00,2025-12-15 17:51:00,long,3063.81,3155.7243,0.1,9.191429999999992,2.9999999999999973,597.0,刺透形态,止盈触发
|
||||
2025-12-15 18:29:00,2025-12-15 22:53:00,short,3166.44,3071.4468,0.1,9.499319999999988,2.9999999999999964,264.0,乌云盖顶形态,止盈触发
|
||||
2025-12-15 23:28:00,2025-12-16 02:35:00,short,2997.16,2907.2452,0.1,8.991480000000003,3.0000000000000004,187.0,乌云盖顶形态,止盈触发
|
||||
2025-12-16 02:48:00,2025-12-17 23:05:00,long,2919.12,3006.6936,0.1,8.757360000000016,3.0000000000000058,2657.0,刺透形态,止盈触发
|
||||
2025-12-17 23:11:00,2025-12-17 23:50:00,short,3011.15,2920.8155,0.1,9.033449999999993,2.999999999999998,39.0,乌云盖顶形态,止盈触发
|
||||
2025-12-18 01:31:00,2025-12-18 02:51:00,long,2866.92,2809.5816,0.1,-5.73384000000001,-2.000000000000003,80.0,刺透形态,止损触发
|
||||
2025-12-18 04:00:00,2025-12-18 21:01:00,short,2818.01,2874.3702000000003,0.1,-5.636020000000008,-2.0000000000000027,1021.0,乌云盖顶形态,止损触发
|
||||
2025-12-18 21:07:00,2025-12-18 21:49:00,long,2881.22,2967.6566,0.1,8.64366,3.0000000000000004,42.0,刺透形态,止盈触发
|
||||
2025-12-18 22:35:00,2025-12-19 01:10:00,short,2945.83,2857.4550999999997,0.1,8.837490000000026,3.0000000000000084,155.0,乌云盖顶形态,止盈触发
|
||||
2025-12-19 01:13:00,2025-12-19 01:23:00,long,2862.15,2804.907,0.1,-5.724299999999994,-1.9999999999999976,10.0,刺透形态,止损触发
|
||||
2025-12-19 01:25:00,2025-12-19 02:12:00,short,2799.24,2855.2248,0.1,-5.598480000000018,-2.0000000000000067,47.0,乌云盖顶形态,止损触发
|
||||
2025-12-19 02:14:00,2025-12-19 11:29:00,short,2855.26,2912.3652,0.1,-5.710519999999996,-1.9999999999999987,555.0,乌云盖顶形态,止损触发
|
||||
2025-12-19 11:38:00,2025-12-19 15:11:00,long,2897.05,2983.9615000000003,0.1,8.691150000000016,3.0000000000000053,213.0,刺透形态,止盈触发
|
||||
|
16929
bitmart/ethusdt_kline.csv
Normal file
16929
bitmart/ethusdt_kline.csv
Normal file
File diff suppressed because it is too large
Load Diff
507832
bitmart/kline_1.csv
Normal file
507832
bitmart/kline_1.csv
Normal file
File diff suppressed because it is too large
Load Diff
33858
bitmart/kline_15.csv
Normal file
33858
bitmart/kline_15.csv
Normal file
File diff suppressed because it is too large
Load Diff
169295
bitmart/kline_3.csv
Normal file
169295
bitmart/kline_3.csv
Normal file
File diff suppressed because it is too large
Load Diff
16930
bitmart/kline_30.csv
Normal file
16930
bitmart/kline_30.csv
Normal file
File diff suppressed because it is too large
Load Diff
19
bitmart/strategy_summary_2025_12.txt
Normal file
19
bitmart/strategy_summary_2025_12.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
============================================================
|
||||
ETHUSDT <20><><EFBFBD>ײ<EFBFBD><D7B2><EFBFBD><EFBFBD>ܽ<EFBFBD>
|
||||
============================================================
|
||||
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD>ƸǶ<C6B8> & <20><><EFBFBD><CDB8>̬
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: 1<><31><EFBFBD><EFBFBD>K<EFBFBD><4B>
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>: 2025<32><35>12<31><32>
|
||||
<EFBFBD><EFBFBD>ʼ<EFBFBD>ʽ<EFBFBD>: $10000.00
|
||||
<EFBFBD><EFBFBD>λ<EFBFBD><EFBFBD>С: 0.1 ETH
|
||||
ֹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: 2.0%
|
||||
ֹӯ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: 3.0%
|
||||
|
||||
<EFBFBD>ܽ<EFBFBD><EFBFBD>״<EFBFBD><EFBFBD><EFBFBD>: 35
|
||||
ӯ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: 15 <20><>
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: 20 <20><>
|
||||
ʤ<EFBFBD><EFBFBD>: 42.86%
|
||||
<EFBFBD><EFBFBD>ӯ<EFBFBD><EFBFBD>: $14.84
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD>: $10014.84
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: 0.15%
|
||||
448
bitmart/乌云盖顶和刺透形态.py
Normal file
448
bitmart/乌云盖顶和刺透形态.py
Normal file
@@ -0,0 +1,448 @@
|
||||
import time
|
||||
import csv
|
||||
from datetime import datetime
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
|
||||
# ------------------ 配置 ------------------
|
||||
START_YEAR = 2025
|
||||
CONTRACT_SYMBOL = "ETHUSDT"
|
||||
STEP = 3 # K 线周期,单位分钟
|
||||
CSV_FILE = f"kline_{STEP}.csv"
|
||||
|
||||
|
||||
# ------------------ 策略参数 ------------------
|
||||
class TradingStrategy:
|
||||
def __init__(self, initial_balance: float = 10000):
|
||||
self.positions = [] # 存储持仓信息
|
||||
self.trades = [] # 存储交易记录
|
||||
self.initial_balance = initial_balance
|
||||
self.current_balance = initial_balance
|
||||
self.position_size = 0.1 # 每次开仓数量(ETH)
|
||||
self.stop_loss_pct = 0.02 # 止损比例 2%
|
||||
self.take_profit_pct = 0.03 # 止盈比例 3%
|
||||
self.max_positions = 1 # 最大同时持仓数
|
||||
|
||||
def detect_dark_cloud_cover(self, df: pd.DataFrame, i: int) -> bool:
|
||||
"""检测乌云盖顶形态(看跌)"""
|
||||
if i < 1:
|
||||
return False
|
||||
|
||||
prev_candle = df.iloc[i - 1]
|
||||
curr_candle = df.iloc[i]
|
||||
|
||||
# 前一根是阳线
|
||||
if prev_candle['close'] <= prev_candle['open']:
|
||||
return False
|
||||
|
||||
# 当前是阴线
|
||||
if curr_candle['close'] >= curr_candle['open']:
|
||||
return False
|
||||
|
||||
# 当前开盘价高于前一根最高价
|
||||
if curr_candle['open'] <= prev_candle['high']:
|
||||
return False
|
||||
|
||||
# 当前收盘价低于前一根实体的50%以下
|
||||
prev_body_mid = (prev_candle['open'] + prev_candle['close']) / 2
|
||||
if curr_candle['close'] >= prev_body_mid:
|
||||
return False
|
||||
|
||||
# 可选:添加成交量确认
|
||||
# if curr_candle['volume'] < prev_candle['volume']:
|
||||
# return False
|
||||
|
||||
return True
|
||||
|
||||
def detect_piercing_pattern(self, df: pd.DataFrame, i: int) -> bool:
|
||||
"""检测刺透形态(看涨)"""
|
||||
if i < 1:
|
||||
return False
|
||||
|
||||
prev_candle = df.iloc[i - 1]
|
||||
curr_candle = df.iloc[i]
|
||||
|
||||
# 前一根是阴线
|
||||
if prev_candle['close'] >= prev_candle['open']:
|
||||
return False
|
||||
|
||||
# 当前是阳线
|
||||
if curr_candle['close'] <= curr_candle['open']:
|
||||
return False
|
||||
|
||||
# 当前开盘价低于前一根最低价
|
||||
if curr_candle['open'] >= prev_candle['low']:
|
||||
return False
|
||||
|
||||
# 当前收盘价高于前一根实体的50%以上
|
||||
prev_body_mid = (prev_candle['open'] + prev_candle['close']) / 2
|
||||
if curr_candle['close'] <= prev_body_mid:
|
||||
return False
|
||||
|
||||
# 可选:添加成交量确认
|
||||
# if curr_candle['volume'] < prev_candle['volume']:
|
||||
# return False
|
||||
|
||||
return True
|
||||
|
||||
def open_position(self, direction: str, price: float, timestamp: int, reason: str):
|
||||
"""开仓"""
|
||||
# 检查是否达到最大持仓限制
|
||||
if len(self.positions) >= self.max_positions:
|
||||
return False
|
||||
|
||||
position = {
|
||||
'direction': direction, # 'long' 或 'short'
|
||||
'open_price': price,
|
||||
'open_time': timestamp,
|
||||
'open_reason': reason,
|
||||
'size': self.position_size,
|
||||
'status': 'open',
|
||||
'stop_loss': self.calculate_stop_loss(direction, price),
|
||||
'take_profit': self.calculate_take_profit(direction, price)
|
||||
}
|
||||
self.positions.append(position)
|
||||
|
||||
print(f"\n📈 开仓信号 @ {datetime.fromtimestamp(timestamp)}")
|
||||
print(f" 方向: {'做多' if direction == 'long' else '做空'}")
|
||||
print(f" 价格: ${price:.2f}")
|
||||
print(f" 数量: {self.position_size} ETH")
|
||||
print(f" 止损: ${position['stop_loss']:.2f}")
|
||||
print(f" 止盈: ${position['take_profit']:.2f}")
|
||||
print(f" 原因: {reason}")
|
||||
|
||||
return True
|
||||
|
||||
def calculate_stop_loss(self, direction: str, price: float) -> float:
|
||||
"""计算止损价"""
|
||||
if direction == 'long':
|
||||
return price * (1 - self.stop_loss_pct)
|
||||
else:
|
||||
return price * (1 + self.stop_loss_pct)
|
||||
|
||||
def calculate_take_profit(self, direction: str, price: float) -> float:
|
||||
"""计算止盈价"""
|
||||
if direction == 'long':
|
||||
return price * (1 + self.take_profit_pct)
|
||||
else:
|
||||
return price * (1 - self.take_profit_pct)
|
||||
|
||||
def close_position(self, position_idx: int, price: float, timestamp: int, reason: str):
|
||||
"""平仓"""
|
||||
position = self.positions[position_idx]
|
||||
|
||||
# 计算盈亏
|
||||
if position['direction'] == 'long':
|
||||
pnl_pct = (price - position['open_price']) / position['open_price']
|
||||
else:
|
||||
pnl_pct = (position['open_price'] - price) / position['open_price']
|
||||
|
||||
pnl_amount = self.position_size * position['open_price'] * pnl_pct
|
||||
|
||||
# 更新持仓状态
|
||||
position.update({
|
||||
'close_price': price,
|
||||
'close_time': timestamp,
|
||||
'close_reason': reason,
|
||||
'pnl_pct': pnl_pct * 100, # 百分比
|
||||
'pnl_amount': pnl_amount,
|
||||
'status': 'closed'
|
||||
})
|
||||
|
||||
# 更新余额
|
||||
self.current_balance += pnl_amount
|
||||
|
||||
print(f"\n📉 平仓信号 @ {datetime.fromtimestamp(timestamp)}")
|
||||
print(f" 方向: {'平多单' if position['direction'] == 'long' else '平空单'}")
|
||||
print(f" 开仓价: ${position['open_price']:.2f}")
|
||||
print(f" 平仓价: ${price:.2f}")
|
||||
print(f" 盈亏: {pnl_pct * 100:.2f}% (${pnl_amount:.2f})")
|
||||
print(f" 原因: {reason}")
|
||||
print(f" 当前余额: ${self.current_balance:.2f}")
|
||||
|
||||
# 记录交易
|
||||
trade_record = position.copy()
|
||||
trade_record['duration'] = timestamp - position['open_time']
|
||||
trade_record['duration_minutes'] = trade_record['duration'] / 60
|
||||
self.trades.append(trade_record)
|
||||
|
||||
# 从持仓列表中移除已平仓的仓位
|
||||
self.positions.pop(position_idx)
|
||||
|
||||
return True
|
||||
|
||||
def check_stop_loss_take_profit(self, df: pd.DataFrame, i: int):
|
||||
"""检查止损止盈"""
|
||||
current_price = df.iloc[i]['close']
|
||||
current_time = df.iloc[i]['id']
|
||||
current_high = df.iloc[i]['high']
|
||||
current_low = df.iloc[i]['low']
|
||||
|
||||
positions_to_close = []
|
||||
|
||||
for idx, position in enumerate(self.positions):
|
||||
if position['status'] != 'open':
|
||||
continue
|
||||
|
||||
close_reason = None
|
||||
close_price = current_price
|
||||
|
||||
if position['direction'] == 'long':
|
||||
# 多头止损检查:最低价是否触及止损
|
||||
if current_low <= position['stop_loss']:
|
||||
close_reason = "止损触发"
|
||||
close_price = position['stop_loss'] # 使用止损价
|
||||
# 多头止盈检查:最高价是否触及止盈
|
||||
elif current_high >= position['take_profit']:
|
||||
close_reason = "止盈触发"
|
||||
close_price = position['take_profit'] # 使用止盈价
|
||||
else:
|
||||
# 空头止损检查:最高价是否触及止损
|
||||
if current_high >= position['stop_loss']:
|
||||
close_reason = "止损触发"
|
||||
close_price = position['stop_loss'] # 使用止损价
|
||||
# 空头止盈检查:最低价是否触及止盈
|
||||
elif current_low <= position['take_profit']:
|
||||
close_reason = "止盈触发"
|
||||
close_price = position['take_profit'] # 使用止盈价
|
||||
|
||||
if close_reason:
|
||||
positions_to_close.append((idx, close_price, close_reason))
|
||||
|
||||
# 按索引从大到小平仓,避免索引错乱
|
||||
positions_to_close.sort(reverse=True)
|
||||
for idx, close_price, close_reason in positions_to_close:
|
||||
self.close_position(idx, close_price, current_time, close_reason)
|
||||
|
||||
def analyze_trades(self):
|
||||
"""分析交易结果"""
|
||||
if not self.trades:
|
||||
return {
|
||||
'total_trades': 0,
|
||||
'winning_trades': 0,
|
||||
'losing_trades': 0,
|
||||
'win_rate': 0,
|
||||
'total_pnl': 0,
|
||||
'avg_pnl': 0,
|
||||
'max_win': 0,
|
||||
'max_loss': 0,
|
||||
'profit_factor': 0
|
||||
}
|
||||
|
||||
total_trades = len(self.trades)
|
||||
winning_trades = [t for t in self.trades if t['pnl_amount'] > 0]
|
||||
losing_trades = [t for t in self.trades if t['pnl_amount'] < 0]
|
||||
|
||||
total_pnl = sum(t['pnl_amount'] for t in self.trades)
|
||||
total_win = sum(t['pnl_amount'] for t in winning_trades)
|
||||
total_loss = abs(sum(t['pnl_amount'] for t in losing_trades))
|
||||
|
||||
win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
|
||||
avg_pnl = total_pnl / total_trades if total_trades > 0 else 0
|
||||
max_win = max(t['pnl_amount'] for t in winning_trades) if winning_trades else 0
|
||||
max_loss = min(t['pnl_amount'] for t in losing_trades) if losing_trades else 0
|
||||
profit_factor = total_win / total_loss if total_loss > 0 else float('inf')
|
||||
|
||||
return {
|
||||
'total_trades': total_trades,
|
||||
'winning_trades': len(winning_trades),
|
||||
'losing_trades': len(losing_trades),
|
||||
'win_rate': win_rate,
|
||||
'total_pnl': total_pnl,
|
||||
'avg_pnl': avg_pnl,
|
||||
'max_win': max_win,
|
||||
'max_loss': max_loss,
|
||||
'profit_factor': profit_factor,
|
||||
'final_balance': self.current_balance,
|
||||
'total_return': ((self.current_balance - self.initial_balance) / self.initial_balance * 100)
|
||||
}
|
||||
|
||||
|
||||
# ------------------ 主程序 ------------------
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("ETHUSDT 交易策略 - 乌云盖顶 & 刺透形态")
|
||||
print(f"数据周期: {STEP}分钟K线")
|
||||
print(f"分析时间: 2025年12月")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 从CSV文件读取数据
|
||||
print(f"\n📥 正在从 {CSV_FILE} 读取数据...")
|
||||
|
||||
try:
|
||||
df = pd.read_csv(CSV_FILE)
|
||||
print(f"成功读取 {len(df)} 条K线数据")
|
||||
|
||||
# 转换时间戳为datetime
|
||||
df['datetime'] = pd.to_datetime(df['id'], unit='s')
|
||||
df.set_index('datetime', inplace=True)
|
||||
|
||||
# 按时间排序
|
||||
df = df.sort_index()
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"❌ 错误: 文件 {CSV_FILE} 不存在")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"❌ 读取文件时出错: {e}")
|
||||
return
|
||||
|
||||
# 2. 过滤出12月份数据
|
||||
dec_2025_start = pd.Timestamp('2025-12-01')
|
||||
dec_2025_end = pd.Timestamp('2025-12-31 23:59:59')
|
||||
|
||||
# 确保数据在指定范围内
|
||||
mask = (df.index >= dec_2025_start) & (df.index <= dec_2025_end)
|
||||
dec_df = df.loc[mask].copy()
|
||||
|
||||
if len(dec_df) == 0:
|
||||
print("❌ 未找到2025年12月的数据")
|
||||
print(f"数据时间范围: {df.index[0]} 到 {df.index[-1]}")
|
||||
return
|
||||
|
||||
print(f"\n📊 12月份数据: {len(dec_df)} 条K线")
|
||||
print(f"时间范围: {dec_df.index[0]} 到 {dec_df.index[-1]}")
|
||||
|
||||
# 显示数据预览
|
||||
print(f"\n数据预览:")
|
||||
print(dec_df[['open', 'high', 'low', 'close', 'volume']].head())
|
||||
|
||||
# 3. 初始化策略
|
||||
strategy = TradingStrategy(initial_balance=10000)
|
||||
|
||||
# 4. 运行策略
|
||||
print("\n" + "=" * 60)
|
||||
print("开始执行交易策略...")
|
||||
print("=" * 60)
|
||||
|
||||
for i in range(1, len(dec_df)):
|
||||
current_time = int(dec_df.iloc[i]['id'])
|
||||
current_price = dec_df.iloc[i]['close']
|
||||
|
||||
# 首先检查止损止盈
|
||||
strategy.check_stop_loss_take_profit(dec_df, i)
|
||||
|
||||
# 如果有持仓,跳过新信号(单次只持有一个仓位)
|
||||
if len(strategy.positions) >= strategy.max_positions:
|
||||
continue
|
||||
|
||||
# 检测形态
|
||||
dark_cloud = strategy.detect_dark_cloud_cover(dec_df, i)
|
||||
piercing = strategy.detect_piercing_pattern(dec_df, i)
|
||||
|
||||
# 处理信号
|
||||
if dark_cloud:
|
||||
# 乌云盖顶 - 看跌信号,开空仓
|
||||
strategy.open_position('short', current_price, current_time, "乌云盖顶形态")
|
||||
|
||||
elif piercing:
|
||||
# 刺透形态 - 看涨信号,开多仓
|
||||
strategy.open_position('long', current_price, current_time, "刺透形态")
|
||||
|
||||
# 5. 强制平掉所有未平仓
|
||||
print("\n" + "=" * 60)
|
||||
print("强制平仓所有持仓...")
|
||||
print("=" * 60)
|
||||
|
||||
if strategy.positions:
|
||||
last_price = dec_df.iloc[-1]['close']
|
||||
last_time = dec_df.iloc[-1]['id']
|
||||
|
||||
# 按索引从大到小平仓
|
||||
for idx in range(len(strategy.positions) - 1, -1, -1):
|
||||
strategy.close_position(idx, last_price, last_time, "策略结束强制平仓")
|
||||
else:
|
||||
print("没有需要平仓的持仓")
|
||||
|
||||
# 6. 生成交易报告
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 交易汇总报告")
|
||||
print("=" * 60)
|
||||
|
||||
analysis = strategy.analyze_trades()
|
||||
|
||||
if strategy.trades:
|
||||
print(f"总交易次数: {analysis['total_trades']}")
|
||||
print(f"盈利交易: {analysis['winning_trades']} 次")
|
||||
print(f"亏损交易: {analysis['losing_trades']} 次")
|
||||
print(f"胜率: {analysis['win_rate']:.2f}%")
|
||||
print(f"总盈亏: ${analysis['total_pnl']:.2f}")
|
||||
print(f"平均每笔盈亏: ${analysis['avg_pnl']:.2f}")
|
||||
print(f"最大盈利: ${analysis['max_win']:.2f}")
|
||||
print(f"最大亏损: ${analysis['max_loss']:.2f}")
|
||||
print(f"盈利因子: {analysis['profit_factor']:.2f}")
|
||||
print(f"初始资金: ${strategy.initial_balance:.2f}")
|
||||
print(f"最终资金: ${analysis['final_balance']:.2f}")
|
||||
print(f"总收益率: {analysis['total_return']:.2f}%")
|
||||
|
||||
# 打印每笔交易详情
|
||||
print("\n" + "-" * 60)
|
||||
print("详细交易记录:")
|
||||
print("-" * 60)
|
||||
|
||||
for i, trade in enumerate(strategy.trades, 1):
|
||||
print(f"\n交易 #{i}:")
|
||||
print(f" 方向: {'做多' if trade['direction'] == 'long' else '做空'}")
|
||||
print(f" 开仓时间: {datetime.fromtimestamp(trade['open_time'])}")
|
||||
print(f" 开仓价格: ${trade['open_price']:.2f}")
|
||||
print(f" 平仓时间: {datetime.fromtimestamp(trade['close_time'])}")
|
||||
print(f" 平仓价格: ${trade['close_price']:.2f}")
|
||||
print(f" 持仓时间: {trade['duration_minutes']:.1f} 分钟")
|
||||
print(f" 盈亏: ${trade['pnl_amount']:.2f} ({trade['pnl_pct']:.2f}%)")
|
||||
print(f" 原因: {trade['open_reason']} -> {trade['close_reason']}")
|
||||
else:
|
||||
print("本月无交易记录")
|
||||
|
||||
# 7. 保存交易记录到CSV
|
||||
if strategy.trades:
|
||||
trades_df = pd.DataFrame(strategy.trades)
|
||||
|
||||
# 格式化时间列
|
||||
trades_df['open_datetime'] = trades_df['open_time'].apply(lambda x: datetime.fromtimestamp(x))
|
||||
trades_df['close_datetime'] = trades_df['close_time'].apply(lambda x: datetime.fromtimestamp(x))
|
||||
|
||||
# 选择要保存的列
|
||||
columns_to_save = [
|
||||
'open_datetime', 'close_datetime', 'direction', 'open_price',
|
||||
'close_price', 'size', 'pnl_amount', 'pnl_pct', 'duration_minutes',
|
||||
'open_reason', 'close_reason'
|
||||
]
|
||||
|
||||
trades_csv = f"eth_trades_{START_YEAR}_12.csv"
|
||||
trades_df[columns_to_save].to_csv(trades_csv, index=False)
|
||||
print(f"\n✅ 交易记录已保存到: {trades_csv}")
|
||||
|
||||
# 8. 保存策略参数和结果
|
||||
with open(f"strategy_summary_{START_YEAR}_12.txt", 'w') as f:
|
||||
f.write("=" * 60 + "\n")
|
||||
f.write("ETHUSDT 交易策略总结\n")
|
||||
f.write("=" * 60 + "\n\n")
|
||||
f.write(f"策略名称: 乌云盖顶 & 刺透形态\n")
|
||||
f.write(f"数据周期: {STEP}分钟K线\n")
|
||||
f.write(f"分析时间: 2025年12月\n")
|
||||
f.write(f"初始资金: ${strategy.initial_balance:.2f}\n")
|
||||
f.write(f"仓位大小: {strategy.position_size} ETH\n")
|
||||
f.write(f"止损比例: {strategy.stop_loss_pct * 100}%\n")
|
||||
f.write(f"止盈比例: {strategy.take_profit_pct * 100}%\n\n")
|
||||
|
||||
if strategy.trades:
|
||||
f.write(f"总交易次数: {analysis['total_trades']}\n")
|
||||
f.write(f"盈利交易: {analysis['winning_trades']} 次\n")
|
||||
f.write(f"亏损交易: {analysis['losing_trades']} 次\n")
|
||||
f.write(f"胜率: {analysis['win_rate']:.2f}%\n")
|
||||
f.write(f"总盈亏: ${analysis['total_pnl']:.2f}\n")
|
||||
f.write(f"最终资金: ${analysis['final_balance']:.2f}\n")
|
||||
f.write(f"总收益率: {analysis['total_return']:.2f}%\n")
|
||||
else:
|
||||
f.write("本月无交易记录\n")
|
||||
|
||||
print(f"\n✅ 策略总结已保存到: strategy_summary_{START_YEAR}_12.txt")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("策略执行完成!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
83
bitmart/回测.py
Normal file
83
bitmart/回测.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import time
|
||||
import csv
|
||||
|
||||
import loguru
|
||||
from bitmart.api_contract import APIContract
|
||||
|
||||
# ------------------ 配置 ------------------
|
||||
START_YEAR = 2025
|
||||
CONTRACT_SYMBOL = "ETHUSDT"
|
||||
STEP = 3 # K 线周期,单位分钟
|
||||
CSV_FILE = f"kline_{STEP}.csv"
|
||||
|
||||
memo = "合约交易"
|
||||
api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
|
||||
secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
|
||||
|
||||
contractAPI = APIContract(api_key, secret_key, memo, timeout=(5, 15))
|
||||
|
||||
# ------------------ 时间戳 ------------------
|
||||
start_of_year = int(time.mktime((START_YEAR, 1, 1, 0, 0, 0, 0, 0, 0)))
|
||||
current_time = int(time.time())
|
||||
|
||||
# ------------------ 抓取数据 ------------------
|
||||
all_data = []
|
||||
existing_ids = set()
|
||||
start_time = start_of_year
|
||||
|
||||
# 每次请求时间长度 = step * 500 条 K 线
|
||||
request_interval_ms = STEP * 60 * 500
|
||||
|
||||
while start_time < current_time:
|
||||
end_time = min(start_time + request_interval_ms, current_time)
|
||||
loguru.logger.info(f"抓取时间段: {start_time} ~ {end_time}")
|
||||
|
||||
try:
|
||||
response = contractAPI.get_kline(
|
||||
contract_symbol=CONTRACT_SYMBOL,
|
||||
step=STEP,
|
||||
start_time=start_time,
|
||||
end_time=end_time
|
||||
)[0]["data"]
|
||||
|
||||
formatted = []
|
||||
for k in response:
|
||||
print(k)
|
||||
k_id = int(k["timestamp"])
|
||||
if k_id in existing_ids:
|
||||
continue
|
||||
existing_ids.add(k_id)
|
||||
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"]),
|
||||
'volume': float(k["volume"])
|
||||
})
|
||||
|
||||
formatted.sort(key=lambda x: x['id'])
|
||||
all_data.extend(formatted)
|
||||
|
||||
if len(response) < 500:
|
||||
start_time = end_time
|
||||
else:
|
||||
start_time = formatted[-1]['id'] + 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"请求出错: {e},等待 60 秒后重试")
|
||||
time.sleep(60)
|
||||
|
||||
time.sleep(0.2) # 控制速率,保证 <= 每 2 秒 12 次
|
||||
|
||||
# ------------------ 保存 CSV ------------------
|
||||
csv_columns = ['id', 'open', 'high', 'low', 'close', 'volume']
|
||||
try:
|
||||
with open(CSV_FILE, 'w', newline='') as csvfile:
|
||||
writer = csv.DictWriter(csvfile, fieldnames=csv_columns)
|
||||
writer.writeheader()
|
||||
for data in all_data:
|
||||
writer.writerow(data)
|
||||
print(f"数据已保存到 {CSV_FILE},共 {len(all_data)} 条")
|
||||
except IOError:
|
||||
print("I/O error")
|
||||
768
bitmart/指数平均线.py
Normal file
768
bitmart/指数平均线.py
Normal file
@@ -0,0 +1,768 @@
|
||||
"""
|
||||
量化交易回测系统 - EMA交叉策略(双EMA/三EMA)
|
||||
"""
|
||||
import csv
|
||||
import datetime
|
||||
import numpy as np
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
|
||||
|
||||
# ========================= EMA计算函数 =========================
|
||||
|
||||
def calculate_ema(prices: List[float], period: int) -> List[Optional[float]]:
|
||||
"""
|
||||
计算指数移动平均线(EMA)
|
||||
|
||||
Args:
|
||||
prices: 价格列表(通常是收盘价)
|
||||
period: EMA周期
|
||||
|
||||
Returns:
|
||||
EMA值列表,前period-1个为None
|
||||
"""
|
||||
if len(prices) < period:
|
||||
return [None] * len(prices)
|
||||
|
||||
ema_values = [None] * (period - 1)
|
||||
|
||||
# 计算初始SMA作为EMA的起点
|
||||
sma = sum(prices[:period]) / period
|
||||
|
||||
# EMA计算公式:EMA_today = (Price_today * (2/(period+1))) + (EMA_yesterday * (1 - (2/(period+1))))
|
||||
multiplier = 2 / (period + 1)
|
||||
|
||||
# 第一个EMA值
|
||||
ema = sma
|
||||
ema_values.append(ema)
|
||||
|
||||
# 计算后续EMA值
|
||||
for price in prices[period:]:
|
||||
ema = (price * multiplier) + (ema * (1 - multiplier))
|
||||
ema_values.append(ema)
|
||||
|
||||
return ema_values
|
||||
|
||||
|
||||
# ========================= 策略核心函数 =========================
|
||||
|
||||
def check_ema_cross(ema_fast: List[Optional[float]],
|
||||
ema_slow: List[Optional[float]],
|
||||
idx: int) -> Optional[str]:
|
||||
"""
|
||||
检查EMA金叉/死叉
|
||||
|
||||
Args:
|
||||
ema_fast: 快线EMA值列表
|
||||
ema_slow: 慢线EMA值列表
|
||||
idx: 当前K线索引
|
||||
|
||||
Returns:
|
||||
"golden" - 金叉(做多信号)
|
||||
"dead" - 死叉(做空信号)
|
||||
None - 无交叉
|
||||
"""
|
||||
if idx < 1:
|
||||
return None
|
||||
|
||||
# 确保有足够的数据
|
||||
if ema_fast[idx] is None or ema_fast[idx - 1] is None:
|
||||
return None
|
||||
if ema_slow[idx] is None or ema_slow[idx - 1] is None:
|
||||
return None
|
||||
|
||||
# 前一根K线快线在慢线下方,当前K线快线上穿慢线 -> 金叉
|
||||
if ema_fast[idx - 1] < ema_slow[idx - 1] and ema_fast[idx] > ema_slow[idx]:
|
||||
return "golden"
|
||||
|
||||
# 前一根K线快线在慢线上方,当前K线快线下穿慢线 -> 死叉
|
||||
if ema_fast[idx - 1] > ema_slow[idx - 1] and ema_fast[idx] < ema_slow[idx]:
|
||||
return "dead"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def check_triple_ema_cross(ema_fast: List[Optional[float]],
|
||||
ema_mid: List[Optional[float]],
|
||||
ema_slow: List[Optional[float]],
|
||||
idx: int) -> Optional[str]:
|
||||
"""
|
||||
检查三EMA交叉(更稳定的信号)
|
||||
规则:快线 > 中线 > 慢线 -> 多头排列 -> 做多
|
||||
快线 < 中线 < 慢线 -> 空头排列 -> 做空
|
||||
|
||||
Args:
|
||||
ema_fast: 快线(如EMA7)
|
||||
ema_mid: 中线(如EMA14)
|
||||
ema_slow: 慢线(如EMA30)
|
||||
idx: 当前K线索引
|
||||
|
||||
Returns:
|
||||
"golden" - 金叉多头排列
|
||||
"dead" - 死叉空头排列
|
||||
None - 无明确信号
|
||||
"""
|
||||
if idx < 1:
|
||||
return None
|
||||
|
||||
# 确保有足够的数据
|
||||
if any(ema[idx] is None or ema[idx - 1] is None for ema in [ema_fast, ema_mid, ema_slow]):
|
||||
return None
|
||||
|
||||
# 检查是否形成多头排列(EMA快 > 中 > 慢)
|
||||
current_fast = ema_fast[idx]
|
||||
current_mid = ema_mid[idx]
|
||||
current_slow = ema_slow[idx]
|
||||
prev_fast = ema_fast[idx - 1]
|
||||
prev_mid = ema_mid[idx - 1]
|
||||
prev_slow = ema_slow[idx - 1]
|
||||
|
||||
# 多头排列条件:快线 > 中线 > 慢线
|
||||
is_golden_triple = current_fast > current_mid > current_slow
|
||||
was_not_golden = not (prev_fast > prev_mid > prev_slow)
|
||||
|
||||
# 空头排列条件:快线 < 中线 < 慢线
|
||||
is_dead_triple = current_fast < current_mid < current_slow
|
||||
was_not_dead = not (prev_fast < prev_mid < prev_slow)
|
||||
|
||||
if is_golden_triple and was_not_golden:
|
||||
return "golden"
|
||||
elif is_dead_triple and was_not_dead:
|
||||
return "dead"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ========================= 回测引擎 =========================
|
||||
|
||||
class EMABacktester:
|
||||
"""EMA交叉策略回测器"""
|
||||
|
||||
def __init__(self,
|
||||
fast_period: int = 12,
|
||||
slow_period: int = 26,
|
||||
signal_period: int = 9, # 用于MACD信号线
|
||||
use_triple_ema: bool = False,
|
||||
mid_period: int = 14, # 三EMA时的中间周期
|
||||
use_macd_confirmation: bool = False,
|
||||
stop_loss_pct: float = 0.02, # 2%止损
|
||||
take_profit_pct: float = 0.05, # 5%止盈
|
||||
trailing_stop_pct: float = 0.03): # 3%移动止损
|
||||
|
||||
self.fast_period = fast_period
|
||||
self.slow_period = slow_period
|
||||
self.signal_period = signal_period
|
||||
self.use_triple_ema = use_triple_ema
|
||||
self.mid_period = mid_period
|
||||
self.use_macd_confirmation = use_macd_confirmation
|
||||
self.stop_loss_pct = stop_loss_pct
|
||||
self.take_profit_pct = take_profit_pct
|
||||
self.trailing_stop_pct = trailing_stop_pct
|
||||
|
||||
self.stats = {
|
||||
'golden_cross': {'count': 0, 'wins': 0, 'total_profit': 0.0, 'name': '金叉做多'},
|
||||
'dead_cross': {'count': 0, 'wins': 0, 'total_profit': 0.0, 'name': '死叉做空'},
|
||||
}
|
||||
|
||||
def calculate_macd(self, prices: List[float]) -> Tuple[List[Optional[float]],
|
||||
List[Optional[float]],
|
||||
List[Optional[float]]]:
|
||||
"""
|
||||
计算MACD指标
|
||||
Returns: (MACD线, 信号线, 柱状图)
|
||||
"""
|
||||
# 计算快慢EMA
|
||||
ema_fast = calculate_ema(prices, self.fast_period)
|
||||
ema_slow = calculate_ema(prices, self.slow_period)
|
||||
|
||||
# 计算MACD线 = EMA快线 - EMA慢线
|
||||
macd_line = []
|
||||
for i in range(len(prices)):
|
||||
if ema_fast[i] is not None and ema_slow[i] is not None:
|
||||
macd_line.append(ema_fast[i] - ema_slow[i])
|
||||
else:
|
||||
macd_line.append(None)
|
||||
|
||||
# 计算信号线(MACD的EMA)
|
||||
signal_line = calculate_ema([x for x in macd_line if x is not None] if any(macd_line) else [],
|
||||
self.signal_period)
|
||||
|
||||
# 补全None值
|
||||
signal_line_extended = [None] * (len(prices) - len(signal_line)) + signal_line if len(signal_line) < len(
|
||||
prices) else signal_line
|
||||
|
||||
# 计算柱状图
|
||||
histogram = []
|
||||
for i in range(len(prices)):
|
||||
if macd_line[i] is not None and signal_line_extended[i] is not None:
|
||||
histogram.append(macd_line[i] - signal_line_extended[i])
|
||||
else:
|
||||
histogram.append(None)
|
||||
|
||||
return macd_line, signal_line_extended, histogram
|
||||
|
||||
def backtest(self, data: List[Dict]) -> Tuple[List[Dict], Dict]:
|
||||
"""
|
||||
执行EMA交叉策略回测
|
||||
|
||||
Args:
|
||||
data: K线数据列表
|
||||
|
||||
Returns:
|
||||
trades: 交易记录列表
|
||||
stats: 统计数据
|
||||
"""
|
||||
# 提取收盘价
|
||||
close_prices = [float(c['close']) for c in data]
|
||||
|
||||
# 计算EMA
|
||||
ema_fast = calculate_ema(close_prices, self.fast_period)
|
||||
ema_slow = calculate_ema(close_prices, self.slow_period)
|
||||
|
||||
# 如果需要三EMA,计算中间EMA
|
||||
ema_mid = None
|
||||
if self.use_triple_ema:
|
||||
ema_mid = calculate_ema(close_prices, self.mid_period)
|
||||
|
||||
# 如果需要MACD确认,计算MACD
|
||||
macd_line, signal_line, histogram = None, None, None
|
||||
if self.use_macd_confirmation:
|
||||
macd_line, signal_line, histogram = self.calculate_macd(close_prices)
|
||||
|
||||
trades: List[Dict] = []
|
||||
current_position: Optional[Dict] = None
|
||||
highest_price_since_entry = 0 # 用于移动止损
|
||||
lowest_price_since_entry = float('inf') # 用于移动止损
|
||||
|
||||
# 遍历K线数据(跳过前几个没有EMA值的)
|
||||
start_idx = max(self.fast_period, self.slow_period)
|
||||
if self.use_triple_ema:
|
||||
start_idx = max(start_idx, self.mid_period)
|
||||
|
||||
for idx in range(start_idx, len(data)):
|
||||
current_bar = data[idx]
|
||||
current_price = float(current_bar['close'])
|
||||
open_price = float(current_bar['open'])
|
||||
|
||||
# ========== 信号检测 ==========
|
||||
signal = None
|
||||
|
||||
# 基础双EMA交叉信号
|
||||
if not self.use_triple_ema:
|
||||
signal = check_ema_cross(ema_fast, ema_slow, idx)
|
||||
# 三EMA排列信号
|
||||
else:
|
||||
signal = check_triple_ema_cross(ema_fast, ema_mid, ema_slow, idx)
|
||||
|
||||
# MACD确认(可选)
|
||||
if signal and self.use_macd_confirmation:
|
||||
macd_confirmed = False
|
||||
if signal == "golden":
|
||||
# 金叉确认:MACD线在信号线上方且柱状图为正
|
||||
if macd_line[idx] is not None and signal_line[idx] is not None:
|
||||
macd_confirmed = (macd_line[idx] > signal_line[idx]) and (
|
||||
histogram[idx] is not None and histogram[idx] > 0)
|
||||
elif signal == "dead":
|
||||
# 死叉确认:MACD线在信号线下方且柱状图为负
|
||||
if macd_line[idx] is not None and signal_line[idx] is not None:
|
||||
macd_confirmed = (macd_line[idx] < signal_line[idx]) and (
|
||||
histogram[idx] is not None and histogram[idx] < 0)
|
||||
|
||||
if not macd_confirmed:
|
||||
signal = None
|
||||
|
||||
# ========== 空仓时开仓 ==========
|
||||
if current_position is None and signal:
|
||||
# 下一根K线开盘价入场
|
||||
if idx + 1 < len(data):
|
||||
entry_price = float(data[idx + 1]['open'])
|
||||
|
||||
if signal == "golden": # 做多
|
||||
current_position = {
|
||||
'direction': 'long',
|
||||
'entry_price': entry_price,
|
||||
'entry_time': data[idx + 1]['id'],
|
||||
'entry_idx': idx + 1,
|
||||
'signal': 'golden_cross',
|
||||
'highest_price': entry_price, # 用于移动止损
|
||||
'lowest_price': entry_price, # 用于空头的移动止损
|
||||
}
|
||||
self.stats['golden_cross']['count'] += 1
|
||||
|
||||
elif signal == "dead": # 做空
|
||||
current_position = {
|
||||
'direction': 'short',
|
||||
'entry_price': entry_price,
|
||||
'entry_time': data[idx + 1]['id'],
|
||||
'entry_idx': idx + 1,
|
||||
'signal': 'dead_cross',
|
||||
'highest_price': entry_price,
|
||||
'lowest_price': entry_price,
|
||||
}
|
||||
self.stats['dead_cross']['count'] += 1
|
||||
|
||||
# 跳过下一根,因为已经在这根K线开盘入场
|
||||
continue
|
||||
|
||||
# ========== 持仓时处理 ==========
|
||||
if current_position:
|
||||
pos_dir = current_position['direction']
|
||||
entry_price = current_position['entry_price']
|
||||
signal_key = current_position['signal']
|
||||
|
||||
# 更新最高/最低价(用于移动止损)
|
||||
if pos_dir == 'long':
|
||||
current_position['highest_price'] = max(current_position['highest_price'], current_price)
|
||||
else: # short
|
||||
current_position['lowest_price'] = min(current_position['lowest_price'], current_price)
|
||||
|
||||
# ========== 止损止盈检查 ==========
|
||||
should_exit = False
|
||||
exit_reason = ""
|
||||
exit_price = current_price # 默认用收盘价平仓
|
||||
|
||||
# 固定止损
|
||||
if pos_dir == 'long':
|
||||
stop_loss_price = entry_price * (1 - self.stop_loss_pct)
|
||||
if current_price <= stop_loss_price:
|
||||
should_exit = True
|
||||
exit_reason = "止损"
|
||||
exit_price = stop_loss_price
|
||||
|
||||
# 固定止盈
|
||||
take_profit_price = entry_price * (1 + self.take_profit_pct)
|
||||
if current_price >= take_profit_price:
|
||||
should_exit = True
|
||||
exit_reason = "止盈"
|
||||
exit_price = take_profit_price
|
||||
|
||||
# 移动止损
|
||||
trailing_stop_price = current_position['highest_price'] * (1 - self.trailing_stop_pct)
|
||||
if current_price <= trailing_stop_price:
|
||||
should_exit = True
|
||||
exit_reason = "移动止损"
|
||||
exit_price = trailing_stop_price
|
||||
|
||||
else: # short
|
||||
stop_loss_price = entry_price * (1 + self.stop_loss_pct)
|
||||
if current_price >= stop_loss_price:
|
||||
should_exit = True
|
||||
exit_reason = "止损"
|
||||
exit_price = stop_loss_price
|
||||
|
||||
# 固定止盈
|
||||
take_profit_price = entry_price * (1 - self.take_profit_pct)
|
||||
if current_price <= take_profit_price:
|
||||
should_exit = True
|
||||
exit_reason = "止盈"
|
||||
exit_price = take_profit_price
|
||||
|
||||
# 移动止损
|
||||
trailing_stop_price = current_position['lowest_price'] * (1 + self.trailing_stop_pct)
|
||||
if current_price >= trailing_stop_price:
|
||||
should_exit = True
|
||||
exit_reason = "移动止损"
|
||||
exit_price = trailing_stop_price
|
||||
|
||||
# ========== 反向信号检查 ==========
|
||||
if signal and (
|
||||
(signal == "dead" and pos_dir == "long") or
|
||||
(signal == "golden" and pos_dir == "short")
|
||||
):
|
||||
should_exit = True
|
||||
exit_reason = "反向信号"
|
||||
# 反向信号用下一根开盘价平仓
|
||||
if idx + 1 < len(data):
|
||||
exit_price = float(data[idx + 1]['open'])
|
||||
|
||||
# ========== 执行平仓 ==========
|
||||
if should_exit:
|
||||
# 计算盈亏
|
||||
if pos_dir == 'long':
|
||||
diff = exit_price - entry_price
|
||||
else: # short
|
||||
diff = entry_price - exit_price
|
||||
|
||||
# 记录交易
|
||||
trade = {
|
||||
'entry_time': datetime.datetime.fromtimestamp(current_position['entry_time']),
|
||||
'exit_time': datetime.datetime.fromtimestamp(current_bar['id']),
|
||||
'signal': self.stats[signal_key]['name'],
|
||||
'direction': '做多' if pos_dir == 'long' else '做空',
|
||||
'entry': entry_price,
|
||||
'exit': exit_price,
|
||||
'diff': diff,
|
||||
'exit_reason': exit_reason,
|
||||
'holding_bars': idx - current_position['entry_idx'] + 1,
|
||||
}
|
||||
trades.append(trade)
|
||||
|
||||
# 更新统计
|
||||
self.stats[signal_key]['total_profit'] += diff
|
||||
if diff > 0:
|
||||
self.stats[signal_key]['wins'] += 1
|
||||
|
||||
# 平仓
|
||||
current_position = None
|
||||
|
||||
# 如果是因为反向信号平仓,立即反手开仓
|
||||
if exit_reason == "反向信号" and signal and idx + 1 < len(data):
|
||||
if signal == "golden": # 反手做多
|
||||
current_position = {
|
||||
'direction': 'long',
|
||||
'entry_price': exit_price, # 同价反手
|
||||
'entry_time': data[idx + 1]['id'],
|
||||
'entry_idx': idx + 1,
|
||||
'signal': 'golden_cross',
|
||||
'highest_price': exit_price,
|
||||
'lowest_price': exit_price,
|
||||
}
|
||||
self.stats['golden_cross']['count'] += 1
|
||||
elif signal == "dead": # 反手做空
|
||||
current_position = {
|
||||
'direction': 'short',
|
||||
'entry_price': exit_price,
|
||||
'entry_time': data[idx + 1]['id'],
|
||||
'entry_idx': idx + 1,
|
||||
'signal': 'dead_cross',
|
||||
'highest_price': exit_price,
|
||||
'lowest_price': exit_price,
|
||||
}
|
||||
self.stats['dead_cross']['count'] += 1
|
||||
|
||||
# 跳过下一根K线
|
||||
continue
|
||||
|
||||
# ========== 尾仓处理 ==========
|
||||
if current_position:
|
||||
last_bar = data[-1]
|
||||
exit_price = float(last_bar['close'])
|
||||
pos_dir = current_position['direction']
|
||||
entry_price = current_position['entry_price']
|
||||
signal_key = current_position['signal']
|
||||
|
||||
diff = (exit_price - entry_price) if pos_dir == 'long' else (entry_price - exit_price)
|
||||
|
||||
trade = {
|
||||
'entry_time': datetime.datetime.fromtimestamp(current_position['entry_time']),
|
||||
'exit_time': datetime.datetime.fromtimestamp(last_bar['id']),
|
||||
'signal': self.stats[signal_key]['name'],
|
||||
'direction': '做多' if pos_dir == 'long' else '做空',
|
||||
'entry': entry_price,
|
||||
'exit': exit_price,
|
||||
'diff': diff,
|
||||
'exit_reason': "尾仓平仓",
|
||||
'holding_bars': len(data) - current_position['entry_idx'],
|
||||
}
|
||||
trades.append(trade)
|
||||
|
||||
self.stats[signal_key]['total_profit'] += diff
|
||||
if diff > 0:
|
||||
self.stats[signal_key]['wins'] += 1
|
||||
|
||||
return trades, self.stats
|
||||
|
||||
|
||||
# ========================= 可视化分析 =========================
|
||||
|
||||
def analyze_trades(trades: List[Dict], stats: Dict) -> Dict:
|
||||
"""深入分析交易结果"""
|
||||
if not trades:
|
||||
return {}
|
||||
|
||||
# 基础统计
|
||||
total_trades = len(trades)
|
||||
winning_trades = [t for t in trades if t['diff'] > 0]
|
||||
losing_trades = [t for t in trades if t['diff'] <= 0]
|
||||
|
||||
win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
|
||||
|
||||
# 盈亏统计
|
||||
total_profit = sum(t['diff'] for t in trades)
|
||||
avg_profit_per_trade = total_profit / total_trades if total_trades > 0 else 0
|
||||
|
||||
# 胜率相关
|
||||
avg_win = np.mean([t['diff'] for t in winning_trades]) if winning_trades else 0
|
||||
avg_loss = np.mean([t['diff'] for t in losing_trades]) if losing_trades else 0
|
||||
|
||||
# 盈亏比
|
||||
profit_factor = abs(avg_win / avg_loss) if avg_loss != 0 else float('inf')
|
||||
|
||||
# 最大连续盈利/亏损
|
||||
max_consecutive_wins = 0
|
||||
max_consecutive_losses = 0
|
||||
current_wins = 0
|
||||
current_losses = 0
|
||||
|
||||
for trade in trades:
|
||||
if trade['diff'] > 0:
|
||||
current_wins += 1
|
||||
current_losses = 0
|
||||
max_consecutive_wins = max(max_consecutive_wins, current_wins)
|
||||
else:
|
||||
current_losses += 1
|
||||
current_wins = 0
|
||||
max_consecutive_losses = max(max_consecutive_losses, current_losses)
|
||||
|
||||
# 持仓时间分析
|
||||
holding_bars = [t.get('holding_bars', 0) for t in trades]
|
||||
avg_holding_bars = np.mean(holding_bars) if holding_bars else 0
|
||||
|
||||
# 按平仓原因分析
|
||||
exit_reasons = {}
|
||||
for trade in trades:
|
||||
reason = trade.get('exit_reason', '未知')
|
||||
exit_reasons[reason] = exit_reasons.get(reason, 0) + 1
|
||||
|
||||
return {
|
||||
'total_trades': total_trades,
|
||||
'win_rate': win_rate,
|
||||
'total_profit': total_profit,
|
||||
'avg_profit_per_trade': avg_profit_per_trade,
|
||||
'avg_win': avg_win,
|
||||
'avg_loss': avg_loss,
|
||||
'profit_factor': profit_factor,
|
||||
'max_consecutive_wins': max_consecutive_wins,
|
||||
'max_consecutive_losses': max_consecutive_losses,
|
||||
'avg_holding_bars': avg_holding_bars,
|
||||
'exit_reasons': exit_reasons,
|
||||
'winning_trades_count': len(winning_trades),
|
||||
'losing_trades_count': len(losing_trades),
|
||||
}
|
||||
|
||||
|
||||
# ========================= 主程序 =========================
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 从CSV文件读取数据
|
||||
csv_file = "kline_3.csv" # 请替换为你的CSV文件路径
|
||||
read_data = []
|
||||
|
||||
try:
|
||||
with open(csv_file, 'r') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
for row in reader:
|
||||
read_data.append({
|
||||
'id': int(row['id']),
|
||||
'open': float(row['open']),
|
||||
'high': float(row['high']),
|
||||
'low': float(row['low']),
|
||||
'close': float(row['close'])
|
||||
})
|
||||
print(f"成功读取 {len(read_data)} 条K线数据")
|
||||
except FileNotFoundError:
|
||||
print(f"文件 {csv_file} 未找到,请检查路径")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f"读取CSV文件时出错: {e}")
|
||||
exit(1)
|
||||
|
||||
# 按时间排序
|
||||
read_data.sort(key=lambda x: x['id'])
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("EMA交叉策略回测系统")
|
||||
print("=" * 60)
|
||||
|
||||
# 策略参数选择
|
||||
print("\n请选择EMA策略类型:")
|
||||
print("1. 双EMA交叉策略 (默认: EMA12/EMA26)")
|
||||
print("2. 三EMA排列策略 (默认: EMA7/EMA14/EMA30)")
|
||||
print("3. EMA+MACD双重确认策略")
|
||||
|
||||
choice = input("\n请输入选择 (1-3,默认1): ").strip()
|
||||
|
||||
if choice == "2":
|
||||
# 三EMA策略
|
||||
backtester = EMABacktester(
|
||||
fast_period=7,
|
||||
slow_period=30,
|
||||
use_triple_ema=True,
|
||||
mid_period=14,
|
||||
use_macd_confirmation=False,
|
||||
stop_loss_pct=0.01, # 1.5%止损
|
||||
take_profit_pct=0.4, # 4%止盈
|
||||
trailing_stop_pct=0.04 # 2%移动止损
|
||||
)
|
||||
strategy_name = "三EMA排列策略(7/14/30)"
|
||||
|
||||
elif choice == "3":
|
||||
# EMA+MACD策略
|
||||
backtester = EMABacktester(
|
||||
fast_period=12,
|
||||
slow_period=26,
|
||||
signal_period=9,
|
||||
use_triple_ema=False,
|
||||
use_macd_confirmation=True,
|
||||
stop_loss_pct=0.1, # 1%止损
|
||||
take_profit_pct=0.4, # 3%止盈
|
||||
trailing_stop_pct=0.04 # 1.5%移动止损
|
||||
)
|
||||
strategy_name = "EMA+MACD双重确认策略(12/26/9)"
|
||||
|
||||
else:
|
||||
# 默认双EMA策略
|
||||
backtester = EMABacktester(
|
||||
fast_period=12,
|
||||
slow_period=26,
|
||||
use_triple_ema=False,
|
||||
use_macd_confirmation=False,
|
||||
stop_loss_pct=0.02, # 2%止损
|
||||
take_profit_pct=0.05, # 5%止盈
|
||||
trailing_stop_pct=0.03 # 3%移动止损
|
||||
)
|
||||
strategy_name = "双EMA交叉策略(12/26)"
|
||||
|
||||
print(f"\n使用策略: {strategy_name}")
|
||||
print("开始回测...")
|
||||
|
||||
# 执行回测
|
||||
trades, stats = backtester.backtest(read_data)
|
||||
|
||||
# ========== 交易详情和盈利计算 ==========
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"回测结果 - {strategy_name}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# 参数设定
|
||||
contract_size = 10000 # 合约规模
|
||||
open_fee_fixed = 5 # 固定开仓手续费
|
||||
close_fee_rate = 0.0005 # 平仓手续费率
|
||||
|
||||
total_points_profit = 0 # 累计点差
|
||||
total_money_profit = 0 # 累计金额盈利
|
||||
total_fee = 0 # 累计手续费
|
||||
|
||||
print(f"\n交易详情 (共{len(trades)}笔):")
|
||||
print("-" * 100)
|
||||
|
||||
for i, t in enumerate(trades, 1):
|
||||
entry = t['entry']
|
||||
exit = t['exit']
|
||||
direction = t['direction']
|
||||
|
||||
# 原始价差
|
||||
point_diff = (exit - entry) if direction == '做多' else (entry - exit)
|
||||
|
||||
# 金额盈利
|
||||
money_profit = point_diff / entry * contract_size
|
||||
|
||||
# 手续费
|
||||
fee = open_fee_fixed + (contract_size / entry * exit * close_fee_rate)
|
||||
|
||||
# 净利润
|
||||
net_profit = money_profit - fee
|
||||
|
||||
# 保存结果
|
||||
t.update({
|
||||
'point_diff': point_diff,
|
||||
'raw_profit': money_profit,
|
||||
'fee': fee,
|
||||
'net_profit': net_profit
|
||||
})
|
||||
|
||||
total_points_profit += point_diff
|
||||
total_money_profit += money_profit
|
||||
total_fee += fee
|
||||
|
||||
# 输出交易详情
|
||||
profit_color = "\033[92m" if net_profit > 0 else "\033[91m"
|
||||
reset_color = "\033[0m"
|
||||
|
||||
print(f"{i:3d}. {t['entry_time'].strftime('%Y-%m-%d %H:%M')} -> "
|
||||
f"{t['exit_time'].strftime('%Y-%m-%d %H:%M')} "
|
||||
f"{direction}({t['signal']}) "
|
||||
f"入={entry:.2f} 出={exit:.2f} "
|
||||
f"{profit_color}净利={net_profit:+.2f}{reset_color} "
|
||||
f"(持有:{t.get('holding_bars', '?')}根K线, 原因:{t.get('exit_reason', '未知')})")
|
||||
|
||||
# ========== 汇总统计 ==========
|
||||
total_net_profit = total_money_profit - total_fee
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("汇总统计")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
print(f"总交易笔数: {len(trades)}")
|
||||
print(f"总点差: {total_points_profit:.2f}")
|
||||
print(f"总原始盈利(未扣费): {total_money_profit:.2f}")
|
||||
print(f"总手续费: {total_fee:.2f}")
|
||||
print(f"总净利润: {total_net_profit:.2f}")
|
||||
|
||||
# 深入分析
|
||||
analysis = analyze_trades(trades, stats)
|
||||
|
||||
if analysis:
|
||||
print(f"\n策略分析:")
|
||||
print(f"- 胜率: {analysis['win_rate']:.2f}%")
|
||||
print(f"- 平均每笔盈利: {analysis['avg_profit_per_trade']:.2f}")
|
||||
print(f"- 平均盈利: {analysis['avg_win']:.2f}")
|
||||
print(f"- 平均亏损: {analysis['avg_loss']:.2f}")
|
||||
print(f"- 盈亏比: {analysis['profit_factor']:.2f}")
|
||||
print(f"- 最大连续盈利: {analysis['max_consecutive_wins']} 笔")
|
||||
print(f"- 最大连续亏损: {analysis['max_consecutive_losses']} 笔")
|
||||
print(f"- 平均持仓K线数: {analysis['avg_holding_bars']:.1f} 根")
|
||||
|
||||
print(f"\n平仓原因统计:")
|
||||
for reason, count in analysis['exit_reasons'].items():
|
||||
percentage = count / len(trades) * 100
|
||||
print(f" - {reason}: {count} 笔 ({percentage:.1f}%)")
|
||||
|
||||
# ========== 信号统计 ==========
|
||||
print(f"\n{'=' * 60}")
|
||||
print("信号统计")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
for k, v in stats.items():
|
||||
name, count, wins, total_p = v['name'], v['count'], v['wins'], v['total_profit']
|
||||
if count > 0:
|
||||
win_rate = (wins / count * 100)
|
||||
avg_p = total_p / count
|
||||
profit_color = "\033[92m" if total_p > 0 else "\033[91m"
|
||||
reset_color = "\033[0m"
|
||||
|
||||
print(f"{name}:")
|
||||
print(f" 信号次数: {count}")
|
||||
print(f" 胜率: {win_rate:.2f}%")
|
||||
print(f" 总价差: {profit_color}{total_p:.2f}{reset_color}")
|
||||
print(f" 平均价差: {avg_p:.2f}")
|
||||
|
||||
# ========== 风险指标 ==========
|
||||
if len(trades) > 1:
|
||||
returns = [t['net_profit'] for t in trades]
|
||||
|
||||
# 夏普比率(简化版)
|
||||
avg_return = np.mean(returns)
|
||||
std_return = np.std(returns)
|
||||
sharpe_ratio = avg_return / std_return if std_return > 0 else 0
|
||||
|
||||
# 最大回撤
|
||||
cumulative_returns = np.cumsum(returns)
|
||||
running_max = np.maximum.accumulate(cumulative_returns)
|
||||
drawdown = cumulative_returns - running_max
|
||||
max_drawdown = abs(np.min(drawdown)) if len(drawdown) > 0 else 0
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("风险指标")
|
||||
print(f"{'=' * 60}")
|
||||
print(f"夏普比率(简化): {sharpe_ratio:.4f}")
|
||||
print(f"最大回撤: {max_drawdown:.2f}")
|
||||
|
||||
# 盈亏分布
|
||||
print(f"\n盈亏分布:")
|
||||
profit_ranges = {
|
||||
"大亏 (< -100)": len([r for r in returns if r < -100]),
|
||||
"中亏 (-100 ~ -50)": len([r for r in returns if -100 <= r < -50]),
|
||||
"小亏 (-50 ~ 0)": len([r for r in returns if -50 <= r < 0]),
|
||||
"小盈 (0 ~ 50)": len([r for r in returns if 0 <= r < 50]),
|
||||
"中盈 (50 ~ 100)": len([r for r in returns if 50 <= r < 100]),
|
||||
"大盈 (> 100)": len([r for r in returns if r >= 100]),
|
||||
}
|
||||
|
||||
for range_name, count in profit_ranges.items():
|
||||
if count > 0:
|
||||
percentage = count / len(returns) * 100
|
||||
print(f" {range_name}: {count} 笔 ({percentage:.1f}%)")
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("回测完成!")
|
||||
print(f"{'=' * 60}")
|
||||
133
bitmart/锤子线和上吊线.py
Normal file
133
bitmart/锤子线和上吊线.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import csv
|
||||
from datetime import datetime, timezone
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# ---------------- 配置 ----------------
|
||||
CSV_FILE = "kline_3.csv" # CSV 文件路径
|
||||
LEVERAGE = 100
|
||||
CAPITAL = 10000
|
||||
POSITION_RATIO = 0.01
|
||||
FEE_RATE = 0.0005
|
||||
|
||||
# ---------------- 读取 CSV ----------------
|
||||
data = []
|
||||
with open(CSV_FILE, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
ts = int(row['id'])
|
||||
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
|
||||
if dt.year == 2025 and dt.month == 1:
|
||||
data.append({
|
||||
'time': dt,
|
||||
'open': float(row['open']),
|
||||
'high': float(row['high']),
|
||||
'low': float(row['low']),
|
||||
'close': float(row['close']),
|
||||
})
|
||||
|
||||
# ---------------- 策略回测 ----------------
|
||||
total_profit = 0
|
||||
total_fee = 0
|
||||
trades = [] # 保存每笔交易详情
|
||||
|
||||
for i in range(len(data) - 1):
|
||||
k = data[i]
|
||||
k_next = data[i + 1]
|
||||
|
||||
body = abs(k['close'] - k['open'])
|
||||
upper_shadow = k['high'] - max(k['close'], k['open'])
|
||||
lower_shadow = min(k['close'], k['open']) - k['low']
|
||||
position_usdt = CAPITAL * POSITION_RATIO
|
||||
leveraged_position = position_usdt * LEVERAGE
|
||||
|
||||
# ---------------- 锤子线 → 做多 ----------------
|
||||
if body != 0 and lower_shadow >= 2 * body:
|
||||
profit_raw = (k_next['close'] - k['close']) / k['close'] * leveraged_position
|
||||
fee_open = leveraged_position * FEE_RATE
|
||||
fee_close = leveraged_position * FEE_RATE
|
||||
profit_net = profit_raw - fee_open - fee_close
|
||||
|
||||
total_profit += profit_raw
|
||||
total_fee += fee_open + fee_close
|
||||
|
||||
trades.append({
|
||||
'方向': '多',
|
||||
'开仓时间': k['time'].strftime("%Y-%m-%d %H:%M"),
|
||||
'开仓价格': k['close'],
|
||||
'平仓时间': k_next['time'].strftime("%Y-%m-%d %H:%M"),
|
||||
'平仓价格': k_next['close'],
|
||||
'本金': position_usdt,
|
||||
'杠杆仓位': leveraged_position,
|
||||
'开仓手续费': fee_open,
|
||||
'平仓手续费': fee_close,
|
||||
'原始盈亏': profit_raw,
|
||||
'净盈亏': profit_net
|
||||
})
|
||||
|
||||
# ---------------- 上吊线 → 做空 ----------------
|
||||
elif body != 0 and upper_shadow >= 2 * body:
|
||||
profit_raw = (k['close'] - k_next['close']) / k['close'] * leveraged_position
|
||||
fee_open = leveraged_position * FEE_RATE
|
||||
fee_close = leveraged_position * FEE_RATE
|
||||
profit_net = profit_raw - fee_open - fee_close
|
||||
|
||||
total_profit += profit_raw
|
||||
total_fee += fee_open + fee_close
|
||||
|
||||
trades.append({
|
||||
'方向': '空',
|
||||
'开仓时间': k['time'].strftime("%Y-%m-%d %H:%M"),
|
||||
'开仓价格': k['close'],
|
||||
'平仓时间': k_next['time'].strftime("%Y-%m-%d %H:%M"),
|
||||
'平仓价格': k_next['close'],
|
||||
'本金': position_usdt,
|
||||
'杠杆仓位': leveraged_position,
|
||||
'开仓手续费': fee_open,
|
||||
'平仓手续费': fee_close,
|
||||
'原始盈亏': profit_raw,
|
||||
'净盈亏': profit_net
|
||||
})
|
||||
|
||||
# ---------------- 输出统计 ----------------
|
||||
print(f"1月总原始盈亏: {total_profit:.2f} USDT")
|
||||
print(f"1月总手续费: {total_fee:.2f} USDT")
|
||||
print(f"1月净盈亏: {total_profit - total_fee:.2f} USDT")
|
||||
print("\n交易明细(前10笔示例):")
|
||||
|
||||
n = 0
|
||||
for t in trades:
|
||||
if t['原始盈亏'] > 10 or t['原始盈亏'] < -10:
|
||||
print(f"{t['方向']}仓 | 开仓: {t['开仓时间']} @ {t['开仓价格']:.2f} | "
|
||||
f"平仓: {t['平仓时间']} @ {t['平仓价格']:.2f} | "
|
||||
f"本金: {t['本金']:.2f} | 杠杆仓位: {t['杠杆仓位']:.2f} | "
|
||||
f"开仓手续费: {t['开仓手续费']:.2f} | 平仓手续费: {t['平仓手续费']:.2f} | "
|
||||
f"原始盈亏: {t['原始盈亏']:.2f} | 净盈亏: {t['净盈亏']:.2f}")
|
||||
|
||||
n += t['原始盈亏']
|
||||
|
||||
print(n)
|
||||
|
||||
# # ---------------- 绘制 K 线图 + 交易点 ----------------
|
||||
# times = [k['time'] for k in data]
|
||||
# closes = [k['close'] for k in data]
|
||||
#
|
||||
# plt.figure(figsize=(16,6))
|
||||
# plt.plot(times, closes, color='black', label='ETH 收盘价')
|
||||
#
|
||||
# for t in trades:
|
||||
# open_time = datetime.strptime(t['开仓时间'], "%Y-%m-%d %H:%M")
|
||||
# close_time = datetime.strptime(t['平仓时间'], "%Y-%m-%d %H:%M")
|
||||
# if t['方向'] == '多':
|
||||
# plt.scatter(open_time, t['开仓价格'], color='green', marker='^', s=100, label='多开' if '多开' not in plt.gca().get_legend_handles_labels()[1] else "")
|
||||
# plt.scatter(close_time, t['平仓价格'], color='red', marker='v', s=100, label='多平' if '多平' not in plt.gca().get_legend_handles_labels()[1] else "")
|
||||
# else:
|
||||
# plt.scatter(open_time, t['开仓价格'], color='red', marker='v', s=100, label='空开' if '空开' not in plt.gca().get_legend_handles_labels()[1] else "")
|
||||
# plt.scatter(close_time, t['平仓价格'], color='green', marker='^', s=100, label='空平' if '空平' not in plt.gca().get_legend_handles_labels()[1] else "")
|
||||
#
|
||||
# plt.xlabel('时间')
|
||||
# plt.ylabel('价格(USDT)')
|
||||
# plt.title('ETH 永续合约 1 月交易回测(100倍杠杆)')
|
||||
# plt.legend()
|
||||
# plt.grid(True)
|
||||
# plt.gcf().autofmt_xdate()
|
||||
# plt.show()
|
||||
@@ -1,7 +1,7 @@
|
||||
from peewee import *
|
||||
|
||||
# 连接到 SQLite 数据库,如果文件不存在会自动创建
|
||||
db = SqliteDatabase(r'E:\新建文件夹\lm_job\models\database.db')
|
||||
db = SqliteDatabase(r'database.db')
|
||||
|
||||
import pymysql
|
||||
|
||||
|
||||
@@ -61,11 +61,11 @@ class Weex30Copy(Model):
|
||||
table_name = 'weex_30_copy1'
|
||||
|
||||
|
||||
# # 连接到数据库
|
||||
# db.connect()
|
||||
# 连接到数据库
|
||||
db.connect()
|
||||
#
|
||||
# # 创建表(如果表不存在)
|
||||
# db.create_tables([Weex15])
|
||||
# db.create_tables([Weex1])
|
||||
db.create_tables([Weex30])
|
||||
|
||||
|
||||
|
||||
321
test.py
321
test.py
@@ -1,33 +1,300 @@
|
||||
import requests
|
||||
import time
|
||||
import uuid
|
||||
import datetime
|
||||
|
||||
cookies = {
|
||||
'PHPSESSID': '8e48c4dd-69ef-561c-3082-e20564b88806',
|
||||
'ipsb': '3oMlNJzDmbhOQ7wtF96V451PWTxSvKfE',
|
||||
}
|
||||
from tqdm import tqdm
|
||||
from loguru import logger
|
||||
|
||||
headers = {
|
||||
'accept': '*/*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
||||
'cache-control': 'no-cache',
|
||||
'dnt': '1',
|
||||
'pragma': 'no-cache',
|
||||
'referer': 'https://ip.sb/',
|
||||
'sec-ch-ua': '"Chromium";v="142", "Microsoft Edge";v="142", "Not_A Brand";v="99"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'script',
|
||||
'sec-fetch-mode': 'no-cors',
|
||||
'sec-fetch-site': 'same-site',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0',
|
||||
# 'cookie': 'PHPSESSID=8e48c4dd-69ef-561c-3082-e20564b88806; ipsb=3oMlNJzDmbhOQ7wtF96V451PWTxSvKfE',
|
||||
}
|
||||
from bitmart.api_contract import APIContract
|
||||
from bitmart.lib.cloud_exceptions import APIException
|
||||
|
||||
proxies = {
|
||||
'http': f'http://104.168.59.92:24016',
|
||||
'https': f'http://104.168.59.92:24016',
|
||||
}
|
||||
from 交易.tools import send_dingtalk_message
|
||||
|
||||
|
||||
response = requests.get('https://ipv4.ip.sb/addrinfo', cookies=cookies, headers=headers, proxies=proxies)
|
||||
print(f"{response.json()}")
|
||||
class BitmartMarketMaker:
|
||||
def __init__(self):
|
||||
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.open_avg_price = None # 开仓均价(字符串或float)
|
||||
self.current_amount = 0
|
||||
self.position_cross = None
|
||||
|
||||
self.pbar = tqdm(total=10, desc="等待下次检查", ncols=80)
|
||||
|
||||
self.leverage = "10" # 低杠杆,安全做市
|
||||
self.open_type = "cross"
|
||||
self.fixed_size = 1 # 每次挂单量
|
||||
self.spread_offset = 5.0 # 挂单偏移(USDT),建议5~10,避免立即成交
|
||||
self.max_position_threshold = 10 # 库存阈值,超过自动平仓
|
||||
|
||||
# 新增:止盈止损参数(基于开仓均价的百分比)
|
||||
self.take_profit_pct = 2.0 # 止盈 2%
|
||||
self.stop_loss_pct = 1.0 # 止损 1%(可根据风险偏好调整)
|
||||
|
||||
self.price_precision = 0.1
|
||||
self.leverage_set = False
|
||||
self.max_retries = 5
|
||||
|
||||
def ding(self, msg, error=False):
|
||||
prefix = "❌bitmart MM:" if error else "🔔bitmart MM:"
|
||||
if error:
|
||||
for i in range(10):
|
||||
send_dingtalk_message(f"{prefix},{msg}")
|
||||
else:
|
||||
send_dingtalk_message(f"{prefix},{msg}")
|
||||
|
||||
def try_set_leverage(self):
|
||||
if self.leverage_set:
|
||||
return True
|
||||
|
||||
for attempt in range(self.max_retries):
|
||||
self.cancel_all_orders()
|
||||
time.sleep(2)
|
||||
|
||||
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 杠杆设置成功")
|
||||
self.leverage_set = True
|
||||
return True
|
||||
else:
|
||||
logger.error(f"杠杆设置失败 (尝试 {attempt+1}): {response}")
|
||||
except Exception as e:
|
||||
logger.error(f"设置杠杆异常 (尝试 {attempt+1}): {e}")
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
self.ding(error=True, msg="杠杆设置多次失败,请手动检查")
|
||||
return False
|
||||
|
||||
def get_depth(self):
|
||||
try:
|
||||
response = self.contractAPI.get_depth(contract_symbol=self.contract_symbol)[0]
|
||||
if response['code'] == 1000:
|
||||
data = response['data']
|
||||
best_bid = float(data['bids'][0][0]) if data['bids'] else None
|
||||
best_ask = float(data['asks'][0][0]) if data['asks'] else None
|
||||
return best_bid, best_ask
|
||||
else:
|
||||
logger.error(f"获取深度失败: {response}")
|
||||
return None, None
|
||||
except Exception as e:
|
||||
logger.error(f"获取深度异常: {e}")
|
||||
return None, None
|
||||
|
||||
def cancel_all_orders(self):
|
||||
try:
|
||||
response = self.contractAPI.post_cancel_orders(contract_symbol=self.contract_symbol)[0]
|
||||
if response['code'] == 1000:
|
||||
logger.success("所有挂单已取消")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"取消挂单失败: {response}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"取消挂单异常(忽略): {e}")
|
||||
return False
|
||||
|
||||
def place_limit_order(self, side: int, price: float, size: int):
|
||||
price = round(price / self.price_precision) * self.price_precision
|
||||
price_str = f"{price:.1f}"
|
||||
|
||||
client_order_id = f"mm_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
response = self.contractAPI.post_submit_order(
|
||||
contract_symbol=self.contract_symbol,
|
||||
client_order_id=client_order_id,
|
||||
side=side,
|
||||
mode=1,
|
||||
type='limit',
|
||||
leverage=self.leverage,
|
||||
open_type=self.open_type,
|
||||
price=price_str,
|
||||
size=size
|
||||
)[0]
|
||||
|
||||
if response['code'] == 1000:
|
||||
action = "挂买(开多)" if side == 1 else "挂卖(开空)"
|
||||
logger.success(f"限价单挂单成功: {action}, 价格={price}, 张数={size}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"挂单失败 (尝试 {attempt+1}): {response}")
|
||||
except APIException as e:
|
||||
logger.error(f"API挂单异常 (尝试 {attempt+1}): {e}")
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
return False
|
||||
|
||||
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.current_amount = 0
|
||||
self.open_avg_price = None
|
||||
return True
|
||||
position = positions[0]
|
||||
self.start = 1 if position['position_type'] == 1 else -1
|
||||
self.open_avg_price = float(position['open_avg_price']) if position['open_avg_price'] else 0.0
|
||||
self.current_amount = int(float(position.get('current_amount', 0)))
|
||||
self.position_cross = position.get("position_cross")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"持仓查询异常: {e}")
|
||||
self.ding(error=True, msg="持仓查询异常")
|
||||
return False
|
||||
|
||||
def get_available_balance(self):
|
||||
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 0.0
|
||||
except Exception as e:
|
||||
logger.error(f"余额查询异常: {e}")
|
||||
return 0.0
|
||||
|
||||
def check_take_profit_stop_loss(self, current_price: float):
|
||||
"""检查是否触发止盈或止损(基于当前价格)"""
|
||||
if self.current_amount == 0 or self.open_avg_price == 0.0:
|
||||
return False, None
|
||||
|
||||
if self.start == 1: # 多头
|
||||
pnl_pct = (current_price - self.open_avg_price) / self.open_avg_price * 100
|
||||
if pnl_pct >= self.take_profit_pct:
|
||||
return True, "止盈平多"
|
||||
if pnl_pct <= -self.stop_loss_pct:
|
||||
return True, "止损平多"
|
||||
|
||||
elif self.start == -1: # 空头
|
||||
pnl_pct = (self.open_avg_price - current_price) / self.open_avg_price * 100
|
||||
if pnl_pct >= self.take_profit_pct:
|
||||
return True, "止盈平空"
|
||||
if pnl_pct <= -self.stop_loss_pct:
|
||||
return True, "止损平空"
|
||||
|
||||
return False, None
|
||||
|
||||
def close_position(self, reason: str = "库存阈值"):
|
||||
"""市场全平仓,并发送通知"""
|
||||
if self.current_amount == 0:
|
||||
return
|
||||
|
||||
side = 3 if self.start == 1 else 2 # 3: 平多, 2: 平空
|
||||
try:
|
||||
response = self.contractAPI.post_submit_order(
|
||||
contract_symbol=self.contract_symbol,
|
||||
client_order_id=f"close_{reason}_{int(time.time())}",
|
||||
side=side,
|
||||
mode=1,
|
||||
type='market',
|
||||
leverage=self.leverage,
|
||||
open_type=self.open_type,
|
||||
size=999999
|
||||
)[0]
|
||||
if response['code'] == 1000:
|
||||
direction_str = "多" if self.start == 1 else "空"
|
||||
logger.success(f"{reason}平仓成功: 平{direction_str}")
|
||||
self.ding(msg=f"{reason}触发,已平仓 {direction_str}头仓位")
|
||||
self.start = 0
|
||||
self.current_amount = 0
|
||||
self.open_avg_price = None
|
||||
return True
|
||||
else:
|
||||
logger.error(f"平仓失败: {response}")
|
||||
return False
|
||||
except APIException as e:
|
||||
logger.error(f"API平仓异常: {e}")
|
||||
return False
|
||||
|
||||
def action(self):
|
||||
logger.info("程序启动,将在循环中动态尝试设置杠杆")
|
||||
|
||||
while True:
|
||||
if not self.try_set_leverage():
|
||||
logger.warning("杠杆未设置成功,继续重试...")
|
||||
|
||||
if not self.get_position_status():
|
||||
self.ding(error=True, msg="获取仓位信息失败!!!")
|
||||
time.sleep(10)
|
||||
continue
|
||||
|
||||
# 获取当前价格(使用中价)
|
||||
best_bid, best_ask = self.get_depth()
|
||||
if best_bid is None or best_ask is None:
|
||||
time.sleep(10)
|
||||
continue
|
||||
mid_price = (best_bid + best_ask) / 2
|
||||
|
||||
# 检查止盈止损
|
||||
trigger, reason = self.check_take_profit_stop_loss(mid_price)
|
||||
if trigger:
|
||||
self.close_position(reason=reason)
|
||||
# 平仓后继续下一轮(重新挂单)
|
||||
|
||||
# 检查库存阈值
|
||||
if abs(self.current_amount) > self.max_position_threshold:
|
||||
self.close_position(reason="库存阈值")
|
||||
|
||||
# 挂单
|
||||
bid_price = mid_price - self.spread_offset
|
||||
ask_price = mid_price + self.spread_offset
|
||||
|
||||
self.cancel_all_orders()
|
||||
|
||||
success_bid = self.place_limit_order(side=1, price=bid_price, size=self.fixed_size)
|
||||
success_ask = self.place_limit_order(side=4, price=ask_price, size=self.fixed_size)
|
||||
|
||||
# 统计盈亏百分比
|
||||
pnl_pct_str = ""
|
||||
if self.current_amount != 0 and self.open_avg_price:
|
||||
if self.start == 1:
|
||||
pnl_pct = (mid_price - self.open_avg_price) / self.open_avg_price * 100
|
||||
else:
|
||||
pnl_pct = (self.open_avg_price - mid_price) / self.open_avg_price * 100
|
||||
pnl_pct_str = f"浮动盈亏:{pnl_pct:+.2f}%"
|
||||
|
||||
balance = self.get_available_balance()
|
||||
leverage_status = "已设置" if self.leverage_set else "未同步(重试中)"
|
||||
msg = (
|
||||
f"【BitMart {self.contract_symbol} MM】\n"
|
||||
f"杠杆状态:{leverage_status}\n"
|
||||
f"当前中价:{mid_price:.2f} USDT\n"
|
||||
f"挂买价:{bid_price:.2f} ({'成功' if success_bid else '失败'})\n"
|
||||
f"挂卖价:{ask_price:.2f} ({'成功' if success_ask else '失败'})\n"
|
||||
f"持仓量:{self.current_amount} 张 {pnl_pct_str}\n"
|
||||
f"账户可用余额:{balance:.2f} USDT\n"
|
||||
f"止盈:+{self.take_profit_pct}% | 止损:-{self.stop_loss_pct}%"
|
||||
)
|
||||
self.ding(msg=msg)
|
||||
|
||||
self.pbar.reset()
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
BitmartMarketMaker().action()
|
||||
113
test1.py
113
test1.py
@@ -1,29 +1,92 @@
|
||||
import requests
|
||||
import csv
|
||||
from datetime import datetime, timezone
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
headers = {
|
||||
'accept': 'application/json, text/plain, */*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9',
|
||||
'cache-control': 'no-cache',
|
||||
'origin': 'https://www.websea.com',
|
||||
'pragma': 'no-cache',
|
||||
'priority': 'u=1, i',
|
||||
'referer': 'https://www.websea.com/',
|
||||
'sec-ch-ua': '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-site',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
|
||||
}
|
||||
# ---------------- 配置 ----------------
|
||||
CSV_FILE = "bitmart/kline_3.csv" # 你的 CSV 文件路径
|
||||
LEVERAGE = 100
|
||||
CAPITAL = 10000
|
||||
POSITION_RATIO = 0.01
|
||||
FEE_RATE = 0.00015
|
||||
|
||||
params = {
|
||||
'symbol': 'ETH-USDT',
|
||||
'period': '30min',
|
||||
'start': '1763480074',
|
||||
'end': '1766072134',
|
||||
}
|
||||
# ---------------- 读取 CSV ----------------
|
||||
data = []
|
||||
with open(CSV_FILE, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
ts = int(row['id'])
|
||||
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
|
||||
if dt.year == 2025 and dt.month == 1:
|
||||
data.append({
|
||||
'time': dt,
|
||||
'open': float(row['open']),
|
||||
'high': float(row['high']),
|
||||
'low': float(row['low']),
|
||||
'close': float(row['close']),
|
||||
})
|
||||
|
||||
response = requests.get('https://capi.websea.com/webApi/market/getKline', params=params, headers=headers)
|
||||
# ---------------- 识别交易信号 ----------------
|
||||
trades = []
|
||||
position_usdt = CAPITAL * POSITION_RATIO
|
||||
leveraged_position = position_usdt * LEVERAGE
|
||||
|
||||
print(response.json())
|
||||
for i in range(len(data)-1):
|
||||
k1 = data[i]
|
||||
k2 = data[i+1]
|
||||
|
||||
# 刺透形态(Piercing Line,多头)
|
||||
if k1['close'] < k1['open'] and k2['close'] > k2['open']:
|
||||
midpoint = (k1['open'] + k1['close']) / 2
|
||||
if k2['open'] < k1['close'] and k2['close'] > midpoint:
|
||||
trades.append({
|
||||
'方向': '多',
|
||||
'开仓时间': k2['time'],
|
||||
'开仓价格': k2['open'],
|
||||
'平仓时间': k2['time'],
|
||||
'平仓价格': k2['close']
|
||||
})
|
||||
|
||||
# 乌云盖顶(Dark Cloud Cover,空头)
|
||||
elif k1['close'] > k1['open'] and k2['close'] < k2['open']:
|
||||
midpoint = (k1['open'] + k1['close']) / 2
|
||||
if k2['open'] > k1['close'] and k2['close'] < midpoint:
|
||||
trades.append({
|
||||
'方向': '空',
|
||||
'开仓时间': k2['time'],
|
||||
'开仓价格': k2['open'],
|
||||
'平仓时间': k2['time'],
|
||||
'平仓价格': k2['close']
|
||||
})
|
||||
|
||||
# ---------------- 绘制 K 线图 ----------------
|
||||
plt.figure(figsize=(16,6))
|
||||
|
||||
times = [k['time'] for k in data]
|
||||
opens = [k['open'] for k in data]
|
||||
closes = [k['close'] for k in data]
|
||||
highs = [k['high'] for k in data]
|
||||
lows = [k['low'] for k in data]
|
||||
|
||||
# 绘制 K 线(用竖线表示最高最低价,用矩形表示开收盘价)
|
||||
for i in range(len(data)):
|
||||
color = 'green' if closes[i] >= opens[i] else 'red'
|
||||
plt.plot([times[i], times[i]], [lows[i], highs[i]], color='black') # 高低价
|
||||
plt.plot([times[i]-0.0005, times[i]+0.0005], [opens[i], opens[i]], color=color, linewidth=5) # 开盘价
|
||||
plt.plot([times[i]-0.0005, times[i]+0.0005], [closes[i], closes[i]], color=color, linewidth=5) # 收盘价
|
||||
|
||||
# ---------------- 标注交易信号 ----------------
|
||||
for t in trades:
|
||||
if t['方向'] == '多':
|
||||
plt.scatter(t['开仓时间'], t['开仓价格'], color='green', marker='^', s=100, label='多开' if '多开' not in plt.gca().get_legend_handles_labels()[1] else "")
|
||||
plt.scatter(t['平仓时间'], t['平仓价格'], color='red', marker='v', s=100, label='多平' if '多平' not in plt.gca().get_legend_handles_labels()[1] else "")
|
||||
else:
|
||||
plt.scatter(t['开仓时间'], t['开仓价格'], color='red', marker='v', s=100, label='空开' if '空开' not in plt.gca().get_legend_handles_labels()[1] else "")
|
||||
plt.scatter(t['平仓时间'], t['平仓价格'], color='green', marker='^', s=100, label='空平' if '空平' not in plt.gca().get_legend_handles_labels()[1] else "")
|
||||
|
||||
plt.xlabel('时间')
|
||||
plt.ylabel('价格(USDT)')
|
||||
plt.title('ETH 永续合约 1 月交易回测(刺透 & 乌云形态)')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
plt.gcf().autofmt_xdate()
|
||||
plt.show()
|
||||
|
||||
187
test2.py
187
test2.py
@@ -1,144 +1,63 @@
|
||||
import time
|
||||
import random
|
||||
import oss2
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import datetime
|
||||
from loguru import *
|
||||
from curl_cffi import requests
|
||||
def upload_file_to_oss(local_file_path):
|
||||
# 建议使用环境变量存储敏感信息,这里为了示例方便,假设已通过环境变量设置
|
||||
endpoint = 'https://oss-cn-beijing.aliyuncs.com'
|
||||
access_key_id = "LTAI5tRMxrM95Pi8JEEmqRcg"
|
||||
access_key_secret = "8vueGCsRVeFyQMcAA7sysO7LSnuJDG"
|
||||
|
||||
from models.combined_table import CombinedTable
|
||||
from models.concrete_wallet import ConcreteWallet
|
||||
from models.ips import Ips
|
||||
from tools import get_evm_wallet_singed_message
|
||||
if not access_key_id or not access_key_secret:
|
||||
print('❌ 错误: 未找到有效的 OSS 访问密钥,请检查环境变量。')
|
||||
return None
|
||||
|
||||
# 生成唯一 Bucket 名称
|
||||
bucket_name = f'oss-bucket-yj'
|
||||
print(f"创建 Bucket: {bucket_name}")
|
||||
|
||||
class Credentials:
|
||||
def __init__(self, sql_info):
|
||||
# 初始化 Bucket 对象
|
||||
auth = oss2.Auth(access_key_id, access_key_secret)
|
||||
bucket = oss2.Bucket(auth, endpoint, bucket_name)
|
||||
|
||||
self.sql_info = sql_info
|
||||
try:
|
||||
# 1. 创建 Bucket
|
||||
bucket.create_bucket(oss2.models.BUCKET_ACL_PUBLIC_READ) # 设置 Bucket 为公共读权限
|
||||
print(f'✅ 成功创建 Bucket: {bucket_name}')
|
||||
except oss2.exceptions.BucketAlreadyExists:
|
||||
print(f'⚠️ Bucket {bucket_name} 已经存在')
|
||||
print('提示:请使用不同的 Bucket 名称或使用现有 Bucket')
|
||||
except oss2.exceptions.OssError as e:
|
||||
print(f'❌ OSS 错误: {e}')
|
||||
print(f' 错误码: {e.code}')
|
||||
print(f' 请求 ID: {e.request_id}')
|
||||
return None
|
||||
|
||||
self.headers = {
|
||||
'accept': '*/*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
||||
'cache-control': 'no-cache',
|
||||
'content-type': 'application/json',
|
||||
'pragma': 'no-cache',
|
||||
'priority': 'u=1, i',
|
||||
'referer': 'https://points.concrete.xyz/home',
|
||||
'sec-ch-ua': '"Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0',
|
||||
# 'cookie': 'client-season=z2zi-tzc2; domain=https%3A%2F%2Fpoints.concrete.xyz; __Host-authjs.csrf-token=afc782765e3380ca973101d02f54c9516d317120fe83faea733390bfc2e46cb4%7Cdca5d6028cadbe52f05820005ed29862e6bd8fff72ae5514142c25bf0d0ad97f; __Secure-authjs.callback-url=https%3A%2F%2Fboost.absinthe.network; redirect-pathname=%2Fhome',
|
||||
}
|
||||
# 2. 验证本地文件是否存在
|
||||
if not Path(local_file_path).exists():
|
||||
print(f'❌ 文件错误: 本地文件 {local_file_path} 不存在')
|
||||
return None
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
file_name = os.path.basename(local_file_path)
|
||||
oss_object_name = uuid.uuid4().hex[:12] + file_name
|
||||
result = bucket.put_object_from_file(oss_object_name, local_file_path)
|
||||
|
||||
def csrf(self):
|
||||
for i in range(3):
|
||||
try:
|
||||
response = self.session.get('https://points.concrete.xyz/api/auth/csrf', )
|
||||
if result.status == 200:
|
||||
print(f'✅ 文件上传成功')
|
||||
print(f' ETag: {result.etag}')
|
||||
print(f' 文件大小: {result.resp.headers.get("content-length")} bytes')
|
||||
|
||||
return response.json()["csrfToken"]
|
||||
except:
|
||||
time.sleep(random.randint(1, 5))
|
||||
# 生成长期可访问的 URL
|
||||
public_url = f'https://{bucket_name}.{endpoint.replace("https://", "")}/{oss_object_name}'
|
||||
print(f' 文件公共 URL(长期有效): {public_url}')
|
||||
return public_url
|
||||
else:
|
||||
print(f'❌ 文件上传失败,状态码: {result.status}')
|
||||
return None
|
||||
|
||||
return False
|
||||
|
||||
def credentials(self, message, signature, csrfToken):
|
||||
params = ''
|
||||
|
||||
data = {
|
||||
'message': message,
|
||||
'redirect': 'false',
|
||||
'signature': "0x" + signature,
|
||||
'csrfToken': csrfToken,
|
||||
'callbackUrl': 'https://points.concrete.xyz/home',
|
||||
}
|
||||
|
||||
for i in range(3):
|
||||
try:
|
||||
response = self.session.post(
|
||||
'https://points.concrete.xyz/api/auth/callback/credentials',
|
||||
params=params,
|
||||
data=data,
|
||||
)
|
||||
print(response.json())
|
||||
|
||||
return True
|
||||
except:
|
||||
time.sleep(random.randint(1, 5))
|
||||
|
||||
return False
|
||||
|
||||
def add_ips_to_combined_table(self, ):
|
||||
# 获取 ips 表中的所有记录
|
||||
all_ips_records = Ips.select()
|
||||
for ips_record in all_ips_records:
|
||||
# 检查该记录是否已经存在于 CombinedTable 中
|
||||
existing_record = CombinedTable.get_or_none(
|
||||
CombinedTable.host == ips_record.host,
|
||||
CombinedTable.port == ips_record.port,
|
||||
CombinedTable.username == ips_record.username,
|
||||
CombinedTable.pwd == ips_record.pwd
|
||||
)
|
||||
if not existing_record:
|
||||
# 如果记录不存在,则将其添加到 CombinedTable 中
|
||||
# 这里假设 mnemonic 字段有默认值或者你可以根据需求传入具体值
|
||||
|
||||
self.com_info.host = ips_record.host
|
||||
self.com_info.port = ips_record.port
|
||||
self.com_info.username = ips_record.username
|
||||
self.com_info.pwd = ips_record.pwd
|
||||
self.com_info.save()
|
||||
|
||||
print("成功添加记录到 CombinedTable。")
|
||||
return True
|
||||
|
||||
print("ips 表中所有数据在 CombinedTable 中都已存在,结束操作。")
|
||||
return False
|
||||
|
||||
def action(self):
|
||||
|
||||
self.com_info, type1 = CombinedTable.get_or_create(
|
||||
mnemonic=self.sql_info.mnemonic,
|
||||
)
|
||||
|
||||
if not self.com_info.host:
|
||||
if self.add_ips_to_combined_table():
|
||||
logger.info("添加ip成功!!!")
|
||||
else:
|
||||
logger.error("没有ip了")
|
||||
return
|
||||
|
||||
proxies = {
|
||||
'http': f'socks5://{self.com_info.username}:{self.com_info.pwd}@{self.com_info.host}:{self.com_info.port}',
|
||||
'https': f'socks5://{self.com_info.username}:{self.com_info.pwd}@{self.com_info.host}:{self.com_info.port}',
|
||||
}
|
||||
self.session.proxies.update(proxies)
|
||||
|
||||
csrf_token = self.csrf()
|
||||
if csrf_token:
|
||||
logger.info("获取签名信息成功!!!")
|
||||
else:
|
||||
logger.error("获取签名信息失败!!!")
|
||||
|
||||
# 获取当前 UTC 时间
|
||||
current_utc_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
original_msg = f"points.concrete.xyz wants you to sign in with your Ethereum account:\n{self.sql_info.address}\n\nPlease sign with your account\n\nURI: https://points.concrete.xyz\nVersion: 1\nChain ID: 1\nNonce: {csrf_token}\nIssued At: {current_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + 'Z'}\nResources:\n- connector://metaMask"
|
||||
signature = get_evm_wallet_singed_message(
|
||||
original_msg=original_msg,
|
||||
private_key=self.sql_info.private_key,
|
||||
|
||||
)
|
||||
|
||||
self.credentials(message=original_msg, signature=signature, csrfToken=csrf_token)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
for concrete_info in ConcreteWallet.select():
|
||||
credentials = Credentials(sql_info=concrete_info)
|
||||
credentials.action()
|
||||
# 示例调用
|
||||
local_file = r'C:\Users\27942\Desktop\codes\lm_code\test.py'
|
||||
signed_url = upload_file_to_oss(local_file)
|
||||
if signed_url:
|
||||
print(f"可访问文件的 URL: {signed_url}")
|
||||
@@ -738,7 +738,7 @@ def run_work(x_token_info, xstart_info):
|
||||
pass
|
||||
|
||||
# hub.to_do_tui() # 发推
|
||||
hub.回复() # 发推
|
||||
# hub.回复() # 发推
|
||||
|
||||
# hub.FollowTwitterAccount() # 关注
|
||||
|
||||
@@ -762,7 +762,7 @@ if __name__ == '__main__':
|
||||
# time.sleep(random.randint(15, 60))
|
||||
|
||||
# 同时运行
|
||||
max_threads = 8
|
||||
max_threads = 10
|
||||
delay_between_start = 15 # 每次启动线程之间的延迟时间(秒)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_threads) as executor:
|
||||
|
||||
Reference in New Issue
Block a user