Update:自定义权重支持不满仓的情况;Update:增加自定义权重总和判断;

Bug:当换仓数量大于持仓数量时全卖出和全买入导致的持仓计算问题‘;
This commit is contained in:
binz 2024-06-08 19:12:23 +08:00
parent b408d22798
commit 10e926bf28
1 changed files with 90 additions and 42 deletions

128
trader.py
View File

@ -209,11 +209,12 @@ class Trader(Account):
# 可执行日期 # 可执行日期
self.avaliable_date = pd.Series(index=[f.split('.')[0] for f in os.listdir(self.data_root['basic'])]).sort_index() self.avaliable_date = pd.Series(index=[f.split('.')[0] for f in os.listdir(self.data_root['basic'])]).sort_index()
def get_weight(self, date, account_weight, next_position): def get_weight(self, date, account_weight, untradable_list, next_position):
""" """
计算个股仓位 计算个股仓位
Args: Args:
untradable_list (list): 无法交易列表
account_weight (float): 总权重即当前持仓比例 account_weight (float): 总权重即当前持仓比例
""" """
if isinstance(self.weight, str): if isinstance(self.weight, str):
@ -221,14 +222,26 @@ class Trader(Account):
return account_weight / len(next_position) return account_weight / len(next_position)
if isinstance(self.weight, pd.DataFrame): if isinstance(self.weight, pd.DataFrame):
date_weight = self.weight.loc[date].dropna().sort_index() date_weight = self.weight.loc[date].dropna().sort_index()
# untradable_list不要求指定权重用昨日权重填充
weight_list = pd.Series(index=next_position['stock_code'])
try: try:
weight_list = date_weight.loc[next_position['stock_code'].to_list()].values # 填充untradable_list权重
if weight_list.sum() > 1 + 1e5: # 防止数据精度的影响,给与一定的宽松 if len(untradable_list) > 0:
raise Exception(f"total weight of {date} is larger then 1.") weight_list.loc[untradable_list] = self.position.set_index('stock_code').loc[untradable_list, 'weight']
weight_list = account_weight * weight_list
return weight_list
except Exception: except Exception:
raise ValueError(f'not found stock weight in {date}') raise ValueError('not found stock weight for untradable stocks in last position.')
try:
# 获取tradable_list权重并对untradable_list占据的仓位进行调整
tradable_list = list(set(next_position['stock_code']) - set(untradable_list))
# 剔除untradable_list仓位后剩余持仓根据自定义权重分配
weight_list.loc[tradable_list] = date_weight.loc[tradable_list].values / date_weight.loc[tradable_list].sum() * (account_weight - weight_list.loc[untradable_list].sum())
weight_list = weight_list.values
if sum(weight_list) > 1 + 1e-5: # 防止数据精度的影响,给与一定的宽松
raise Exception(f"total weight of {date} is larger then 1.")
return weight_list
except Exception as e:
print(e)
raise ValueError(f'not found specified stock weight in {date}')
def get_next_position(self, date, factor): def get_next_position(self, date, factor):
""" """
@ -280,17 +293,30 @@ class Trader(Account):
normal_exclude = list(set(normal_exclude)) normal_exclude = list(set(normal_exclude))
# 交易列表 # 交易列表
if self.today_position_ratio <= 1.0: # 仓位判断给与计算误差冗余
if self.today_position_ratio <= 1.0 + 1e-5:
# 如果没有杠杆: # 如果没有杠杆:
buy_list = [] # 交易逻辑:
sell_list = [] # 1 判断卖出,如果当天跌停则减少实际卖出数量
# 2 判断买入:根据实际卖出数量和距离目标持仓数量判断买入数量,如果当天涨停则减少实际买入数量
untradable_list = [] untradable_list = []
target_list = []
# ----- 卖出 ----- # ----- 卖出 -----
# 异常强制卖出 sell_list = []
limit_down_list = [] # 跌停股记录
# 遍历昨日持仓状态:
# 1 记录持仓状态
# 2 获取停牌股列表
# 3 获取异常强制卖出列表
last_position_status = pd.Series()
for stock in last_position.index: for stock in last_position.index:
if stock_status.loc[stock] in [0,2,5,7]: last_position_status.loc[stock] = stock_status.loc[stock]
if last_position_status.loc[stock] in [0,2]:
untradable_list.append(stock) untradable_list.append(stock)
else:
if last_position_status.loc[stock] in [5,7]:
continue
else: else:
if stock in force_exclude: if stock in force_exclude:
sell_list.append(stock) sell_list.append(stock)
@ -305,13 +331,19 @@ class Trader(Account):
for stock in factor_filled.loc[list(set(last_position.index)-set(untradable_list)-set(sell_list))].sort_values(ascending=self.ascending).index.values[::-1]: for stock in factor_filled.loc[list(set(last_position.index)-set(untradable_list)-set(sell_list))].sort_values(ascending=self.ascending).index.values[::-1]:
if len(sell_list) >= max_sell_num + force_sell_num: if len(sell_list) >= max_sell_num + force_sell_num:
break break
if stock_status.loc[stock] in [0,2,5,7]: if last_position_status.loc[stock] in [0,2]:
continue continue
else: else:
if last_position_status.loc[stock] in [5,7]:
limit_down_list.append(stock)
sell_list.append(stock) sell_list.append(stock)
sell_list = list(set(sell_list)) sell_list = list(set(sell_list))
# 实际卖出列表 = 卖出列表 - 跌停列表
sell_list = list(set(sell_list) - set(limit_down_list))
# ----- 买入 ----- # ----- 买入 -----
buy_list = []
# 剔除过滤条件后可买入列表 # 剔除过滤条件后可买入列表
after_filter_list = list(set(factor.index) - set(normal_exclude) - set(force_exclude)) after_filter_list = list(set(factor.index) - set(normal_exclude) - set(force_exclude))
target_list = factor.loc[after_filter_list].dropna().sort_values(ascending=self.ascending).index.to_list() target_list = factor.loc[after_filter_list].dropna().sort_values(ascending=self.ascending).index.to_list()
@ -321,6 +353,8 @@ class Trader(Account):
limit_up_list = [] # 涨停股记录 limit_up_list = [] # 涨停股记录
max_buy_num = max(0, self.num-len(last_position)+len(sell_list)) max_buy_num = max(0, self.num-len(last_position)+len(sell_list))
for stock in target_list: for stock in target_list:
if len(buy_list) == max_buy_num:
break
if stock in after_sell_list: if stock in after_sell_list:
continue continue
else: else:
@ -332,29 +366,23 @@ class Trader(Account):
if stock_status.loc[stock] in [4,6]: if stock_status.loc[stock] in [4,6]:
limit_up_list.append(stock) limit_up_list.append(stock)
buy_list.append(stock) buy_list.append(stock)
if len(buy_list) == max_buy_num: buy_list = list(set(buy_list))
break
# 剔除同时在sell_list和buy_list的股票 # 剔除同时在sell_list和buy_list的股票
duplicate_stock = set(sell_list) & set(buy_list) duplicate_stock = set(sell_list) & set(buy_list)
sell_list = list(set(sell_list) - duplicate_stock) sell_list = list(set(sell_list) - duplicate_stock)
buy_list = list(set(buy_list) - duplicate_stock) buy_list = list(set(buy_list) - duplicate_stock)
# 生成下一期持仓 # 生成下一期持仓
next_position = pd.DataFrame({'stock_code': list((set(last_position.index) - set(sell_list)) | set(buy_list))}) next_position = pd.DataFrame({'stock_code': list((set(last_position.index) - set(sell_list)) | set(buy_list))})
next_position['date'] = date next_position['date'] = date
next_position['weight'] = self.get_weight(date, self.today_position_ratio, next_position) next_position['weight'] = self.get_weight(date, self.today_position_ratio, untradable_list+limit_down_list, next_position)
# 剔除无法买入的涨停股,这部分仓位空出
next_position = next_position[~next_position['stock_code'].isin(limit_up_list)] # 剔除无法买入且不在昨日持仓中的涨停股,这部分仓位空出
next_position = next_position[~next_position['stock_code'].isin(list(set(limit_up_list)-set(last_position.index)))]
next_position['margin_trade'] = 0 next_position['margin_trade'] = 0
else: else:
# 如果有杠杆: # 如果有杠杆:
def assign_stock(normal_list, margin_list, margin_needed, stock, status):
if status == 1:
if len(margin_list) < margin_needed:
margin_list.append(stock)
else:
if len(normal_list) < self.num - margin_needed:
normal_list.append(stock)
return normal_list, margin_list
# 计算需要融资融券标的数量 # 计算需要融资融券标的数量
margin_ratio = max(self.today_position_ratio-1, 0) margin_ratio = max(self.today_position_ratio-1, 0)
margin_needed = round(self.num * margin_ratio) margin_needed = round(self.num * margin_ratio)
@ -372,7 +400,6 @@ class Trader(Account):
last_normal_list = [] last_normal_list = []
# ----- 卖出 ----- # ----- 卖出 -----
buy_list = []
sell_list = [] sell_list = []
untradable_list = [] untradable_list = []
# 分别更新融资融券池的和非融资融券池 # 分别更新融资融券池的和非融资融券池
@ -424,6 +451,8 @@ class Trader(Account):
next_normal_list = list(set(last_normal_list) - set(sell_list)) next_normal_list = list(set(last_normal_list) - set(sell_list))
# ----- 买入 ----- # ----- 买入 -----
buy_list = []
# 剔除过滤条件后可买入列表 # 剔除过滤条件后可买入列表
after_filter_list = list(set(factor.index) - set(normal_exclude) - set(force_exclude)) after_filter_list = list(set(factor.index) - set(normal_exclude) - set(force_exclude))
target_list = factor.loc[after_filter_list].dropna().sort_values(ascending=self.ascending).index.to_list() target_list = factor.loc[after_filter_list].dropna().sort_values(ascending=self.ascending).index.to_list()
@ -434,6 +463,8 @@ class Trader(Account):
# 融资融券池的和非融资融券池的分开更新 # 融资融券池的和非融资融券池的分开更新
# 更新融资融券池 # 更新融资融券池
for stock in target_list: for stock in target_list:
if len(next_margin_list) >= margin_needed:
break
if stock in after_sell_list: if stock in after_sell_list:
continue continue
else: else:
@ -446,10 +477,11 @@ class Trader(Account):
if stock_status.loc[stock] in [4,6]: if stock_status.loc[stock] in [4,6]:
limit_up_list.append(stock) limit_up_list.append(stock)
next_margin_list.append(stock) next_margin_list.append(stock)
if len(next_margin_list) >= margin_needed: next_margin_list = list(set(next_margin_list))
break
# 更新非融资融券池 # 更新非融资融券池
for stock in target_list: for stock in target_list:
if len(next_normal_list) >= self.num - len(next_margin_list):
break
if stock in (set(after_sell_list) | set(next_margin_list)): if stock in (set(after_sell_list) | set(next_margin_list)):
continue continue
else: else:
@ -461,8 +493,7 @@ class Trader(Account):
if stock_status.loc[stock] in [4,6]: if stock_status.loc[stock] in [4,6]:
limit_up_list.append(stock) limit_up_list.append(stock)
next_normal_list.append(stock) next_normal_list.append(stock)
if len(next_normal_list) >= self.num - len(next_margin_list): next_normal_list = list(set(next_normal_list))
break
next_position = pd.DataFrame({'stock_code': next_margin_list + next_normal_list}) next_position = pd.DataFrame({'stock_code': next_margin_list + next_normal_list})
next_position['date'] = date next_position['date'] = date
@ -475,6 +506,7 @@ class Trader(Account):
next_position = next_position.reset_index() next_position = next_position.reset_index()
# 剔除无法买入的涨停股,这部分仓位空出 # 剔除无法买入的涨停股,这部分仓位空出
next_position = next_position[~next_position['stock_code'].isin(limit_up_list)] next_position = next_position[~next_position['stock_code'].isin(limit_up_list)]
# 检测当前持仓是否可以交易 # 检测当前持仓是否可以交易
frozen_list = [] frozen_list = []
if len(self.position) > 0: if len(self.position) > 0:
@ -601,7 +633,8 @@ class Trader(Account):
if cur_pos['weight'].sum() == 0: if cur_pos['weight'].sum() == 0:
pnl = 0 pnl = 0
else: else:
pnl = (cur_pos['end_weight'].sum() - cur_pos['weight'].sum()) cash = 1 - cur_pos['weight'].sum()
pnl = ((cur_pos['end_weight'].sum() + cash) / (cur_pos['weight'].sum() + cash)) - 1
self.account *= 1+pnl self.account *= 1+pnl
self.account_history = self.account_history.append({ self.account_history = self.account_history.append({
'date': date, 'date': date,
@ -612,6 +645,14 @@ class Trader(Account):
}, ignore_index=True) }, ignore_index=True)
return True return True
@staticmethod
def update_by_end_weight(position):
"""
根据收盘权重计算新的个股权重
"""
cash = 1 - position['weight'].sum()
return position['end_weight'] / (cash + position['end_weight'].sum())
def update_signal(self, def update_signal(self,
date:str, date:str,
update_type='rtn'): update_type='rtn'):
@ -630,7 +671,9 @@ class Trader(Account):
self.account_history = self.account_history.query(f'date != "{date}" ', engine='python') self.account_history = self.account_history.query(f'date != "{date}" ', engine='python')
if date in self.position_history: if date in self.position_history:
self.position_history.pop(date) self.position_history.pop(date)
# 更新持仓信号 # ----- 更新当日回测数据 ------
# 更新当前日期和持仓信号
self.current_date = date
self.load_data(date, update_type) self.load_data(date, update_type)
# 更新当日持仓比例 # 更新当日持仓比例
if isinstance(self.position_ratio, float): if isinstance(self.position_ratio, float):
@ -646,13 +689,14 @@ class Trader(Account):
fee = (fee[0] + current_tax[0], fee[1] + current_tax[1]) fee = (fee[0] + current_tax[0], fee[1] + current_tax[1])
self.current_fee = fee self.current_fee = fee
# 如果当前持仓不空,添加隔夜收益,否则直接买入 # 如果当前持仓不空,添加隔夜收益,否则直接买入
position_fields = ['stock_code','date','weight','margin_trade','open','close','end_weight']
if len(self.position) == 0: if len(self.position) == 0:
cur_pos = pd.DataFrame(columns=['stock_code','date','weight','open','close','margin_trade']) cur_pos = pd.DataFrame(columns=position_fields)
else: else:
cur_pos = self.position.copy() cur_pos = self.position.copy()
# 冻结列表 # 冻结列表
frozen_list = [] frozen_list = []
# 遍历各个交易时间的信号 # ----- 遍历各个交易时间的信号 -----
for _,trade_time in enumerate(self.signal): for _,trade_time in enumerate(self.signal):
if self.check_update_status(date, trade_time): if self.check_update_status(date, trade_time):
continue continue
@ -663,6 +707,7 @@ class Trader(Account):
factor = self.signal[trade_time].loc[date] factor = self.signal[trade_time].loc[date]
# 获取当前、持仓 # 获取当前、持仓
sell_list, buy_list, frozen_list, next_position = self.get_next_position(date, factor) sell_list, buy_list, frozen_list, next_position = self.get_next_position(date, factor)
# 区分回测模型和仓位模式:回撤模式会记录收益,仓位模式只记录下一日持仓并结束计算 # 区分回测模型和仓位模式:回撤模式会记录收益,仓位模式只记录下一日持仓并结束计算
if update_type == 'position': if update_type == 'position':
self.position_history[date] = next_position.copy() self.position_history[date] = next_position.copy()
@ -676,7 +721,7 @@ class Trader(Account):
cur_pos['end_weight'] = cur_pos['weight'] * cur_pos['rtn'] cur_pos['end_weight'] = cur_pos['weight'] * cur_pos['rtn']
self.update_account(date, trade_time, cur_pos, next_position) self.update_account(date, trade_time, cur_pos, next_position)
# 更新仓位 # 更新仓位
cur_pos['weight'] = (cur_pos['end_weight'] / cur_pos['end_weight'].sum()) * cur_pos['weight'].sum() cur_pos['weight'] = self.update_by_end_weight(cur_pos)
# 调整权重:买入、卖出、仓位再平衡 # 调整权重:买入、卖出、仓位再平衡
next_position = self.reblance_weight(trade_time, cur_pos, next_position) next_position = self.reblance_weight(trade_time, cur_pos, next_position)
else: else:
@ -691,14 +736,17 @@ class Trader(Account):
# 停牌价格不变 # 停牌价格不变
cur_pos.loc[cur_pos['stock_code'].isin(frozen_list), 'close'] = cur_pos.loc[cur_pos['stock_code'].isin(frozen_list), 'open'] cur_pos.loc[cur_pos['stock_code'].isin(frozen_list), 'close'] = cur_pos.loc[cur_pos['stock_code'].isin(frozen_list), 'open']
cur_pos.loc[cur_pos['open'] == 0, 'close'] = cur_pos.loc[cur_pos['open'] == 0, 'open'] cur_pos.loc[cur_pos['open'] == 0, 'close'] = cur_pos.loc[cur_pos['open'] == 0, 'open']
# 更新当日收益
cur_pos['rtn'] = (cur_pos['close'] / cur_pos['open']) - 1 cur_pos['rtn'] = (cur_pos['close'] / cur_pos['open']) - 1
cur_pos['end_weight'] = cur_pos['weight'] * (cur_pos['rtn'] + 1) cur_pos['end_weight'] = cur_pos['weight'] * (cur_pos['rtn'] + 1)
position_record = cur_pos.copy() position_record = cur_pos.copy()
position_record['end_weight'] = (position_record['end_weight'] / position_record['end_weight'].sum()) * position_record['weight'].sum() position_record['end_weight'] = self.update_by_end_weight(position_record)
cur_pos['weight'] = (cur_pos['end_weight'] / cur_pos['end_weight'].sum()) * cur_pos['weight'].sum() self.position_history[date] = position_record.copy()[position_fields]
# 更新当期收盘后个股仓位作为下一期的开盘仓位
cur_pos['weight'] = self.update_by_end_weight(cur_pos)
next_position = cur_pos.copy()[['stock_code','date','weight','margin_trade']] next_position = cur_pos.copy()[['stock_code','date','weight','margin_trade']]
next_position['open'] = cur_pos['close'] next_position['open'] = cur_pos['close']
self.update_account(date, trade_time, cur_pos, cur_pos) self.update_account(date, trade_time, cur_pos, cur_pos)
# 记录当前时刻最终持仓和个股权重
self.position = next_position.copy() self.position = next_position.copy()
self.position_history[date] = position_record.copy()
return True return True