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