diff --git a/15分钟,三分之一,止盈.py b/15分钟,三分之一,止盈.py new file mode 100644 index 0000000..e15e1e6 --- /dev/null +++ b/15分钟,三分之一,止盈.py @@ -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() diff --git a/bb_trade.py b/bb_trade.py new file mode 100644 index 0000000..157b1af --- /dev/null +++ b/bb_trade.py @@ -0,0 +1,546 @@ +""" +布林带均值回归策略 — 实盘交易 +BB(10, 2.5) | 5分钟K线 | ETH | 50x杠杆 | 每单权益1% + +逻辑: + - 价格触及上布林带 → 平多(如有) + 开空 + - 价格触及下布林带 → 平空(如有) + 开多 + - 始终持仓(多空翻转) + +使用 BitMart Futures API 进行开平仓 +""" +import time +import uuid +import numpy as np +from datetime import datetime +from pathlib import Path + +from loguru import logger +from bitmart.api_contract import APIContract + + +# --------------------------------------------------------------------------- +# 配置 +# --------------------------------------------------------------------------- +class BBTradeConfig: + # API 凭证 + API_KEY = "6104088c65a68d7e53df5d9395b67d78e555293a" + SECRET_KEY = "a8b14312330d8e6b9b09acfd972b34e32022fdfa9f2b06f0a0a31723b873fd01" + MEMO = "me" + + # 合约 + CONTRACT_SYMBOL = "ETHUSDT" + + # 布林带参数 + BB_PERIOD = 10 # 10根5分钟K线 = 50分钟回看 + BB_STD = 2.5 # 标准差倍数 + + # 仓位管理 + LEVERAGE = 50 # 杠杆倍数 + OPEN_TYPE = "cross" # 全仓模式 + MARGIN_PCT = 0.01 # 每单用权益的1%作为保证金 + + # 风控 + 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) + ) + + # 持仓状态: -1=空, 0=无, 1=多 + self.position = 0 + self.open_avg_price = None + self.current_amount = None + + # 风控 + 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 _gen_client_order_id(self) -> str: + return f"BB_{uuid.uuid4().hex[:12]}" + + def submit_order(self, side: int, size: int) -> bool: + """ + 提交市价单 + side: 1=买入开多, 2=买入平空, 3=卖出平多, 4=卖出开空 + size: 张数 + """ + side_names = {1: "买入开多", 2: "买入平空", 3: "卖出平多", 4: "卖出开空"} + logger.info(f"下单: {side_names.get(side, side)} {size}张") + try: + resp = self.api.post_submit_order( + contract_symbol=self.cfg.CONTRACT_SYMBOL, + client_order_id=self._gen_client_order_id(), + side=side, + mode=1, # GTC + type="market", + leverage=str(self.cfg.LEVERAGE), + open_type=self.cfg.OPEN_TYPE, + size=size, + )[0] + if resp.get("code") == 1000: + logger.success(f"下单成功: {side_names.get(side)} {size}张 resp={resp}") + return True + else: + logger.error(f"下单失败: {resp}") + return False + except Exception as e: + logger.error(f"下单异常: {e}") + return False + + # ------------------------------------------------------------------ + # 仓位操作 + # ------------------------------------------------------------------ + def calc_order_size(self, price: float) -> int: + """ + 根据当前权益的1%计算开仓张数 + BitMart ETH合约: 1张 = 0.01 ETH + 保证金 = equity * margin_pct + 名义价值 = margin * leverage + 数量(ETH) = 名义价值 / price + 张数 = 数量 / 0.01 + """ + balance = self.get_balance() + if balance is None or balance <= 0: + logger.warning(f"余额不足或查询失败: {balance}") + return 0 + margin = balance * self.cfg.MARGIN_PCT + notional = margin * self.cfg.LEVERAGE + qty_eth = notional / price + size = max(1, int(qty_eth / 0.01)) # 1张=0.01ETH + logger.info(f"仓位计算: 余额={balance:.2f} 保证金={margin:.2f} " + f"名义={notional:.2f} 数量={qty_eth:.4f}ETH = {size}张") + return size + + def close_current_position(self) -> bool: + """平掉当前持仓""" + if not self.get_position_status(): + return False + if self.position == 0: + logger.info("无持仓,无需平仓") + return True + if self.position == 1: + # 平多: side=3 + size = int(self.current_amount) + return self.submit_order(side=3, size=size) + else: + # 平空: side=2 + size = int(self.current_amount) + return self.submit_order(side=2, size=size) + + def open_long(self, price: float) -> bool: + """开多""" + size = self.calc_order_size(price) + if size <= 0: + return False + return self.submit_order(side=1, size=size) + + def open_short(self, price: float) -> bool: + """开空""" + size = self.calc_order_size(price) + if size <= 0: + return False + return self.submit_order(side=4, size=size) + + def flip_to_long(self, price: float) -> bool: + """平空 → 开多""" + logger.info("=== 翻转为多 ===") + if self.position == -1: + if not self.close_current_position(): + logger.error("平空失败,放弃开多") + return False + time.sleep(2) + # 确认已无仓 + for _ in range(5): + if self.get_position_status() and self.position == 0: + break + time.sleep(1) + if self.position != 0: + logger.warning(f"平仓后仍有持仓({self.position}),放弃开多") + return False + return self.open_long(price) + + def flip_to_short(self, price: float) -> bool: + """平多 → 开空""" + logger.info("=== 翻转为空 ===") + if self.position == 1: + if not self.close_current_position(): + logger.error("平多失败,放弃开空") + return False + time.sleep(2) + for _ in range(5): + if self.get_position_status() and self.position == 0: + break + time.sleep(1) + if self.position != 0: + logger.warning(f"平仓后仍有持仓({self.position}),放弃开空") + return False + return self.open_short(price) + + # ------------------------------------------------------------------ + # 风控 + # ------------------------------------------------------------------ + def check_daily_reset(self): + """每日重置(UTC+8 00:00 = UTC 16:00)""" + now = datetime.utcnow() + # 用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 run(self): + """策略主循环""" + logger.info("=" * 60) + logger.info(f" BB策略启动: BB({self.cfg.BB_PERIOD},{self.cfg.BB_STD})") + logger.info(f" 合约: {self.cfg.CONTRACT_SYMBOL} | {self.cfg.LEVERAGE}x {self.cfg.OPEN_TYPE}") + logger.info(f" 每单: 权益×{self.cfg.MARGIN_PCT:.0%}") + logger.info("=" * 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线重复触发 + + while True: + try: + 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 + + # 当前K线 = 最后一根(未收盘),信号用已收盘的K线 + # 使用倒数第二根及之前的收盘价算BB(已收盘的K线) + closed_klines = klines[:-1] # 已收盘的K线 + current_kline = klines[-1] # 当前未收盘K线 + + 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 + + # 用当前K线的 high/low 判断是否触及布林带 + cur_high = current_kline["high"] + cur_low = current_kline["low"] + touched_upper = cur_high >= bb_upper + touched_lower = cur_low <= bb_lower + + 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. 信号判断 + 执行 + # 同一根K线只触发一次 + kline_id = current_kline["id"] + if kline_id == last_kline_id: + # 已在这根K线触发过,不重复操作 + time.sleep(self.cfg.POLL_INTERVAL) + continue + + # 同时触及上下轨(极端波动)→ 跳过 + if touched_upper and touched_lower: + logger.warning("同时触及上下轨,跳过") + time.sleep(self.cfg.POLL_INTERVAL) + continue + + action = None + reason = "" + + 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 = "翻转: 平多→开空" + success = self.flip_to_short(current_price) + elif self.position == 0: + action = "开空" + success = self.open_short(current_price) + else: + # 已经是空仓,不操作 + logger.info("已持空仓,触上轨无需操作") + success = False + + if success: + last_kline_id = kline_id + self.last_trade_time = time.time() + self.write_trade_log(action, current_price, + bb_upper, bb_mid, bb_lower, reason) + logger.success(f"{action} 执行成功") + + 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 = "翻转: 平空→开多" + success = self.flip_to_long(current_price) + elif self.position == 0: + action = "开多" + success = self.open_long(current_price) + else: + logger.info("已持多仓,触下轨无需操作") + success = False + + if success: + last_kline_id = kline_id + self.last_trade_time = time.time() + self.write_trade_log(action, current_price, + bb_upper, bb_mid, bb_lower, reason) + logger.success(f"{action} 执行成功") + + time.sleep(self.cfg.POLL_INTERVAL) + + except KeyboardInterrupt: + logger.info("用户中断,程序退出") + break + except Exception as e: + logger.error(f"主循环异常: {e}") + time.sleep(10) + + +# --------------------------------------------------------------------------- +# 入口 +# --------------------------------------------------------------------------- +if __name__ == "__main__": + trader = BBTrader() + trader.run() diff --git a/main.py b/main.py deleted file mode 100644 index b56c695..0000000 --- a/main.py +++ /dev/null @@ -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 帮助 diff --git a/models/grid_sweep_results.csv b/models/grid_sweep_results.csv new file mode 100644 index 0000000..ab40c77 --- /dev/null +++ b/models/grid_sweep_results.csv @@ -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 diff --git a/models/optuna_best_params.pkl b/models/optuna_best_params.pkl new file mode 100644 index 0000000..69e5236 Binary files /dev/null and b/models/optuna_best_params.pkl differ diff --git a/models/trading_model.pkl b/models/trading_model.pkl new file mode 100644 index 0000000..aa759e3 Binary files /dev/null and b/models/trading_model.pkl differ diff --git a/requirements.txt b/requirements.txt index cb3254e..bb90a3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ scikit-learn joblib lightgbm>=3.0.0 optuna>=3.0.0 +matplotlib diff --git a/strategy/__init__.py b/strategy/__init__.py new file mode 100644 index 0000000..f9428e7 --- /dev/null +++ b/strategy/__init__.py @@ -0,0 +1 @@ +from .bb_backtest import BBConfig, BBResult, BBTrade, run_bb_backtest diff --git a/strategy/bb_backtest.py b/strategy/bb_backtest.py new file mode 100644 index 0000000..e4f6fe4 --- /dev/null +++ b/strategy/bb_backtest.py @@ -0,0 +1,315 @@ +"""Bollinger Band mean-reversion strategy backtest. + +Logic: + - Price touches upper BB → close any long, open short + - Price touches lower BB → close any short, open long + - Always in position (flip between long and short) + +Uses 5-minute OHLC data from the database. +""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import List, Optional + +import numpy as np +import pandas as pd + +from .data_loader import KlineSource, load_klines +from .indicators import bollinger + + +# --------------------------------------------------------------------------- +# Config & result types +# --------------------------------------------------------------------------- + +@dataclass +class BBConfig: + # Bollinger Band parameters + bb_period: int = 20 # SMA window + bb_std: float = 2.0 # standard deviation multiplier + + # Position sizing + margin_per_trade: float = 80.0 + leverage: float = 100.0 + initial_capital: float = 1000.0 + + # Risk management + max_daily_loss: float = 150.0 # stop trading after this daily loss + stop_loss_pct: float = 0.0 # 0 = disabled; e.g. 0.02 = 2% SL from entry + + # Dynamic sizing: if > 0, margin = equity * margin_pct (overrides margin_per_trade) + margin_pct: float = 0.0 # e.g. 0.01 = 1% of equity per trade + + # Fee structure (taker) + fee_rate: float = 0.0006 # 0.06% + rebate_rate: float = 0.0 # instant maker rebate (if any) + + # Delayed rebate: rebate_pct of daily fees returned next day at rebate_hour UTC + rebate_pct: float = 0.0 # e.g. 0.70 = 70% rebate + rebate_hour_utc: int = 0 # hour in UTC when rebate arrives (0 = 8am UTC+8) + + +@dataclass +class BBTrade: + side: str # "long" or "short" + entry_price: float + exit_price: float + entry_time: object # pd.Timestamp + exit_time: object + margin: float + leverage: float + qty: float + gross_pnl: float + fee: float + net_pnl: float + + +@dataclass +class BBResult: + equity_curve: pd.DataFrame # columns: equity, balance, price, position + trades: List[BBTrade] + daily_stats: pd.DataFrame # daily equity + pnl + total_fee: float + total_rebate: float + config: BBConfig + + +# --------------------------------------------------------------------------- +# Backtest engine +# --------------------------------------------------------------------------- + +def run_bb_backtest(df: pd.DataFrame, cfg: BBConfig) -> BBResult: + """Run Bollinger Band mean-reversion backtest on 5m OHLC data.""" + + close = df["close"].astype(float) + high = df["high"].astype(float) + low = df["low"].astype(float) + n = len(df) + + # Compute Bollinger Bands + bb_mid, bb_upper, bb_lower, bb_width = bollinger(close, cfg.bb_period, cfg.bb_std) + + # Convert to numpy for speed + arr_close = close.values + arr_high = high.values + arr_low = low.values + arr_upper = bb_upper.values + arr_lower = bb_lower.values + ts_index = df.index + + # State + balance = cfg.initial_capital + position = 0 # +1 = long, -1 = short, 0 = flat + entry_price = 0.0 + entry_time = None + entry_margin = 0.0 + entry_qty = 0.0 + + trades: List[BBTrade] = [] + total_fee = 0.0 + total_rebate = 0.0 + + # Daily tracking + day_pnl = 0.0 + day_stopped = False + current_day = None + + # Delayed rebate tracking + pending_rebate = 0.0 # fees from previous day to be rebated + today_fees = 0.0 # fees accumulated today + rebate_applied_today = False + + # Output arrays + out_equity = np.full(n, np.nan) + out_balance = np.full(n, np.nan) + out_position = np.zeros(n) + + def unrealised(price): + if position == 0: + return 0.0 + if position == 1: + return entry_qty * (price - entry_price) + else: + return entry_qty * (entry_price - price) + + def close_position(exit_price, exit_idx): + nonlocal balance, position, entry_price, entry_time, entry_margin, entry_qty + nonlocal total_fee, total_rebate, day_pnl, today_fees + + if position == 0: + return + + if position == 1: + gross = entry_qty * (exit_price - entry_price) + else: + gross = entry_qty * (entry_price - exit_price) + + exit_notional = entry_qty * exit_price + fee = exit_notional * cfg.fee_rate + rebate = exit_notional * cfg.rebate_rate # instant rebate only + net = gross - fee + rebate + + trades.append(BBTrade( + side="long" if position == 1 else "short", + entry_price=entry_price, + exit_price=exit_price, + entry_time=entry_time, + exit_time=ts_index[exit_idx], + margin=entry_margin, + leverage=cfg.leverage, + qty=entry_qty, + gross_pnl=gross, + fee=fee, + net_pnl=net, + )) + + balance += net + total_fee += fee + total_rebate += rebate + today_fees += fee + day_pnl += net + position = 0 + entry_price = 0.0 + entry_time = None + entry_margin = 0.0 + entry_qty = 0.0 + + def open_position(side, price, idx): + nonlocal position, entry_price, entry_time, entry_margin, entry_qty + nonlocal balance, total_fee, day_pnl, today_fees + + if cfg.margin_pct > 0: + equity = balance + unrealised(price) if position != 0 else balance + margin = equity * cfg.margin_pct + else: + margin = cfg.margin_per_trade + margin = min(margin, balance * 0.95) + if margin <= 0: + return + notional = margin * cfg.leverage + qty = notional / price + fee = notional * cfg.fee_rate + + balance -= fee + total_fee += fee + today_fees += fee + day_pnl -= fee + + position = 1 if side == "long" else -1 + entry_price = price + entry_time = ts_index[idx] + entry_margin = margin + entry_qty = qty + + # Main loop + for i in range(n): + # Daily reset + delayed rebate + bar_day = ts_index[i].date() if hasattr(ts_index[i], 'date') else None + bar_hour = ts_index[i].hour if hasattr(ts_index[i], 'hour') else 0 + if bar_day is not None and bar_day != current_day: + # New day: move today's fees to pending, reset + if cfg.rebate_pct > 0: + pending_rebate = today_fees * cfg.rebate_pct + today_fees = 0.0 + rebate_applied_today = False + day_pnl = 0.0 + day_stopped = False + current_day = bar_day + + # Apply delayed rebate at specified hour + if cfg.rebate_pct > 0 and not rebate_applied_today and bar_hour >= cfg.rebate_hour_utc and pending_rebate > 0: + balance += pending_rebate + total_rebate += pending_rebate + pending_rebate = 0.0 + rebate_applied_today = True + + # Skip if BB not ready + if np.isnan(arr_upper[i]) or np.isnan(arr_lower[i]): + out_equity[i] = balance + unrealised(arr_close[i]) + out_balance[i] = balance + out_position[i] = position + continue + + # Daily loss check + if day_stopped: + out_equity[i] = balance + unrealised(arr_close[i]) + out_balance[i] = balance + out_position[i] = position + continue + + cur_equity = balance + unrealised(arr_close[i]) + if day_pnl + unrealised(arr_close[i]) <= -cfg.max_daily_loss: + close_position(arr_close[i], i) + day_stopped = True + out_equity[i] = balance + out_balance[i] = balance + out_position[i] = 0 + continue + + # Stop loss check + if position != 0 and cfg.stop_loss_pct > 0: + if position == 1 and arr_low[i] <= entry_price * (1 - cfg.stop_loss_pct): + sl_price = entry_price * (1 - cfg.stop_loss_pct) + close_position(sl_price, i) + elif position == -1 and arr_high[i] >= entry_price * (1 + cfg.stop_loss_pct): + sl_price = entry_price * (1 + cfg.stop_loss_pct) + close_position(sl_price, i) + + # Signal detection: use high/low to check if price touched BB + touched_upper = arr_high[i] >= arr_upper[i] + touched_lower = arr_low[i] <= arr_lower[i] + + if touched_upper and touched_lower: + # Both touched in same bar (wide bar) — skip, too volatile + pass + elif touched_upper: + # Price touched upper BB → go short + if position == 1: + # Close long at upper BB price + close_position(arr_upper[i], i) + if position != -1: + # Open short + open_position("short", arr_upper[i], i) + elif touched_lower: + # Price touched lower BB → go long + if position == -1: + # Close short at lower BB price + close_position(arr_lower[i], i) + if position != 1: + # Open long + open_position("long", arr_lower[i], i) + + # Record equity + out_equity[i] = balance + unrealised(arr_close[i]) + out_balance[i] = balance + out_position[i] = position + + # Force close at end + if position != 0: + close_position(arr_close[n - 1], n - 1) + out_equity[n - 1] = balance + out_balance[n - 1] = balance + out_position[n - 1] = 0 + + # Build equity DataFrame + eq_df = pd.DataFrame({ + "equity": out_equity, + "balance": out_balance, + "price": arr_close, + "position": out_position, + }, index=ts_index) + + # Daily stats + daily_eq = eq_df["equity"].resample("1D").last().dropna().to_frame("equity") + daily_eq["pnl"] = daily_eq["equity"].diff().fillna(0.0) + + return BBResult( + equity_curve=eq_df, + trades=trades, + daily_stats=daily_eq, + total_fee=total_fee, + total_rebate=total_rebate, + config=cfg, + ) diff --git a/strategy/data_loader.py b/strategy/data_loader.py new file mode 100644 index 0000000..cd2827e --- /dev/null +++ b/strategy/data_loader.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +import pandas as pd +import sqlite3 + + +@dataclass(frozen=True) +class KlineSource: + db_path: Path + table_name: str + + +def _to_ms(dt: datetime) -> int: + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return int(dt.timestamp() * 1000) + + +def load_klines( + source: KlineSource, + start: datetime, + end: datetime, +) -> pd.DataFrame: + start_ms = _to_ms(start) + end_ms = _to_ms(end) + + con = sqlite3.connect(str(source.db_path)) + try: + df = pd.read_sql_query( + f"SELECT id, open, high, low, close FROM {source.table_name} WHERE id >= ? AND id <= ? ORDER BY id ASC", + con, + params=(start_ms, end_ms), + ) + finally: + con.close() + + if df.empty: + return df + + df["timestamp_ms"] = df["id"].astype("int64") + df["dt"] = pd.to_datetime(df["timestamp_ms"], unit="ms", utc=True) + df = df.drop(columns=["id"]).set_index("dt") + + for c in ("open", "high", "low", "close"): + df[c] = pd.to_numeric(df[c], errors="coerce") + + df = df.dropna(subset=["open", "high", "low", "close"]) + return df diff --git a/strategy/indicators.py b/strategy/indicators.py new file mode 100644 index 0000000..1d78690 --- /dev/null +++ b/strategy/indicators.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import numpy as np +import pandas as pd + + +def ema(s: pd.Series, span: int) -> pd.Series: + return s.ewm(span=span, adjust=False).mean() + + +def rsi(close: pd.Series, period: int) -> pd.Series: + delta = close.diff() + up = delta.clip(lower=0.0) + down = (-delta).clip(lower=0.0) + + roll_up = up.ewm(alpha=1 / period, adjust=False).mean() + roll_down = down.ewm(alpha=1 / period, adjust=False).mean() + + rs = roll_up / roll_down.replace(0.0, np.nan) + return 100.0 - (100.0 / (1.0 + rs)) + + +def atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int) -> pd.Series: + prev_close = close.shift(1) + tr = pd.concat( + [ + (high - low).abs(), + (high - prev_close).abs(), + (low - prev_close).abs(), + ], + axis=1, + ).max(axis=1) + return tr.ewm(alpha=1 / period, adjust=False).mean() + + +def bollinger(close: pd.Series, window: int, n_std: float): + mid = close.rolling(window=window, min_periods=window).mean() + std = close.rolling(window=window, min_periods=window).std(ddof=0) + upper = mid + n_std * std + lower = mid - n_std * std + width = (upper - lower) / mid + return mid, upper, lower, width + + +def macd(close: pd.Series, fast: int, slow: int, signal: int): + fast_ema = ema(close, fast) + slow_ema = ema(close, slow) + line = fast_ema - slow_ema + sig = ema(line, signal) + hist = line - sig + return line, sig, hist + + +def stochastic(high: pd.Series, low: pd.Series, close: pd.Series, + k_period: int = 14, d_period: int = 3): + """Stochastic Oscillator (%K and %D).""" + lowest = low.rolling(window=k_period, min_periods=k_period).min() + highest = high.rolling(window=k_period, min_periods=k_period).max() + denom = highest - lowest + k = 100.0 * (close - lowest) / denom.replace(0.0, np.nan) + d = k.rolling(window=d_period, min_periods=d_period).mean() + return k, d + + +def cci(high: pd.Series, low: pd.Series, close: pd.Series, + period: int = 20) -> pd.Series: + """Commodity Channel Index.""" + tp = (high + low + close) / 3.0 + sma = tp.rolling(window=period, min_periods=period).mean() + mad = tp.rolling(window=period, min_periods=period).apply( + lambda x: np.mean(np.abs(x - np.mean(x))), raw=True + ) + return (tp - sma) / (0.015 * mad.replace(0.0, np.nan)) + + +def adx(high: pd.Series, low: pd.Series, close: pd.Series, + period: int = 14) -> pd.Series: + """Average Directional Index (returns ADX line only).""" + up_move = high.diff() + down_move = -low.diff() + + plus_dm = pd.Series(np.where((up_move > down_move) & (up_move > 0), up_move, 0.0), + index=high.index) + minus_dm = pd.Series(np.where((down_move > up_move) & (down_move > 0), down_move, 0.0), + index=high.index) + + atr_val = atr(high, low, close, period) + + plus_di = 100.0 * plus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr_val.replace(0.0, np.nan) + minus_di = 100.0 * minus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr_val.replace(0.0, np.nan) + + dx = 100.0 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0.0, np.nan) + adx_line = dx.ewm(alpha=1 / period, adjust=False).mean() + return adx_line + + +def keltner_channel(high: pd.Series, low: pd.Series, close: pd.Series, + ema_period: int = 20, atr_period: int = 14, atr_mult: float = 1.5): + """Keltner Channel (mid, upper, lower).""" + mid = ema(close, ema_period) + atr_val = atr(high, low, close, atr_period) + upper = mid + atr_mult * atr_val + lower = mid - atr_mult * atr_val + return mid, upper, lower diff --git a/strategy/results/bb_200u_2025.png b/strategy/results/bb_200u_2025.png new file mode 100644 index 0000000..b150302 Binary files /dev/null and b/strategy/results/bb_200u_2025.png differ diff --git a/strategy/results/bb_200u_2026.png b/strategy/results/bb_200u_2026.png new file mode 100644 index 0000000..076e998 Binary files /dev/null and b/strategy/results/bb_200u_2026.png differ diff --git a/strategy/results/bb_2025_report.png b/strategy/results/bb_2025_report.png new file mode 100644 index 0000000..9ac89bb Binary files /dev/null and b/strategy/results/bb_2025_report.png differ diff --git a/strategy/results/bb_79rebate_report.png b/strategy/results/bb_79rebate_report.png new file mode 100644 index 0000000..8e5f562 Binary files /dev/null and b/strategy/results/bb_79rebate_report.png differ diff --git a/strategy/results/bb_strategy_report.png b/strategy/results/bb_strategy_report.png new file mode 100644 index 0000000..5ba45f6 Binary files /dev/null and b/strategy/results/bb_strategy_report.png differ diff --git a/strategy/run_bb_backtest.py b/strategy/run_bb_backtest.py new file mode 100644 index 0000000..ef4716c --- /dev/null +++ b/strategy/run_bb_backtest.py @@ -0,0 +1,199 @@ +"""Run Bollinger Band mean-reversion backtest on ETH 2023+2024. + +Preloads data once, then sweeps parameters in-memory for speed. +""" +import sys, time +sys.stdout.reconfigure(line_buffering=True) +sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parents[1])) + +import numpy as np +import pandas as pd +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from pathlib import Path +from collections import defaultdict + +from strategy.bb_backtest import BBConfig, run_bb_backtest +from strategy.data_loader import KlineSource, load_klines +from datetime import datetime, timezone + +root = Path(__file__).resolve().parents[1] +src = KlineSource(db_path=root / "models" / "database.db", table_name="bitmart_eth_5m") +out_dir = root / "strategy" / "results" +out_dir.mkdir(parents=True, exist_ok=True) + +t0 = time.time() + +# Preload data once +print("Loading data...") +df_23 = load_klines(src, datetime(2023,1,1,tzinfo=timezone.utc), + datetime(2023,12,31,23,59,tzinfo=timezone.utc)) +df_24 = load_klines(src, datetime(2024,1,1,tzinfo=timezone.utc), + datetime(2024,12,31,23,59,tzinfo=timezone.utc)) +data = {2023: df_23, 2024: df_24} +print(f"Loaded: 2023={len(df_23)} bars, 2024={len(df_24)} bars ({time.time()-t0:.1f}s)") + +# ================================================================ +# Sweep +# ================================================================ +print("\n" + "=" * 120) +print(" Bollinger Band Mean-Reversion — ETH 5min | 1000U capital") +print(" touch upper BB -> short, touch lower BB -> long (flip)") +print("=" * 120) + +results = [] + +def test(label, cfg): + """Run on both years, print summary, store results.""" + row = {"label": label, "cfg": cfg} + for year in [2023, 2024]: + r = run_bb_backtest(data[year], cfg) + d = r.daily_stats + pnl = d["pnl"].astype(float) + eq = d["equity"].astype(float) + dd = float((eq - eq.cummax()).min()) + final = float(eq.iloc[-1]) + nt = len(r.trades) + wr = sum(1 for t in r.trades if t.net_pnl > 0) / max(nt, 1) * 100 + nf = r.total_fee - r.total_rebate + row[f"a{year}"] = float(pnl.mean()) + row[f"d{year}"] = dd + row[f"r{year}"] = r + row[f"n{year}"] = nt + row[f"w{year}"] = wr + row[f"f{year}"] = nf + row[f"eq{year}"] = final + mn = min(row["a2023"], row["a2024"]) + avg = (row["a2023"] + row["a2024"]) / 2 + mark = " <<<" if mn >= 20 else (" **" if mn >= 10 else "") + print(f" {label:52s} 23:{row['a2023']:+6.1f} 24:{row['a2024']:+6.1f} " + f"avg:{avg:+5.1f} n23:{row['n2023']:3d} n24:{row['n2024']:3d} " + f"dd:{min(row['d2023'],row['d2024']):+7.0f}{mark}") + row["mn"] = mn; row["avg"] = avg + results.append(row) + +# [1] BB period +print("\n[1] Period sweep") +for p in [10, 15, 20, 30, 40]: + test(f"BB({p},2.0) 80u 100x", BBConfig(bb_period=p, bb_std=2.0, margin_per_trade=80, leverage=100)) + +# [2] BB std +print("\n[2] Std sweep") +for s in [1.5, 1.8, 2.0, 2.5, 3.0]: + test(f"BB(20,{s}) 80u 100x", BBConfig(bb_period=20, bb_std=s, margin_per_trade=80, leverage=100)) + +# [3] Margin +print("\n[3] Margin sweep") +for m in [40, 60, 80, 100, 120]: + test(f"BB(20,2.0) {m}u 100x", BBConfig(bb_period=20, bb_std=2.0, margin_per_trade=m, leverage=100)) + +# [4] SL +print("\n[4] Stop-loss sweep") +for sl in [0.0, 0.01, 0.02, 0.03, 0.05]: + test(f"BB(20,2.0) 80u SL={sl:.0%}", BBConfig(bb_period=20, bb_std=2.0, margin_per_trade=80, leverage=100, stop_loss_pct=sl)) + +# [5] MDL +print("\n[5] Max daily loss") +for mdl in [50, 100, 150, 200]: + test(f"BB(20,2.0) 80u mdl={mdl}", BBConfig(bb_period=20, bb_std=2.0, margin_per_trade=80, leverage=100, max_daily_loss=mdl)) + +# [6] Combined fine-tune +print("\n[6] Fine-tune") +for p in [15, 20, 30]: + for s in [1.5, 2.0, 2.5]: + for m in [80, 100]: + test(f"BB({p},{s}) {m}u mdl=150", + BBConfig(bb_period=p, bb_std=s, margin_per_trade=m, leverage=100, max_daily_loss=150)) + +# ================================================================ +# Ranking +# ================================================================ +results.sort(key=lambda x: x["mn"], reverse=True) +print(f"\n{'='*120}") +print(f" TOP 10 — ranked by min(daily_avg_2023, daily_avg_2024)") +print(f"{'='*120}") +for i, r in enumerate(results[:10]): + print(f" {i+1:2d}. {r['label']:50s} 23:{r['a2023']:+6.1f} 24:{r['a2024']:+6.1f} " + f"min:{r['mn']:+6.1f} dd:{min(r['d2023'],r['d2024']):+7.0f} " + f"wr23:{r['w2023']:.0f}% wr24:{r['w2024']:.0f}%") + +# ================================================================ +# Detailed report for best +# ================================================================ +best = results[0] +print(f"\n{'#'*70}") +print(f" BEST: {best['label']}") +print(f"{'#'*70}") + +for year in [2023, 2024]: + r = best[f"r{year}"] + cfg = best["cfg"] + d = r.daily_stats + pnl = d["pnl"].astype(float) + eq = d["equity"].astype(float) + dd = (eq - eq.cummax()).min() + final = float(eq.iloc[-1]) + nt = len(r.trades) + wr = sum(1 for t in r.trades if t.net_pnl > 0) / max(nt, 1) + nf = r.total_fee - r.total_rebate + + loss_streak = max_ls = 0 + for v in pnl.values: + if v < 0: loss_streak += 1; max_ls = max(max_ls, loss_streak) + else: loss_streak = 0 + + print(f"\n --- {year} ---") + print(f" Final equity : {final:,.2f} U ({final-cfg.initial_capital:+,.2f}, " + f"{(final-cfg.initial_capital)/cfg.initial_capital*100:+.1f}%)") + print(f" Max drawdown : {dd:,.2f} U") + print(f" Avg daily PnL : {pnl.mean():+,.2f} U") + print(f" Median daily PnL : {pnl.median():+,.2f} U") + print(f" Best/worst day : {pnl.max():+,.2f} / {pnl.min():+,.2f}") + print(f" Profitable days : {(pnl>0).sum()}/{len(pnl)} ({(pnl>0).mean():.1%})") + print(f" Days >= 20U : {(pnl>=20).sum()}") + print(f" Max loss streak : {max_ls} days") + print(f" Trades : {nt} (win rate {wr:.1%})") + print(f" Net fees : {nf:,.0f} U") + sharpe = pnl.mean() / max(pnl.std(), 1e-10) * np.sqrt(365) + print(f" Sharpe (annual) : {sharpe:.2f}") + +# ================================================================ +# Chart +# ================================================================ +fig, axes = plt.subplots(3, 2, figsize=(18, 12), + gridspec_kw={"height_ratios": [3, 1.5, 1]}) + +for col, year in enumerate([2023, 2024]): + r = best[f"r{year}"] + cfg = best["cfg"] + d = r.daily_stats + eq = d["equity"].astype(float) + pnl = d["pnl"].astype(float) + dd = eq - eq.cummax() + + axes[0, col].plot(eq.index, eq.values, linewidth=1.2, color="#1f77b4") + axes[0, col].axhline(cfg.initial_capital, color="gray", ls="--", lw=0.5) + axes[0, col].set_title(f"BB Strategy Equity — {year}\n" + f"BB({cfg.bb_period},{cfg.bb_std}) {cfg.margin_per_trade}u {cfg.leverage:.0f}x", + fontsize=11) + axes[0, col].set_ylabel("Equity (U)") + axes[0, col].grid(True, alpha=0.3) + + colors = ["#2ca02c" if v >= 0 else "#d62728" for v in pnl.values] + axes[1, col].bar(pnl.index, pnl.values, color=colors, width=0.8) + axes[1, col].axhline(20, color="orange", ls="--", lw=1, label="20U target") + axes[1, col].axhline(0, color="gray", lw=0.5) + axes[1, col].set_ylabel("Daily PnL (U)") + axes[1, col].legend(fontsize=8) + axes[1, col].grid(True, alpha=0.3) + + axes[2, col].fill_between(dd.index, dd.values, 0, color="#d62728", alpha=0.4) + axes[2, col].set_ylabel("Drawdown (U)") + axes[2, col].grid(True, alpha=0.3) + +fig.tight_layout() +fig.savefig(out_dir / "bb_strategy_report.png", dpi=150) +plt.close(fig) +print(f"\nChart: {out_dir / 'bb_strategy_report.png'}") +print(f"Total time: {time.time()-t0:.0f}s")