294 lines
13 KiB
Python
294 lines
13 KiB
Python
"""
|
||
WEEX 三分之一策略 — 5分钟K线交易
|
||
基于 weex/框架.py,策略逻辑与 bitmart/三分之一策略-5分钟交易.py 一致。
|
||
|
||
策略规则:
|
||
1. 触发价(前一根有效K线实体>=0.1):做多=收盘+实体/3,做空=收盘-实体/3
|
||
2. 信号:当前5分钟K线最高>=做多触发→多;最低<=做空触发→空;同根都触发用开盘价距离判断先后
|
||
3. 反手一:持空且当前涨到上根最高且上根上影线>0.01%→反手多;持多且当前跌到上根最低且上根下影线>0.01%→反手空
|
||
4. 反手二:持多且上根上影线>0.01%且当前跌到上根开盘→反手空;持空且上根下影线>0.01%且当前涨到上根开盘→反手多
|
||
5. 同一根5分钟K线内只交易一次
|
||
"""
|
||
import random
|
||
import sys
|
||
import time
|
||
import datetime
|
||
from pathlib import Path
|
||
from typing import Optional, Dict, List, Tuple
|
||
|
||
from tqdm import tqdm
|
||
from loguru import logger
|
||
|
||
# 保证可导入同目录下的 框架
|
||
_weex_dir = Path(__file__).resolve().parent
|
||
if str(_weex_dir) not in sys.path:
|
||
sys.path.insert(0, str(_weex_dir))
|
||
from 框架 import Config, WeexFuturesTransaction
|
||
|
||
# 5 分钟 K 线类型(若 WEEX 接口名不同可改为实际值)
|
||
KLINE_TYPE_5M = "MINUTE_5"
|
||
|
||
|
||
class WeexOneThirdStrategy(WeexFuturesTransaction):
|
||
"""WEEX 三分之一策略(5分钟K线),继承 weex/框架.py"""
|
||
|
||
def __init__(self, tge_id):
|
||
super().__init__(tge_id)
|
||
self.pbar = tqdm(total=5, desc="等待5分钟K线", ncols=80)
|
||
self.min_body_size = 0.1
|
||
self.min_shadow_pct = 0.01
|
||
self.check_interval = 5
|
||
self.position_ratio = 100 # 开仓金额 = 余额 / position_ratio
|
||
self.last_trigger_kline_id: Optional[int] = None
|
||
self.last_trigger_direction: Optional[str] = None
|
||
self.last_trade_kline_id: Optional[int] = None
|
||
|
||
# ------------------------- 5分钟K线 -------------------------
|
||
def get_klines_5m(self) -> Optional[List[Dict]]:
|
||
"""获取 5 分钟 K 线"""
|
||
return self.get_klines(kline_type=KLINE_TYPE_5M, limit=100)
|
||
|
||
# ------------------------- 三分之一策略核心 -------------------------
|
||
def get_body_size(self, candle: Dict) -> float:
|
||
return abs(float(candle["open"]) - float(candle["close"]))
|
||
|
||
def find_valid_prev_bar(self, all_data: List[Dict], current_idx: int) -> Tuple[Optional[int], Optional[Dict]]:
|
||
if current_idx <= 0:
|
||
return None, None
|
||
for i in range(current_idx - 1, -1, -1):
|
||
prev = all_data[i]
|
||
if self.get_body_size(prev) >= self.min_body_size:
|
||
return i, prev
|
||
return None, None
|
||
|
||
def get_one_third_levels(self, prev: Dict) -> Tuple[Optional[float], Optional[float]]:
|
||
p_open, p_close = float(prev["open"]), float(prev["close"])
|
||
body = abs(p_open - p_close)
|
||
if body < 0.001:
|
||
return None, None
|
||
return p_close + body / 3, p_close - body / 3
|
||
|
||
def get_upper_shadow(self, candle: Dict) -> float:
|
||
o, c, h = float(candle["open"]), float(candle["close"]), float(candle["high"])
|
||
return h - max(o, c)
|
||
|
||
def get_lower_shadow(self, candle: Dict) -> float:
|
||
o, c, l = float(candle["open"]), float(candle["close"]), float(candle["low"])
|
||
return min(o, c) - l
|
||
|
||
def upper_shadow_pct(self, candle: Dict) -> float:
|
||
o = float(candle["open"])
|
||
return (self.get_upper_shadow(candle) / o * 100) if o > 0 else 0.0
|
||
|
||
def lower_shadow_pct(self, candle: Dict) -> float:
|
||
o = float(candle["open"])
|
||
return (self.get_lower_shadow(candle) / o * 100) if o > 0 else 0.0
|
||
|
||
def check_reverse_by_prev_high_low(self, kline_data: List[Dict]) -> Tuple[Optional[str], Optional[Dict]]:
|
||
if len(kline_data) < 2:
|
||
return None, None
|
||
curr, prev = kline_data[-1], kline_data[-2]
|
||
curr_high = float(curr["high"])
|
||
curr_low = float(curr["low"])
|
||
prev_high = float(prev["high"])
|
||
prev_low = float(prev["low"])
|
||
if self.start == -1 and curr_high >= prev_high and self.upper_shadow_pct(prev) > self.min_shadow_pct:
|
||
return "long", prev
|
||
if self.start == 1 and curr_low <= prev_low and self.lower_shadow_pct(prev) > self.min_shadow_pct:
|
||
return "short", prev
|
||
return None, None
|
||
|
||
def check_reverse_by_prev_open(self, kline_data: List[Dict]) -> Tuple[Optional[str], Optional[Dict]]:
|
||
if len(kline_data) < 2:
|
||
return None, None
|
||
curr, prev = kline_data[-1], kline_data[-2]
|
||
curr_high = float(curr["high"])
|
||
curr_low = float(curr["low"])
|
||
prev_open = float(prev["open"])
|
||
if self.start == 1 and self.upper_shadow_pct(prev) > self.min_shadow_pct and curr_low <= prev_open:
|
||
return "short", prev
|
||
if self.start == -1 and self.lower_shadow_pct(prev) > self.min_shadow_pct and curr_high >= prev_open:
|
||
return "long", prev
|
||
return None, None
|
||
|
||
def check_realtime_trigger(
|
||
self, kline_data: List[Dict]
|
||
) -> Tuple[Optional[str], Optional[float], Optional[Dict], Optional[Dict]]:
|
||
if len(kline_data) < 2:
|
||
return None, None, None, None
|
||
curr = kline_data[-1]
|
||
curr_kline_id = curr["id"]
|
||
curr_high = float(curr["high"])
|
||
curr_low = float(curr["low"])
|
||
curr_open = float(curr["open"])
|
||
|
||
valid_prev_idx, prev = self.find_valid_prev_bar(kline_data, len(kline_data) - 1)
|
||
if prev is None:
|
||
return None, None, None, None
|
||
|
||
long_trigger, short_trigger = self.get_one_third_levels(prev)
|
||
if long_trigger is None:
|
||
return None, None, None, None
|
||
|
||
long_triggered = curr_high >= long_trigger
|
||
short_triggered = curr_low <= short_trigger
|
||
both_triggered = long_triggered and short_triggered
|
||
|
||
direction = None
|
||
trigger_price = None
|
||
if both_triggered:
|
||
dist_to_long = abs(long_trigger - curr_open)
|
||
dist_to_short = abs(short_trigger - curr_open)
|
||
if dist_to_short <= dist_to_long:
|
||
direction, trigger_price = "short", short_trigger
|
||
else:
|
||
direction, trigger_price = "long", long_trigger
|
||
elif short_triggered:
|
||
direction, trigger_price = "short", short_trigger
|
||
elif long_triggered:
|
||
direction, trigger_price = "long", long_trigger
|
||
|
||
if direction is None:
|
||
return None, None, None, None
|
||
if self.last_trigger_kline_id == curr_kline_id and self.last_trigger_direction == direction:
|
||
return None, None, None, None
|
||
return direction, trigger_price, prev, curr
|
||
|
||
# ------------------------- 主循环 -------------------------
|
||
def action(self) -> None:
|
||
n = 0
|
||
while True:
|
||
|
||
for i in range(3):
|
||
if self.openBrowser():
|
||
break
|
||
else:
|
||
self.ding("打开 TGE 失败!", error=True)
|
||
continue
|
||
|
||
logger.info("TGE 浏览器已打开")
|
||
self.close_extra_tabs()
|
||
self.page.get(url=Config.TRADING_URL)
|
||
time.sleep(self.check_interval)
|
||
|
||
if n == 0 or random.randint(1,11)> 6:
|
||
n = 1
|
||
if not self._get_token():
|
||
self.ding("获取 token 失败", error=True)
|
||
return
|
||
|
||
self.click_safe('x:(//button[normalize-space(text()) = "市价"])')
|
||
logger.info("WEEX 三分之一策略(5分钟K线)开始监测")
|
||
|
||
try:
|
||
kline_data = self.get_klines_5m()
|
||
if not kline_data or len(kline_data) < 3:
|
||
logger.warning("K线数据不足,等待重试...")
|
||
time.sleep(self.check_interval)
|
||
continue
|
||
|
||
curr = kline_data[-1]
|
||
curr_kline_id = curr["id"]
|
||
curr_time_str = datetime.datetime.fromtimestamp(curr["id"] / 1000).strftime("%H:%M:%S")
|
||
|
||
if not self.get_position_status():
|
||
logger.warning("获取仓位失败,使用缓存")
|
||
|
||
# 反手一:涨到上根最高/跌到上根最低 + 影线>0.01%
|
||
rev_dir, rev_prev = self.check_reverse_by_prev_high_low(kline_data)
|
||
rev_type = "一"
|
||
if not rev_dir:
|
||
rev_dir, rev_prev = self.check_reverse_by_prev_open(kline_data)
|
||
rev_type = "二"
|
||
|
||
if rev_dir and self.last_trade_kline_id != curr_kline_id:
|
||
balance = self.get_available_balance()
|
||
amount = int((balance or 0) / self.position_ratio)
|
||
if amount > 0:
|
||
self.ding(f"反手信号{rev_type} {rev_dir} 金额={amount}")
|
||
if rev_dir == "long":
|
||
self.平仓()
|
||
time.sleep(1)
|
||
self.开单(marketPriceLongOrder=1, size=amount)
|
||
else:
|
||
self.平仓()
|
||
time.sleep(1)
|
||
self.开单(marketPriceLongOrder=-1, size=amount)
|
||
self.last_trade_kline_id = curr_kline_id
|
||
self.last_trigger_kline_id = curr_kline_id
|
||
self.last_trigger_direction = rev_dir
|
||
time.sleep(self.check_interval)
|
||
continue
|
||
|
||
# 主信号:三分之一触发
|
||
direction, trigger_price, valid_prev, curr_kline = self.check_realtime_trigger(kline_data)
|
||
|
||
if direction:
|
||
if self.last_trade_kline_id == curr_kline_id:
|
||
self.last_trigger_kline_id = curr_kline_id
|
||
self.last_trigger_direction = direction
|
||
time.sleep(self.check_interval)
|
||
continue
|
||
|
||
if (direction == "long" and self.start == 1) or (direction == "short" and self.start == -1):
|
||
self.last_trigger_kline_id = curr_kline_id
|
||
self.last_trigger_direction = direction
|
||
time.sleep(self.check_interval)
|
||
continue
|
||
|
||
balance = self.get_available_balance()
|
||
amount = int((balance or 0) / self.position_ratio)
|
||
if amount <= 0:
|
||
time.sleep(self.check_interval)
|
||
continue
|
||
|
||
executed = False
|
||
if direction == "long":
|
||
if self.start == -1:
|
||
self.ding("平空反手开多")
|
||
self.平仓()
|
||
time.sleep(1)
|
||
self.开单(marketPriceLongOrder=1, size=amount)
|
||
executed = True
|
||
elif self.start == 0:
|
||
self.ding("无仓位开多")
|
||
self.开单(marketPriceLongOrder=1, size=amount)
|
||
executed = True
|
||
elif direction == "short":
|
||
if self.start == 1:
|
||
self.ding("平多反手开空")
|
||
self.平仓()
|
||
time.sleep(1)
|
||
self.开单(marketPriceLongOrder=-1, size=amount)
|
||
executed = True
|
||
elif self.start == 0:
|
||
self.ding("无仓位开空")
|
||
self.开单(marketPriceLongOrder=-1, size=amount)
|
||
executed = True
|
||
|
||
if executed:
|
||
self.last_trade_kline_id = curr_kline_id
|
||
self.get_position_status()
|
||
self.ding(f"三分之一信号 {direction} 触发价={trigger_price:.2f} 金额={amount}")
|
||
self.last_trigger_kline_id = curr_kline_id
|
||
self.last_trigger_direction = direction
|
||
else:
|
||
logger.debug(f"[{curr_time_str}] O={curr['open']:.2f} H={curr['high']:.2f} L={curr['low']:.2f}")
|
||
|
||
time.sleep(self.check_interval)
|
||
if random.randint(1, 11) > 7:
|
||
self.page.close()
|
||
time.sleep(5)
|
||
|
||
except KeyboardInterrupt:
|
||
logger.info("用户中断")
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"主循环异常: {e}")
|
||
self.ding(f"运行异常: {e}", error=True)
|
||
time.sleep(self.check_interval)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
WeexOneThirdStrategy(tge_id="86837a981aba4576be6916a0ef6ad785").action()
|