Update:自定义权重支持不满仓的情况;Update:增加自定义权重总和判断;
Bug:当换仓数量大于持仓数量时全卖出和全买入导致的持仓计算问题‘;
This commit is contained in:
parent
b408d22798
commit
10e926bf28
132
trader.py
132
trader.py
|
@ -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,20 +293,33 @@ 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:
|
else:
|
||||||
if stock in force_exclude:
|
if last_position_status.loc[stock] in [5,7]:
|
||||||
sell_list.append(stock)
|
continue
|
||||||
|
else:
|
||||||
|
if stock in force_exclude:
|
||||||
|
sell_list.append(stock)
|
||||||
force_sell_num = len(sell_list)
|
force_sell_num = len(sell_list)
|
||||||
|
|
||||||
# 剔除无法交易列表后,按照当日因子反向排名逐个卖出
|
# 剔除无法交易列表后,按照当日因子反向排名逐个卖出
|
||||||
|
@ -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
|
Loading…
Reference in New Issue