Compare commits
31 Commits
d504f720d5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1deb97843 | ||
|
|
b2515bb7ce | ||
|
|
c473c738a3 | ||
|
|
79f4e03d6a | ||
|
|
6b1a707e3f | ||
|
|
3a6678089c | ||
|
|
5530a008b3 | ||
|
|
01b6a0fdcb | ||
|
|
b74449989b | ||
|
|
89bc5a7a00 | ||
|
|
4b5a66d588 | ||
|
|
de22d1f3ae | ||
|
|
2a98d431a9 | ||
|
|
79bf079548 | ||
|
|
aec9fc7830 | ||
|
|
9a383143e0 | ||
|
|
1d68fcd925 | ||
|
|
0ec23b28eb | ||
|
|
088f94c5c4 | ||
|
|
302086b7d5 | ||
|
|
fe87f49734 | ||
|
|
c45017dcf4 | ||
|
|
2e0a2bc74f | ||
|
|
85212a462d | ||
|
|
cf499863a3 | ||
|
|
0edf741849 | ||
|
|
905ce34aa7 | ||
|
|
c8fb43a700 | ||
|
|
2712ec598d | ||
|
|
a1c001797f | ||
|
|
df9d4a069b |
904
15分钟,三分之一,止盈.py
Normal file
904
15分钟,三分之一,止盈.py
Normal file
@@ -0,0 +1,904 @@
|
|||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
PRECOMPUTED_TRADE_PARAMS = {
|
||||||
|
# 你已经算好参数时,把 enabled 改成 True,并在下面填入你的值。
|
||||||
|
# 直接运行本文件即可生效,不需要任何命令行参数。
|
||||||
|
"enabled": False,
|
||||||
|
"params": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BitmartFuturesTransaction:
|
||||||
|
def __init__(self, bit_id):
|
||||||
|
|
||||||
|
self.page: ChromiumPage | None = None
|
||||||
|
|
||||||
|
self.api_key = "6104088c65a68d7e53df5d9395b67d78e555293a"
|
||||||
|
self.secret_key = "a8b14312330d8e6b9b09acfd972b34e32022fdfa9f2b06f0a0a31723b873fd01"
|
||||||
|
self.memo = "me"
|
||||||
|
|
||||||
|
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.last_reverse_time = None # 上次反手时间
|
||||||
|
self.last_reverse_kline_id = None # 已反手过的 K 线 id,该 K 线内不再操作仓位
|
||||||
|
|
||||||
|
# 开仓频率控制
|
||||||
|
self.open_cooldown_seconds = 60 # 开仓冷却时间(秒),两次开仓至少间隔此时长
|
||||||
|
self.last_open_time = None # 上次开仓时间
|
||||||
|
self.last_open_kline_id = None # 上次开仓所在 K 线 id,同一根 K 线只允许开仓一次
|
||||||
|
|
||||||
|
self.leverage = "40" # 高杠杆(全仓模式下可开更大仓位)
|
||||||
|
self.open_type = "cross" # 全仓模式
|
||||||
|
self.risk_percent = 0 # 未使用;若启用则可为每次开仓占可用余额的百分比
|
||||||
|
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线开盘价
|
||||||
|
|
||||||
|
# 策略优化参数
|
||||||
|
self.min_prev_entity_pct = 0.1 # 上一根K线实体涨幅(%),仅当实体涨幅 > 此值才用作“上一根”
|
||||||
|
|
||||||
|
# 自动止盈:基于当前K线振幅四等分(多仓用 open~high,空仓用 open~low)
|
||||||
|
self.take_profit_close_quartile = 3 # 达到极值后,回到 3/4 位置平仓
|
||||||
|
self.take_profit_reentry_quartile = 2 # 止盈后,继续回到 2/4 位置同向再开仓
|
||||||
|
self.take_profit_triggered_kline_id = None # 多仓:本K线内是否出现过 open~high 的极值触发
|
||||||
|
self.take_profit_reentry_threshold = None # 多仓止盈平仓后,价格 <= 此值则开多(同向)
|
||||||
|
self.take_profit_triggered_kline_id_short = None # 空仓:本K线内是否出现过 open~low 的极值触发
|
||||||
|
self.take_profit_reentry_threshold_short = None # 空仓止盈平仓后,价格 >= 此值则开空(同向)
|
||||||
|
self.last_take_profit_kline_id = None # 本K线已止盈一次(不区分多空)则不再止盈
|
||||||
|
|
||||||
|
self.optimized_params_file = Path(__file__).resolve().parent / "atr_best_params.json"
|
||||||
|
self.strategy_log_dir = Path(__file__).resolve().parent # 策略开仓日志目录
|
||||||
|
self.apply_precomputed_params()
|
||||||
|
self.load_optimized_params()
|
||||||
|
|
||||||
|
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=15, # 15分钟
|
||||||
|
start_time=end_time - 3600 * 9, # 取最近9小时(与原5分钟*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'])
|
||||||
|
|
||||||
|
# 返回最近多根K线列表,供主循环按「实体涨幅>0.1%」向前选取上一根
|
||||||
|
if len(formatted) >= 2:
|
||||||
|
return formatted
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取K线异常: {e}")
|
||||||
|
self.ding(text="获取K线异常", error=True)
|
||||||
|
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 * 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 _log_take_profit_action(self, operation: str, reason: str):
|
||||||
|
"""止盈相关操作日志:时间、操作、原因"""
|
||||||
|
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
logger.info(f"[止盈日志] 时间={time_str} | 操作={operation} | 原因={reason}")
|
||||||
|
|
||||||
|
def _write_open_log(self, operation: str, reason: str, current_kline: dict, prev_kline: dict | None):
|
||||||
|
"""写入策略开仓日志文件:时间、参考的两根K线、操作、开仓原因"""
|
||||||
|
try:
|
||||||
|
date_str = datetime.now().strftime("%Y%m%d")
|
||||||
|
log_file = self.strategy_log_dir / f"strategy_log_{date_str}.txt"
|
||||||
|
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
cur = current_kline
|
||||||
|
cur_line = f"id={cur['id']} open={cur['open']:.2f} high={cur['high']:.2f} low={cur['low']:.2f} close={cur['close']:.2f}"
|
||||||
|
if prev_kline:
|
||||||
|
prev = prev_kline
|
||||||
|
prev_line = f"id={prev['id']} open={prev['open']:.2f} high={prev['high']:.2f} low={prev['low']:.2f} close={prev['close']:.2f}"
|
||||||
|
else:
|
||||||
|
prev_line = "(无)"
|
||||||
|
block = (
|
||||||
|
f"\n{'='*60}\n"
|
||||||
|
f"时间: {time_str}\n"
|
||||||
|
f"操作: {operation}\n"
|
||||||
|
f"参考K线:\n 当前K线: {cur_line}\n 上一根K线: {prev_line}\n"
|
||||||
|
f"开仓原因: {reason}\n"
|
||||||
|
f"{'='*60}\n"
|
||||||
|
)
|
||||||
|
with open(log_file, "a", encoding="utf-8") as f:
|
||||||
|
f.write(block)
|
||||||
|
logger.info(f"已写入开仓日志: {log_file.name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"写入开仓日志失败: {e}")
|
||||||
|
|
||||||
|
def load_optimized_params(self):
|
||||||
|
"""从本地优化结果文件加载参数(可选)。"""
|
||||||
|
try:
|
||||||
|
if PRECOMPUTED_TRADE_PARAMS.get("enabled"):
|
||||||
|
logger.info("已启用 PRECOMPUTED_TRADE_PARAMS,跳过 atr_best_params.json 加载")
|
||||||
|
return
|
||||||
|
if not self.optimized_params_file.exists():
|
||||||
|
return
|
||||||
|
data = json.loads(self.optimized_params_file.read_text(encoding="utf-8"))
|
||||||
|
if data.get("apply_live") is not True:
|
||||||
|
logger.info(f"检测到优化参数文件但 apply_live != true,跳过加载: {self.optimized_params_file}")
|
||||||
|
return
|
||||||
|
params = data.get("params_for_trade_py", data)
|
||||||
|
allow_keys = {"min_prev_entity_pct"}
|
||||||
|
applied = []
|
||||||
|
for key, val in params.items():
|
||||||
|
if key in allow_keys and hasattr(self, key):
|
||||||
|
setattr(self, key, val)
|
||||||
|
applied.append(key)
|
||||||
|
if applied:
|
||||||
|
logger.info(f"已加载优化参数文件: {self.optimized_params_file},字段数: {len(applied)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"加载优化参数文件失败,将继续使用默认参数: {e}")
|
||||||
|
|
||||||
|
def apply_precomputed_params(self):
|
||||||
|
"""
|
||||||
|
直接应用本文件内预计算参数(不依赖命令)。
|
||||||
|
当 PRECOMPUTED_TRADE_PARAMS.enabled=True 时生效。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conf = PRECOMPUTED_TRADE_PARAMS or {}
|
||||||
|
if conf.get("enabled") is not True:
|
||||||
|
return
|
||||||
|
params = conf.get("params") or {}
|
||||||
|
if not isinstance(params, dict):
|
||||||
|
logger.warning("PRECOMPUTED_TRADE_PARAMS.params 不是字典,忽略")
|
||||||
|
return
|
||||||
|
|
||||||
|
allow_keys = {"min_prev_entity_pct"}
|
||||||
|
applied = []
|
||||||
|
for key, val in params.items():
|
||||||
|
if key in allow_keys and hasattr(self, key):
|
||||||
|
setattr(self, key, val)
|
||||||
|
applied.append(key)
|
||||||
|
logger.info(f"已应用文件内预计算参数,字段数: {len(applied)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"应用文件内预计算参数失败,将继续使用默认参数: {e}")
|
||||||
|
|
||||||
|
def calculate_entity(self, kline):
|
||||||
|
"""计算K线实体大小(绝对值)"""
|
||||||
|
return abs(kline['close'] - kline['open'])
|
||||||
|
|
||||||
|
def get_entity_edge(self, kline):
|
||||||
|
"""获取K线实体边(收盘价或开盘价,取决于是阳线还是阴线)"""
|
||||||
|
# 阳线(收盘>开盘):实体上边=收盘价,实体下边=开盘价
|
||||||
|
# 阴线(收盘<开盘):实体上边=开盘价,实体下边=收盘价
|
||||||
|
return {
|
||||||
|
'upper': max(kline['open'], kline['close']), # 实体上边
|
||||||
|
'lower': min(kline['open'], kline['close']) # 实体下边
|
||||||
|
}
|
||||||
|
|
||||||
|
def calc_long_take_profit_levels(self, current_kline):
|
||||||
|
"""
|
||||||
|
多仓止盈阈值(自动四等分):
|
||||||
|
基于当前K线 open~high 计算:
|
||||||
|
- extreme: high(4/4 极值)
|
||||||
|
- close: open + 3/4 * (high-open)
|
||||||
|
- reentry: open + 2/4 * (high-open)
|
||||||
|
"""
|
||||||
|
k_open = float(current_kline['open'])
|
||||||
|
k_high = float(current_kline['high'])
|
||||||
|
if k_high <= k_open:
|
||||||
|
return None
|
||||||
|
|
||||||
|
move = k_high - k_open
|
||||||
|
close_threshold = k_open + move * (self.take_profit_close_quartile / 4)
|
||||||
|
reentry_threshold = k_open + move * (self.take_profit_reentry_quartile / 4)
|
||||||
|
return {
|
||||||
|
"open": k_open,
|
||||||
|
"extreme": k_high,
|
||||||
|
"close_threshold": close_threshold,
|
||||||
|
"reentry_threshold": reentry_threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
def calc_short_take_profit_levels(self, current_kline):
|
||||||
|
"""
|
||||||
|
空仓止盈阈值(自动四等分):
|
||||||
|
基于当前K线 open~low 计算:
|
||||||
|
- extreme: low(4/4 极值)
|
||||||
|
- close: open - 3/4 * (open-low)
|
||||||
|
- reentry: open - 2/4 * (open-low)
|
||||||
|
"""
|
||||||
|
k_open = float(current_kline['open'])
|
||||||
|
k_low = float(current_kline['low'])
|
||||||
|
if k_low >= k_open:
|
||||||
|
return None
|
||||||
|
|
||||||
|
move = k_open - k_low
|
||||||
|
close_threshold = k_open - move * (self.take_profit_close_quartile / 4)
|
||||||
|
reentry_threshold = k_open - move * (self.take_profit_reentry_quartile / 4)
|
||||||
|
return {
|
||||||
|
"open": k_open,
|
||||||
|
"extreme": k_low,
|
||||||
|
"close_threshold": close_threshold,
|
||||||
|
"reentry_threshold": reentry_threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_signal(self, current_price, prev_kline, current_kline):
|
||||||
|
"""
|
||||||
|
检查交易信号
|
||||||
|
返回: ('long', trigger_price) / ('short', trigger_price) / None
|
||||||
|
"""
|
||||||
|
# 计算上一根K线实体(主循环已保证所选 prev 实体涨幅 > 0.1%)
|
||||||
|
prev_entity = self.calculate_entity(prev_kline)
|
||||||
|
|
||||||
|
# 获取上一根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 / 3
|
||||||
|
short_trigger = calc_upper - prev_entity / 3
|
||||||
|
long_breakout = calc_upper + prev_entity / 3
|
||||||
|
short_breakout = calc_lower - prev_entity / 3
|
||||||
|
else:
|
||||||
|
# 原有计算方式
|
||||||
|
long_trigger = prev_entity_lower + prev_entity / 3 # 做多触发价 = 实体下边 + 实体/3(下三分之一处)
|
||||||
|
short_trigger = prev_entity_upper - prev_entity / 3 # 做空触发价 = 实体上边 - 实体/3(上三分之一处)
|
||||||
|
long_breakout = prev_entity_upper + prev_entity / 3 # 做多突破价 = 实体上边 + 实体/3
|
||||||
|
short_breakout = prev_entity_lower - prev_entity / 3 # 做空突破价 = 实体下边 - 实体/3
|
||||||
|
|
||||||
|
# 上一根阴线 + 当前阳线:做多形态,不按上一根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/3): {long_trigger:.2f}, 做空触发价(上1/3): {short_trigger:.2f}")
|
||||||
|
logger.info(f"突破做多价(上1/3外): {long_breakout:.2f}, 突破做空价(下1/3外): {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/3外) {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/3外) {short_breakout:.2f}"
|
||||||
|
)
|
||||||
|
return ('short', short_breakout)
|
||||||
|
|
||||||
|
# 持仓时检查反手信号
|
||||||
|
elif self.start == 1: # 持多仓
|
||||||
|
# 反手条件: 价格跌到上一根K线的上三分之一处(做空触发价);上一根阴线+当前阳线做多时跳过
|
||||||
|
if current_price <= short_trigger and not skip_short_by_upper_third:
|
||||||
|
logger.info(f"持多反手做空!价格 {current_price:.2f} <= 触发价(上1/3) {short_trigger:.2f}")
|
||||||
|
return ('reverse_short', short_trigger)
|
||||||
|
|
||||||
|
elif self.start == -1: # 持空仓
|
||||||
|
# 反手条件: 价格涨到上一根K线的下三分之一处(做多触发价);上一根阳线+当前阴线做空时跳过
|
||||||
|
if current_price >= long_trigger and not skip_long_by_lower_third:
|
||||||
|
logger.info(f"持空反手做多!价格 {current_price:.2f} >= 触发价(下1/3) {long_trigger:.2f}")
|
||||||
|
return ('reverse_long', long_trigger)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.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.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):
|
||||||
|
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):
|
||||||
|
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("开始运行三分之一策略交易(15分钟K线)...")
|
||||||
|
|
||||||
|
# 启动时设置全仓高杠杆
|
||||||
|
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线=最后一根;上一根=从后往前第一根实体涨幅>0.1%的K线
|
||||||
|
formatted = self.get_klines()
|
||||||
|
if not formatted or len(formatted) < 2:
|
||||||
|
logger.warning("获取K线失败,等待重试...")
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_kline = formatted[-1]
|
||||||
|
prev_kline = None
|
||||||
|
for i in range(len(formatted) - 2, -1, -1):
|
||||||
|
k = formatted[i]
|
||||||
|
entity = abs(k['close'] - k['open'])
|
||||||
|
entity_pct = entity / k['open'] * 100 if k['open'] else 0
|
||||||
|
if entity_pct > self.min_prev_entity_pct:
|
||||||
|
prev_kline = k
|
||||||
|
break
|
||||||
|
if prev_kline is None:
|
||||||
|
logger.info(f"没有实体涨幅>{self.min_prev_entity_pct:.3f}%的上一根K线,跳过信号检测")
|
||||||
|
time.sleep(0.1)
|
||||||
|
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}")
|
||||||
|
# 新K线内重新判断多/空止盈触发
|
||||||
|
if self.start == 1:
|
||||||
|
self.take_profit_triggered_kline_id = None
|
||||||
|
if self.start == -1:
|
||||||
|
self.take_profit_triggered_kline_id_short = None
|
||||||
|
|
||||||
|
# 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=空)")
|
||||||
|
|
||||||
|
signal = None
|
||||||
|
|
||||||
|
# 3.5 多仓止盈(自动四等分):open~high 达到极值后,回到 3/4 平仓
|
||||||
|
if self.start == 1:
|
||||||
|
# 同一根K线多仓只允许止盈一次,避免“止盈->再开->再止盈”循环
|
||||||
|
if self.last_take_profit_kline_id == current_kline_time:
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
else:
|
||||||
|
long_levels = self.calc_long_take_profit_levels(current_kline)
|
||||||
|
if long_levels is not None:
|
||||||
|
retrace_close_threshold = long_levels["close_threshold"]
|
||||||
|
# 使用当前K线 high 判定是否已触发到 4/4 极值,避免依赖分钟收盘价漏判
|
||||||
|
if long_levels["extreme"] > long_levels["open"]:
|
||||||
|
self.take_profit_triggered_kline_id = current_kline_time
|
||||||
|
if (
|
||||||
|
self.take_profit_triggered_kline_id == current_kline_time
|
||||||
|
and current_price <= retrace_close_threshold
|
||||||
|
):
|
||||||
|
reason = (
|
||||||
|
f"当前K线开盘价={long_levels['open']:.2f},最高价={long_levels['extreme']:.2f}(4/4);"
|
||||||
|
f"现价{current_price:.2f}已回调至≤{retrace_close_threshold:.2f}(3/4),按规则止盈平多"
|
||||||
|
)
|
||||||
|
self._log_take_profit_action("止盈平多仓", reason)
|
||||||
|
self.平仓()
|
||||||
|
self.take_profit_reentry_threshold = long_levels["reentry_threshold"]
|
||||||
|
self.take_profit_triggered_kline_id = None
|
||||||
|
self.last_take_profit_kline_id = current_kline_time
|
||||||
|
page_start = True
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
else:
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
# 3.6 止盈平多后:价格继续回调到 2/4 则同向开多
|
||||||
|
elif self.start == 0 and self.take_profit_reentry_threshold is not None:
|
||||||
|
if current_price <= self.take_profit_reentry_threshold:
|
||||||
|
reason = (
|
||||||
|
f"止盈平仓后价格继续回调至≤{self.take_profit_reentry_threshold:.2f}(2/4),按规则同向开多"
|
||||||
|
)
|
||||||
|
self._log_take_profit_action("止盈后同向开多", reason)
|
||||||
|
self.开单(marketPriceLongOrder=1, size=self.default_order_size)
|
||||||
|
time.sleep(3)
|
||||||
|
if self.verify_position_direction(1):
|
||||||
|
self.last_open_time = time.time()
|
||||||
|
self.last_open_kline_id = current_kline_time
|
||||||
|
self._write_open_log("止盈后同向开多", reason, current_kline, prev_kline)
|
||||||
|
logger.success("止盈后再开多成功")
|
||||||
|
page_start = True
|
||||||
|
else:
|
||||||
|
logger.error("止盈后再开多验证失败")
|
||||||
|
self.take_profit_reentry_threshold = None
|
||||||
|
else:
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
# 3.7 空仓止盈(自动四等分):open~low 达到极值后,回到 3/4 平仓
|
||||||
|
elif self.start == -1:
|
||||||
|
# 同一根K线空仓只允许止盈一次,避免“止盈->再开->再止盈”循环
|
||||||
|
if self.last_take_profit_kline_id == current_kline_time:
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
else:
|
||||||
|
short_levels = self.calc_short_take_profit_levels(current_kline)
|
||||||
|
if short_levels is not None:
|
||||||
|
retrace_close_threshold_short = short_levels["close_threshold"]
|
||||||
|
# 使用当前K线 low 判定是否已触发到 4/4 极值,避免依赖分钟收盘价漏判
|
||||||
|
if short_levels["extreme"] < short_levels["open"]:
|
||||||
|
self.take_profit_triggered_kline_id_short = current_kline_time
|
||||||
|
if (
|
||||||
|
self.take_profit_triggered_kline_id_short == current_kline_time
|
||||||
|
and current_price >= retrace_close_threshold_short
|
||||||
|
):
|
||||||
|
reason = (
|
||||||
|
f"当前K线开盘价={short_levels['open']:.2f},最低价={short_levels['extreme']:.2f}(4/4);"
|
||||||
|
f"现价{current_price:.2f}已反弹至≥{retrace_close_threshold_short:.2f}(3/4),按规则止盈平空"
|
||||||
|
)
|
||||||
|
self._log_take_profit_action("止盈平空仓", reason)
|
||||||
|
self.平仓()
|
||||||
|
self.take_profit_reentry_threshold_short = short_levels["reentry_threshold"]
|
||||||
|
self.take_profit_triggered_kline_id_short = None
|
||||||
|
self.last_take_profit_kline_id = current_kline_time
|
||||||
|
page_start = True
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
else:
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
# 3.8 止盈平空后:价格继续反弹到 2/4 则同向开空
|
||||||
|
elif self.start == 0 and self.take_profit_reentry_threshold_short is not None:
|
||||||
|
if current_price >= self.take_profit_reentry_threshold_short:
|
||||||
|
reason = (
|
||||||
|
f"止盈平空后价格继续反弹至≥{self.take_profit_reentry_threshold_short:.2f}(2/4),按规则同向开空"
|
||||||
|
)
|
||||||
|
self._log_take_profit_action("止盈后同向开空", reason)
|
||||||
|
self.开单(marketPriceLongOrder=-1, size=self.default_order_size)
|
||||||
|
time.sleep(3)
|
||||||
|
if self.verify_position_direction(-1):
|
||||||
|
self.last_open_time = time.time()
|
||||||
|
self.last_open_kline_id = current_kline_time
|
||||||
|
self._write_open_log("止盈后同向开空", reason, current_kline, prev_kline)
|
||||||
|
logger.success("止盈后再开空成功")
|
||||||
|
page_start = True
|
||||||
|
else:
|
||||||
|
logger.error("止盈后再开空验证失败")
|
||||||
|
self.take_profit_reentry_threshold_short = None
|
||||||
|
else:
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
else:
|
||||||
|
# 4. 检查信号
|
||||||
|
signal = self.check_signal(current_price, prev_kline, current_kline)
|
||||||
|
|
||||||
|
# 6. 反手过滤:冷却时间
|
||||||
|
if signal and signal[0].startswith('reverse_'):
|
||||||
|
if not self.can_reverse(current_price, signal[1]):
|
||||||
|
signal = None
|
||||||
|
|
||||||
|
# 6.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.6 当前 K 线已反手过则本 K 线内不再操作仓位
|
||||||
|
if signal and self.last_reverse_kline_id == current_kline_time:
|
||||||
|
logger.info(f"本 K 线({current_kline_time})已反手过,本 K 线内不再操作仓位")
|
||||||
|
signal = None
|
||||||
|
|
||||||
|
# 7. 有信号则执行交易
|
||||||
|
if signal:
|
||||||
|
trade_success = self.execute_trade(signal)
|
||||||
|
if trade_success:
|
||||||
|
if signal[0] in ('reverse_long', 'reverse_short'):
|
||||||
|
self.last_reverse_kline_id = current_kline_time # 本 K 线已反手,本 K 线内不再操作
|
||||||
|
# 写入开仓日志:参考的两根K线 + 开仓原因
|
||||||
|
sig_type, trigger_price = signal
|
||||||
|
op_name = {"long": "开多", "short": "开空", "reverse_long": "反手做多", "reverse_short": "反手做空"}.get(sig_type, sig_type)
|
||||||
|
if sig_type == "long":
|
||||||
|
open_reason = f"三分之一策略:当前价{current_price:.2f}>=突破做多价{trigger_price:.2f}(实体上边+1/3)"
|
||||||
|
elif sig_type == "short":
|
||||||
|
open_reason = f"三分之一策略:当前价{current_price:.2f}<=突破做空价{trigger_price:.2f}(实体下边-1/3)"
|
||||||
|
elif sig_type == "reverse_long":
|
||||||
|
open_reason = f"持空反手做多:当前价{current_price:.2f}>=做多触发价{trigger_price:.2f}(下1/3)"
|
||||||
|
else:
|
||||||
|
open_reason = f"持多反手做空:当前价{current_price:.2f}<=做空触发价{trigger_price:.2f}(上1/3)"
|
||||||
|
self._write_open_log(op_name, open_reason, current_kline, prev_kline)
|
||||||
|
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="62f9107d0c674925972084e282df55b3").action()
|
||||||
310
backtest_outputs/charts/bb_backtest_2026_full_visualization.html
Normal file
310
backtest_outputs/charts/bb_backtest_2026_full_visualization.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
310
backtest_outputs/charts/bb_backtest_visualization.html
Normal file
310
backtest_outputs/charts/bb_backtest_visualization.html
Normal file
File diff suppressed because one or more lines are too long
145
backtest_outputs/charts/bb_chart_2026_03_02.html
Normal file
145
backtest_outputs/charts/bb_chart_2026_03_02.html
Normal 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>
|
||||||
|
|
||||||
17941
backtest_outputs/logs/backtest_result.txt
Normal file
17941
backtest_outputs/logs/backtest_result.txt
Normal file
File diff suppressed because it is too large
Load Diff
142073
backtest_outputs/reports/bb_sweep_results.csv
Normal file
142073
backtest_outputs/reports/bb_sweep_results.csv
Normal file
File diff suppressed because it is too large
Load Diff
43
backtest_outputs/reports/bb_sweep_results_snapshot.csv
Normal file
43
backtest_outputs/reports/bb_sweep_results_snapshot.csv
Normal 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
|
||||||
|
42500
backtest_outputs/trades/bb_backtest_20250101_20251231_trades.csv
Normal file
42500
backtest_outputs/trades/bb_backtest_20250101_20251231_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
19004
backtest_outputs/trades/bb_backtest_2025_15m_trades.csv
Normal file
19004
backtest_outputs/trades/bb_backtest_2025_15m_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
247966
backtest_outputs/trades/bb_backtest_2025_1m_trades.csv
Normal file
247966
backtest_outputs/trades/bb_backtest_2025_1m_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
88456
backtest_outputs/trades/bb_backtest_2025_3m_trades.csv
Normal file
88456
backtest_outputs/trades/bb_backtest_2025_3m_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
54689
backtest_outputs/trades/bb_backtest_2025_5m_trades.csv
Normal file
54689
backtest_outputs/trades/bb_backtest_2025_5m_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
7346
backtest_outputs/trades/bb_backtest_20260101_20260228_trades.csv
Normal file
7346
backtest_outputs/trades/bb_backtest_20260101_20260228_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
17941
backtest_outputs/trades/bb_backtest_20260101_20261231_trades.csv
Normal file
17941
backtest_outputs/trades/bb_backtest_20260101_20261231_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
157
backtest_outputs/trades/bb_backtest_march_2026_trades.csv
Normal file
157
backtest_outputs/trades/bb_backtest_march_2026_trades.csv
Normal 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
|
||||||
|
580
bb_backtest_2025_multi_timeframe.py
Normal file
580
bb_backtest_2025_multi_timeframe.py
Normal 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
754
bb_backtest_march_2026.py
Normal 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
626
bb_delay_reversal_trade.py
Normal 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
189789
bb_sweep_results.csv
Normal file
File diff suppressed because it is too large
Load Diff
624
bb_trade.py
Normal file
624
bb_trade.py
Normal file
@@ -0,0 +1,624 @@
|
|||||||
|
"""
|
||||||
|
布林带均值回归策略 — 实盘交易 (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 BBTradeConfig:
|
||||||
|
# API 凭证
|
||||||
|
API_KEY = "6104088c65a68d7e53df5d9395b67d78e555293a"
|
||||||
|
SECRET_KEY = "a8b14312330d8e6b9b09acfd972b34e32022fdfa9f2b06f0a0a31723b873fd01"
|
||||||
|
MEMO = "me"
|
||||||
|
|
||||||
|
# 合约
|
||||||
|
CONTRACT_SYMBOL = "ETHUSDT"
|
||||||
|
TRADE_URL = "https://derivatives.bitmart.com/zh-CN/futures/ETHUSDT"
|
||||||
|
|
||||||
|
# 浏览器
|
||||||
|
BIT_ID = "62f9107d0c674925972084e282df55b3"
|
||||||
|
|
||||||
|
# 布林带参数
|
||||||
|
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 BBTrader:
|
||||||
|
def __init__(self, cfg: BBTradeConfig = None):
|
||||||
|
self.cfg = cfg or BBTradeConfig()
|
||||||
|
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_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_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 = BBTrader()
|
||||||
|
trader.run()
|
||||||
262
bit_tools.py
Normal file
262
bit_tools.py
Normal 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
268
bitmart/框架.py
Normal 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
527
generate_backtest_chart.py
Normal 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()
|
||||||
16
main.py
16
main.py
@@ -1,16 +0,0 @@
|
|||||||
# 这是一个示例 Python 脚本。
|
|
||||||
|
|
||||||
# 按 ⌃R 执行或将其替换为您的代码。
|
|
||||||
# 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。
|
|
||||||
|
|
||||||
|
|
||||||
def print_hi(name):
|
|
||||||
# 在下面的代码行中使用断点来调试脚本。
|
|
||||||
print(f'Hi, {name}') # 按 ⌘F8 切换断点。
|
|
||||||
|
|
||||||
|
|
||||||
# 按装订区域中的绿色按钮以运行脚本。
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print_hi('PyCharm')
|
|
||||||
|
|
||||||
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
|
|
||||||
Binary file not shown.
277
models/grid_sweep_results.csv
Normal file
277
models/grid_sweep_results.csv
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
n_grids,mode,half_pct,grid_sl,eq,ret,trades,gross,nfee,worst_day,max_dd,wr,score,max_open,profit_lock,max_daily_loss
|
||||||
|
25,long,0.08,0.0,1397.9111280963439,39.791112809634384,1829,1118.61138190761,89.34110427803469,-54.66635046647207,-101.88736561661199,0.9781301257517769,18.296839388862164,8,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1392.6959362266246,39.269593622662455,1822,1113.1328682608444,89.07778250098772,-54.66635046647207,-101.88736561661199,0.9780461031833151,17.775320201890235,8,40.0,50.0
|
||||||
|
25,long,0.08,0.08,1377.4361374244675,37.74361374244675,1894,1020.5273176511827,92.77597302857623,-54.66635046647207,-102.50464117878687,0.9693769799366421,16.218476543565785,8,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1372.2209455547481,37.22209455547481,1887,1015.0488040044162,92.51265125152923,-54.66635046647207,-102.50464117878687,0.969263381028087,15.696957356593849,8,40.0,50.0
|
||||||
|
25,long,0.08,0.0,1358.8279680292997,35.88279680292997,1760,1064.0701032765814,86.79599035314052,-54.66635046647207,-104.92067967367007,0.9772727272727273,14.236857679304844,8,20.0,50.0
|
||||||
|
25,long,0.08,0.08,1339.133685213397,33.913368521339706,1826,966.8043696257179,90.26848185327287,-54.66635046647207,-105.53795523584495,0.968236582694414,12.236565619605837,8,20.0,50.0
|
||||||
|
25,long,0.08,0.05,1305.7377273966035,30.573772739660352,2243,660.9272563596152,106.21458290512612,-54.82484156455007,-134.23780091713024,0.9259919750334373,7.414430224438818,10,40.0,50.0
|
||||||
|
25,long,0.08,0.05,1305.7377273966035,30.573772739660352,2243,660.9272563596152,106.21458290512612,-54.82484156455007,-134.23780091713024,0.9259919750334373,7.414430224438818,10,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1273.2290820455269,27.322908204552686,2200,626.800992068894,104.59696396548253,-54.82484156455007,-134.23780091713024,0.9245454545454546,4.1635656893311515,10,20.0,50.0
|
||||||
|
25,long,0.08,0.05,1274.1268173351598,27.412681733515978,2194,625.3080156191049,103.98444015678419,-55.57015591838194,-133.67404726819632,0.9247948951686418,4.057932594591579,8,40.0,50.0
|
||||||
|
25,long,0.08,0.05,1274.1268173351598,27.412681733515978,2194,625.3080156191049,103.98444015678419,-55.57015591838194,-133.67404726819632,0.9247948951686418,4.057932594591579,8,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1269.6333024989083,26.963330249890827,2237,630.6752392403805,106.17254510255816,-54.82484156455007,-135.87368396513898,0.9226642825212338,3.722193582268856,0,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1269.6333024989083,26.963330249890827,2237,630.6752392403805,106.17254510255816,-54.82484156455007,-135.87368396513898,0.9226642825212338,3.722193582268856,0,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1269.6333024989083,26.963330249890827,2237,630.6752392403805,106.17254510255816,-54.82484156455007,-135.87368396513898,0.9226642825212338,3.722193582268856,0,40.0,50.0
|
||||||
|
25,long,0.08,0.08,1304.459491306357,30.4459491306357,2087,855.6829832347544,100.87587182425239,-62.51677693859392,-173.81100512689272,0.9458552946813608,3.0003657927128877,10,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1304.2820463031949,30.428204630319488,2075,878.5740502089094,100.46650937600748,-62.51677693859392,-173.98845013005507,0.946987951807229,2.9737490422385573,10,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1303.3736239387122,30.337362393871217,2179,842.8262746516338,104.98721868527448,-65.84157418233985,-167.3725604145027,0.9343735658558971,2.216262118444126,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1303.3736239387122,30.337362393871217,2179,842.8262746516338,104.98721868527448,-65.84157418233985,-167.3725604145027,0.9343735658558971,2.216262118444126,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1292.3418470283234,29.23418470283234,2071,842.9634310238881,100.27396389142075,-62.51677693859392,-173.81100512689272,0.9454369869628199,1.7886013649095283,10,40.0,50.0
|
||||||
|
25,long,0.08,0.0,1292.1644020251613,29.21644020251613,2059,865.8544979980437,99.8646014431759,-62.51677693859392,-173.98845013005507,0.9465760077707625,1.7619846144351978,10,40.0,50.0
|
||||||
|
25,long,0.08,0.05,1247.8009310396624,24.780093103966237,2159,597.6654829437048,102.66779377688283,-55.57015591838194,-138.20581671564264,0.9235757295044001,1.1987554926695223,8,20.0,50.0
|
||||||
|
25,long,0.08,0.08,1296.157481099035,29.615748109903507,2182,812.2098595260189,104.9076262230056,-65.84157418233985,-174.5887032541798,0.9330889092575618,1.1338406924925604,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1296.157481099035,29.615748109903507,2182,812.2098595260189,104.9076262230056,-65.84157418233985,-174.5887032541798,0.9330889092575618,1.1338406924925604,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1289.7808097173345,28.978080971733448,2161,828.5563199370484,104.3100781920673,-65.84157418233985,-167.3725604145027,0.933826931975937,0.8569806963063566,0,40.0,50.0
|
||||||
|
25,long,0.08,0.05,1236.34927096897,23.63492709689699,2193,595.7359668195779,104.51730421169512,-54.82484156455007,-135.87368396513898,0.9211126310989513,0.39379042927502006,0,20.0,50.0
|
||||||
|
25,long,0.08,0.08,1282.5646668776574,28.256466687765737,2164,797.9399048114334,104.23048572979843,-65.84157418233985,-174.5887032541798,0.9325323475046211,-0.225440729645209,0,40.0,50.0
|
||||||
|
25,long,0.08,0.08,1259.3235564374731,25.932355643747314,2012,795.9619663232609,97.92940397913668,-62.51677693859392,-175.36061401718916,0.9438369781312127,-1.5907081386903208,10,20.0,50.0
|
||||||
|
25,long,0.08,0.0,1259.146111434311,25.914611143431102,2000,818.8530332974159,97.52004153089177,-62.51677693859392,-175.53805902035128,0.945,-1.6173248891646388,10,20.0,50.0
|
||||||
|
25,long,0.08,0.05,1147.8412009571211,14.784120095712115,1785,420.34470412045823,84.48944027534574,-35.21316528488592,-120.10936470512252,0.9165266106442577,-1.7852977250097855,5,0.0,30.0
|
||||||
|
25,long,0.08,0.05,1147.8412009571211,14.784120095712115,1785,420.34470412045823,84.48944027534574,-35.21316528488592,-120.10936470512252,0.9165266106442577,-1.7852977250097855,5,40.0,30.0
|
||||||
|
25,long,0.08,0.0,1261.5124700601637,26.151247006016366,2101,786.1802135581369,101.84092566781933,-65.84157418233985,-168.14269369398585,0.9319371727748691,-2.0083599333848827,0,20.0,50.0
|
||||||
|
25,long,0.08,0.08,1254.296327220487,25.4296327220487,2104,755.563798432521,101.76133320555047,-65.84157418233985,-175.35883653366284,0.9306083650190115,-3.090781359336397,0,20.0,50.0
|
||||||
|
25,long,0.08,0.05,1135.205793534383,13.520579353438302,1768,407.0698054629229,83.84994904054716,-35.21316528488592,-123.03902246406142,0.9157239819004525,-3.1953213552305435,5,20.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,0,0.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,5,40.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,0,0.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,0,20.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,0,20.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,0,40.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,0,40.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,5,0.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,5,0.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,10,40.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,8,20.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,8,40.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,8,40.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,10,0.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,10,0.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,10,20.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,10,20.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,10,40.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,5,20.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,5,20.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,5,40.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,8,0.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,8,0.0,30.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,0,0.0,50.0
|
||||||
|
12,long,0.12,0.05,1180.5141440437385,18.05141440437385,308,282.92810755714413,19.2010653555889,-63.63293586820578,-80.54084655171164,0.8409090909090909,-5.065508683673463,8,20.0,30.0
|
||||||
|
25,long,0.08,0.0,1137.3810945856771,13.738109458567715,1474,590.1090690766704,71.4449647368382,-34.93805315434156,-168.20950929824642,0.9525101763907734,-5.1537819526470745,5,40.0,30.0
|
||||||
|
25,long,0.08,0.0,1137.3810945856771,13.738109458567715,1474,590.1090690766704,71.4449647368382,-34.93805315434156,-168.20950929824642,0.9525101763907734,-5.1537819526470745,5,0.0,30.0
|
||||||
|
25,long,0.08,0.08,1136.1945657236706,13.619456572367062,1493,550.5861371661551,72.43633240984111,-34.93805315434156,-168.77876259807783,0.9497655726724715,-5.300897503839298,5,0.0,30.0
|
||||||
|
25,long,0.08,0.08,1136.1945657236706,13.619456572367062,1493,550.5861371661551,72.43633240984111,-34.93805315434156,-168.77876259807783,0.9497655726724715,-5.300897503839298,5,40.0,30.0
|
||||||
|
25,long,0.08,0.0,1165.97069931178,16.597069931177998,2143,575.931136840335,101.70221229800816,-46.11878114338265,-192.7322510212599,0.9066728884741018,-6.875176962899792,0,0.0,30.0
|
||||||
|
25,long,0.08,0.08,1165.97069931178,16.597069931177998,2143,575.931136840335,101.70221229800816,-46.11878114338265,-192.7322510212599,0.9066728884741018,-6.875176962899792,0,0.0,30.0
|
||||||
|
20,long,0.12,0.03,1135.6843456332908,13.568434563329083,847,231.87397292139397,42.89311882444578,-50.24276668427581,-107.62095367657184,0.7922077922077922,-6.885443125782251,5,40.0,30.0
|
||||||
|
20,long,0.12,0.03,1135.6843456332908,13.568434563329083,847,231.87397292139397,42.89311882444578,-50.24276668427581,-107.62095367657184,0.7922077922077922,-6.885443125782251,5,0.0,30.0
|
||||||
|
20,long,0.12,0.03,1135.6843456332908,13.568434563329083,847,231.87397292139397,42.89311882444578,-50.24276668427581,-107.62095367657184,0.7922077922077922,-6.885443125782251,5,20.0,50.0
|
||||||
|
20,long,0.12,0.03,1135.6843456332908,13.568434563329083,847,231.87397292139397,42.89311882444578,-50.24276668427581,-107.62095367657184,0.7922077922077922,-6.885443125782251,5,40.0,50.0
|
||||||
|
20,long,0.12,0.03,1135.6843456332908,13.568434563329083,847,231.87397292139397,42.89311882444578,-50.24276668427581,-107.62095367657184,0.7922077922077922,-6.885443125782251,5,20.0,30.0
|
||||||
|
20,long,0.12,0.03,1135.6843456332908,13.568434563329083,847,231.87397292139397,42.89311882444578,-50.24276668427581,-107.62095367657184,0.7922077922077922,-6.885443125782251,5,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1122.4863591809087,12.248635918090873,1444,566.5065840356414,70.31642436408208,-34.93805315434156,-175.07793896588328,0.9515235457063712,-6.986676976505759,5,20.0,30.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,8,20.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,0,0.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,10,0.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,0,20.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,5,20.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,5,0.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,8,40.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,8,0.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,5,40.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,10,20.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,0,40.0,80.0
|
||||||
|
12,long,0.12,0.05,1175.608365031793,17.560836503179303,311,280.58636652303625,19.37571409443378,-67.89589136744712,-85.93785129322646,0.8392282958199357,-7.104823471716155,10,40.0,80.0
|
||||||
|
25,long,0.08,0.08,1121.2998303189036,12.129983031890356,1463,526.9836521251272,71.30779203708497,-34.93805315434156,-175.64719226571356,0.9487354750512645,-7.133792527697789,5,20.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,0,40.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,0,0.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,0,20.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,0,20.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,0,40.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,0,0.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,10,40.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,10,40.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,8,40.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,8,0.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,8,0.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,8,20.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,8,40.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,10,20.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,10,0.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,8,20.0,30.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,10,0.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,0,0.0,50.0
|
||||||
|
20,long,0.12,0.03,1127.938046241879,12.793804624187896,849,224.2015226624435,42.96696795690694,-50.24276668427581,-107.62095367657184,0.790341578327444,-7.660073064923438,10,20.0,30.0
|
||||||
|
25,long,0.08,0.0,1157.7954939202243,15.779549392022432,2132,567.3421430997186,101.28842394894701,-46.11878114338265,-192.73225102126014,0.9061913696060038,-7.6926975020553705,0,40.0,30.0
|
||||||
|
25,long,0.08,0.08,1157.7954939202243,15.779549392022432,2132,567.3421430997186,101.28842394894701,-46.11878114338265,-192.73225102126014,0.9061913696060038,-7.6926975020553705,0,40.0,30.0
|
||||||
|
12,long,0.1,0.05,1160.639981828195,16.0639981828195,415,287.8808246742429,24.34881068136027,-65.96155392833498,-87.71954981539272,0.8506024096385543,-8.11044548645063,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1152.2873508564207,15.228735085642075,2116,557.8141899897886,100.12306620572392,-46.11878114338265,-196.98067925385715,0.9068998109640832,-8.455933220065576,10,0.0,30.0
|
||||||
|
25,long,0.08,0.08,1152.2873508564207,15.228735085642075,2116,557.8141899897886,100.12306620572392,-46.11878114338265,-196.98067925385715,0.9068998109640832,-8.455933220065576,10,0.0,30.0
|
||||||
|
25,long,0.08,0.05,1130.5880659042618,13.058806590426183,2165,429.6712922463619,101.57412180073922,-42.96178493326261,-183.68928172463336,0.8965357967667437,-9.014192975784267,10,0.0,30.0
|
||||||
|
25,long,0.08,0.05,1130.5880659042618,13.058806590426183,2165,429.6712922463619,101.57412180073922,-42.96178493326261,-183.68928172463336,0.8965357967667437,-9.014192975784267,10,40.0,30.0
|
||||||
|
25,long,0.08,0.0,1145.5873154082087,14.558731540820872,2107,550.7755987528917,99.78451041703838,-46.11878114338265,-196.9806792538576,0.9065021357380162,-9.125936764886802,10,40.0,30.0
|
||||||
|
25,long,0.08,0.08,1145.5873154082087,14.558731540820872,2107,550.7755987528917,99.78451041703838,-46.11878114338265,-196.9806792538576,0.9065021357380162,-9.125936764886802,10,40.0,30.0
|
||||||
|
25,long,0.08,0.05,1127.3665122706623,12.736651227066227,2165,429.6712922463619,101.63662180073922,-42.96178493326261,-186.91083535823395,0.8965357967667437,-9.497426020824252,0,40.0,30.0
|
||||||
|
25,long,0.08,0.05,1127.3665122706623,12.736651227066227,2165,429.6712922463619,101.63662180073922,-42.96178493326261,-186.91083535823395,0.8965357967667437,-9.497426020824252,0,0.0,30.0
|
||||||
|
25,long,0.08,0.0,1141.560014818118,14.15600148181179,2095,537.8633318701434,99.68370601897158,-46.11878114338265,-198.02270069179235,0.9045346062052506,-9.580767895792622,0,20.0,30.0
|
||||||
|
25,long,0.08,0.08,1141.560014818118,14.15600148181179,2095,537.8633318701434,99.68370601897158,-46.11878114338265,-198.02270069179235,0.9045346062052506,-9.580767895792622,0,20.0,30.0
|
||||||
|
15,long,0.12,0.05,1146.4138461034074,14.641384610340742,444,281.9958657009046,25.54491016368575,-65.10586571841566,-113.39236837709984,0.8513513513513513,-10.559993524038948,0,0.0,50.0
|
||||||
|
12,long,0.12,0.03,1110.6656937601829,11.066569376018288,361,142.41245281010382,19.9916028853587,-59.77484153931823,-78.2840404351266,0.6980609418282548,-10.780085107533509,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1130.147250491773,13.01472504917731,2071,522.2176262505083,98.30521702858394,-46.11878114338265,-203.0793978984076,0.9048768710767745,-10.974879188757864,10,20.0,30.0
|
||||||
|
25,long,0.08,0.0,1130.147250491773,13.01472504917731,2071,522.2176262505083,98.30521702858394,-46.11878114338265,-203.0793978984076,0.9048768710767745,-10.974879188757864,10,20.0,30.0
|
||||||
|
12,long,0.12,0.0,1224.732709882688,22.47327098826879,264,549.896055719594,19.312854894621676,-94.17262348471081,-109.39570457022182,0.9810606060606061,-11.248301285655543,0,0.0,50.0
|
||||||
|
25,long,0.12,0.05,1283.2152523741586,28.32152523741586,1218,557.1262213962846,62.60538994244905,-110.98849465466697,-141.9289131411324,0.9121510673234812,-12.071468816040852,0,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1105.7826258452312,10.578262584523122,2132,403.62444516026403,100.33271477367626,-42.96178493326261,-197.15031497435166,0.8949343339587242,-12.167788644173243,10,20.0,30.0
|
||||||
|
25,long,0.08,0.08,1117.966475303976,11.796647530397603,2014,516.252592545815,95.09100037625154,-44.815807347324835,-211.43426210996347,0.9131082423038729,-12.219807779298021,8,0.0,30.0
|
||||||
|
25,long,0.08,0.0,1117.966475303976,11.796647530397603,2014,516.252592545815,95.09100037625154,-44.815807347324835,-211.43426210996347,0.9131082423038729,-12.219807779298021,8,0.0,30.0
|
||||||
|
25,long,0.08,0.05,1108.8895958521423,10.888959585214229,2120,405.51821071028405,99.73320631136474,-44.81012850141826,-200.08066373027987,0.8981132075471698,-12.558112151725243,8,0.0,30.0
|
||||||
|
25,long,0.08,0.05,1108.8895958521423,10.888959585214229,2120,405.51821071028405,99.73320631136474,-44.81012850141826,-200.08066373027987,0.8981132075471698,-12.558112151725243,8,40.0,30.0
|
||||||
|
25,long,0.08,0.05,1101.785686032769,10.178568603276904,2131,402.8114370301827,100.35759282245672,-42.96178493326261,-201.14725478681396,0.8948850305021117,-12.767329616042575,0,20.0,30.0
|
||||||
|
25,long,0.08,0.0,1112.7512834342563,11.275128343425626,2007,510.7740788990486,94.82767859920457,-44.815807347324835,-214.5082036791364,0.9128051818634778,-12.895024044728643,8,40.0,30.0
|
||||||
|
25,long,0.08,0.08,1112.7512834342563,11.275128343425626,2007,510.7740788990486,94.82767859920457,-44.815807347324835,-214.5082036791364,0.9128051818634778,-12.895024044728643,8,40.0,30.0
|
||||||
|
15,long,0.1,0.03,1094.6458586065482,9.464585860654825,683,158.66641909829264,34.10293603391873,-61.25513021004292,-82.42727579859945,0.7628111273792094,-13.033316992288022,0,0.0,50.0
|
||||||
|
12,long,0.1,0.0,1218.1921573711056,21.81921573711056,370,558.8663558516799,24.378060357085793,-97.53392094072365,-113.18691429679939,0.9648648648648649,-13.1003062599465,0,0.0,50.0
|
||||||
|
15,long,0.1,0.05,1168.8056578277817,16.88056578277817,608,332.66918416530905,33.09163814654082,-84.03484815008869,-96.03995553948675,0.8766447368421053,-13.131886439222775,0,0.0,50.0
|
||||||
|
12,long,0.1,0.08,1215.462729528697,21.546272952869707,377,489.653836130157,24.213987894964383,-97.53392094072365,-112.85866997895482,0.946949602122016,-13.356836828295126,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1108.728703758097,10.872870375809702,1986,494.4803779573687,94.15080181096374,-44.815807347324835,-215.94985471803068,0.9118831822759316,-13.369364564289283,8,20.0,30.0
|
||||||
|
25,long,0.08,0.0,1108.728703758097,10.872870375809702,1986,494.4803779573687,94.15080181096374,-44.815807347324835,-215.94985471803068,0.9118831822759316,-13.369364564289283,8,20.0,30.0
|
||||||
|
12,long,0.12,0.08,1202.135538914119,20.2135538914119,277,417.44371934726576,19.051796460002638,-94.17262348471081,-109.39570457022182,0.9350180505415162,-13.508018382512432,0,0.0,50.0
|
||||||
|
20,long,0.12,0.05,1200.4075158078238,20.040751580782377,772,407.6951606504391,42.131827934710174,-88.78100275208044,-138.8849499303052,0.8886010362694301,-13.537796741357013,0,0.0,50.0
|
||||||
|
15,long,0.12,0.08,1206.931478920573,20.6931478920573,403,499.84954109113613,25.74372989411244,-95.83391499090408,-112.73654229682552,0.9478908188585607,-13.693853720055197,0,0.0,50.0
|
||||||
|
12,long,0.1,0.03,1076.1396752509584,7.613967525095836,478,120.51267212115341,25.074418902266103,-56.93595340542038,-84.69167915779826,0.7196652719665272,-13.70140245442019,0,0.0,50.0
|
||||||
|
20,long,0.12,0.03,1121.2162789700935,12.121627897009352,852,217.59126385183671,43.078476418085344,-66.15135242200427,-122.08902033976858,0.789906103286385,-13.828228846580355,5,20.0,80.0
|
||||||
|
20,long,0.12,0.03,1121.2162789700935,12.121627897009352,852,217.59126385183671,43.078476418085344,-66.15135242200427,-122.08902033976858,0.789906103286385,-13.828228846580355,5,0.0,80.0
|
||||||
|
20,long,0.12,0.03,1121.2162789700935,12.121627897009352,852,217.59126385183671,43.078476418085344,-66.15135242200427,-122.08902033976858,0.789906103286385,-13.828228846580355,5,40.0,80.0
|
||||||
|
15,long,0.12,0.0,1214.7169049952067,21.471690499520673,396,586.2432808688322,25.804940849395216,-95.83391499090408,-133.0859518383436,0.9646464646464646,-13.932781589667728,0,0.0,50.0
|
||||||
|
25,long,0.12,0.03,1130.2847694808067,13.028476948080675,1319,262.16363740691367,63.660170440323625,-70.4809411677835,-126.9113436164962,0.8225928733889311,-14.461372583079186,0,0.0,50.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,8,20.0,80.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,8,0.0,80.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,10,0.0,80.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,0,20.0,80.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,0,0.0,80.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,10,40.0,80.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,8,40.0,80.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,0,40.0,80.0
|
||||||
|
20,long,0.12,0.03,1113.4699795786817,11.346997957868165,854,209.91881359288624,43.15232555054651,-66.15135242200427,-122.08902033976858,0.788056206088993,-14.602858785721542,10,20.0,80.0
|
||||||
|
25,long,0.08,0.05,1089.496781569205,8.949678156920504,2094,385.1473407968583,98.7551506808777,-44.81012850141815,-213.54169697999976,0.8968481375358166,-15.170445242504929,8,20.0,30.0
|
||||||
|
15,long,0.1,0.08,1243.0202705911236,24.302027059112355,567,572.5946309565628,33.29402202030417,-114.68147922291314,-118.16886726038092,0.9506172839506173,-16.01086007078063,0,0.0,50.0
|
||||||
|
12,neutral,0.12,0.05,1037.9895852623313,3.7989585262331276,556,226.17952089224386,34.03563049266339,-56.46939726378719,-58.7057166817433,0.7823741007194245,-16.077146486990195,0,0.0,50.0
|
||||||
|
15,long,0.1,0.0,1231.6688825911422,23.166888259114216,562,624.0818356728452,33.2374811459689,-114.68147922291314,-118.16886726038092,0.9590747330960854,-17.14599887077877,0,0.0,50.0
|
||||||
|
15,long,0.12,0.03,1064.3778106743348,6.437781067433479,507,117.68982521873309,26.456260866499733,-60.38601930790628,-113.29885817579066,0.7238658777120316,-17.342967633727937,0,0.0,50.0
|
||||||
|
15,neutral,0.1,0.03,993.9890197090041,-0.6010980290995918,1192,136.12983051289334,59.865484701015816,-44.5593963794538,-67.62637117847862,0.7390939597315436,-17.350235501859665,0,0.0,50.0
|
||||||
|
12,long,0.08,0.03,1058.3809240671826,5.8380924067182605,650,116.62721487361696,32.129238689975715,-61.97745949961916,-98.7286258146944,0.7584615384615384,-17.691576733902206,0,0.0,50.0
|
||||||
|
15,long,0.08,0.03,1065.030855040643,6.503085504064302,967,152.4218403959458,45.73911677613086,-62.65861339021251,-113.75718075857355,0.795243019648397,-17.98235755092813,0,0.0,50.0
|
||||||
|
12,neutral,0.12,0.03,998.8232078658049,-0.11767921341951251,652,75.79458766142683,35.54253268540669,-52.61130293489987,-55.604272508392796,0.651840490797546,-18.68128371930911,0,0.0,50.0
|
||||||
|
20,long,0.1,0.03,1112.3511799795551,11.235117997955513,1160,225.20618140656305,56.28680677582321,-80.9271140265929,-117.33321152758344,0.8146551724137931,-18.90967678640153,0,0.0,50.0
|
||||||
|
25,long,0.1,0.03,1137.8204040487715,13.782040404877147,1738,298.6606490222889,81.70297366660668,-87.57493412407553,-128.86633923936358,0.8469505178365938,-18.933756794313688,0,0.0,50.0
|
||||||
|
12,long,0.08,0.05,1122.5811342325073,12.258113423250734,585,277.1917030386695,31.280576441343342,-85.28680718476664,-121.57700457535657,0.864957264957265,-19.406778960947086,0,0.0,50.0
|
||||||
|
25,long,0.12,0.08,1371.744259157642,37.17442591576421,1160,871.6224107175224,62.565532809174414,-161.30721102473558,-171.214038359484,0.9594827586206897,-19.778439309630663,0,0.0,50.0
|
||||||
|
25,long,0.1,0.05,1262.711774762407,26.2711774762407,1636,579.9023988493022,80.68691090263451,-130.16444853221992,-151.29168430641596,0.9199266503667481,-20.34274129874607,0,0.0,50.0
|
||||||
|
20,neutral,0.1,0.05,1041.6653566596644,4.166535665966444,1824,470.56950844861683,94.94622264486853,-64.9166578552639,-108.61916219869659,0.8788377192982456,-20.739419800547555,0,0.0,50.0
|
||||||
|
15,neutral,0.1,0.05,1023.4740346121534,2.3474034612153445,1069,327.45375194862976,58.23512402896476,-63.931022033571594,-100.97187700510881,0.842843779232928,-21.880496999111575,0,0.0,50.0
|
||||||
|
20,long,0.12,0.08,1260.4752961208446,26.047529612084464,723,667.0280626385457,42.16092334302708,-135.50865972723386,-150.8608790368212,0.9571230982019364,-22.148112257926755,0,0.0,50.0
|
||||||
|
20,neutral,0.1,0.03,956.1987905231379,-4.380120947686214,1999,178.43581268591768,97.88269633548947,-42.335442452572465,-104.20000006556529,0.7893946973486743,-22.290753686736217,0,0.0,50.0
|
||||||
|
20,long,0.12,0.0,1251.5216767776158,25.152167677761575,718,727.4776651327481,42.273681367564386,-135.50865972723386,-154.88750732866333,0.9637883008356546,-23.24480560684175,0,0.0,50.0
|
||||||
|
15,long,0.08,0.05,1143.6244005813899,14.362440058138986,886,345.81889856321624,44.79509595151441,-104.18340085279203,-128.58926435853732,0.8893905191873589,-23.322043415625487,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1162.638999863216,16.263899986321597,1313,919.3682609557522,65.60442349285192,-105.56034411395922,-159.58649466306406,0.9961919268849961,-23.38352798101937,5,40.0,80.0
|
||||||
|
25,long,0.08,0.0,1162.638999863216,16.263899986321597,1313,919.3682609557522,65.60442349285192,-105.56034411395922,-159.58649466306406,0.9961919268849961,-23.38352798101937,5,40.0,50.0
|
||||||
|
25,long,0.08,0.0,1162.638999863216,16.263899986321597,1313,919.3682609557522,65.60442349285192,-105.56034411395922,-159.58649466306406,0.9961919268849961,-23.38352798101937,5,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1162.638999863216,16.263899986321597,1313,919.3682609557522,65.60442349285192,-105.56034411395922,-159.58649466306406,0.9961919268849961,-23.38352798101937,5,0.0,80.0
|
||||||
|
15,neutral,0.12,0.03,945.6312327561075,-5.436876724389253,914,64.06999239417084,48.03964097029774,-38.33682334954847,-144.63841270170803,0.6947483588621444,-24.169844364339195,0,0.0,50.0
|
||||||
|
25,long,0.12,0.0,1374.08970296183,37.408970296183,1150,955.8881185470308,62.82790901214145,-175.1490649951843,-185.06073940235842,0.9678260869565217,-24.38878617249021,0,0.0,50.0
|
||||||
|
15,neutral,0.08,0.03,916.2774810214778,-8.37225189785222,1636,92.48960072730145,78.83031964227641,-29.068823055436724,-149.19914767533828,0.7707823960880196,-24.552856198250147,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1150.5467855406948,15.054678554069483,1413,677.8783233720811,70.45508910566215,-106.69741850405876,-161.62723530921096,0.9752300070771408,-25.035908762608692,5,0.0,80.0
|
||||||
|
25,long,0.08,0.08,1150.5467855406948,15.054678554069483,1413,677.8783233720811,70.45508910566215,-106.69741850405876,-161.62723530921096,0.9752300070771408,-25.035908762608692,5,40.0,80.0
|
||||||
|
25,long,0.08,0.08,1150.5467855406948,15.054678554069483,1413,677.8783233720811,70.45508910566215,-106.69741850405876,-161.62723530921096,0.9752300070771408,-25.035908762608692,5,40.0,50.0
|
||||||
|
25,long,0.08,0.08,1150.5467855406948,15.054678554069483,1413,677.8783233720811,70.45508910566215,-106.69741850405876,-161.62723530921096,0.9752300070771408,-25.035908762608692,5,0.0,50.0
|
||||||
|
20,long,0.1,0.05,1186.6696459394852,18.666964593948524,1064,432.0676359261743,54.9231060535204,-120.30407942169711,-153.4168352434191,0.900375939849624,-25.09510099473156,0,0.0,50.0
|
||||||
|
25,neutral,0.12,0.05,1012.0465648674691,1.20465648674691,2054,488.07004801319835,107.44242585435785,-72.28568569651452,-95.78787952852781,0.8816942551119766,-25.270443198633835,0,0.0,50.0
|
||||||
|
12,long,0.08,0.0,1210.6524467238366,21.065244672383663,527,627.2986835412452,31.51050188915172,-131.2305899868711,-141.04418369609425,0.9715370018975332,-25.356141508482384,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1143.0144737579244,14.30144737579244,1275,889.5293442627606,64.28800233046684,-105.56034411395922,-168.9886655282604,0.996078431372549,-25.816089134808347,5,20.0,50.0
|
||||||
|
25,long,0.08,0.0,1143.0144737579244,14.30144737579244,1275,889.5293442627606,64.28800233046684,-105.56034411395922,-168.9886655282604,0.996078431372549,-25.816089134808347,5,20.0,80.0
|
||||||
|
12,neutral,0.08,0.05,978.4938613340059,-2.150613866599406,1037,254.85703759537702,55.63214630748209,-63.898405808383586,-101.80238064086086,0.8341369334619093,-26.41025464115753,0,0.0,50.0
|
||||||
|
12,long,0.08,0.08,1195.4002596892822,19.540025968928216,538,526.1334261643764,31.304136516382002,-131.2305899868711,-141.04418369609425,0.9516728624535316,-26.88136021193783,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1130.9222594354017,13.092225943540166,1375,648.0394066790885,69.13866794327703,-106.69741850405887,-171.02940617440856,0.9745454545454545,-27.468469916397925,5,20.0,50.0
|
||||||
|
25,long,0.08,0.08,1130.9222594354017,13.092225943540166,1375,648.0394066790885,69.13866794327703,-106.69741850405887,-171.02940617440856,0.9745454545454545,-27.468469916397925,5,20.0,80.0
|
||||||
|
20,long,0.1,0.0,1307.0048036488877,30.70048036488877,1008,785.3832957491421,55.20914801241078,-166.9392973327656,-173.9969693214946,0.9533730158730159,-28.08115730101564,0,0.0,50.0
|
||||||
|
12,neutral,0.1,0.05,948.24922165223,-5.175077834777005,750,171.7366268104928,43.22284706683776,-62.63829821262266,-110.01846066629491,0.796,-29.467490331878547,0,0.0,50.0
|
||||||
|
12,neutral,0.12,0.08,1008.9190139207014,0.8919013920701445,492,438.1749220568439,34.19374707352298,-87.00908488029222,-89.24540429824833,0.8861788617886179,-29.67309428692994,0,0.0,50.0
|
||||||
|
20,neutral,0.12,0.05,964.5506878824742,-3.5449312117525777,1366,336.32174432541854,74.64673397079011,-68.4316393371688,-126.46280859431408,0.8521229868228404,-30.39756344261892,0,0.0,50.0
|
||||||
|
12,neutral,0.08,0.03,915.8994957302318,-8.410050426976818,1156,40.918312957083714,57.188082139095386,-50.40523183454059,-141.06559746668916,0.726643598615917,-30.584899850673448,0,0.0,50.0
|
||||||
|
15,neutral,0.1,0.08,1026.7049567353506,2.6704956735350605,984,658.9147969900778,58.93390459925652,-94.57765310639593,-112.4327831029899,0.9227642276422764,-31.324439413533213,0,0.0,50.0
|
||||||
|
15,long,0.08,0.08,1229.0364111861206,22.903641118612065,838,624.5993026406226,45.355033808331484,-154.1585934057598,-160.7601507757945,0.9474940334128878,-31.381944441905603,0,0.0,50.0
|
||||||
|
15,neutral,0.12,0.05,955.8990723136604,-4.410092768633956,805,215.94264205295738,46.60735243116032,-64.67672564451289,-152.12460481925052,0.8074534161490683,-31.419340702950343,0,0.0,50.0
|
||||||
|
20,long,0.1,0.08,1268.588132553505,26.85881325535049,1015,693.826328971641,55.10276224506073,-166.9392973327656,-173.9969693214947,0.9467980295566503,-31.922824410553922,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1268.0414109758044,26.80414109758044,1775,1232.4699757286367,87.45391338479479,-164.3795939103586,-189.1770745020549,0.9954929577464788,-31.968590800629887,8,0.0,80.0
|
||||||
|
15,neutral,0.12,0.08,972.4274717848173,-2.757252821518273,722,537.0352221027357,47.17328332457399,-74.0082277391358,-140.1880329926729,0.9058171745152355,-31.96912279289266,0,0.0,50.0
|
||||||
|
20,long,0.08,0.03,1029.5129914577938,2.951299145779376,1594,168.21235588421675,74.41258491762432,-86.32167848217352,-183.87671254553186,0.8343789209535759,-32.139040026149274,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1262.826219106085,26.28262191060851,1768,1226.9914620818697,87.19059160774773,-164.3795939103586,-189.177074502055,0.995475113122172,-32.490109987601826,8,40.0,80.0
|
||||||
|
25,long,0.08,0.05,1096.2653230093817,9.626532300938175,1817,387.1731107607151,86.51594748229121,-111.92692120922152,-176.61022966954397,0.9257017061089708,-32.78205554530548,5,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1096.2653230093817,9.626532300938175,1817,387.1731107607151,86.51594748229121,-111.92692120922152,-176.61022966954397,0.9257017061089708,-32.78205554530548,5,40.0,50.0
|
||||||
|
12,neutral,0.1,0.03,892.2612677882854,-10.77387322117146,862,-5.7148548369195185,45.07079750191549,-53.61269768970783,-126.5004025339041,0.6740139211136891,-33.182702654779014,0,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1092.1819524536968,9.21819524536968,1817,383.0891276075564,86.51533488481823,-111.92692120922152,-176.61022966954397,0.9257017061089708,-33.19039260087398,5,0.0,80.0
|
||||||
|
25,long,0.08,0.05,1092.1819524536968,9.21819524536968,1817,383.0891276075564,86.51533488481823,-111.92692120922152,-176.61022966954397,0.9257017061089708,-33.19039260087398,5,40.0,80.0
|
||||||
|
15,neutral,0.08,0.05,905.1757000990156,-9.482429990098444,1496,269.5513433703725,76.95965566395986,-55.92848521443341,-147.88151729119272,0.858957219251337,-33.6550514189881,0,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1083.629915586647,8.362991558664703,1800,373.89821210317984,85.8764562474926,-111.92692120922152,-179.57504991988685,0.925,-34.19383730009609,5,20.0,50.0
|
||||||
|
25,long,0.08,0.05,1079.5465450309614,7.9546545030961395,1800,369.81422895002106,85.87584365001965,-111.92692120922152,-180.3029057185688,0.925,-34.638567145598756,5,20.0,80.0
|
||||||
|
25,long,0.08,0.08,1239.5468088164787,23.954680881647867,1881,927.819736467546,92.58407163905943,-165.51666830045815,-193.5361077653356,0.9766081871345029,-35.37712499675636,8,0.0,80.0
|
||||||
|
25,long,0.08,0.08,1234.331616946761,23.433161694676095,1874,922.3412228207794,92.32074986201238,-165.51666830045815,-193.53610776533537,0.9765208110992529,-35.89864418372812,8,40.0,80.0
|
||||||
|
25,neutral,0.1,0.03,906.3628160481298,-9.363718395187016,2888,207.19204221547852,138.1737804893506,-64.94214030179364,-145.47984940481342,0.8244459833795014,-36.12035295596578,0,0.0,50.0
|
||||||
|
15,long,0.08,0.0,1227.3961128748017,22.73961128748017,829,697.9347268062245,45.5905195229829,-167.99310421760356,-174.59950608867211,0.9577804583835947,-36.3882952822345,0,0.0,50.0
|
||||||
|
12,neutral,0.12,0.0,969.3566043989616,-3.064339560103838,453,745.0479952647905,35.217155627846786,-87.00908488029222,-149.45137337681047,0.9624724061810155,-36.639633693032025,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1228.9582509087586,22.89582509087586,1706,1177.9286970976077,84.90879945990056,-164.37959391035884,-204.808440494122,0.9953106682297772,-36.658475106937885,8,20.0,80.0
|
||||||
|
12,neutral,0.08,0.08,1017.5268988730409,1.7526898873040864,939,642.0429221626582,55.486604857858254,-109.84218861048737,-110.42637689916171,0.9243876464323749,-36.72128554080021,0,0.0,50.0
|
||||||
|
25,long,0.1,0.08,1316.9263500053653,31.69263500053653,1567,863.4338174117532,80.21478847713934,-195.1137402743459,-202.50425458038717,0.9489470325462668,-36.966699810786594,0,0.0,50.0
|
||||||
|
20,neutral,0.12,0.03,874.4226048516447,-12.557739514835532,1518,62.59357657113197,76.88600010573118,-57.30136960786092,-163.13382195213535,0.7483530961791831,-37.90484149480058,0,0.0,50.0
|
||||||
|
12,neutral,0.1,0.08,959.0611392166089,-4.093886078339108,667,490.30403423333235,43.2330110457878,-94.21066522501121,-117.41301240124483,0.9010494752623688,-38.22773626590472,0,0.0,50.0
|
||||||
|
25,neutral,0.12,0.03,885.7225423471225,-11.42774576528775,2234,139.27716934255434,109.78796064866359,-65.18830656293073,-163.99841781207726,0.7931960608773501,-39.18415862477083,0,0.0,50.0
|
||||||
|
20,neutral,0.1,0.08,1019.9155307951296,1.99155307951296,1714,889.0116003101779,95.50972109837602,-111.55187576633205,-169.96014848227196,0.9387397899649942,-39.97201707450024,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1201.244356605409,20.124435660540893,1813,874.0967884420804,90.07658046375604,-165.5166683004585,-209.48201046918098,0.97573083287369,-40.0046653530557,8,20.0,80.0
|
||||||
|
20,long,0.08,0.05,1124.5819461650653,12.458194616506535,1492,411.2270180323591,73.11772134759346,-142.86133759521113,-222.21167486844843,0.9088471849865952,-41.51079040547922,0,0.0,50.0
|
||||||
|
15,neutral,0.08,0.08,962.2733614713102,-3.7726638528689818,1390,731.6268276728515,77.71704193631616,-97.82223007855794,-173.32372563648516,0.9338129496402877,-41.78551915826062,0,0.0,50.0
|
||||||
|
25,long,0.1,0.0,1313.1987801079035,31.319878010790354,1562,900.3658461399156,80.34530684975778,-208.96662361050812,-216.36198885118438,0.9519846350832266,-42.1882085149213,0,0.0,50.0
|
||||||
|
20,neutral,0.08,0.03,829.4339026671721,-17.05660973328279,2636,98.19159368509234,124.5297032156403,-50.45636658278261,-215.42374875258838,0.8171471927162367,-42.96470714574699,0,0.0,50.0
|
||||||
|
15,neutral,0.1,0.0,958.7020508349266,-4.1297949165073415,952,877.943488361327,59.83872248961458,-94.57765310639593,-220.2343940423009,0.9537815126050421,-43.51481055054116,0,0.0,50.0
|
||||||
|
15,neutral,0.12,0.0,915.1260245021808,-8.487397549781917,681,852.6782963054246,48.21569346198492,-80.02389538542047,-229.63375181486947,0.960352422907489,-43.976253756151536,0,0.0,50.0
|
||||||
|
25,neutral,0.1,0.05,957.9505861549574,-4.204941384504264,2693,508.3431872295519,135.33386878994648,-107.53165470993815,-162.52408992777555,0.8971407352395099,-44.59064229387449,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1261.2510255067548,26.12510255067548,1962,1241.518453175452,97.51089000134886,-199.61125096898286,-228.045925259608,0.9898063200815495,-45.16056900299978,10,0.0,80.0
|
||||||
|
25,long,0.08,0.08,1263.004536227681,26.3004536227681,2047,994.3289831906201,100.93357667030716,-200.74832535908251,-234.30900361061288,0.9755740107474352,-45.6394941654873,10,0.0,80.0
|
||||||
|
25,long,0.08,0.03,977.8030252525732,-2.2196974747426794,2360,171.53530836186184,107.69807878640938,-108.99847089762079,-218.1390860908691,0.8593220338983051,-45.82619304857237,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1247.6337518181654,24.763375181816535,1944,1227.2240353241866,96.83374583867123,-199.6112509689831,-229.54555467016212,0.9897119341563786,-46.59727784238649,10,40.0,80.0
|
||||||
|
20,long,0.08,0.0,1253.9827072613166,25.39827072613166,1425,842.2815443716456,73.60409985214305,-205.2864268826097,-215.18491317778285,0.9536842105263158,-46.94690299754039,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1249.3872625390923,24.938726253909227,2029,980.0345653393548,100.25643250762954,-200.74832535908274,-235.80863302116722,0.9753573188762937,-47.07620300487395,10,40.0,80.0
|
||||||
|
25,long,0.08,0.05,1156.3011102933785,15.630111029337854,2244,527.1697975637469,106.47584069366704,-170.6942396219963,-239.92570159008744,0.9295900178253119,-47.57444593676541,10,40.0,80.0
|
||||||
|
25,long,0.08,0.05,1156.3011102933785,15.630111029337854,2244,527.1697975637469,106.47584069366704,-170.6942396219963,-239.92570159008744,0.9295900178253119,-47.57444593676541,10,0.0,80.0
|
||||||
|
12,neutral,0.1,0.0,908.7043890282025,-9.12956109717975,631,763.220257812243,44.28853244365189,-94.21066522501121,-203.76964277656498,0.9492868462757528,-47.58124280351136,0,0.0,50.0
|
||||||
|
12,neutral,0.08,0.0,942.564965429586,-5.743503457041402,905,887.4298730720966,56.35697626431882,-109.84218861048794,-177.77224939405562,0.958011049723757,-47.584772509890556,0,0.0,50.0
|
||||||
|
20,long,0.08,0.08,1238.5115422947035,23.851154229470353,1432,770.8758322716842,73.50250478563372,-205.2864268826097,-215.18491317778285,0.9490223463687151,-48.494019494201694,0,0.0,50.0
|
||||||
|
25,neutral,0.12,0.08,951.1715940359319,-4.882840596406811,1934,893.587242548015,107.84552558600296,-122.6044020665829,-157.87084081117223,0.93846949327818,-49.55770325694029,0,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1140.2052710481846,14.020527104818461,2192,507.0421467208481,104.22221702668796,-171.42644373863436,-243.43094321578212,0.9292883211678832,-49.57895317756095,8,0.0,80.0
|
||||||
|
25,long,0.08,0.05,1140.2052710481846,14.020527104818461,2192,507.0421467208481,104.22221702668796,-171.42644373863436,-243.43094321578212,0.9292883211678832,-49.57895317756095,8,40.0,80.0
|
||||||
|
25,long,0.08,0.05,1130.7394017322986,13.073940173229857,2247,507.79962905841853,106.77293516839114,-170.6942396219963,-246.24068604468005,0.9283489096573209,-50.44636601560303,0,0.0,80.0
|
||||||
|
25,long,0.08,0.05,1130.7394017322986,13.073940173229857,2247,507.79962905841853,106.77293516839114,-170.6942396219963,-246.24068604468005,0.9283489096573209,-50.44636601560303,0,40.0,80.0
|
||||||
|
20,neutral,0.12,0.08,922.7526156562791,-7.724738434372091,1268,674.6355346682385,74.98528389955857,-115.15929631232211,-168.85788441401166,0.9211356466876972,-50.715421548769314,0,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1123.792464942301,12.379246494230097,2201,493.0435332730257,104.85822175402342,-170.6942396219963,-244.45747103753422,0.9282144479781917,-51.0518989442455,10,20.0,80.0
|
||||||
|
25,long,0.08,0.08,1213.0890273538441,21.308902735384414,1952,918.5667128387636,97.1097026371753,-200.74832535908274,-245.18204336073813,0.9743852459016393,-51.17469704037731,10,20.0,80.0
|
||||||
|
25,long,0.08,0.0,1204.8948358787318,20.489483587873178,1869,1167.522764050186,93.69978095540102,-199.61125096898297,-245.57495399993752,0.9892990904226859,-51.67263940281858,10,20.0,80.0
|
||||||
|
20,neutral,0.08,0.05,843.9156120270152,-15.608438797298481,2457,345.8560483495332,121.90691029950048,-81.38855124452266,-240.35299600281314,0.8917378917378918,-52.04265397079594,0,0.0,50.0
|
||||||
|
25,long,0.08,0.05,1113.8793847526845,11.387938475268447,2157,479.3996140454485,102.90557064678663,-171.42644373863436,-247.9627126632289,0.9281409364858599,-52.4381302794833,8,20.0,80.0
|
||||||
|
25,long,0.08,0.05,1101.8441240869888,10.184412408698881,2198,477.0622408048723,104.93082456015318,-170.6942396219963,-248.1048958985108,0.9272065514103731,-53.42910427282554,0,20.0,80.0
|
||||||
|
20,neutral,0.08,0.08,933.7151944535493,-6.628480554645068,2335,909.5540468508254,122.11614293315958,-127.75056090327416,-222.0555457341295,0.9408993576017131,-56.05642611233379,0,0.0,50.0
|
||||||
|
15,neutral,0.08,0.0,893.2798589855879,-10.67201410144121,1351,999.0722921013755,78.81239461941462,-111.65674089040158,-259.08071225882327,0.9607698001480385,-57.12307198150284,0,0.0,50.0
|
||||||
|
20,neutral,0.12,0.0,893.2481570336073,-10.67518429663927,1226,965.1856697126141,76.02582293352991,-115.15929631232211,-246.80425997403324,0.9502446982055465,-57.563186189037566,0,0.0,50.0
|
||||||
|
20,neutral,0.1,0.0,892.2381775176088,-10.776182248239115,1680,1015.4979053676461,96.11117696167355,-111.55187576633205,-275.58768912213407,0.9517857142857142,-58.02112943424543,0,0.0,50.0
|
||||||
|
20,neutral,0.08,0.0,907.7165314110839,-9.228346858891609,2310,1073.1891314194672,123.22985895655859,-127.75056090327416,-224.72118979536845,0.9493506493506494,-58.78957461964228,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1246.3778914445222,24.63778914445222,2174,966.0754452181279,107.87418295211393,-249.51298036242326,-268.0871479032645,0.9668813247470102,-63.62046235943798,0,0.0,80.0
|
||||||
|
25,neutral,0.12,0.0,876.9849725322533,-12.301502746774668,1883,1173.335843416292,108.5738726747974,-136.44625603703173,-233.93550504181076,0.9569835369091875,-64.93215480997472,0,0.0,50.0
|
||||||
|
25,long,0.08,0.08,1231.2854478125905,23.128544781259052,2154,950.2306248631428,107.1218062290607,-249.5129803624235,-269.5867773138185,0.9665738161559888,-65.20468819315892,0,40.0,80.0
|
||||||
|
25,long,0.08,0.0,1250.129294503415,25.012929450341495,2159,1095.3846784387028,108.18324616531072,-263.3769106549943,-280.311776303294,0.9735988883742474,-68.01573256132149,0,0.0,80.0
|
||||||
|
25,long,0.08,0.0,1235.0368508714841,23.503685087148416,2139,1079.5398580837168,107.43086944225746,-263.37691065499405,-281.81140571384753,0.9733520336605891,-69.59995839504218,0,40.0,80.0
|
||||||
|
25,long,0.08,0.08,1186.755101595493,18.675510159549297,2078,889.6085148619517,103.98741680426937,-249.51298036242326,-285.6161766435938,0.9653512993262753,-70.45919278135737,0,20.0,80.0
|
||||||
|
25,neutral,0.1,0.08,896.2498454068138,-10.375015459318615,2557,951.9026603241215,135.6213145995644,-172.48094645206368,-211.1600905658389,0.9421196714900274,-72.67730392322966,0,0.0,50.0
|
||||||
|
25,neutral,0.08,0.03,667.6452507448552,-33.23547492551448,3848,25.05767319412064,178.75037479320912,-83.26174105150335,-354.0556578479617,0.8435550935550935,-75.91678013336357,0,0.0,50.0
|
||||||
|
25,long,0.08,0.0,1178.6678874078664,17.86678874078664,2047,1005.4810992466884,103.56946452014068,-263.37691065499405,-297.84080504362305,0.9721543722520762,-76.03832470789273,0,20.0,80.0
|
||||||
|
25,neutral,0.08,0.05,754.8750095955463,-24.51249904044537,3639,400.9700275229705,176.1617337979003,-127.58504206956457,-301.28041541625385,0.9084913437757626,-77.85203243212743,0,0.0,50.0
|
||||||
|
25,neutral,0.08,0.0,956.1631788661102,-4.383682113388977,3465,1342.2749906710937,175.64727713885594,-204.1786834260496,-249.2685574601693,0.9512265512265512,-78.10071501421231,0,0.0,50.0
|
||||||
|
25,neutral,0.08,0.08,899.6206335365904,-10.037936646340961,3490,1093.1066894313235,175.5638321579994,-190.3147531334788,-234.46730157901732,0.9449856733524356,-78.85572766533546,0,0.0,50.0
|
||||||
|
25,neutral,0.1,0.0,856.4548169989196,-14.354518300108044,2508,1267.1084544600399,136.81912532955567,-186.333829788226,-238.37432065779933,0.9589314194577353,-82.1733832694658,0,0.0,50.0
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
bitmart-python-sdk-api
|
|
||||||
loguru
|
|
||||||
peewee
|
|
||||||
pymysql
|
|
||||||
numpy
|
|
||||||
pandas
|
|
||||||
scikit-learn
|
|
||||||
joblib
|
|
||||||
lightgbm>=3.0.0
|
|
||||||
optuna>=3.0.0
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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]}")
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
"""
|
|
||||||
技术指标计算模块 - 纯 pandas/numpy 实现所有指标
|
|
||||||
所有函数返回 DataFrame/Series,可直接拼接到主 DataFrame
|
|
||||||
"""
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 辅助函数
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def _ema(series: pd.Series, period: int) -> pd.Series:
|
|
||||||
return series.ewm(span=period, adjust=False).mean()
|
|
||||||
|
|
||||||
|
|
||||||
def _sma(series: pd.Series, period: int) -> pd.Series:
|
|
||||||
return series.rolling(window=period).mean()
|
|
||||||
|
|
||||||
|
|
||||||
def _wma(series: pd.Series, period: int) -> pd.Series:
|
|
||||||
weights = np.arange(1, period + 1, dtype=float)
|
|
||||||
return series.rolling(window=period).apply(
|
|
||||||
lambda x: np.dot(x, weights) / weights.sum(), raw=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _true_range(high: pd.Series, low: pd.Series, close: pd.Series) -> pd.Series:
|
|
||||||
prev_close = close.shift(1)
|
|
||||||
tr1 = high - low
|
|
||||||
tr2 = (high - prev_close).abs()
|
|
||||||
tr3 = (low - prev_close).abs()
|
|
||||||
return pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
|
||||||
|
|
||||||
|
|
||||||
def _atr(high, low, close, period: int) -> pd.Series:
|
|
||||||
tr = _true_range(high, low, close)
|
|
||||||
return _ema(tr, period)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 布林带 (Bollinger Bands)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def bollinger_bands(close: pd.Series, period: int = 20, std_dev: float = 2.0):
|
|
||||||
mid = _sma(close, period)
|
|
||||||
std = close.rolling(window=period).std()
|
|
||||||
upper = mid + std_dev * std
|
|
||||||
lower = mid - std_dev * std
|
|
||||||
# 百分比位置 %B
|
|
||||||
pct_b = (close - lower) / (upper - lower)
|
|
||||||
return mid, upper, lower, pct_b
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 肯特纳通道 (Keltner Channel)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def keltner_channel(high, low, close, period: int = 20, atr_mult: float = 1.5):
|
|
||||||
mid = _ema(close, period)
|
|
||||||
atr = _atr(high, low, close, period)
|
|
||||||
upper = mid + atr_mult * atr
|
|
||||||
lower = mid - atr_mult * atr
|
|
||||||
pct = (close - lower) / (upper - lower)
|
|
||||||
return mid, upper, lower, pct
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 唐奇安通道 (Donchian Channel)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def donchian_channel(high, low, period: int = 20):
|
|
||||||
upper = high.rolling(window=period).max()
|
|
||||||
lower = low.rolling(window=period).min()
|
|
||||||
mid = (upper + lower) / 2
|
|
||||||
pct = (high.combine(low, lambda a, b: (a + b) / 2) - lower) / (upper - lower)
|
|
||||||
return mid, upper, lower, pct
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# EMA 交叉
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def ema_cross(close, fast_period: int = 9, slow_period: int = 21):
|
|
||||||
fast = _ema(close, fast_period)
|
|
||||||
slow = _ema(close, slow_period)
|
|
||||||
diff = fast - slow
|
|
||||||
return fast, slow, diff
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# MACD
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def macd(close, fast: int = 12, slow: int = 26, signal: int = 9):
|
|
||||||
ema_fast = _ema(close, fast)
|
|
||||||
ema_slow = _ema(close, slow)
|
|
||||||
macd_line = ema_fast - ema_slow
|
|
||||||
signal_line = _ema(macd_line, signal)
|
|
||||||
histogram = macd_line - signal_line
|
|
||||||
return macd_line, signal_line, histogram
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# ADX (Average Directional Index)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def adx(high, low, close, period: int = 14):
|
|
||||||
prev_high = high.shift(1)
|
|
||||||
prev_low = low.shift(1)
|
|
||||||
|
|
||||||
plus_dm = np.where((high - prev_high) > (prev_low - low),
|
|
||||||
np.maximum(high - prev_high, 0), 0)
|
|
||||||
minus_dm = np.where((prev_low - low) > (high - prev_high),
|
|
||||||
np.maximum(prev_low - low, 0), 0)
|
|
||||||
|
|
||||||
plus_dm = pd.Series(plus_dm, index=high.index)
|
|
||||||
minus_dm = pd.Series(minus_dm, index=high.index)
|
|
||||||
|
|
||||||
atr = _atr(high, low, close, period)
|
|
||||||
plus_di = 100 * _ema(plus_dm, period) / atr
|
|
||||||
minus_di = 100 * _ema(minus_dm, period) / atr
|
|
||||||
|
|
||||||
dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, 1e-10)
|
|
||||||
adx_val = _ema(dx, period)
|
|
||||||
return adx_val, plus_di, minus_di
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Supertrend
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def supertrend(high, low, close, period: int = 10, multiplier: float = 3.0):
|
|
||||||
hl2 = (high + low) / 2
|
|
||||||
atr = _atr(high, low, close, period)
|
|
||||||
|
|
||||||
ub = (hl2 + multiplier * atr).values.copy()
|
|
||||||
lb = (hl2 - multiplier * atr).values.copy()
|
|
||||||
c = close.values
|
|
||||||
n = len(c)
|
|
||||||
|
|
||||||
direction = np.ones(n, dtype=np.int8)
|
|
||||||
st = np.empty(n)
|
|
||||||
st[0] = lb[0]
|
|
||||||
|
|
||||||
for i in range(1, n):
|
|
||||||
if c[i] > ub[i - 1]:
|
|
||||||
direction[i] = 1
|
|
||||||
elif c[i] < lb[i - 1]:
|
|
||||||
direction[i] = -1
|
|
||||||
else:
|
|
||||||
direction[i] = direction[i - 1]
|
|
||||||
|
|
||||||
if direction[i] == 1:
|
|
||||||
if direction[i - 1] == 1:
|
|
||||||
lb[i] = max(lb[i], lb[i - 1])
|
|
||||||
st[i] = lb[i]
|
|
||||||
else:
|
|
||||||
if direction[i - 1] == -1:
|
|
||||||
ub[i] = min(ub[i], ub[i - 1])
|
|
||||||
st[i] = ub[i]
|
|
||||||
|
|
||||||
return (pd.Series(st, index=close.index),
|
|
||||||
pd.Series(direction, index=close.index))
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# RSI
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def rsi(close, period: int = 14):
|
|
||||||
delta = close.diff()
|
|
||||||
gain = delta.clip(lower=0)
|
|
||||||
loss = (-delta).clip(lower=0)
|
|
||||||
avg_gain = _ema(gain, period)
|
|
||||||
avg_loss = _ema(loss, period)
|
|
||||||
rs = avg_gain / avg_loss.replace(0, 1e-10)
|
|
||||||
return 100 - (100 / (1 + rs))
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Stochastic Oscillator
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def stochastic(high, low, close, k_period: int = 14, d_period: int = 3, smooth: int = 3):
|
|
||||||
lowest_low = low.rolling(window=k_period).min()
|
|
||||||
highest_high = high.rolling(window=k_period).max()
|
|
||||||
raw_k = 100 * (close - lowest_low) / (highest_high - lowest_low).replace(0, 1e-10)
|
|
||||||
k = _sma(raw_k, smooth)
|
|
||||||
d = _sma(k, d_period)
|
|
||||||
return k, d
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# CCI (Commodity Channel Index)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def cci(high, low, close, period: int = 20):
|
|
||||||
tp = (high + low + close) / 3
|
|
||||||
sma_tp = _sma(tp, period)
|
|
||||||
mad = tp.rolling(window=period).apply(lambda x: np.abs(x - x.mean()).mean(), raw=True)
|
|
||||||
return (tp - sma_tp) / (0.015 * mad.replace(0, 1e-10))
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Williams %R
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def williams_r(high, low, close, period: int = 14):
|
|
||||||
highest_high = high.rolling(window=period).max()
|
|
||||||
lowest_low = low.rolling(window=period).min()
|
|
||||||
return -100 * (highest_high - close) / (highest_high - lowest_low).replace(0, 1e-10)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# WMA (替代 VWMA,因为没有 volume)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def wma(close, period: int = 20):
|
|
||||||
return _wma(close, period)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 统一计算所有指标并拼接到 DataFrame
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def compute_all_indicators(df: pd.DataFrame, params: dict) -> pd.DataFrame:
|
|
||||||
"""
|
|
||||||
根据参数字典计算所有指标,返回带指标列的 DataFrame
|
|
||||||
params 示例:
|
|
||||||
{
|
|
||||||
'bb_period': 20, 'bb_std': 2.0,
|
|
||||||
'kc_period': 20, 'kc_mult': 1.5,
|
|
||||||
'dc_period': 20,
|
|
||||||
'ema_fast': 9, 'ema_slow': 21,
|
|
||||||
'macd_fast': 12, 'macd_slow': 26, 'macd_signal': 9,
|
|
||||||
'adx_period': 14,
|
|
||||||
'st_period': 10, 'st_mult': 3.0,
|
|
||||||
'rsi_period': 14,
|
|
||||||
'stoch_k': 14, 'stoch_d': 3, 'stoch_smooth': 3,
|
|
||||||
'cci_period': 20,
|
|
||||||
'wr_period': 14,
|
|
||||||
'wma_period': 20,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
out = df.copy()
|
|
||||||
h, l, c = out['high'], out['low'], out['close']
|
|
||||||
|
|
||||||
# 布林带
|
|
||||||
bb_mid, bb_up, bb_lo, bb_pct = bollinger_bands(c, params['bb_period'], params['bb_std'])
|
|
||||||
out['bb_pct'] = bb_pct
|
|
||||||
|
|
||||||
# 肯特纳通道
|
|
||||||
_, _, _, kc_pct = keltner_channel(h, l, c, params['kc_period'], params['kc_mult'])
|
|
||||||
out['kc_pct'] = kc_pct
|
|
||||||
|
|
||||||
# 唐奇安通道
|
|
||||||
_, _, _, dc_pct = donchian_channel(h, l, params['dc_period'])
|
|
||||||
out['dc_pct'] = dc_pct
|
|
||||||
|
|
||||||
# EMA 交叉
|
|
||||||
_, _, ema_diff = ema_cross(c, params['ema_fast'], params['ema_slow'])
|
|
||||||
out['ema_diff'] = ema_diff
|
|
||||||
|
|
||||||
# MACD
|
|
||||||
macd_l, macd_s, macd_h = macd(c, params['macd_fast'], params['macd_slow'], params['macd_signal'])
|
|
||||||
out['macd_hist'] = macd_h
|
|
||||||
|
|
||||||
# ADX
|
|
||||||
adx_val, plus_di, minus_di = adx(h, l, c, params['adx_period'])
|
|
||||||
out['adx'] = adx_val
|
|
||||||
out['di_diff'] = plus_di - minus_di
|
|
||||||
|
|
||||||
# Supertrend
|
|
||||||
_, st_dir = supertrend(h, l, c, params['st_period'], params['st_mult'])
|
|
||||||
out['st_dir'] = st_dir
|
|
||||||
|
|
||||||
# RSI
|
|
||||||
out['rsi'] = rsi(c, params['rsi_period'])
|
|
||||||
|
|
||||||
# Stochastic
|
|
||||||
stoch_k, stoch_d = stochastic(h, l, c, params['stoch_k'], params['stoch_d'], params['stoch_smooth'])
|
|
||||||
out['stoch_k'] = stoch_k
|
|
||||||
out['stoch_d'] = stoch_d
|
|
||||||
|
|
||||||
# CCI
|
|
||||||
out['cci'] = cci(h, l, c, params['cci_period'])
|
|
||||||
|
|
||||||
# Williams %R
|
|
||||||
out['wr'] = williams_r(h, l, c, params['wr_period'])
|
|
||||||
|
|
||||||
# WMA
|
|
||||||
out['wma'] = wma(c, params['wma_period'])
|
|
||||||
out['wma_diff'] = c - out['wma']
|
|
||||||
|
|
||||||
return out
|
|
||||||
@@ -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
|
|
||||||
@@ -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
38
test.py
Normal 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
1
token.json
Normal 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"}
|
||||||
686
四分之一,五分钟,反手条件充足.py
Normal file
686
四分之一,五分钟,反手条件充足.py
Normal 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
540
抓取币安K线.py
Normal 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
408
抓取币安K线_ccxt.py
Normal 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()
|
||||||
Reference in New Issue
Block a user