758 lines
25 KiB
Python
758 lines
25 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
进阶多因子+动态加权+估值择时 A股量化中低频策略
|
|
单文件版本,无需额外导入
|
|
集成单票止损风控模块
|
|
"""
|
|
import pandas as pd
|
|
import numpy as np
|
|
from vnpy.app.cta_strategy import CtaTemplate
|
|
from vnpy.trader.object import BarData, TickData
|
|
|
|
# 导入风控模块 - 关羽开发的单票止损+四层风控体系
|
|
from risk_control import RiskController, StockInfo, PortfolioInfo
|
|
|
|
# 自己实现spearman相关系数,不用依赖scipy
|
|
def spearmanr(a, b):
|
|
"""简单实现spearman秩相关系数"""
|
|
a_rank = a.rank()
|
|
b_rank = b.rank()
|
|
d = a_rank - b_rank
|
|
n = len(a.dropna())
|
|
if n <= 1:
|
|
return 0, None
|
|
rho = 1 - 6 * (d ** 2).sum() / (n * (n ** 2 - 1))
|
|
return rho, None
|
|
|
|
|
|
class BaseFactor:
|
|
"""因子基类 - 所有因子继承此类,定义统一的因子计算接口"""
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.data = None
|
|
|
|
def calculate_raw(self, df):
|
|
"""计算原始因子值"""
|
|
pass
|
|
|
|
def normalize(self, factor_values):
|
|
"""中位数去极值 + z-score标准化"""
|
|
values = factor_values.dropna()
|
|
if len(values) == 0:
|
|
return factor_values
|
|
|
|
median = values.median()
|
|
mad = (values - median).abs().median()
|
|
|
|
lower = median - 3 * mad
|
|
upper = median + 3 * mad
|
|
|
|
factor_clipped = factor_values.clip(lower, upper)
|
|
|
|
mean = factor_clipped.mean()
|
|
std = factor_clipped.std()
|
|
|
|
if std > 0:
|
|
factor_normalized = (factor_clipped - mean) / std
|
|
else:
|
|
factor_normalized = factor_clipped - mean
|
|
|
|
return factor_normalized
|
|
|
|
def cross_sectional_rank(self, factor_values):
|
|
"""横截面rank 0-1"""
|
|
if isinstance(factor_values.index, pd.MultiIndex):
|
|
ranked = factor_values.groupby(level=0).rank(pct=True)
|
|
else:
|
|
ranked = factor_values.rank(pct=True)
|
|
|
|
return ranked
|
|
|
|
def process(self, df):
|
|
"""完整处理流程: 原始计算 → 标准化 → rank"""
|
|
raw = self.calculate_raw(df)
|
|
normalized = self.normalize(raw)
|
|
ranked = self.cross_sectional_rank(normalized)
|
|
|
|
return ranked
|
|
|
|
|
|
class PEFactor(BaseFactor):
|
|
"""市盈率PE因子 - 越小越好"""
|
|
def __init__(self):
|
|
super().__init__("pe")
|
|
|
|
def calculate_raw(self, df):
|
|
if 'pe' not in df.columns:
|
|
raise ValueError("DataFrame中缺少pe列")
|
|
|
|
pe = df['pe']
|
|
pe = pe.where(pe > 0, pd.NA)
|
|
return -pe
|
|
|
|
|
|
class PBFactor(BaseFactor):
|
|
"""市净PB因子 - 越小越好"""
|
|
def __init__(self):
|
|
super().__init__("pb")
|
|
|
|
def calculate_raw(self, df):
|
|
if 'pb' not in df.columns:
|
|
raise ValueError("DataFrame中缺少pb列")
|
|
|
|
pb = df['pb']
|
|
pb = pb.where(pb > 0, pd.NA)
|
|
return -pb
|
|
|
|
|
|
class ROEFactor(BaseFactor):
|
|
"""ROE净资产收益率因子 - 越大越好"""
|
|
def __init__(self):
|
|
super().__init__("roe")
|
|
|
|
def calculate_raw(self, df):
|
|
if 'roe' not in df.columns:
|
|
raise ValueError("DataFrame中缺少roe列")
|
|
|
|
return df['roe']
|
|
|
|
|
|
class Momentum1MFactor(BaseFactor):
|
|
"""1个月动量因子 - 越大越好"""
|
|
def __init__(self, periods=21):
|
|
super().__init__("momentum_1m")
|
|
self.periods = periods
|
|
|
|
def calculate_raw(self, df):
|
|
if 'close' not in df.columns:
|
|
raise ValueError("DataFrame缺少close列")
|
|
|
|
if isinstance(df.index, pd.MultiIndex) and 'symbol' in df.index.names:
|
|
momentum = df['close'].groupby(level='symbol').pct_change(self.periods)
|
|
else:
|
|
momentum = df.groupby('symbol')['close'].pct_change(self.periods)
|
|
|
|
return momentum
|
|
|
|
|
|
class Momentum3MFactor(BaseFactor):
|
|
"""3个月动量因子 - 越大越好"""
|
|
def __init__(self, periods=63):
|
|
super().__init__("momentum_3m")
|
|
self.periods = periods
|
|
|
|
def calculate_raw(self, df):
|
|
if 'close' not in df.columns:
|
|
raise ValueError("DataFrame缺少close列")
|
|
|
|
if isinstance(df.index, pd.MultiIndex) and 'symbol' in df.index.names:
|
|
momentum = df['close'].groupby(level='symbol').pct_change(self.periods)
|
|
else:
|
|
momentum = df.groupby('symbol')['close'].pct_change(self.periods)
|
|
|
|
return momentum
|
|
|
|
|
|
class VolatilityFactor(BaseFactor):
|
|
"""波动率因子 - 越小越好,所以取负"""
|
|
def __init__(self, periods=63):
|
|
super().__init__("volatility_3m")
|
|
self.periods = periods
|
|
|
|
def calculate_raw(self, df):
|
|
if 'close' not in df.columns:
|
|
raise ValueError("DataFrame缺少close列")
|
|
|
|
if isinstance(df.index, pd.MultiIndex) and 'symbol' in df.index.names:
|
|
returns = df['close'].groupby(level='symbol').pct_change()
|
|
volatility = returns.groupby(level='symbol').rolling(self.periods).std()
|
|
volatility = volatility.droplevel(0)
|
|
else:
|
|
returns = df.groupby('symbol')['close'].pct_change()
|
|
volatility = returns.rolling(self.periods).std()
|
|
|
|
return -volatility
|
|
|
|
|
|
class SizeFactor(BaseFactor):
|
|
"""市值因子 - 中小盘偏好,市值越小越好"""
|
|
def __init__(self):
|
|
super().__init__("size")
|
|
|
|
def calculate_raw(self, df):
|
|
if 'market_cap' not in df.columns:
|
|
if 'close' in df.columns and 'circulating_cap' in df.columns:
|
|
market_cap = df['close'] * df['circulating_cap']
|
|
else:
|
|
raise ValueError("缺少market_cap列,也没有close+circulating_cap")
|
|
else:
|
|
market_cap = df['market_cap']
|
|
|
|
log_cap = np.log(market_cap)
|
|
return -log_cap
|
|
|
|
|
|
class SectorStrengthFactor(BaseFactor):
|
|
"""板块强度因子 - 板块强度越高越好,适配结构化行情"""
|
|
def __init__(self, periods=21):
|
|
super().__init__("sector_strength")
|
|
self.periods = periods
|
|
|
|
def calculate_raw(self, df):
|
|
if 'close' not in df.columns or 'sector' not in df.columns:
|
|
raise ValueError("需要close和sector列")
|
|
|
|
if isinstance(df.index, pd.MultiIndex) and 'symbol' in df.index.names:
|
|
returns = df['close'].groupby(level='symbol').pct_change(self.periods)
|
|
else:
|
|
returns = df.groupby('symbol')['close'].pct_change(self.periods)
|
|
|
|
if isinstance(df.index, pd.MultiIndex) and 'date' in df.index.names:
|
|
latest_date = df.index.get_level_values('date').max()
|
|
latest_df = df.xs(latest_date, level='date').copy()
|
|
latest_df['return_1m'] = returns.loc[latest_df.index]
|
|
else:
|
|
latest_date = df['date'].max()
|
|
latest_df = df[df['date'] == latest_date].copy()
|
|
latest_df['return_1m'] = returns.loc[latest_df.index]
|
|
|
|
latest_df = latest_df.dropna(subset=['return_1m'])
|
|
sector_avg_ret = latest_df.groupby('sector')['return_1m'].mean()
|
|
market_avg_ret = latest_df['return_1m'].mean()
|
|
sector_strength = sector_avg_ret / market_avg_ret
|
|
|
|
result = pd.Series(index=df.index, dtype=float)
|
|
for symbol, row in latest_df.iterrows():
|
|
sector = row['sector']
|
|
strength = sector_strength.loc[sector]
|
|
if symbol in result.index:
|
|
result.loc[symbol] = strength
|
|
else:
|
|
mask = df['symbol'] == symbol
|
|
result.loc[mask] = strength
|
|
|
|
return result
|
|
|
|
|
|
class FactorCombiner:
|
|
"""因子合成器 - 把多个因子按权重合成最终得分"""
|
|
def __init__(self, factors, weights=None):
|
|
self.factors = factors
|
|
|
|
if weights is None:
|
|
total = len(factors)
|
|
self.weights = {name: 1.0 / total for name in factors}
|
|
else:
|
|
total = sum(weights.values())
|
|
self.weights = {k: v / total for k, v in weights.items()}
|
|
|
|
def combine(self, data):
|
|
combined = None
|
|
|
|
for name, factor in self.factors.items():
|
|
weight = self.weights[name]
|
|
factor_score = factor.process(data)
|
|
weighted = factor_score * weight
|
|
|
|
if combined is None:
|
|
combined = weighted
|
|
else:
|
|
combined = combined.add(weighted, fill_value=0)
|
|
|
|
if combined is not None:
|
|
combined = combined.sort_values(ascending=False)
|
|
|
|
return combined
|
|
|
|
def update_weights(self, new_weights):
|
|
total = sum(new_weights.values())
|
|
self.weights = {k: v / total for k, v in new_weights.items()}
|
|
|
|
def get_weights(self):
|
|
return self.weights.copy()
|
|
|
|
|
|
class DynamicWeightAdjuster:
|
|
"""动态权重调整器 - 根据滚动IC自动调整因子权重"""
|
|
|
|
def __init__(
|
|
self,
|
|
factor_names,
|
|
window_size=12,
|
|
min_ic=-0.02,
|
|
base_weight=0.02
|
|
):
|
|
self.factor_names = factor_names
|
|
self.window_size = window_size
|
|
self.min_ic = min_ic
|
|
self.base_weight = base_weight
|
|
|
|
self.ic_history = {name: [] for name in factor_names}
|
|
|
|
def update_monthly_ic(
|
|
self,
|
|
factor_df,
|
|
forward_returns
|
|
):
|
|
current_ic = {}
|
|
combined = pd.concat([factor_df, forward_returns], axis=1).dropna()
|
|
|
|
for name in self.factor_names:
|
|
if name not in combined.columns:
|
|
continue
|
|
|
|
ic, _ = spearmanr(combined[name], combined[forward_returns.name])
|
|
current_ic[name] = ic
|
|
self.ic_history[name].append(ic)
|
|
|
|
if len(self.ic_history[name]) > self.window_size:
|
|
self.ic_history[name].pop(0)
|
|
|
|
return current_ic
|
|
|
|
def calculate_weights(self):
|
|
avg_ic = {}
|
|
for name in self.factor_names:
|
|
ics = self.ic_history[name]
|
|
if len(ics) > 0:
|
|
avg_ic[name] = np.mean(ics)
|
|
else:
|
|
avg_ic[name] = 0
|
|
|
|
weights = {}
|
|
for name in self.factor_names:
|
|
ic = avg_ic[name]
|
|
|
|
if ic < self.min_ic:
|
|
weights[name] = self.base_weight
|
|
else:
|
|
weights[name] = self.base_weight + max(0, ic)
|
|
|
|
total = sum(weights.values())
|
|
if total > 0:
|
|
weights = {k: v / total for k, v in weights.items()}
|
|
else:
|
|
n = len(self.factor_names)
|
|
weights = {k: 1.0 / n for k in self.factor_names}
|
|
|
|
return weights
|
|
|
|
def get_avg_ic(self):
|
|
avg = {}
|
|
for name, ics in self.ic_history.items():
|
|
if len(ics) > 0:
|
|
avg[name] = np.mean(ics)
|
|
else:
|
|
avg[name] = 0
|
|
return avg
|
|
|
|
|
|
class MarketValuationTiming:
|
|
"""全市场估值择时 - 根据PE分位数调整整体仓位"""
|
|
def __init__(
|
|
self,
|
|
min_position=0.3,
|
|
max_position=1.0,
|
|
quantile_low=0.2,
|
|
quantile_high=0.8,
|
|
lookback_months=60
|
|
):
|
|
self.min_position = min_position
|
|
self.max_position = max_position
|
|
self.quantile_low = quantile_low
|
|
self.quantile_high = quantile_high
|
|
self.lookback_months = lookback_months
|
|
|
|
self.history = []
|
|
|
|
def update_monthly(self, market_pe):
|
|
self.history.append(market_pe)
|
|
|
|
if len(self.history) > self.lookback_months:
|
|
self.history.pop(0)
|
|
|
|
def calculate_target_position(self):
|
|
if len(self.history) < 12:
|
|
return 0.8 * self.max_position
|
|
|
|
current_pe = self.history[-1]
|
|
history_arr = np.array(self.history)
|
|
quantile = np.mean(history_arr <= current_pe)
|
|
|
|
if quantile <= self.quantile_low:
|
|
return self.max_position
|
|
elif quantile >= self.quantile_high:
|
|
return self.min_position
|
|
else:
|
|
ratio = (quantile - self.quantile_low) / (self.quantile_high - self.quantile_low)
|
|
position = self.max_position - ratio * (self.max_position - self.min_position)
|
|
return position
|
|
|
|
def get_current_quantile(self):
|
|
if len(self.history) < 2:
|
|
return None
|
|
|
|
current = self.history[-1]
|
|
arr = np.array(self.history)
|
|
return np.mean(arr <= current)
|
|
|
|
|
|
class MultiFactorDynamicStrategy(CtaTemplate):
|
|
"""
|
|
进阶多因子动态加权策略
|
|
特点:
|
|
1. 多因子复合选股
|
|
2. 动态加权(根据IC调整)
|
|
3. 估值择时(调整整体仓位)
|
|
4. 板块强度适配结构化行情
|
|
5. 单板块仓位限制
|
|
"""
|
|
|
|
author = "翼德"
|
|
parameters = [
|
|
"rebalance_freq",
|
|
"holding_size",
|
|
"top_select",
|
|
"dynamic_weight",
|
|
"ic_window",
|
|
"market_timing",
|
|
"min_position",
|
|
"max_position",
|
|
"max_sector_pct",
|
|
]
|
|
|
|
variables = [
|
|
"current_factor_scores",
|
|
"current_weights",
|
|
"target_position",
|
|
"last_rebalance_date",
|
|
]
|
|
|
|
def __init__(self, cta_engine, strategy_name, setting_dict):
|
|
super().__init__(cta_engine, strategy_name, setting_dict)
|
|
|
|
self.rebalance_freq = getattr(self, 'rebalance_freq', 'M')
|
|
self.holding_size = getattr(self, 'holding_size', 50)
|
|
self.top_select = getattr(self, 'top_select', 0.1)
|
|
self.dynamic_weight = getattr(self, 'dynamic_weight', True)
|
|
self.ic_window = getattr(self, 'ic_window', 12)
|
|
self.market_timing = getattr(self, 'market_timing', True)
|
|
self.min_position = getattr(self, 'min_position', 0.3)
|
|
self.max_position = getattr(self, 'max_position', 1.0)
|
|
self.max_sector_pct = getattr(self, 'max_sector_pct', 0.20)
|
|
|
|
self.factors_list = [
|
|
PEFactor(),
|
|
PBFactor(),
|
|
ROEFactor(),
|
|
Momentum1MFactor(),
|
|
Momentum3MFactor(),
|
|
VolatilityFactor(),
|
|
SizeFactor(),
|
|
SectorStrengthFactor()
|
|
]
|
|
|
|
default_weights = {
|
|
'pe': 0.15,
|
|
'pb': 0.15,
|
|
'roe': 0.15,
|
|
'momentum_1m': 0.08,
|
|
'momentum_3m': 0.12,
|
|
'volatility_3m': 0.15,
|
|
'size': 0.15,
|
|
'sector_strength': 0.10,
|
|
}
|
|
|
|
factor_dict = {f.name: f for f in self.factors_list}
|
|
self.factor_combiner = FactorCombiner(factor_dict, default_weights)
|
|
|
|
if self.dynamic_weight:
|
|
factor_names = list(factor_dict.keys())
|
|
self.dynamic_adjuster = DynamicWeightAdjuster(
|
|
factor_names,
|
|
window_size=self.ic_window
|
|
)
|
|
|
|
if self.market_timing:
|
|
self.market_timer = MarketValuationTiming(
|
|
min_position=self.min_position,
|
|
max_position=self.max_position
|
|
)
|
|
self.target_position = self.max_position
|
|
else:
|
|
self.target_position = self.max_position
|
|
|
|
self.current_factor_scores = None
|
|
self.current_weights = self.factor_combiner.get_weights()
|
|
self.last_rebalance_date = None
|
|
self.last_data = None
|
|
|
|
# 初始化风控控制器 - 关羽风控模块
|
|
self.risk_controller = RiskController()
|
|
|
|
def on_init(self):
|
|
self.write_log("策略初始化完成")
|
|
self.load_bar(1000)
|
|
|
|
def on_start(self):
|
|
self.write_log("策略启动")
|
|
|
|
def on_stop(self):
|
|
self.write_log("策略停止")
|
|
|
|
def on_bar(self, bar):
|
|
current_date = bar.datetime.date()
|
|
|
|
# 每日执行风控检查 - 单票止损
|
|
self._check_risk_control()
|
|
|
|
if not self._need_rebalance(current_date):
|
|
return
|
|
|
|
self.rebalance()
|
|
self.last_rebalance_date = current_date
|
|
|
|
def _check_risk_control(self):
|
|
"""风控检查:执行单票止损和组合降仓"""
|
|
# 获取当前持仓转换为风控数据结构
|
|
holds = self.get_all_holds()
|
|
if not holds:
|
|
return
|
|
|
|
portfolio = self._get_portfolio_info()
|
|
stocks = self._get_stock_info_list(holds)
|
|
|
|
# 执行风控检查
|
|
risk_result = self.risk_controller.post_trade_check(stocks, portfolio)
|
|
|
|
# 执行止损
|
|
if risk_result['stop_loss_required']:
|
|
for stop_item in risk_result['stop_loss_stocks']:
|
|
code = stop_item['code']
|
|
if code in holds:
|
|
hold = holds[code]
|
|
self.write_log(f"⚠️ 触发单票止损: {code}, 回撤: {stop_item['current_drawdown']:.2%}")
|
|
self.sell(code, hold.price, hold.volume)
|
|
|
|
# 执行组合降仓
|
|
if risk_result['rebalance_required']:
|
|
target_ratio = risk_result['target_position_ratio']
|
|
current_ratio = risk_result['current_position_ratio']
|
|
self.write_log(f"⚠️ 组合回撤触发降仓: 当前回撤 {risk_result['current_drawdown']:.2%}, 当前仓位 {current_ratio:.2%}, 目标仓位 {target_ratio:.2%}")
|
|
|
|
self.put_order()
|
|
|
|
def _need_rebalance(self, current_date):
|
|
if self.last_rebalance_date is None:
|
|
return True
|
|
|
|
if self.rebalance_freq == 'M':
|
|
if current_date.month != self.last_rebalance_date.month:
|
|
return True
|
|
elif self.rebalance_freq == 'W':
|
|
current_week = current_date.isocalendar()[1]
|
|
last_week = self.last_rebalance_date.isocalendar()[1]
|
|
if current_week != last_week:
|
|
return True
|
|
|
|
return False
|
|
|
|
def calculate_factors(self, data):
|
|
final_scores = self.factor_combiner.combine(data)
|
|
return final_scores
|
|
|
|
def select_stocks(self, scores):
|
|
n_total = len(scores.dropna())
|
|
|
|
if self.holding_size:
|
|
n_select = self.holding_size
|
|
else:
|
|
n_select = int(n_total * self.top_select)
|
|
|
|
selected = scores.head(n_select).index.tolist()
|
|
return selected
|
|
|
|
def calculate_weights_for_selected(self, selected, data):
|
|
n = len(selected)
|
|
if n == 0:
|
|
return {}
|
|
|
|
selected_df = data.loc[data['symbol'].isin(selected)]
|
|
|
|
single_weight = self.target_position / n
|
|
weights = {symbol: single_weight for symbol in selected}
|
|
|
|
if 'sector' in selected_df.columns:
|
|
sector_weights = {}
|
|
for symbol in selected:
|
|
sector = selected_df[selected_df['symbol'] == symbol]['sector'].iloc[0]
|
|
w = weights[symbol]
|
|
if sector not in sector_weights:
|
|
sector_weights[sector] = 0
|
|
sector_weights[sector] += w
|
|
|
|
max_allowed = self.target_position * self.max_sector_pct
|
|
for sector, total_w in sector_weights.items():
|
|
if total_w > max_allowed:
|
|
ratio = max_allowed / total_w
|
|
for symbol in selected:
|
|
s = selected_df[selected_df['symbol'] == symbol]['sector'].iloc[0]
|
|
if s == sector:
|
|
weights[symbol] *= ratio
|
|
|
|
total = sum(weights.values())
|
|
if total > 0:
|
|
ratio = self.target_position / total
|
|
for symbol in weights:
|
|
weights[symbol] *= ratio
|
|
|
|
return weights
|
|
|
|
def update_dynamic_weights(self, last_data, current_data):
|
|
if not self.dynamic_weight:
|
|
return
|
|
|
|
last_close = last_data.groupby('symbol')['close'].last()
|
|
current_close = current_data.groupby('symbol')['close'].last()
|
|
forward_returns = (current_close - last_close) / last_close
|
|
|
|
factor_scores = {}
|
|
for name, factor in self.factor_combiner.factors.items():
|
|
factor_scores[name] = factor.process(current_data)
|
|
|
|
factor_df = pd.DataFrame(factor_scores)
|
|
self.dynamic_adjuster.update_monthly_ic(factor_df, forward_returns)
|
|
new_weights = self.dynamic_adjuster.calculate_weights()
|
|
self.factor_combiner.update_weights(new_weights)
|
|
self.current_weights = new_weights
|
|
|
|
self.write_log(f"动态权重更新完成: {new_weights}")
|
|
|
|
def update_market_timing(self, market_pe):
|
|
if not self.market_timing:
|
|
return
|
|
|
|
self.market_timer.update_monthly(market_pe)
|
|
self.target_position = self.market_timer.calculate_target_position()
|
|
|
|
q = self.market_timer.get_current_quantile()
|
|
if q is not None:
|
|
self.write_log(f"估值择时更新: 分位数={q:.2f}, 目标仓位={self.target_position:.2f}")
|
|
|
|
def rebalance(self):
|
|
data = self.get_current_market_data()
|
|
|
|
if data is None or len(data) == 0:
|
|
self.write_log("没有可用数据,跳过调仓")
|
|
return
|
|
|
|
if self.dynamic_weight and hasattr(self, 'last_data') and self.last_data is not None:
|
|
self.update_dynamic_weights(self.last_data, data)
|
|
|
|
if self.market_timing:
|
|
market_pe = self.calculate_market_pe(data)
|
|
self.update_market_timing(market_pe)
|
|
|
|
scores = self.calculate_factors(data)
|
|
|
|
if scores is None or len(scores.dropna()) == 0:
|
|
self.write_log("计算得分失败,跳过调仓")
|
|
return
|
|
|
|
selected = self.select_stocks(scores)
|
|
|
|
if len(selected) == 0:
|
|
self.write_log("没有选中股票,跳过调仓")
|
|
return
|
|
|
|
target_weights = self.calculate_weights_for_selected(selected, data)
|
|
|
|
self.rebalance_portfolio(target_weights)
|
|
|
|
self.last_data = data
|
|
|
|
self.write_log(f"调仓完成,选中{len(selected)}只股票,目标仓位{self.target_position:.2f}")
|
|
|
|
def get_current_market_data(self):
|
|
return None
|
|
|
|
def calculate_market_pe(self, data):
|
|
pes = data['pe'].dropna()
|
|
pes = pes[pes > 0]
|
|
if len(pes) == 0:
|
|
return 15
|
|
return pes.median()
|
|
|
|
def rebalance_portfolio(self, target_weights):
|
|
current_holds = self.get_all_holds()
|
|
|
|
for symbol in current_holds:
|
|
if symbol not in target_weights:
|
|
self.sell(symbol, current_holds[symbol].price, 0)
|
|
|
|
for symbol, target_weight in target_weights.items():
|
|
target_value = self.balance * target_weight
|
|
current_price = self.get_last_price(symbol)
|
|
if current_price <= 0:
|
|
continue
|
|
|
|
target_volume = int(target_value / current_price / 100) * 100
|
|
|
|
if target_volume <= 0:
|
|
continue
|
|
|
|
current_hold = current_holds.get(symbol, None)
|
|
current_volume = current_hold.volume if current_hold else 0
|
|
|
|
if target_volume > current_volume:
|
|
volume = target_volume - current_volume
|
|
self.buy(symbol, current_price, volume)
|
|
elif target_volume < current_volume:
|
|
volume = current_volume - target_volume
|
|
self.sell(symbol, current_price, volume)
|
|
|
|
self.put_order()
|
|
|
|
def _get_stock_info_list(self, holds):
|
|
"""转换持仓为风控StockInfo列表"""
|
|
stocks = []
|
|
for symbol, hold in holds.items():
|
|
# 获取最新价格,这里简化处理
|
|
current_price = self.get_last_price(symbol)
|
|
if current_price is None or current_price <= 0:
|
|
continue
|
|
|
|
# 创建StockInfo
|
|
# 这里假设已经完成了黑天鹅过滤,ST等风险已经在选股阶段排除
|
|
stock = StockInfo(
|
|
code=symbol,
|
|
name=symbol, # 回测中简化处理
|
|
cost_price=hold.price,
|
|
current_price=current_price,
|
|
is_st=False,
|
|
is_limit_down=False,
|
|
is_fraud=False,
|
|
volume=0.0
|
|
)
|
|
stocks.append(stock)
|
|
return stocks
|
|
|
|
def _get_portfolio_info(self):
|
|
"""转换组合信息为风控PortfolioInfo"""
|
|
total_capital = self.initial_balance
|
|
current_capital = self.balance
|
|
positions = {}
|
|
|
|
holds = self.get_all_holds()
|
|
for symbol, hold in holds.items():
|
|
current_price = self.get_last_price(symbol)
|
|
if current_price > 0:
|
|
positions[symbol] = hold.volume * current_price
|
|
|
|
return PortfolioInfo(
|
|
total_capital=total_capital,
|
|
current_capital=current_capital,
|
|
positions=positions
|
|
)
|