From 10e926bf288743e93dc92758ce4e8d3a437bd4f4 Mon Sep 17 00:00:00 2001 From: binz <123@123.com> Date: Sat, 8 Jun 2024 19:12:23 +0800 Subject: [PATCH] =?UTF-8?q?Update=EF=BC=9A=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E6=9D=83=E9=87=8D=E6=94=AF=E6=8C=81=E4=B8=8D=E6=BB=A1=E4=BB=93?= =?UTF-8?q?=E7=9A=84=E6=83=85=E5=86=B5=EF=BC=9BUpdate=EF=BC=9A=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E8=87=AA=E5=AE=9A=E4=B9=89=E6=9D=83=E9=87=8D=E6=80=BB?= =?UTF-8?q?=E5=92=8C=E5=88=A4=E6=96=AD;=20Bug=EF=BC=9A=E5=BD=93=E6=8D=A2?= =?UTF-8?q?=E4=BB=93=E6=95=B0=E9=87=8F=E5=A4=A7=E4=BA=8E=E6=8C=81=E4=BB=93?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E6=97=B6=E5=85=A8=E5=8D=96=E5=87=BA=E5=92=8C?= =?UTF-8?q?=E5=85=A8=E4=B9=B0=E5=85=A5=E5=AF=BC=E8=87=B4=E7=9A=84=E6=8C=81?= =?UTF-8?q?=E4=BB=93=E8=AE=A1=E7=AE=97=E9=97=AE=E9=A2=98=E2=80=98=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader.py | 132 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 42 deletions(-) diff --git a/trader.py b/trader.py index 7cf45ed..277841a 100644 --- a/trader.py +++ b/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() - def get_weight(self, date, account_weight, next_position): + def get_weight(self, date, account_weight, untradable_list, next_position): """ 计算个股仓位 Args: + untradable_list (list): 无法交易列表 account_weight (float): 总权重,即当前持仓比例 """ if isinstance(self.weight, str): @@ -221,14 +222,26 @@ class Trader(Account): return account_weight / len(next_position) if isinstance(self.weight, pd.DataFrame): date_weight = self.weight.loc[date].dropna().sort_index() + # untradable_list不要求指定权重用昨日权重填充 + weight_list = pd.Series(index=next_position['stock_code']) try: - weight_list = date_weight.loc[next_position['stock_code'].to_list()].values - if weight_list.sum() > 1 + 1e5: # 防止数据精度的影响,给与一定的宽松 - raise Exception(f"total weight of {date} is larger then 1.") - weight_list = account_weight * weight_list - return weight_list + # 填充untradable_list权重 + if len(untradable_list) > 0: + weight_list.loc[untradable_list] = self.position.set_index('stock_code').loc[untradable_list, 'weight'] 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): """ @@ -280,20 +293,33 @@ class Trader(Account): 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 = [] - target_list = [] + # ----- 卖出 ----- - # 异常强制卖出 + sell_list = [] + limit_down_list = [] # 跌停股记录 + + # 遍历昨日持仓状态: + # 1 记录持仓状态 + # 2 获取停牌股列表 + # 3 获取异常强制卖出列表 + last_position_status = pd.Series() 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) else: - if stock in force_exclude: - sell_list.append(stock) + if last_position_status.loc[stock] in [5,7]: + continue + else: + if stock in force_exclude: + sell_list.append(stock) 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]: if len(sell_list) >= max_sell_num + force_sell_num: break - if stock_status.loc[stock] in [0,2,5,7]: + if last_position_status.loc[stock] in [0,2]: continue else: + if last_position_status.loc[stock] in [5,7]: + limit_down_list.append(stock) sell_list.append(stock) 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)) 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 = [] # 涨停股记录 max_buy_num = max(0, self.num-len(last_position)+len(sell_list)) for stock in target_list: + if len(buy_list) == max_buy_num: + break if stock in after_sell_list: continue else: @@ -332,29 +366,23 @@ class Trader(Account): if stock_status.loc[stock] in [4,6]: limit_up_list.append(stock) buy_list.append(stock) - if len(buy_list) == max_buy_num: - break + buy_list = list(set(buy_list)) + # 剔除同时在sell_list和buy_list的股票 duplicate_stock = set(sell_list) & set(buy_list) sell_list = list(set(sell_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['date'] = date - next_position['weight'] = self.get_weight(date, self.today_position_ratio, next_position) - # 剔除无法买入的涨停股,这部分仓位空出 - next_position = next_position[~next_position['stock_code'].isin(limit_up_list)] + 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(list(set(limit_up_list)-set(last_position.index)))] next_position['margin_trade'] = 0 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_needed = round(self.num * margin_ratio) @@ -372,7 +400,6 @@ class Trader(Account): last_normal_list = [] # ----- 卖出 ----- - buy_list = [] sell_list = [] untradable_list = [] # 分别更新融资融券池的和非融资融券池 @@ -424,6 +451,8 @@ class Trader(Account): 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)) 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: + if len(next_margin_list) >= margin_needed: + break if stock in after_sell_list: continue else: @@ -446,10 +477,11 @@ class Trader(Account): if stock_status.loc[stock] in [4,6]: limit_up_list.append(stock) next_margin_list.append(stock) - if len(next_margin_list) >= margin_needed: - break + next_margin_list = list(set(next_margin_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)): continue else: @@ -461,8 +493,7 @@ class Trader(Account): if stock_status.loc[stock] in [4,6]: limit_up_list.append(stock) next_normal_list.append(stock) - if len(next_normal_list) >= self.num - len(next_margin_list): - break + next_normal_list = list(set(next_normal_list)) next_position = pd.DataFrame({'stock_code': next_margin_list + next_normal_list}) next_position['date'] = date @@ -475,6 +506,7 @@ class Trader(Account): next_position = next_position.reset_index() # 剔除无法买入的涨停股,这部分仓位空出 next_position = next_position[~next_position['stock_code'].isin(limit_up_list)] + # 检测当前持仓是否可以交易 frozen_list = [] if len(self.position) > 0: @@ -601,7 +633,8 @@ class Trader(Account): if cur_pos['weight'].sum() == 0: pnl = 0 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_history = self.account_history.append({ 'date': date, @@ -612,6 +645,14 @@ class Trader(Account): }, ignore_index=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, date:str, update_type='rtn'): @@ -630,7 +671,9 @@ class Trader(Account): self.account_history = self.account_history.query(f'date != "{date}" ', engine='python') if date in self.position_history: self.position_history.pop(date) - # 更新持仓信号 + # ----- 更新当日回测数据 ------ + # 更新当前日期和持仓信号 + self.current_date = date self.load_data(date, update_type) # 更新当日持仓比例 if isinstance(self.position_ratio, float): @@ -646,13 +689,14 @@ class Trader(Account): fee = (fee[0] + current_tax[0], fee[1] + current_tax[1]) self.current_fee = fee # 如果当前持仓不空,添加隔夜收益,否则直接买入 + position_fields = ['stock_code','date','weight','margin_trade','open','close','end_weight'] 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: cur_pos = self.position.copy() # 冻结列表 frozen_list = [] - # 遍历各个交易时间的信号 + # ----- 遍历各个交易时间的信号 ----- for _,trade_time in enumerate(self.signal): if self.check_update_status(date, trade_time): continue @@ -663,6 +707,7 @@ class Trader(Account): factor = self.signal[trade_time].loc[date] # 获取当前、持仓 sell_list, buy_list, frozen_list, next_position = self.get_next_position(date, factor) + # 区分回测模型和仓位模式:回撤模式会记录收益,仓位模式只记录下一日持仓并结束计算 if update_type == 'position': self.position_history[date] = next_position.copy() @@ -676,7 +721,7 @@ class Trader(Account): cur_pos['end_weight'] = cur_pos['weight'] * cur_pos['rtn'] 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) 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['open'] == 0, 'close'] = cur_pos.loc[cur_pos['open'] == 0, 'open'] + # 更新当日收益 cur_pos['rtn'] = (cur_pos['close'] / cur_pos['open']) - 1 cur_pos['end_weight'] = cur_pos['weight'] * (cur_pos['rtn'] + 1) position_record = cur_pos.copy() - position_record['end_weight'] = (position_record['end_weight'] / position_record['end_weight'].sum()) * position_record['weight'].sum() - cur_pos['weight'] = (cur_pos['end_weight'] / cur_pos['end_weight'].sum()) * cur_pos['weight'].sum() + position_record['end_weight'] = self.update_by_end_weight(position_record) + 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['open'] = cur_pos['close'] self.update_account(date, trade_time, cur_pos, cur_pos) + # 记录当前时刻最终持仓和个股权重 self.position = next_position.copy() - self.position_history[date] = position_record.copy() return True \ No newline at end of file