Files
qmt_strategy/雪球6.0/源代码/monitor_id.py
T
2025-11-06 10:26:02 +08:00

423 lines
16 KiB
Python

# 导入必要的库
import pandas as pd
import subprocess
import time
import os
from datetime import datetime
import yaml
def load_config():
"""加载配置文件"""
config_path = '参数设置.yaml'
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
else:
print(f'[警告] 配置文件 {config_path} 不存在,使用默认配置')
return {}
class XueqiuMonitor:
"""
雪球调仓监控器 - 基于记录ID去重
功能:每3秒执行xueqiu.py,计算调仓幅度,基于记录ID去重
"""
def __init__(self, config=None):
"""
初始化监控器
"""
print('[初始化] 雪球调仓监控器初始化中...')
# 加载配置文件
if config is None:
self.config = self.load_config()
else:
self.config = config
self.monitor_interval = self.config.get('监控间隔', 3)
# 从组合配置中获取组合ID列表
portfolio_configs = self.config.get('组合配置', [])
if portfolio_configs:
self.assembly_id = [config.get('组合ID') for config in portfolio_configs if config.get('组合ID')]
if not self.assembly_id:
print('[错误] 配置文件中未找到有效的组合ID')
raise ValueError("配置文件中未找到有效的组合ID")
else:
self.assembly_id = ['ZH3361149'] # 默认组合ID(修正为实际数据中的组合ID)
print('[警告] 配置文件中未找到组合配置,使用默认组合ID: ZH3361149')
# 如果只有一个组合,保持向后兼容
if len(self.assembly_id) == 1:
self.assembly_id = self.assembly_id[0]
# 初始化数据存储
self.accumulated_data = pd.DataFrame() # 累积数据
self.accumulated_data_en = pd.DataFrame() # 累积数据(英文列名)
self.processed_record_ids = set() # 已处理的记录ID集合
# 统计信息
self.execution_count = 0 # 执行次数
self.total_new_records = 0 # 累积新增记录数
print('[初始化] 雪球调仓监控器初始化完成')
print(f'[设置] 组合ID: {self.assembly_id}')
print(f'[设置] 监控间隔: {self.monitor_interval}')
print('[设置] 去重方式: 基于记录ID')
print('[设置] 调仓幅度计算: 目标权重 - 前期调整权重')
# 加载现有数据
self.load_existing_data()
def load_config(self):
"""
从YAML配置文件读取参数
"""
config = load_config()
# 设置默认值
default_config = {
'监控间隔': 3,
'累积数据文件': '累积数据_ID去重.csv',
'已处理ID文件': '已处理记录ID.txt'
}
# 合并配置
for key, value in default_config.items():
if key not in config:
config[key] = value
print(f'[配置] 成功加载配置文件')
return config
def load_existing_data(self):
"""
从原始数据.csv文件加载现有数据和已处理的记录ID
"""
try:
# 加载原始数据.csv文件
if os.path.exists('./辅助文件/数据.csv'):
self.accumulated_data = pd.read_csv('./辅助文件/数据.csv', encoding='utf-8-sig')
print(f'[加载] 已加载现有数据: {len(self.accumulated_data)} 条记录')
# 提取已有的记录ID
if '记录ID' in self.accumulated_data.columns:
self.processed_record_ids = set(self.accumulated_data['记录ID'].astype(str))
print(f'[记录] 已记录 {len(self.processed_record_ids)} 个记录ID')
# 加载英文列名版本
if os.path.exists('./辅助文件/数据_英文列名.csv'):
self.accumulated_data_en = pd.read_csv('./辅助文件/数据_英文列名.csv', encoding='utf-8-sig')
print(f'[加载] 已加载英文版本数据: {len(self.accumulated_data_en)} 条记录')
if len(self.processed_record_ids) == 0:
print('[状态] 首次运行,将获取所有历史数据')
else:
print(f'[状态] 继续监控,已处理 {len(self.processed_record_ids)} 条记录')
except Exception as e:
print(f'[错误] 加载现有数据失败: {str(e)}')
print('[继续] 将从空数据开始监控')
def execute_xueqiu_script(self):
"""
执行xueqiu.py脚本
返回: (是否成功, 错误信息)
"""
try:
# 执行xueqiu.py脚本(修正路径为源代码目录)
result = subprocess.run(['python', './源代码/xueqiu.py'],
capture_output=True,
text=True,
encoding='gbk')
if result.returncode == 0:
return True, None
else:
return False, result.stderr
except Exception as e:
return False, str(e)
def calculate_adjustment_amplitude(self, df):
"""
计算调仓幅度
调仓幅度 = 目标权重 - 前期调整权重(为空按0计算)
"""
if df.empty:
return df
df_copy = df.copy()
# 处理前期调整权重为空的情况
if '前期调整权重' in df_copy.columns:
df_copy['前期调整权重'] = df_copy['前期调整权重'].fillna(0)
else:
df_copy['前期调整权重'] = 0
# 计算调仓幅度
if '目标权重' in df_copy.columns:
df_copy['调仓幅度'] = df_copy['目标权重'] - df_copy['前期调整权重']
else:
df_copy['调仓幅度'] = 0
return df_copy
def calculate_adjustment_amplitude_en(self, df):
"""
计算调仓幅度(英文列名版本)
"""
if df.empty:
return df
df_copy = df.copy()
# 处理前期调整权重为空的情况
if 'prev_weight_adjusted' in df_copy.columns:
df_copy['prev_weight_adjusted'] = df_copy['prev_weight_adjusted'].fillna(0)
else:
df_copy['prev_weight_adjusted'] = 0
# 计算调仓幅度
if 'target_weight' in df_copy.columns:
df_copy['adjustment_amplitude'] = df_copy['target_weight'] - df_copy['prev_weight_adjusted']
else:
df_copy['adjustment_amplitude'] = 0
return df_copy
def filter_new_data(self, new_data):
"""
基于记录ID筛选新数据
"""
if new_data.empty or '记录ID' not in new_data.columns:
return pd.DataFrame()
# 转换记录ID为字符串进行比较
new_data['记录ID_str'] = new_data['记录ID'].astype(str)
# 筛选出未处理过的记录ID
filtered_data = new_data[~new_data['记录ID_str'].isin(self.processed_record_ids)]
# 删除临时列
if len(filtered_data) > 0:
filtered_data = filtered_data.drop('记录ID_str', axis=1)
return filtered_data
def filter_new_data_en(self, new_data_en):
"""
基于记录ID筛选新数据(英文列名版本)
"""
if new_data_en.empty or 'id' not in new_data_en.columns:
return pd.DataFrame()
# 转换记录ID为字符串进行比较
new_data_en['id_str'] = new_data_en['id'].astype(str)
# 筛选出未处理过的记录ID
filtered_data_en = new_data_en[~new_data_en['id_str'].isin(self.processed_record_ids)]
# 删除临时列
if len(filtered_data_en) > 0:
filtered_data_en = filtered_data_en.drop('id_str', axis=1)
return filtered_data_en
def save_accumulated_data(self):
"""
直接更新原始数据.csv文件,不创建新的累积数据文件
"""
try:
# 直接更新原始数据.csv文件
if not self.accumulated_data.empty:
self.accumulated_data.to_csv('./辅助文件/数据.csv', index=False, encoding='utf-8-sig')
print(f'[保存] 数据已更新到原始文件: ./辅助文件/数据.csv ({len(self.accumulated_data)} 条记录)')
# 保存英文列名版本(如果需要)
if not self.accumulated_data_en.empty:
self.accumulated_data_en.to_csv('./辅助文件/数据_英文列名.csv', index=False, encoding='utf-8-sig')
print(f'[保存] 英文版本数据已保存: ./辅助文件/数据_英文列名.csv ({len(self.accumulated_data_en)} 条记录)')
except Exception as e:
print(f'[错误] 保存数据失败: {str(e)}')
def process_new_data(self):
"""
处理新获取的数据
"""
try:
# 读取xueqiu.py生成的数据文件
if not os.path.exists('./辅助文件/数据.csv'):
print('[警告] 未找到数据.csv文件')
return
# 读取新数据
new_data = pd.read_csv('./辅助文件/数据.csv', encoding='utf-8-sig')
new_data_en = pd.read_csv('./辅助文件/数据_英文列名.csv', encoding='utf-8-sig') if os.path.exists('./辅助文件/数据_英文列名.csv') else pd.DataFrame()
print(f'[读取] 本次获取 {len(new_data)} 条记录')
# 添加组合ID列
if '组合ID' not in new_data.columns:
new_data['组合ID'] = self.assembly_id
if len(new_data_en) > 0 and 'assembly_id' not in new_data_en.columns:
new_data_en['assembly_id'] = self.assembly_id
# 计算调仓幅度
new_data = self.calculate_adjustment_amplitude(new_data)
new_data_en = self.calculate_adjustment_amplitude_en(new_data_en) if len(new_data_en) > 0 else pd.DataFrame()
print(f'[计算] 已计算调仓幅度,共 {len(new_data)} 条记录')
# 筛选新数据(基于记录ID去重)
filtered_data = self.filter_new_data(new_data)
filtered_data_en = self.filter_new_data_en(new_data_en) if len(new_data_en) > 0 else pd.DataFrame()
if len(filtered_data) > 0:
# 添加新记录到累积数据
if self.accumulated_data.empty:
self.accumulated_data = filtered_data.copy()
else:
self.accumulated_data = pd.concat([self.accumulated_data, filtered_data], ignore_index=True)
# 添加英文版本数据
if len(filtered_data_en) > 0:
if self.accumulated_data_en.empty:
self.accumulated_data_en = filtered_data_en.copy()
else:
self.accumulated_data_en = pd.concat([self.accumulated_data_en, filtered_data_en], ignore_index=True)
# 更新已处理的记录ID集合
if '记录ID' in filtered_data.columns:
new_ids = set(filtered_data['记录ID'].astype(str))
self.processed_record_ids.update(new_ids)
new_records_count = len(filtered_data)
self.total_new_records += new_records_count
print(f'[新增] 发现 {new_records_count} 条新记录')
# 显示新增记录的详细信息
for _, row in filtered_data.iterrows():
adjustment = row.get('调仓幅度', 0)
print(f' - {row["股票名称"]} ({row["股票代码"]}): 调仓幅度 {adjustment:.2f}%')
# 保存累积数据
self.save_accumulated_data()
else:
print('[检查] 未发现新记录(所有记录ID已存在)')
except Exception as e:
print(f'[异常] 处理新数据时发生错误: {str(e)}')
def print_status(self):
"""
打印监控状态信息
"""
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f'\n[状态] 监控状态 [{current_time}]')
print(f' [周期] 执行周期: {self.execution_count}')
print(f' [新增] 本次新增: {self.total_new_records - (0 if self.execution_count == 1 else getattr(self, "prev_total", 0))}')
print(f' [累积] 累积记录: {len(self.accumulated_data)}')
print(f' [ID数] 已处理ID: {len(self.processed_record_ids)}')
print('-' * 50)
# 保存当前总数用于下次计算新增
self.prev_total = self.total_new_records
def run(self):
"""
启动监控循环
"""
print('[启动] 开始监控雪球调仓数据...')
print('[提示] 按 Ctrl+C 停止监控')
print('[间隔] 每 3 秒执行一次')
print('=' * 50)
try:
while True:
self.execution_count += 1
current_time = datetime.now().strftime('%H:%M:%S')
print(f'\n[执行] 第 {self.execution_count} 次执行 [{current_time}]')
# 执行xueqiu.py脚本
success, error = self.execute_xueqiu_script()
if success:
print('[成功] xueqiu.py 执行成功')
# 处理新数据
self.process_new_data()
else:
print(f'[失败] xueqiu.py 执行失败: {error}')
# 打印状态信息
self.print_status()
# 等待指定间隔时间
print(f'[等待] 等待 {self.monitor_interval} 秒...')
time.sleep(self.monitor_interval)
except KeyboardInterrupt:
print('\n[停止] 监控已停止')
print(f'[统计] 总执行次数: {self.execution_count}')
print(f'[统计] 累积记录数: {len(self.accumulated_data)}')
print(f'[统计] 已处理ID数: {len(self.processed_record_ids)}')
# 最后保存一次数据
if not self.accumulated_data.empty:
self.save_accumulated_data()
print('[保存] 数据已保存完成')
print('[完成] 程序已安全退出')
except Exception as e:
print(f'[异常] 程序异常: {str(e)}')
# 异常时也要保存数据
if not self.accumulated_data.empty:
self.save_accumulated_data()
print('[保存] 异常退出前已保存数据')
def main():
# 加载配置
config = load_config()
# 获取配置参数
portfolio_configs = config.get('组合配置', [])
if not portfolio_configs:
print('[错误] 配置文件中未找到组合配置')
return
assembly_id = portfolio_configs[0].get('组合ID')
if not assembly_id:
print('[错误] 配置文件中未找到有效的组合ID')
return
monitor_interval = config.get('监控间隔', 3)
print(f'[启动] 雪球组合监控程序')
print(f'[配置] 组合ID: {assembly_id}')
print(f'[配置] 监控间隔: {monitor_interval}')
# 创建监控对象
monitor = XueqiuMonitor(config)
# 开始监控
monitor.run()
# 主程序入口
if __name__ == '__main__':
print('[程序] 雪球调仓监控器 - 基于记录ID去重版本')
print('[版本] v1.0 - ID去重 + 调仓幅度计算')
print('=' * 60)
# 启动主函数
main()