新建回测系统,并提交

This commit is contained in:
2026-01-17 21:37:42 +08:00
commit fe50ea935a
68 changed files with 108208 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
10分钟 均线红绿灯策略
流程:
流程再钉一次,避免周期混用:
日线 15:00 收市后
运行「初筛公式」→ 得到自选池46 只左右)
次日 09:25 以前
把「再筛公式」仍在日线周期跑一遍(可夜盘定时选股):
市值、振幅、SLOPE(DAYMA10,3) 都是昨日日 K 数据
→ 输出「今日观察池」≤10 只)
09:30-10:00 切 1-min 周期
只对「观察池」做分时预警:
用 #DAYCLOSE 拿当天实时日级收盘
用 DAYMA10:=ROUND(MA(#DAYCLOSE,10),2) 拿实时日 10 日线
踩线、站回逻辑都在 1-min 里完成

48
strategies/OCZ策略.txt Normal file
View File

@@ -0,0 +1,48 @@
{参数}
N:=30; {阻力回望长度}
B:=60; {实体占比%}
V1:=1.5; {放量倍数}
TOL:=1.5; {回踩容错%}
R:=4; {收盘涨幅门槛 %}
ATR周期:=14; {ATR周期}
VOL30:=MA(VOL,30); {30日均量}
{1. 关键阻力 = 最近 N 日最高}
阻力:HHV(H,N),NODRAW;
{2. ONE CANDLE 突破}
实体:=ABS(C-O);
总幅:=H-L;
突破:=C>REF(阻力,1) AND 实体/总幅*100>B AND (C/REF(C,1)-1)*100>=R AND VOL>MA(VOL,N)*V1;
DRAWICON(突破,L*0.96,34);
{3. 波动率过滤2.5%-8%}
真实波幅:=MA(H-L,30);
波动率:=真实波幅/C*100;
波动够:波动率>=2.5 AND 波动率<=8.0,NODRAW;
{4. 市值过滤30亿-120亿}
流通市值:=FINANCE(40)/10000; {单位:亿}
市值合适:=流通市值>=30 AND 流通市值<=120;
{3. 画阻力线}
{阻力线:阻力,COLORYELLOW,DOTLINE;}
DRAWSL(突破,REF(阻力,1),0,5,2)COLORYELLOW;
DRAWICON(突破,H*1.05,1); {洋红箭头}
{4. 首次回踩}
BAR突:=BARSLAST(突破);
D2ZL:=REF(阻力,2),NODRAW;
回踩区:=BETWEEN(L,D2ZL*0.985,D2ZL*1.015); {±1.5% 区间}
{回踩区:=BETWEEN(L,阻力*(1-TOL/100),阻力*(1+TOL/100));}
收回:=C>D2ZL;
{回踩成交量}
缩量:=VOL<REF(VOL,BAR突) ; { BAR突=突破日距今天数 }
回踩信号:BAR突=1 AND 收回 AND 回踩区 AND 缩量 ,NODRAW ;

5
strategies/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""策略包初始化文件。"""
from utils.logger import setup_logger
logger = setup_logger(__name__)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

292
strategies/base_strategy.py Normal file
View File

@@ -0,0 +1,292 @@
"""策略基类与回测主循环实现。
BaseStrategy 定义:
- 账户与持仓管理;
- 买卖接口;
- 回测主循环 run_backtest
- 集成止盈止损和仓位管理钩子;
子类只需实现 on_bar在每个交易日根据行情数据决策买卖。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Dict, List, Optional
import numpy as np
import pandas as pd
from utils.logger import setup_logger
logger = setup_logger(__name__)
@dataclass
class Position:
"""单只股票的持仓信息。"""
ts_code: str
quantity: int
cost: float # 成本价
days_held: int = 0
class BaseStrategy(ABC):
"""回测策略抽象基类。
- 管理资金与持仓;
- 提供买卖接口;
- 实现回测主循环;
- 支持止盈止损和仓位管理;
- 子类只需实现 on_bar 方法。
"""
def __init__(
self,
initial_cash: float,
max_positions: int = 2,
stop_loss: Optional[object] = None,
take_profit: Optional[object] = None,
position_sizer: Optional[object] = None,
date_index_dict: Optional[Dict[str, Dict[str, int]]] = None,
buy_signal_index: Optional[Dict[str, List[str]]] = None,
):
"""初始化策略。
参数:
initial_cash: 初始资金
max_positions: 最大持仓数
stop_loss: 止损管理器StopLoss实例
take_profit: 止盈管理器StopLoss实例
position_sizer: 仓位管理器PositionSizing实例
date_index_dict: 日期索引字典 {ts_code: {date: idx}}
buy_signal_index: 买入信号索引 {date: [ts_code1, ts_code2, ...]}
"""
self.initial_cash = float(initial_cash)
self.cash: float = float(initial_cash)
self.max_positions: int = int(max_positions)
self.positions: Dict[str, Position] = {}
self.equity_curve: List[Dict] = []
self.trade_count: int = 0 # 交易次数统计(买入+卖出)
# 交易历史记录(用于计算胜率和盈亏比)
self.trade_history: List[Dict] = [] # 记录每笔完整交易(买入->卖出)
# 风险管理模块(可选)
self.stop_loss = stop_loss
self.take_profit = take_profit
self.position_sizer = position_sizer
# 性能优化日期索引字典避免deepcopypandas.DataFrame.attrs
self.date_index_dict = date_index_dict if date_index_dict is not None else {}
self.buy_signal_index = buy_signal_index if buy_signal_index is not None else {}
# ------------ 需要子类实现的接口 ------------
@abstractmethod
def on_bar(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
"""每个交易日调用一次,由子类实现交易逻辑。"""
# ------------ 买卖接口 ------------
def buy(self, ts_code: str, price: float, quantity: int) -> None:
"""以指定价格和数量买入股票。
A股交易规则
- 最小交易单位为 1 手100 股);
- 买入数量必须为 100 的整数倍。
"""
# 向下取整到 100 的整数倍
quantity = (quantity // 100) * 100
if quantity <= 0:
return
cost_amount = float(price) * int(quantity)
if cost_amount > self.cash:
return
self.cash -= cost_amount
if ts_code in self.positions:
pos = self.positions[ts_code]
total_cost = pos.cost * pos.quantity + cost_amount
total_qty = pos.quantity + quantity
pos.cost = total_cost / total_qty
pos.quantity = total_qty
# 继续累积 days_held
else:
self.positions[ts_code] = Position(ts_code=ts_code, quantity=quantity, cost=float(price))
self.trade_count += 1 # 记录买入交易
def sell(self, ts_code: str, price: float, quantity: int) -> None:
"""以指定价格和数量卖出股票。"""
if ts_code not in self.positions:
return
pos = self.positions[ts_code]
sell_qty = min(quantity, pos.quantity)
if sell_qty <= 0:
return
# 计算盈亏(用于更新仓位管理器统计)
profit_pct = (price - pos.cost) / pos.cost
profit_amount = (price - pos.cost) * sell_qty
# 记录交易历史
self.trade_history.append({
"ts_code": ts_code,
"buy_price": pos.cost,
"sell_price": price,
"quantity": sell_qty,
"profit_pct": profit_pct,
"profit_amount": profit_amount,
"is_win": profit_pct > 0,
})
self.cash += float(price) * sell_qty
pos.quantity -= sell_qty
if pos.quantity == 0:
del self.positions[ts_code]
# 清理跟踪止盈状态
if self.stop_loss is not None:
self.stop_loss.reset_tracking(ts_code)
if self.take_profit is not None:
self.take_profit.reset_tracking(ts_code)
# 更新仓位管理器的交易统计用于Kelly公式
if self.position_sizer is not None and hasattr(self.position_sizer, 'update_trade_stats'):
self.position_sizer.update_trade_stats(ts_code, profit_pct)
self.trade_count += 1 # 记录卖出交易
# ------------ 回测主循环 ------------
def _calc_market_value(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> float:
"""计算当前持仓市值。"""
total = 0.0
for ts_code, pos in self.positions.items():
df = data_dict.get(ts_code)
if df is None or df.empty:
continue
# 性能优化:使用预计算的日期索引
date_index = self.date_index_dict.get(ts_code)
if date_index is not None and current_date in date_index:
idx = date_index[current_date]
price = float(df.iloc[idx]["close"])
else:
# 备用方案:若当日无行情,则以最近一条收盘价估值
price = float(df["close"].iloc[-1])
total += price * pos.quantity
return total
def _update_days_held(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
"""默认每天将所有持仓的持有天数 +1。
若子类需要更复杂逻辑,可以覆盖或在 on_bar 中自行管理。
"""
for pos in self.positions.values():
pos.days_held += 1
def _check_stop_loss_take_profit(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
"""检查所有持仓的止盈止损条件。
在每个交易日开盘前或收盘后调用,自动触发止损/止盈卖出。
子类可以覆盖此方法以实现自定义逻辑。
"""
for ts_code in list(self.positions.keys()):
df = data_dict.get(ts_code)
if df is None or df.empty:
continue
# 性能优化:使用预计算的日期索引
date_index = self.date_index_dict.get(ts_code)
if date_index is None or current_date not in date_index:
continue
idx = date_index[current_date]
row = df.iloc[idx]
pos = self.positions[ts_code]
close = float(row["close"])
high = float(row["high"]) if "high" in df.columns else None
low = float(row["low"]) if "low" in df.columns else None
# 计算ATR如果需要
atr = None
if self.stop_loss is not None and self.stop_loss.method == "atr":
from risk.stop_loss import calculate_atr
atr_series = calculate_atr(df, self.stop_loss.atr_period)
if not atr_series.empty:
atr = float(atr_series.iloc[-1])
# 检查止损
if self.stop_loss is not None:
should_exit, reason = self.stop_loss.should_exit(
ts_code=ts_code,
current_price=close,
cost_price=pos.cost,
high=high,
low=low,
atr=atr,
)
if should_exit:
quantity = pos.quantity
self.sell(ts_code, close, quantity)
# logger.info(f"{current_date} 止损卖出 {ts_code} 数量 {quantity} 价格 {close:.2f} 原因: {reason}") # 已移除:止损日志改用进度条
continue # 已卖出,不再检查止盈
# 检查止盈
if self.take_profit is not None:
should_exit, reason = self.take_profit.should_exit(
ts_code=ts_code,
current_price=close,
cost_price=pos.cost,
high=high,
low=low,
atr=atr,
)
if should_exit:
quantity = pos.quantity
self.sell(ts_code, close, quantity)
# logger.info(f"{current_date} 止盈卖出 {ts_code} 数量 {quantity} 价格 {close:.2f} 原因: {reason}") # 已移除:止盈日志改用进度条
def run_backtest(self, data_dict: Dict[str, pd.DataFrame], calendar: List[str]) -> pd.DataFrame:
"""主回测入口。
参数:
- data_dict: {ts_code: DataFrame},每个 df 至少含 ['trade_date', 'open', 'high', 'low', 'close']
- calendar: 统一交易日列表(升序,字符串 YYYYMMDD
返回:
- 资金曲线 DataFrame列为 ['trade_date', 'total_asset', 'cash', 'market_value']。
"""
from tqdm import tqdm
logger.info(f"回测开始,共 {len(calendar)} 个交易日")
# 使用进度条显示回测进度
for current_date in tqdm(calendar, desc="回测进度", unit=""):
# 更新持有天数
self._update_days_held(current_date, data_dict)
# 检查止盈止损(在策略决策前)
self._check_stop_loss_take_profit(current_date, data_dict)
# 策略决策
try:
self.on_bar(current_date, data_dict)
except Exception as e: # noqa: BLE001
logger.error(f"on_bar 异常: {e}")
# 计算当日资产
market_value = self._calc_market_value(current_date, data_dict)
total_asset = self.cash + market_value
self.equity_curve.append(
{
"trade_date": current_date,
"total_asset": total_asset,
"cash": self.cash,
"market_value": market_value,
}
)
logger.info("回测结束")
return pd.DataFrame(self.equity_curve)

193
strategies/ma_cross.py Normal file
View File

@@ -0,0 +1,193 @@
"""示例策略:均线交叉(买入持有 5 天,最多 2 只)。
策略规则(可通过 config.settings 配置参数):
- 使用短期均线 ma_short 和长期均线 ma_long
- 当短期均线向上突破长期均线(金叉)时,若当前仓位未满且无该股持仓,则买入;
- 持有达到 hold_days 天,或出现均线死叉(短期下穿长期)时卖出;
- 最多持有 max_positions 只股票,按可用资金等权分配买入;
- 支持可选的止盈止损和仓位管理(通过 BaseStrategy 集成)。
"""
from __future__ import annotations
from typing import Dict, List, Optional
import pandas as pd
from strategies.base_strategy import BaseStrategy
from utils.logger import setup_logger
logger = setup_logger(__name__)
class MaCrossStrategy(BaseStrategy):
"""均线交叉示例策略。"""
def __init__(
self,
initial_cash: float,
ma_short: int,
ma_long: int,
hold_days: int = 5,
max_positions: int = 2,
position_pct_per_stock: float = 0.2,
stop_loss: Optional[object] = None,
take_profit: Optional[object] = None,
position_sizer: Optional[object] = None,
date_index_dict: Optional[Dict[str, Dict[str, int]]] = None,
buy_signal_index: Optional[Dict[str, List[str]]] = None,
):
"""初始化均线交叉策略。
参数:
initial_cash: 初始资金
ma_short: 短期均线周期
ma_long: 长期均线周期
hold_days: 持有天数
max_positions: 最大持仓数
position_pct_per_stock: 每只个股占总资金的比例0.2 = 20%
stop_loss: 止损管理器(可选)
take_profit: 止盈管理器(可选)
position_sizer: 仓位管理器(可选)
date_index_dict: 日期索引字典 {ts_code: {date: idx}}
buy_signal_index: 买入信号索引 {date: [ts_code1, ts_code2, ...]}
"""
super().__init__(
initial_cash=initial_cash,
max_positions=max_positions,
stop_loss=stop_loss,
take_profit=take_profit,
position_sizer=position_sizer,
date_index_dict=date_index_dict,
buy_signal_index=buy_signal_index,
)
self.ma_short = int(ma_short)
self.ma_long = int(ma_long)
self.hold_days = int(hold_days)
self.position_pct_per_stock = float(position_pct_per_stock)
# 输出策略实例化参数(验证参数读取)
logger.info(f"MaCrossStrategy 初始化参数: ma_short={self.ma_short}, ma_long={self.ma_long}, "
f"hold_days={self.hold_days}, max_positions={max_positions}, "
f"position_pct_per_stock={self.position_pct_per_stock}")
def on_bar(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
"""每个交易日的交易决策逻辑。
性能优化:直接读取预计算的信号列,无需每天重复计算均线和成交量变化。
"""
# 1. 先处理卖出逻辑(持有到期或均线死叉)
for ts_code in list(self.positions.keys()):
df = data_dict.get(ts_code)
if df is None or df.empty:
continue
# 性能优化:使用预计算的日期索引
date_index = self.date_index_dict.get(ts_code)
if date_index is None or current_date not in date_index:
continue
idx = date_index[current_date]
row = df.iloc[idx]
close = float(row["close"])
pos = self.positions[ts_code]
# 性能优化:直接读取预计算的死叉信号
death_cross = bool(row["death_cross"]) if "death_cross" in df.columns else False
# 满足持有天数或死叉则卖出
if pos.days_held >= self.hold_days or death_cross:
quantity = pos.quantity
if quantity > 0:
self.sell(ts_code, close, quantity)
# logger.info(f"{current_date} 卖出 {ts_code} 数量 {quantity} 价格 {close}") # 已移除:交易日志改用进度条
# 2. 再处理买入逻辑(金叉 + 放量按成交量排序选择前N只
if len(self.positions) >= self.max_positions:
return
# 优化如果现金过少不足初始资金1%),直接返回
if self.cash < self.initial_cash * 0.01:
return
# 性能优化核心:直接从买入信号索引获取今天有信号的股票列表
signal_stocks = self.buy_signal_index.get(current_date, [])
if not signal_stocks:
return # 今天没有任何买入信号
candidates = [] # 候选股票列表:[(ts_code, volume, price), ...]
# 只需遍历有买入信号的股票从3776只减少到99%以上)
for ts_code in signal_stocks:
if ts_code in self.positions:
continue
df = data_dict.get(ts_code)
if df is None or df.empty:
continue
# 性能优化:使用预计算的日期索引
date_index = self.date_index_dict.get(ts_code)
if date_index is None or current_date not in date_index:
continue
idx = date_index[current_date]
row = data_dict[ts_code].iloc[idx]
# 满足条件,加入候选列表
price = float(row["close"])
volume = float(row["vol"])
candidates.append((ts_code, volume, price))
# 第二步按成交量倒序排序选择成交量最大的前N只
if not candidates:
return # 没有满足条件的股票
# 按成交量降序排序
candidates.sort(key=lambda x: x[1], reverse=True)
# 计算还能买入几只
remain_slots = self.max_positions - len(self.positions)
selected = candidates[:remain_slots] # 选择前N只
# 第三步:依次买入选中的股票
for ts_code, volume, price in selected:
# 使用仓位管理器计算买入数量(如果有的话)
if self.position_sizer is not None:
df = data_dict.get(ts_code)
quantity = self.position_sizer.calc_shares(
ts_code=ts_code,
cash=self.cash,
price=price,
remain_slots=remain_slots,
df=df,
)
else:
# 默认:按配置的比例分配仓位
# 目标:使用初始资金的 position_pct_per_stock 比例例如50%
target_cash = self.initial_cash * self.position_pct_per_stock
# 实际:如果当前现金不足目标金额,就用剩余的所有现金
actual_cash = min(target_cash, self.cash)
# 计算能买多少股向下取整到100的整数倍
quantity = int(actual_cash // price)
quantity = (quantity // 100) * 100
if quantity <= 0:
continue
# 买入前记录当前现金和仓位
before_cash = self.cash
before_positions = len(self.positions)
self.buy(ts_code, price, quantity)
# 交易日志已移除,改用进度条显示
# if self.cash < before_cash and len(self.positions) > before_positions:
# actual_quantity = self.positions[ts_code].quantity
# actual_cost = before_cash - self.cash
# logger.info(
# f"{current_date} 买入 {ts_code} 数量 {actual_quantity} "
# f"价格 {price:.2f} 成本 {actual_cost:.2f} 成交量 {volume:.0f} "
# f"剩余现金 {self.cash:.2f} 当前持仓数 {len(self.positions)}/{self.max_positions}"
# )

242
strategies/ocz_strategy.py Normal file
View File

@@ -0,0 +1,242 @@
"""OCZ策略One Candle Zone突破回踩策略
策略逻辑:
1. 寻找关键阻力位最近N日最高价
2. 等待大阳线突破(实体占比>B%,涨幅>R%,放量>V1倍
3. 首次回踩到阻力位±TOL容错且缩量时买入
4. 持有期间:
- 止损:跌破阻力位立即卖出
- 止盈达到盈亏比1:2的目标价位立即卖出
- 或持有指定天数后卖出
特殊风控说明:
- 止损位 = 突破的阻力位(买入时记录)
- 止盈位 = 买入价 + (买入价 - 止损位) * 2
- 例如阻力位100元买入价105元则止损100元-5元止盈115元+10元
参数说明:
- N: 阻力回望长度默认30日
- B: 实体占比门槛默认60%
- V1: 放量倍数默认1.5倍)
- TOL: 回踩容错百分比默认1.5%
- R: 收盘涨幅门槛默认6%
- hold_days: 持有天数默认5天
- volatility_min: 最小波动率默认2.5%
- volatility_max: 最大波动率默认8.0%
"""
from typing import Dict, List, Optional
import pandas as pd
from strategies.base_strategy import BaseStrategy
from utils.logger import setup_logger
logger = setup_logger(__name__)
class OczStrategy(BaseStrategy):
"""OCZ突破回踩策略。"""
def __init__(
self,
initial_cash: float,
N: int = 30, # 阻力回望长度
B: float = 60.0, # 实体占比门槛(%
V1: float = 1.5, # 放量倍数
TOL: float = 1.5, # 回踩容错(%
R: float = 4.0, # 收盘涨幅门槛(%
hold_days: int = 5, # 持有天数
volatility_min: float = 2.5, # 最小波动率(%
volatility_max: float = 8.0, # 最大波动率(%
max_positions: int = 2,
position_pct_per_stock: float = 0.2,
stop_loss: Optional[object] = None,
take_profit: Optional[object] = None,
position_sizer: Optional[object] = None,
date_index_dict: Optional[Dict[str, Dict[str, int]]] = None,
buy_signal_index: Optional[Dict[str, List[str]]] = None,
):
"""初始化OCZ策略。"""
super().__init__(
initial_cash=initial_cash,
max_positions=max_positions,
stop_loss=stop_loss,
take_profit=take_profit,
position_sizer=position_sizer,
date_index_dict=date_index_dict,
buy_signal_index=buy_signal_index,
)
self.N = int(N)
self.B = float(B)
self.V1 = float(V1)
self.TOL = float(TOL)
self.R = float(R)
self.hold_days = int(hold_days)
self.volatility_min = float(volatility_min)
self.volatility_max = float(volatility_max)
self.position_pct_per_stock = float(position_pct_per_stock)
# 记录每只股票的突破信息(用于特殊止损止盈)
# 格式: {ts_code: {"resistance": 阻力位, "buy_price": 买入价, "stop_loss": 止损价, "take_profit": 止盈价}}
self.position_risk_info: Dict[str, Dict[str, float]] = {}
logger.info(
f"OczStrategy 初始化参数: N={self.N}, B={self.B}, V1={self.V1}, "
f"TOL={self.TOL}, R={self.R}, hold_days={self.hold_days}, "
f"max_positions={max_positions}, position_pct_per_stock={self.position_pct_per_stock}"
)
def on_bar(self, current_date: str, data_dict: Dict[str, pd.DataFrame]) -> None:
"""每个交易日的交易决策逻辑。"""
# 1. 先处理卖出逻辑(止损/止盈/持有到期)
for ts_code in list(self.positions.keys()):
df = data_dict.get(ts_code)
if df is None or df.empty:
continue
# 使用预计算的日期索引
date_index = self.date_index_dict.get(ts_code)
if date_index is None or current_date not in date_index:
continue
idx = date_index[current_date]
row = df.iloc[idx]
close = float(row["close"])
low = float(row["low"]) # 用于检查是否跌破阻力位
pos = self.positions[ts_code]
risk_info = self.position_risk_info.get(ts_code, {})
should_sell = False
sell_reason = ""
# 检查特殊止损:跌破阻力位
if risk_info and "stop_loss" in risk_info:
stop_loss_price = risk_info["stop_loss"]
if low <= stop_loss_price: # 最低价跌破止损位
should_sell = True
sell_reason = f"跌破阻力位止损({stop_loss_price:.2f})"
# 检查特殊止盈达到盈亏比1:2目标
if not should_sell and risk_info and "take_profit" in risk_info:
take_profit_price = risk_info["take_profit"]
if close >= take_profit_price: # 收盘价达到止盈位
should_sell = True
sell_reason = f"达到止盈位({take_profit_price:.2f})"
# 检查持有到期
if not should_sell and pos.days_held >= self.hold_days:
should_sell = True
sell_reason = f"持有到期({self.hold_days}天)"
# 执行卖出
if should_sell:
quantity = pos.quantity
if quantity > 0:
self.sell(ts_code, close, quantity)
# 清理风控信息
if ts_code in self.position_risk_info:
del self.position_risk_info[ts_code]
# logger.info(f"{current_date} 卖出 {ts_code} 原因: {sell_reason}") # 已移除:交易日志
# 2. 处理买入逻辑(回踩信号)
if len(self.positions) >= self.max_positions:
return
# 优化如果现金过少不足初始资金1%),直接返回
if self.cash < self.initial_cash * 0.01:
return
# 使用买入信号索引获取今天有信号的股票列表
signal_stocks = self.buy_signal_index.get(current_date, [])
if not signal_stocks:
return # 今天没有任何买入信号
candidates = [] # 候选股票列表:[(ts_code, price), ...]
# 只遍历有买入信号的股票
for ts_code in signal_stocks:
if ts_code in self.positions:
continue
df = data_dict.get(ts_code)
if df is None or df.empty:
continue
# 使用预计算的日期索引
date_index = self.date_index_dict.get(ts_code)
if date_index is None or current_date not in date_index:
continue
idx = date_index[current_date]
# 检查是否有足够的历史数据
if idx < self.N + 2:
continue
row = df.iloc[idx]
# 确认买入信号存在并获取阻力位
if "pullback_signal" in df.columns and row["pullback_signal"]:
price = float(row["close"])
# 获取突破的阻力位从预计算的resistance列中读取
resistance = float(row["resistance"]) if "resistance" in df.columns else price * 0.95
candidates.append((ts_code, price, resistance))
if not candidates:
return
# 按价格排序(可选,这里简单按顺序买入)
remain_slots = self.max_positions - len(self.positions)
selected = candidates[:remain_slots]
# 依次买入选中的股票
for item in selected:
ts_code, price, resistance = item # 解包包含阻力位的元组
# 使用仓位管理器计算买入数量(如果有的话)
if self.position_sizer is not None:
df = data_dict.get(ts_code)
quantity = self.position_sizer.calc_shares(
ts_code=ts_code,
cash=self.cash,
price=price,
remain_slots=remain_slots,
df=df,
)
else:
# 默认:按配置的比例分配仓位
target_cash = self.initial_cash * self.position_pct_per_stock
actual_cash = min(target_cash, self.cash)
# 计算能买多少股向下取整到100的整数倍
quantity = int(actual_cash // price)
quantity = (quantity // 100) * 100
if quantity <= 0:
continue
# 买入前记录当前现金和仓位
before_cash = self.cash
before_positions = len(self.positions)
self.buy(ts_code, price, quantity)
# 买入成功(现金减少且仓位增加)
if self.cash < before_cash and len(self.positions) > before_positions:
# 计算并记录止损止盈价位
stop_loss_price = resistance # 止损位 = 阻力位
risk_distance = price - resistance # 风险距离 = 买入价 - 阻力位
take_profit_price = price + risk_distance * 2 # 止盈位 = 买入价 + 风险距离 * 2 (盈亏比1:2)
# 保存风控信息
self.position_risk_info[ts_code] = {
"resistance": resistance,
"buy_price": price,
"stop_loss": stop_loss_price,
"take_profit": take_profit_price,
}
# logger.info(
# f"{current_date} 买入 {ts_code} 价格:{price:.2f} 阻力位:{resistance:.2f} "
# f"止损:{stop_loss_price:.2f}(-{risk_distance:.2f}) 止盈:{take_profit_price:.2f}(+{risk_distance*2:.2f})"
# ) # 已移除:交易日志