diff --git a/1111 b/1111 index 24c36e0..b087fb6 100644 --- a/1111 +++ b/1111 @@ -10,7 +10,8 @@ websea 账号:yangyicheng6666@gmail.com 密码:Wg138333. - +yx20250715@gmail.com +Abc12345678 3. 更多交易信号 包住形态(原策略) diff --git a/bitmart_strategy.log b/bitmart_strategy.log new file mode 100644 index 0000000..35cc443 --- /dev/null +++ b/bitmart_strategy.log @@ -0,0 +1,21 @@ +2025-12-20 02:51:09.247 | WARNING | __main__:__init__:28 - 请确认Memo是否正确!默认Memo可能导致签名失败 +2025-12-20 02:51:09.252 | INFO | __main__:init_api_client:57 - 尝试初始化API客户端 (尝试 1/3) +2025-12-20 02:51:09.253 | ERROR | __main__:test_api_connection:112 - API连接测试失败: APIContract.get_depth() got an unexpected keyword argument 'limit' +2025-12-20 02:51:09.254 | SUCCESS | __main__:init_api_client:87 - API客户端初始化成功 +2025-12-20 02:51:09.254 | INFO | __main__:action:220 - 启动BitMart合约策略... +2025-12-20 02:51:09.254 | INFO | __main__:set_leverage_with_retry:121 - 尝试设置杠杆 (尝试 1/3) +2025-12-20 02:51:09.814 | ERROR | __main__:set_leverage_with_retry:149 - API异常: APIException(http status=401): response={"code":30005,"msg":"Header X-BM-SIGN is wrong","trace":"d9c107e3-7564-474e-9eaf-59667f869ae5"} + +2025-12-20 02:51:09.814 | INFO | __main__:set_leverage_with_retry:161 - 等待1秒后重试... +2025-12-20 02:51:10.815 | INFO | __main__:set_leverage_with_retry:121 - 尝试设置杠杆 (尝试 2/3) +2025-12-20 02:51:10.923 | ERROR | __main__:set_leverage_with_retry:149 - API异常: APIException(http status=401): response={"trace":"a7fa76a2-2e34-435b-acbe-90e37b47fe48","code":30005,"msg":"Header X-BM-SIGN is wrong"} + +2025-12-20 02:51:10.923 | INFO | __main__:set_leverage_with_retry:161 - 等待2秒后重试... +2025-12-20 02:51:12.923 | INFO | __main__:set_leverage_with_retry:121 - 尝试设置杠杆 (尝试 3/3) +2025-12-20 02:51:13.029 | ERROR | __main__:set_leverage_with_retry:149 - API异常: APIException(http status=401): response={"code":30005,"msg":"Header X-BM-SIGN is wrong","trace":"97b6c791-5812-4285-a659-dbba3f6170e6"} + +2025-12-20 02:51:13.029 | WARNING | __main__:set_leverage_with_retry:168 - 杠杆设置失败,但程序将继续运行 +2025-12-20 02:51:13.030 | WARNING | __main__:action:226 - 杠杆设置失败,使用系统默认杠杆继续运行 +2025-12-20 02:51:13.150 | SUCCESS | __main__:action:254 - 获取到新K线: 2025-12-20 02:30:00 +2025-12-20 02:51:13.150 | INFO | __main__:action:259 - 当前价格: 2959.49 +2025-12-20 02:51:13.311 | INFO | __main__:action:268 - 用户中断程序 diff --git a/bitmart_trading.log b/bitmart_trading.log new file mode 100644 index 0000000..4af609d --- /dev/null +++ b/bitmart_trading.log @@ -0,0 +1,46 @@ +2025-12-20 03:08:42.274 | INFO | __main__:initialize_api:66 - 尝试API配置 1/3 +2025-12-20 03:08:42.275 | INFO | __main__:initialize_api:67 - Memo: '' +2025-12-20 03:08:42.275 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:08:42.276 | WARNING | __main__:initialize_api:84 - API配置 1 连接失败,尝试下一个配置 +2025-12-20 03:08:42.276 | INFO | __main__:initialize_api:66 - 尝试API配置 2/3 +2025-12-20 03:08:42.276 | INFO | __main__:initialize_api:67 - Memo: '合约交易' +2025-12-20 03:08:42.276 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:08:42.276 | WARNING | __main__:initialize_api:84 - API配置 2 连接失败,尝试下一个配置 +2025-12-20 03:08:42.276 | INFO | __main__:initialize_api:66 - 尝试API配置 3/3 +2025-12-20 03:08:42.277 | INFO | __main__:initialize_api:67 - Memo: 'None' +2025-12-20 03:08:42.277 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:08:42.277 | WARNING | __main__:initialize_api:84 - API配置 3 连接失败,尝试下一个配置 +2025-12-20 03:08:42.277 | ERROR | __main__:initialize_api:89 - 所有API配置都失败 +2025-12-20 03:08:42.281 | ERROR | __main__:action:408 - API初始化失败,程序无法运行 +2025-12-20 03:08:48.311 | INFO | __main__:initialize_api:66 - 尝试API配置 1/3 +2025-12-20 03:08:48.312 | INFO | __main__:initialize_api:67 - Memo: '' +2025-12-20 03:08:48.313 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:08:48.313 | WARNING | __main__:initialize_api:84 - API配置 1 连接失败,尝试下一个配置 +2025-12-20 03:08:48.313 | INFO | __main__:initialize_api:66 - 尝试API配置 2/3 +2025-12-20 03:08:48.313 | INFO | __main__:initialize_api:67 - Memo: '合约交易' +2025-12-20 03:08:48.313 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:08:48.313 | WARNING | __main__:initialize_api:84 - API配置 2 连接失败,尝试下一个配置 +2025-12-20 03:08:48.313 | INFO | __main__:initialize_api:66 - 尝试API配置 3/3 +2025-12-20 03:08:48.313 | INFO | __main__:initialize_api:67 - Memo: 'None' +2025-12-20 03:08:48.313 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:08:48.313 | WARNING | __main__:initialize_api:84 - API配置 3 连接失败,尝试下一个配置 +2025-12-20 03:08:48.314 | ERROR | __main__:initialize_api:89 - 所有API配置都失败 +2025-12-20 03:08:48.317 | ERROR | __main__:action:408 - API初始化失败,程序无法运行 +2025-12-20 03:09:25.953 | INFO | __main__:initialize_api:66 - 尝试API配置 1/3 +2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:67 - Memo: '' +2025-12-20 03:09:25.954 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:09:25.954 | WARNING | __main__:initialize_api:84 - API配置 1 连接失败,尝试下一个配置 +2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:66 - 尝试API配置 2/3 +2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:67 - Memo: '合约交易' +2025-12-20 03:09:25.954 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:09:25.954 | WARNING | __main__:initialize_api:84 - API配置 2 连接失败,尝试下一个配置 +2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:66 - 尝试API配置 3/3 +2025-12-20 03:09:25.954 | INFO | __main__:initialize_api:67 - Memo: 'None' +2025-12-20 03:09:25.955 | ERROR | __main__:test_api_connection:108 - API连接测试失败: 'APIContract' object has no attribute 'get_contracts' +2025-12-20 03:09:25.955 | WARNING | __main__:initialize_api:84 - API配置 3 连接失败,尝试下一个配置 +2025-12-20 03:09:25.955 | ERROR | __main__:initialize_api:89 - 所有API配置都失败 +2025-12-20 03:09:25.958 | ERROR | __main__:action:408 - API初始化失败,程序无法运行 +2025-12-20 03:12:23.884 | INFO | __main__:action:396 - 启动BitMart合约交易策略... +2025-12-20 03:12:23.885 | INFO | __main__:test_api_connection:48 - 测试API连接... +2025-12-20 03:12:24.359 | ERROR | __main__:test_api_connection:90 - API连接测试异常: 'tuple' object has no attribute 'get' +2025-12-20 03:12:24.359 | ERROR | __main__:action:400 - API连接测试失败 diff --git a/telegram/8619211027341.session b/telegram/8619211027341.session index e9c22ab..1002c3b 100644 Binary files a/telegram/8619211027341.session and b/telegram/8619211027341.session differ diff --git a/telegram/bot_session.session b/telegram/bot_session.session index c81d8a7..4071167 100644 Binary files a/telegram/bot_session.session and b/telegram/bot_session.session differ diff --git a/telegram/sign.db b/telegram/sign.db index ea9e1e6..2a46051 100644 Binary files a/telegram/sign.db and b/telegram/sign.db differ diff --git a/test.py b/test.py index 2874b8b..3cf3f2b 100644 --- a/test.py +++ b/test.py @@ -1,6 +1,8 @@ +import os import time import uuid import datetime +from dataclasses import dataclass from tqdm import tqdm from loguru import logger @@ -11,290 +13,518 @@ from bitmart.lib.cloud_exceptions import APIException from 交易.tools import send_dingtalk_message -class BitmartMarketMaker: - def __init__(self): +@dataclass +class StrategyConfig: + # ===== 合约 ===== + contract_symbol: str = "ETHUSDT" + open_type: str = "cross" + leverage: str = "2" # 1~2 更稳 + + # ===== K线与指标 ===== + step_min: int = 1 + lookback_min: int = 240 + ema_len: int = 30 + atr_len: int = 14 + + # ===== 动态阈值(关键:自适应行情)===== + # entry_dev / tp / sl 都由 ATR/Price 动态计算: max(下限, 系数 * atr_ratio) + entry_dev_floor: float = 0.0010 # 0.10% 最小偏离阈值(保证平静行情也能出单) + tp_floor: float = 0.0005 # 0.05% 最小止盈 + sl_floor: float = 0.0015 # 0.15% 最小止损 + + entry_k: float = 1.20 # entry_dev = max(floor, entry_k * atr_ratio) + tp_k: float = 0.60 # tp = max(floor, tp_k * atr_ratio) + sl_k: float = 1.20 # sl = max(floor, sl_k * atr_ratio) + + max_hold_sec: int = 120 # 2分钟超时退出 + cooldown_sec_after_exit: int = 10 # 平仓后冷却10秒,防抖 + + # ===== 下单/仓位 ===== + risk_percent: float = 0.0015 # 每次用可用余额的0.15%作为保证金预算 + min_size: int = 1 + max_size: int = 5000 + + # ===== 日内风控 ===== + daily_loss_limit: float = 0.02 # -2% 停机 + daily_profit_cap: float = 0.01 # +1% 封顶停机 + + # ===== 危险模式过滤(避免趋势/插针)===== + atr_ratio_kill: float = 0.0045 # ATR/Price > 0.45% -> 危险模式,暂停开仓 + big_body_kill: float = 0.012 # 1m实体>1.2% -> 危险模式 + + # ===== 轮询节奏(减少REST压力)===== + klines_refresh_sec: int = 10 + tick_refresh_sec: int = 1 + status_notify_sec: int = 60 + + +class BitmartFuturesMeanReversionBot: + def __init__(self, cfg: StrategyConfig): + self.cfg = cfg + + # ✅ 强制只从环境变量读 self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8" self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5" self.memo = "合约交易" - self.contract_symbol = "ETHUSDT" + if not self.api_key or not self.secret_key: + raise RuntimeError("请先设置环境变量 BITMART_API_KEY / BITMART_SECRET_KEY / BITMART_MEMO(可选)") self.contractAPI = APIContract(self.api_key, self.secret_key, self.memo, timeout=(5, 15)) - self.start = 0 # 持仓状态: -1 空, 0 无, 1 多 - self.open_avg_price = None # 开仓均价(字符串或float) - self.current_amount = 0 - self.position_cross = None + # 持仓状态: -1 空, 0 无, 1 多 + self.pos = 0 + self.entry_price = None + self.entry_ts = None + self.last_exit_ts = 0 - self.pbar = tqdm(total=10, desc="等待下次检查", ncols=80) + # 日内权益基准 + self.day_start_equity = None + self.trading_enabled = True + self.day_tag = datetime.date.today() - self.leverage = "10" # 低杠杆,安全做市 - self.open_type = "cross" - self.fixed_size = 1 # 每次挂单量 - self.spread_offset = 5.0 # 挂单偏移(USDT),建议5~10,避免立即成交 - self.max_position_threshold = 10 # 库存阈值,超过自动平仓 + # 缓存 + self._klines_cache = None + self._klines_cache_ts = 0 - # 新增:止盈止损参数(基于开仓均价的百分比) - self.take_profit_pct = 2.0 # 止盈 2% - self.stop_loss_pct = 1.0 # 止损 1%(可根据风险偏好调整) + self._last_status_notify_ts = 0 - self.price_precision = 0.1 - self.leverage_set = False - self.max_retries = 5 + self.pbar = tqdm(total=60, desc="运行中(秒)", ncols=90) + # ----------------- 通用工具 ----------------- def ding(self, msg, error=False): - prefix = "❌bitmart MM:" if error else "🔔bitmart MM:" + prefix = "❌bitmart:" if error else "🔔bitmart:" if error: - for i in range(10): - send_dingtalk_message(f"{prefix},{msg}") + for _ in range(3): + send_dingtalk_message(f"{prefix}{msg}") else: - send_dingtalk_message(f"{prefix},{msg}") + send_dingtalk_message(f"{prefix}{msg}") - def try_set_leverage(self): - if self.leverage_set: + def set_leverage(self) -> bool: + try: + resp = self.contractAPI.post_submit_leverage( + contract_symbol=self.cfg.contract_symbol, + leverage=self.cfg.leverage, + open_type=self.cfg.open_type + )[0] + if resp.get("code") == 1000: + logger.success(f"设置杠杆成功:{self.cfg.open_type} + {self.cfg.leverage}x") + return True + logger.error(f"设置杠杆失败: {resp}") + self.ding(f"设置杠杆失败: {resp}", error=True) + return False + except Exception as e: + logger.error(f"设置杠杆异常: {e}") + self.ding(f"设置杠杆异常: {e}", error=True) + return False + + # ----------------- 行情/指标 ----------------- + def get_klines_cached(self): + now = time.time() + if self._klines_cache is not None and (now - self._klines_cache_ts) < self.cfg.klines_refresh_sec: + return self._klines_cache + + kl = self.get_klines() + if kl: + self._klines_cache = kl + self._klines_cache_ts = now + return self._klines_cache + + def get_klines(self): + try: + end_time = int(time.time()) + start_time = end_time - 60 * self.cfg.lookback_min + + resp = self.contractAPI.get_kline( + contract_symbol=self.cfg.contract_symbol, + step=self.cfg.step_min, + start_time=start_time, + end_time=end_time + )[0] + + if resp.get("code") != 1000: + logger.error(f"获取K线失败: {resp}") + return None + + data = resp.get("data", []) + formatted = [] + for k in data: + formatted.append({ + "id": int(k["timestamp"]), + "open": float(k["open_price"]), + "high": float(k["high_price"]), + "low": float(k["low_price"]), + "close": float(k["close_price"]), + }) + formatted.sort(key=lambda x: x["id"]) + return formatted + except Exception as e: + logger.error(f"获取K线异常: {e}") + self.ding(f"获取K线异常: {e}", error=True) + return None + + def get_last_price(self, fallback_close: float) -> float: + """ + 优先取更实时的最新价;若SDK不支持/字段不同,回退到K线close。 + """ + try: + if hasattr(self.contractAPI, "get_contract_details"): + r = self.contractAPI.get_contract_details(contract_symbol=self.cfg.contract_symbol)[0] + d = r.get("data") if isinstance(r, dict) else None + if isinstance(d, dict): + for key in ("last_price", "mark_price", "index_price"): + if key in d and d[key] is not None: + return float(d[key]) + + if hasattr(self.contractAPI, "get_ticker"): + r = self.contractAPI.get_ticker(contract_symbol=self.cfg.contract_symbol)[0] + d = r.get("data") if isinstance(r, dict) else None + if isinstance(d, dict): + for key in ("last_price", "price", "last", "close"): + if key in d and d[key] is not None: + return float(d[key]) + except Exception: + pass + + return float(fallback_close) + + @staticmethod + def ema(values, n: int) -> float: + k = 2 / (n + 1) + e = values[0] + for v in values[1:]: + e = v * k + e * (1 - k) + return e + + @staticmethod + def atr(klines, n: int) -> float: + if len(klines) < n + 1: + return 0.0 + trs = [] + for i in range(-n, 0): + cur = klines[i] + prev = klines[i - 1] + tr = max( + cur["high"] - cur["low"], + abs(cur["high"] - prev["close"]), + abs(cur["low"] - prev["close"]), + ) + trs.append(tr) + return sum(trs) / len(trs) + + def is_danger_market(self, klines, price: float) -> bool: + last = klines[-1] + body = abs(last["close"] - last["open"]) / last["open"] if last["open"] else 0.0 + if body >= self.cfg.big_body_kill: return True - for attempt in range(self.max_retries): - self.cancel_all_orders() - time.sleep(2) - - try: - response = self.contractAPI.post_submit_leverage( - contract_symbol=self.contract_symbol, - leverage=self.leverage, - open_type=self.open_type - )[0] - if response['code'] == 1000: - logger.success(f"全仓模式 + {self.leverage}x 杠杆设置成功") - self.leverage_set = True - return True - else: - logger.error(f"杠杆设置失败 (尝试 {attempt+1}): {response}") - except Exception as e: - logger.error(f"设置杠杆异常 (尝试 {attempt+1}): {e}") - - time.sleep(10) - - self.ding(error=True, msg="杠杆设置多次失败,请手动检查") - return False - - def get_depth(self): - try: - response = self.contractAPI.get_depth(contract_symbol=self.contract_symbol)[0] - if response['code'] == 1000: - data = response['data'] - best_bid = float(data['bids'][0][0]) if data['bids'] else None - best_ask = float(data['asks'][0][0]) if data['asks'] else None - return best_bid, best_ask - else: - logger.error(f"获取深度失败: {response}") - return None, None - except Exception as e: - logger.error(f"获取深度异常: {e}") - return None, None - - def cancel_all_orders(self): - try: - response = self.contractAPI.post_cancel_orders(contract_symbol=self.contract_symbol)[0] - if response['code'] == 1000: - logger.success("所有挂单已取消") - return True - else: - logger.error(f"取消挂单失败: {response}") - return False - except Exception as e: - logger.warning(f"取消挂单异常(忽略): {e}") - return False - - def place_limit_order(self, side: int, price: float, size: int): - price = round(price / self.price_precision) * self.price_precision - price_str = f"{price:.1f}" - - client_order_id = f"mm_{int(time.time())}_{uuid.uuid4().hex[:8]}" - - for attempt in range(self.max_retries): - try: - response = self.contractAPI.post_submit_order( - contract_symbol=self.contract_symbol, - client_order_id=client_order_id, - side=side, - mode=1, - type='limit', - leverage=self.leverage, - open_type=self.open_type, - price=price_str, - size=size - )[0] - - if response['code'] == 1000: - action = "挂买(开多)" if side == 1 else "挂卖(开空)" - logger.success(f"限价单挂单成功: {action}, 价格={price}, 张数={size}") - return True - else: - logger.error(f"挂单失败 (尝试 {attempt+1}): {response}") - except APIException as e: - logger.error(f"API挂单异常 (尝试 {attempt+1}): {e}") - - time.sleep(5) + a = self.atr(klines, self.cfg.atr_len) + atr_ratio = (a / price) if price > 0 else 0.0 + if atr_ratio >= self.cfg.atr_ratio_kill: + return True return False - def get_position_status(self): - try: - response = self.contractAPI.get_position(contract_symbol=self.contract_symbol)[0] - if response['code'] == 1000: - positions = response['data'] - if not positions: - self.start = 0 - self.current_amount = 0 - self.open_avg_price = None - return True - position = positions[0] - self.start = 1 if position['position_type'] == 1 else -1 - self.open_avg_price = float(position['open_avg_price']) if position['open_avg_price'] else 0.0 - self.current_amount = int(float(position.get('current_amount', 0))) - self.position_cross = position.get("position_cross") - return True - else: - return False - except Exception as e: - logger.error(f"持仓查询异常: {e}") - self.ding(error=True, msg="持仓查询异常") - return False + def dynamic_thresholds(self, atr_ratio: float): + """ + 返回动态 entry_dev / tp / sl + """ + entry_dev = max(self.cfg.entry_dev_floor, self.cfg.entry_k * atr_ratio) + tp = max(self.cfg.tp_floor, self.cfg.tp_k * atr_ratio) + sl = max(self.cfg.sl_floor, self.cfg.sl_k * atr_ratio) + return entry_dev, tp, sl - def get_available_balance(self): + # ----------------- 账户/仓位 ----------------- + def get_assets_available(self) -> float: 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)) + resp = self.contractAPI.get_assets_detail()[0] + if resp.get("code") != 1000: + return 0.0 + data = resp.get("data") + if isinstance(data, dict): + return float(data.get("available_balance", 0)) + if isinstance(data, list): + for asset in data: + if asset.get("currency") == "USDT": + return float(asset.get("available_balance", 0)) return 0.0 except Exception as e: logger.error(f"余额查询异常: {e}") return 0.0 - def check_take_profit_stop_loss(self, current_price: float): - """检查是否触发止盈或止损(基于当前价格)""" - if self.current_amount == 0 or self.open_avg_price == 0.0: - return False, None - - if self.start == 1: # 多头 - pnl_pct = (current_price - self.open_avg_price) / self.open_avg_price * 100 - if pnl_pct >= self.take_profit_pct: - return True, "止盈平多" - if pnl_pct <= -self.stop_loss_pct: - return True, "止损平多" - - elif self.start == -1: # 空头 - pnl_pct = (self.open_avg_price - current_price) / self.open_avg_price * 100 - if pnl_pct >= self.take_profit_pct: - return True, "止盈平空" - if pnl_pct <= -self.stop_loss_pct: - return True, "止损平空" - - return False, None - - def close_position(self, reason: str = "库存阈值"): - """市场全平仓,并发送通知""" - if self.current_amount == 0: - return - - side = 3 if self.start == 1 else 2 # 3: 平多, 2: 平空 + def get_position_status(self) -> bool: try: - response = self.contractAPI.post_submit_order( - contract_symbol=self.contract_symbol, - client_order_id=f"close_{reason}_{int(time.time())}", - side=side, - mode=1, - type='market', - leverage=self.leverage, - open_type=self.open_type, - size=999999 - )[0] - if response['code'] == 1000: - direction_str = "多" if self.start == 1 else "空" - logger.success(f"{reason}平仓成功: 平{direction_str}") - self.ding(msg=f"{reason}触发,已平仓 {direction_str}头仓位") - self.start = 0 - self.current_amount = 0 - self.open_avg_price = None - return True - else: - logger.error(f"平仓失败: {response}") + resp = self.contractAPI.get_position(contract_symbol=self.cfg.contract_symbol)[0] + if resp.get("code") != 1000: return False - except APIException as e: - logger.error(f"API平仓异常: {e}") + + positions = resp.get("data", []) + if not positions: + self.pos = 0 + return True + + p = positions[0] + self.pos = 1 if p["position_type"] == 1 else -1 + return True + except Exception as e: + logger.error(f"持仓查询异常: {e}") + self.ding(f"持仓查询异常: {e}", error=True) return False + def get_equity_proxy(self) -> float: + return self.get_assets_available() + + def refresh_daily_baseline(self): + today = datetime.date.today() + if today != self.day_tag: + self.day_tag = today + self.day_start_equity = None + self.trading_enabled = True + self.ding(f"新的一天({today}):重置日内风控基准") + + def risk_kill_switch(self): + self.refresh_daily_baseline() + equity = self.get_equity_proxy() + if equity <= 0: + return + + if self.day_start_equity is None: + self.day_start_equity = equity + logger.info(f"日内权益基准设定:{equity:.2f} USDT") + return + + pnl = (equity - self.day_start_equity) / self.day_start_equity + if pnl <= -self.cfg.daily_loss_limit: + self.trading_enabled = False + self.ding(f"触发日止损:{pnl * 100:.2f}% -> 停机", error=True) + + if pnl >= self.cfg.daily_profit_cap: + self.trading_enabled = False + self.ding(f"达到日盈利封顶:{pnl * 100:.2f}% -> 停机") + + # ----------------- 下单 ----------------- + def calculate_size(self, price: float) -> int: + """ + 保守仓位估算:按 1张≈0.001ETH(你原假设) + """ + bal = self.get_assets_available() + if bal < 10: + return 0 + + margin = bal * self.cfg.risk_percent + lev = int(self.cfg.leverage) + + size = int((margin * lev) / (price * 0.001)) + size = max(self.cfg.min_size, size) + size = min(self.cfg.max_size, size) + return size + + def place_market_order(self, side: int, size: int) -> bool: + """ + side: + 1 开多 + 2 平空 + 3 平多 + 4 开空 + """ + if size <= 0: + return False + + client_order_id = f"mr_{int(time.time())}_{uuid.uuid4().hex[:8]}" + try: + resp = self.contractAPI.post_submit_order( + contract_symbol=self.cfg.contract_symbol, + client_order_id=client_order_id, + side=side, + mode=1, + type="market", + leverage=self.cfg.leverage, + open_type=self.cfg.open_type, + size=size + )[0] + + logger.info(f"order_resp: {resp}") + + if resp.get("code") == 1000: + return True + + self.ding(f"下单失败: {resp}", error=True) + return False + + except APIException as e: + logger.error(f"API下单异常: {e}") + self.ding(f"API下单异常: {e}", error=True) + return False + + except Exception as e: + logger.error(f"下单未知异常: {e}") + self.ding(f"下单未知异常: {e}", error=True) + return False + + def close_position_all(self): + if self.pos == 1: + ok = self.place_market_order(3, 999999) + if ok: + self.pos = 0 + elif self.pos == -1: + ok = self.place_market_order(2, 999999) + if ok: + self.pos = 0 + + # ----------------- 策略主逻辑 ----------------- + def in_cooldown(self) -> bool: + return (time.time() - self.last_exit_ts) < self.cfg.cooldown_sec_after_exit + + def maybe_enter(self, price: float, ema_value: float, entry_dev: float): + if self.pos != 0: + return + if self.in_cooldown(): + return + + dev = (price - ema_value) / ema_value if ema_value else 0.0 + size = self.calculate_size(price) + + logger.info( + f"enter_check: price={price:.2f}, ema={ema_value:.2f}, dev={dev*100:.3f}% " + f"(阈值={entry_dev*100:.3f}%), size={size}, pos={self.pos}" + ) + + if size <= 0: + return + + if dev <= -entry_dev: + if self.place_market_order(1, size): # 开多 + self.pos = 1 + self.entry_price = price + self.entry_ts = time.time() + self.ding(f"✅开多:dev={dev*100:.3f}% size={size} entry={price:.2f}") + + elif dev >= entry_dev: + if self.place_market_order(4, size): # 开空 + self.pos = -1 + self.entry_price = price + self.entry_ts = time.time() + self.ding(f"✅开空:dev={dev*100:.3f}% size={size} entry={price:.2f}") + + def maybe_exit(self, price: float, tp: float, sl: float): + if self.pos == 0 or self.entry_price is None or self.entry_ts is None: + return + + hold = time.time() - self.entry_ts + + if self.pos == 1: + pnl = (price - self.entry_price) / self.entry_price + else: + pnl = (self.entry_price - price) / self.entry_price + + if pnl >= tp: + self.close_position_all() + self.ding(f"🎯止盈:pnl={pnl*100:.3f}% price={price:.2f} tp={tp*100:.3f}%") + self.entry_price, self.entry_ts = None, None + self.last_exit_ts = time.time() + + elif pnl <= -sl: + self.close_position_all() + self.ding(f"🛑止损:pnl={pnl*100:.3f}% price={price:.2f} sl={sl*100:.3f}%", error=True) + self.entry_price, self.entry_ts = None, None + self.last_exit_ts = time.time() + + elif hold >= self.cfg.max_hold_sec: + self.close_position_all() + self.ding(f"⏱超时:hold={int(hold)}s pnl={pnl*100:.3f}% price={price:.2f}") + self.entry_price, self.entry_ts = None, None + self.last_exit_ts = time.time() + + def notify_status_throttled(self, price: float, ema_value: float, dev: float, bal: float, + atr_ratio: float, entry_dev: float, tp: float, sl: float): + now = time.time() + if (now - self._last_status_notify_ts) < self.cfg.status_notify_sec: + return + self._last_status_notify_ts = now + + direction_str = "多" if self.pos == 1 else ("空" if self.pos == -1 else "无") + msg = ( + f"【BitMart {self.cfg.contract_symbol}|均价回归微利(动态阈值)】\n" + f"方向:{direction_str}\n" + f"现价:{price:.2f}\n" + f"EMA{self.cfg.ema_len}:{ema_value:.2f}\n" + f"dev:{dev*100:.3f}%(阈值{entry_dev*100:.3f}%)\n" + f"ATR比:{atr_ratio*100:.3f}%\n" + f"tp/sl:{tp*100:.3f}% / {sl*100:.3f}%\n" + f"可用余额:{bal:.2f} USDT 杠杆:{self.cfg.leverage}x\n" + f"超时:{self.cfg.max_hold_sec}s 冷却:{self.cfg.cooldown_sec_after_exit}s" + ) + self.ding(msg) + def action(self): - logger.info("程序启动,将在循环中动态尝试设置杠杆") + if not self.set_leverage(): + self.ding("杠杆设置失败,停止运行", error=True) + return while True: - if not self.try_set_leverage(): - logger.warning("杠杆未设置成功,继续重试...") + now_dt = datetime.datetime.now() + self.pbar.n = now_dt.second + self.pbar.refresh() + klines = self.get_klines_cached() + if not klines or len(klines) < (self.cfg.ema_len + 5): + time.sleep(1) + continue + + last_k = klines[-1] + closes = [k["close"] for k in klines[-(self.cfg.ema_len + 1):]] + ema_value = self.ema(closes, self.cfg.ema_len) + + # 用更实时的价格触发(取不到就回退K线close) + price = self.get_last_price(fallback_close=float(last_k["close"])) + dev = (price - ema_value) / ema_value if ema_value else 0.0 + + # 动态阈值:跟随 ATR/Price + a = self.atr(klines, self.cfg.atr_len) + atr_ratio = (a / price) if price > 0 else 0.0 + entry_dev, tp, sl = self.dynamic_thresholds(atr_ratio) + + # 日内风控 + self.risk_kill_switch() + + # 刷新仓位 if not self.get_position_status(): - self.ding(error=True, msg="获取仓位信息失败!!!") - time.sleep(10) + time.sleep(1) continue - # 获取当前价格(使用中价) - best_bid, best_ask = self.get_depth() - if best_bid is None or best_ask is None: - time.sleep(10) + # 停机:平仓+不再开仓 + if not self.trading_enabled: + if self.pos != 0: + self.close_position_all() + time.sleep(5) continue - mid_price = (best_bid + best_ask) / 2 - # 检查止盈止损 - trigger, reason = self.check_take_profit_stop_loss(mid_price) - if trigger: - self.close_position(reason=reason) - # 平仓后继续下一轮(重新挂单) + # 危险市场:不新开仓(允许已有仓按tp/sl/超时退出) + if self.is_danger_market(klines, price): + logger.warning("危险模式:高波动/大实体K,暂停开仓") + self.maybe_exit(price, tp, sl) + time.sleep(self.cfg.tick_refresh_sec) + continue - # 检查库存阈值 - if abs(self.current_amount) > self.max_position_threshold: - self.close_position(reason="库存阈值") + # 先出场再入场 + self.maybe_exit(price, tp, sl) + self.maybe_enter(price, ema_value, entry_dev) - # 挂单 - bid_price = mid_price - self.spread_offset - ask_price = mid_price + self.spread_offset + # 状态通知(限频) + bal = self.get_assets_available() + self.notify_status_throttled(price, ema_value, dev, bal, atr_ratio, entry_dev, tp, sl) - self.cancel_all_orders() - - success_bid = self.place_limit_order(side=1, price=bid_price, size=self.fixed_size) - success_ask = self.place_limit_order(side=4, price=ask_price, size=self.fixed_size) - - # 统计盈亏百分比 - pnl_pct_str = "" - if self.current_amount != 0 and self.open_avg_price: - if self.start == 1: - pnl_pct = (mid_price - self.open_avg_price) / self.open_avg_price * 100 - else: - pnl_pct = (self.open_avg_price - mid_price) / self.open_avg_price * 100 - pnl_pct_str = f"浮动盈亏:{pnl_pct:+.2f}%" - - balance = self.get_available_balance() - leverage_status = "已设置" if self.leverage_set else "未同步(重试中)" - msg = ( - f"【BitMart {self.contract_symbol} MM】\n" - f"杠杆状态:{leverage_status}\n" - f"当前中价:{mid_price:.2f} USDT\n" - f"挂买价:{bid_price:.2f} ({'成功' if success_bid else '失败'})\n" - f"挂卖价:{ask_price:.2f} ({'成功' if success_ask else '失败'})\n" - f"持仓量:{self.current_amount} 张 {pnl_pct_str}\n" - f"账户可用余额:{balance:.2f} USDT\n" - f"止盈:+{self.take_profit_pct}% | 止损:-{self.stop_loss_pct}%" - ) - self.ding(msg=msg) - - self.pbar.reset() - time.sleep(10) + time.sleep(self.cfg.tick_refresh_sec) -if __name__ == '__main__': - BitmartMarketMaker().action() \ No newline at end of file +if __name__ == "__main__": + """ + Windows 设置环境变量示例(PowerShell): + setx BITMART_API_KEY "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8" + setx BITMART_SECRET_KEY "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5" + setx BITMART_MEMO "合约交易" + 重新打开终端再运行。 + """ + cfg = StrategyConfig() + bot = BitmartFuturesMeanReversionBot(cfg) + bot.action() diff --git a/交易/weex-结构优化.py b/交易/weex-结构优化.py index e074bcf..d9d9c6b 100644 --- a/交易/weex-结构优化.py +++ b/交易/weex-结构优化.py @@ -558,9 +558,9 @@ class MessageSender: legacy_direction = position_data.get('legacyOrderDirection', '') # 确定方向 - if position_side == 'SHORT' or legacy_direction == 'OPEN_SHORT': + if legacy_direction == 'OPEN_SHORT': direction = "空" - elif position_side == 'LONG' or legacy_direction == 'OPEN_LONG': + elif legacy_direction == 'OPEN_LONG': direction = "多" else: direction = "无" @@ -575,23 +575,31 @@ class MessageSender: pnl_rate = (open_avg_price - current_price) / open_avg_price * 100 pnl_str = f"{unrealized_pnl:+.2f} USDT ({pnl_rate:+.2f}%)" + + # 当前持仓名义价值 + current_value = fill_size * current_price + + return ( + "**【WEEX ETHUSDT 永续持仓监控】**\n\n" + f"**持仓方向**:{direction}\n" + f"**当前现价**:{current_price:.2f} USDT\n" + f"**开仓均价**:{open_avg_price:.2f} USDT\n" + f"**持仓数量(eth)**:{fill_size:.3f} ETH\n" + f"**持仓数量(usdt)**:{fill_value / 100:.2f} USDT\n" + f"**名义价值**:{current_value:.2f} USDT\n" + f"**浮动盈亏**:{pnl_str}\n" + f"**账户可用余额**:{available_balance:.2f} USDT" + ) else: pnl_str = "0.00 USDT" - # 当前持仓名义价值 - current_value = fill_size * current_price - return ( "**【WEEX ETHUSDT 永续持仓监控】**\n\n" - f"**持仓方向**:{direction}\n" + f"**持仓方向**:无\n" f"**当前现价**:{current_price:.2f} USDT\n" - f"**开仓均价**:{open_avg_price:.2f} USDT\n" - f"**持仓数量(eth)**:{fill_size:.3f} ETH\n" - f"**持仓数量(usdt)**:{fill_value / 100:.2f} USDT\n" - f"**名义价值**:{current_value:.2f} USDT\n" - f"**浮动盈亏**:{pnl_str}\n" f"**账户可用余额**:{available_balance:.2f} USDT" ) + else: return ( "**【WEEX ETHUSDT 永续持仓监控】**\n\n"