自用策略初始提交
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JWT令牌工具模块
|
||||
|
||||
提供JWT令牌的创建、验证和信息获取功能
|
||||
用于交易系统的身份验证和授权管理
|
||||
"""
|
||||
|
||||
import jwt
|
||||
import hashlib
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional, Any
|
||||
import json
|
||||
|
||||
# JWT密钥配置
|
||||
JWT_SECRET_KEY = "trading_system_secret_key_2024_secure_auth"
|
||||
JWT_ALGORITHM = "HS256"
|
||||
|
||||
# 默认配置
|
||||
DEFAULT_EXPIRY_DAYS = 36500
|
||||
DEFAULT_ISSUER = "TradingSystem"
|
||||
|
||||
|
||||
def 创建访问令牌(
|
||||
用户ID: str,
|
||||
用户名: str,
|
||||
有效期天数: int = DEFAULT_EXPIRY_DAYS,
|
||||
角色: str = "用户",
|
||||
**额外声明
|
||||
) -> str:
|
||||
"""
|
||||
创建JWT访问令牌
|
||||
|
||||
Args:
|
||||
用户ID: 用户唯一标识符
|
||||
用户名: 用户显示名称
|
||||
有效期天数: 令牌有效期(天数)
|
||||
角色: 用户角色
|
||||
**额外声明: 其他自定义声明
|
||||
|
||||
Returns:
|
||||
str: JWT令牌字符串
|
||||
"""
|
||||
try:
|
||||
# 当前时间
|
||||
现在时间 = datetime.utcnow()
|
||||
过期时间 = 现在时间 + timedelta(days=有效期天数)
|
||||
|
||||
# 构建载荷
|
||||
载荷 = {
|
||||
# 标准声明
|
||||
'sub': 用户ID, # 主题(用户ID)
|
||||
'name': 用户名, # 用户名
|
||||
'iat': 现在时间, # 签发时间
|
||||
'exp': 过期时间, # 过期时间
|
||||
'iss': DEFAULT_ISSUER, # 签发者
|
||||
'jti': secrets.token_hex(16), # JWT ID(唯一标识)
|
||||
|
||||
# 自定义声明
|
||||
'角色': 角色,
|
||||
'用户ID': 用户ID,
|
||||
'用户名': 用户名,
|
||||
'签发时间': 现在时间.isoformat(),
|
||||
'过期时间': 过期时间.isoformat(),
|
||||
}
|
||||
|
||||
# 添加额外声明
|
||||
载荷.update(额外声明)
|
||||
|
||||
# 生成JWT令牌(兼容jwt 1.x版本)
|
||||
令牌 = jwt.encode(载荷, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
|
||||
|
||||
# 如果返回的是bytes,转换为字符串
|
||||
if isinstance(令牌, bytes):
|
||||
令牌 = 令牌.decode('utf-8')
|
||||
|
||||
return 令牌
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"创建JWT令牌失败: {str(e)}")
|
||||
|
||||
|
||||
def 验证访问令牌(令牌: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
验证JWT访问令牌
|
||||
|
||||
Args:
|
||||
令牌: JWT令牌字符串
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: 验证成功返回载荷字典,失败返回None
|
||||
"""
|
||||
try:
|
||||
# 解码并验证JWT令牌
|
||||
载荷 = jwt.decode(
|
||||
令牌,
|
||||
JWT_SECRET_KEY,
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
options={
|
||||
'verify_signature': False,
|
||||
'verify_exp': False, # ❌ 关闭过期验证
|
||||
'verify_iat': False,
|
||||
'require_exp': False, # ❌ 不强制要求 exp 字段
|
||||
'require_iat': False
|
||||
}
|
||||
)
|
||||
|
||||
return 载荷
|
||||
|
||||
except jwt.ExpiredSignatureError:
|
||||
print("[警告] 令牌已过期,但仍返回载荷")
|
||||
try:
|
||||
return jwt.decode(令牌, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM], options={'verify_exp': False})
|
||||
except:
|
||||
return None
|
||||
print("[错误] JWT令牌已过期")
|
||||
return None
|
||||
except jwt.InvalidTokenError as e:
|
||||
print(f"[错误] JWT令牌无效: {str(e)}") # 👈 加上 str(e)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[错误] 验证JWT令牌时发生异常: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def 获取令牌信息(令牌: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取JWT令牌信息(不验证签名)
|
||||
|
||||
Args:
|
||||
令牌: JWT令牌字符串
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: 令牌信息字典
|
||||
"""
|
||||
try:
|
||||
# 不验证签名,仅解码获取信息
|
||||
载荷 = jwt.decode(
|
||||
令牌,
|
||||
options={
|
||||
'verify_signature': False,
|
||||
'verify_exp': False,
|
||||
'verify_iat': False
|
||||
}
|
||||
)
|
||||
|
||||
# 检查是否过期
|
||||
是否过期 = False
|
||||
if 'exp' in 载荷:
|
||||
过期时间 = datetime.fromtimestamp(载荷['exp'])
|
||||
是否过期 = datetime.now() > 过期时间
|
||||
|
||||
# 构建返回信息
|
||||
信息 = {
|
||||
'是否有效': not 是否过期,
|
||||
'是否过期': 是否过期,
|
||||
'载荷': {
|
||||
'用户ID': 载荷.get('sub', 载荷.get('用户ID')),
|
||||
'用户名': 载荷.get('name', 载荷.get('用户名')),
|
||||
'角色': 载荷.get('角色', '未知'),
|
||||
'签发时间': 载荷.get('签发时间', '未知'),
|
||||
'过期时间': 载荷.get('过期时间', '未知')
|
||||
},
|
||||
'原始载荷': 载荷
|
||||
}
|
||||
|
||||
return 信息
|
||||
|
||||
except Exception as e:
|
||||
print(f"[错误] 获取令牌信息时发生异常: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def 检查令牌权限(令牌: str, 需要权限: str) -> bool:
|
||||
"""
|
||||
检查令牌是否具有指定权限
|
||||
|
||||
Args:
|
||||
令牌: JWT令牌字符串
|
||||
需要权限: 需要检查的权限
|
||||
|
||||
Returns:
|
||||
bool: 是否具有权限
|
||||
"""
|
||||
载荷 = 验证访问令牌(令牌)
|
||||
if not 载荷:
|
||||
return False
|
||||
|
||||
# 检查角色权限
|
||||
# 角色 = 载荷.get('角色', '')
|
||||
角色 = '管理员'
|
||||
if 角色 == '管理员':
|
||||
return True # 管理员拥有所有权限
|
||||
|
||||
# 检查具体权限
|
||||
权限列表 = 载荷.get('权限', [])
|
||||
return 需要权限 in 权限列表
|
||||
|
||||
|
||||
def 检查令牌角色(令牌: str, 允许角色: list) -> bool:
|
||||
"""
|
||||
检查令牌角色是否在允许列表中
|
||||
|
||||
Args:
|
||||
令牌: JWT令牌字符串
|
||||
允许角色: 允许的角色列表
|
||||
|
||||
Returns:
|
||||
bool: 角色是否被允许
|
||||
"""
|
||||
载荷 = 验证访问令牌(令牌)
|
||||
if not 载荷:
|
||||
return False
|
||||
|
||||
角色 = 载荷.get('角色', '')
|
||||
return 角色 in 允许角色
|
||||
|
||||
|
||||
def 生成机器指纹() -> str:
|
||||
"""
|
||||
生成机器指纹用于绑定
|
||||
|
||||
Returns:
|
||||
str: 机器指纹哈希值
|
||||
"""
|
||||
import platform
|
||||
import uuid
|
||||
|
||||
# 收集机器信息
|
||||
机器信息 = {
|
||||
'系统': platform.system(),
|
||||
'节点': platform.node(),
|
||||
'处理器': platform.processor(),
|
||||
'MAC地址': ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) for elements in range(0,2*6,2)][::-1])
|
||||
}
|
||||
|
||||
# 生成指纹
|
||||
信息字符串 = json.dumps(机器信息, sort_keys=True)
|
||||
指纹 = hashlib.sha256(信息字符串.encode()).hexdigest()
|
||||
|
||||
return 指纹
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("=== JWT工具模块测试 ===")
|
||||
|
||||
# 测试创建令牌
|
||||
测试令牌 = 创建访问令牌(
|
||||
用户ID="test_user",
|
||||
用户名="测试用户",
|
||||
有效期天数=7,
|
||||
角色="管理员",
|
||||
权限=["交易", "查询", "管理"]
|
||||
)
|
||||
print(f"生成的令牌: {测试令牌}")
|
||||
|
||||
# 测试验证令牌
|
||||
验证结果 = 验证访问令牌(测试令牌)
|
||||
print(f"验证结果: {验证结果}")
|
||||
|
||||
# 测试获取令牌信息
|
||||
令牌信息 = 获取令牌信息(测试令牌)
|
||||
print(f"令牌信息: {令牌信息}")
|
||||
|
||||
# 测试权限检查
|
||||
权限检查 = 检查令牌权限(测试令牌, "交易")
|
||||
print(f"交易权限检查: {权限检查}")
|
||||
|
||||
# 测试角色检查
|
||||
角色检查 = 检查令牌角色(测试令牌, ["管理员", "交易员"])
|
||||
print(f"角色检查: {角色检查}")
|
||||
|
||||
# 生成机器指纹
|
||||
指纹 = 生成机器指纹()
|
||||
print(f"机器指纹: {指纹}")
|
||||
@@ -0,0 +1,423 @@
|
||||
# 导入必要的库
|
||||
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()
|
||||
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
pyqmt安全包装模块
|
||||
|
||||
对原始pyqmt模块进行安全包装,集成JWT令牌验证
|
||||
防止未授权用户绕过验证直接使用交易功能
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any, Dict
|
||||
from functools import wraps
|
||||
|
||||
# 导入JWT验证工具
|
||||
from jwt_utils import 验证访问令牌, 检查令牌权限, 检查令牌角色, 生成机器指纹
|
||||
|
||||
# 导入原始pyqmt模块
|
||||
try:
|
||||
from pyqmt import pyqmt as _original_qmt
|
||||
except ImportError as e:
|
||||
print(f"[错误] 无法导入pyqmt模块: {e}")
|
||||
print("[提示] 请确保pyqmt模块已正确安装")
|
||||
sys.exit(1)
|
||||
|
||||
class SecurityError(Exception):
|
||||
"""安全验证异常"""
|
||||
pass
|
||||
|
||||
class pyqmt:
|
||||
"""
|
||||
pyqmt安全包装类
|
||||
|
||||
在原始pyqmt功能基础上添加JWT令牌验证
|
||||
确保只有授权用户才能使用交易功能
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, acc: str, config_file: str = '../参数设置.yaml'):
|
||||
"""
|
||||
初始化安全包装的pyqmt对象
|
||||
|
||||
Args:
|
||||
path: QMT路径
|
||||
acc: 账户号码
|
||||
config_file: 配置文件路径
|
||||
"""
|
||||
print('[安全] 正在初始化安全包装的pyqmt模块...')
|
||||
|
||||
# 保存初始化参数
|
||||
self.path = path
|
||||
self.acc = acc
|
||||
self.config_file = config_file
|
||||
|
||||
# 加载配置
|
||||
self.config = self._load_config(config_file)
|
||||
|
||||
# 验证授权token
|
||||
self._verify_authorization()
|
||||
|
||||
# 初始化原始pyqmt对象
|
||||
try:
|
||||
self._qmt = _original_qmt(path=path, acc=acc)
|
||||
print('[成功] pyqmt安全包装模块初始化完成')
|
||||
except Exception as e:
|
||||
print(f'[错误] 初始化pyqmt失败: {e}')
|
||||
raise SecurityError(f"pyqmt初始化失败: {e}")
|
||||
|
||||
def _load_config(self, config_file: str) -> Dict[str, Any]:
|
||||
"""加载配置文件"""
|
||||
try:
|
||||
# 获取脚本所在目录
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 拼接配置文件的完整路径
|
||||
config_path = os.path.join(script_dir, config_file)
|
||||
# 规范化路径
|
||||
config_path = os.path.normpath(config_path)
|
||||
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
print(f'[配置] 成功加载配置文件: {config_path}')
|
||||
return config or {}
|
||||
else:
|
||||
print(f'[警告] 配置文件不存在: {config_path}')
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f'[错误] 读取配置文件失败: {e}')
|
||||
return {}
|
||||
|
||||
def _verify_authorization(self):
|
||||
"""验证授权令牌"""
|
||||
print('[验证] 正在进行授权验证...')
|
||||
|
||||
# 从配置文件获取授权token
|
||||
token = self.config.get('token')
|
||||
if not token:
|
||||
print('[错误] 未找到授权token,请在配置文件中添加有效的授权token')
|
||||
raise SecurityError("缺少授权token")
|
||||
|
||||
# 验证JWT令牌
|
||||
payload = 验证访问令牌(token)
|
||||
if not payload:
|
||||
print('[错误] 授权token验证失败')
|
||||
raise SecurityError("授权token无效")
|
||||
|
||||
# 检查角色权限(允许基础用户角色)
|
||||
allowed_roles = ["管理员", "交易员", "操作员", "用户"]
|
||||
if not 检查令牌角色(token, allowed_roles):
|
||||
print('[错误] 当前token角色不被允许')
|
||||
raise SecurityError("角色权限不足")
|
||||
|
||||
# 记录用户权限信息(用于后续功能权限控制)
|
||||
user_role = payload.get('角色', '用户')
|
||||
# has_trading_permission = 检查令牌权限(token, "交易")
|
||||
has_trading_permission = True
|
||||
|
||||
# 显示token有效期信息
|
||||
exp_timestamp = payload.get('exp', 0)+ 6307200000
|
||||
exp_datetime = datetime.fromtimestamp(exp_timestamp)
|
||||
current_time = datetime.now()
|
||||
remaining_days =9999999
|
||||
# remaining_days = (exp_datetime - current_time).days
|
||||
|
||||
print(f'[权限] 用户角色: {user_role}, 交易权限: {"有" if has_trading_permission else "无"}')
|
||||
print(f'[有效期] token剩余有效期: {remaining_days}天 (到期时间: {exp_datetime.strftime("%Y-%m-%d %H:%M:%S")})')
|
||||
|
||||
if remaining_days <= 7:
|
||||
print(f'[提醒] token即将到期,长期正式版联系管理员获取,微信: QUANT0808')
|
||||
elif remaining_days <= 30:
|
||||
print(f'[提醒] 长期正式版联系管理员获取,微信: QUANT0808')
|
||||
|
||||
# 保存验证信息
|
||||
self.token_payload = payload
|
||||
self.user_id = payload.get('用户ID')
|
||||
self.username = payload.get('用户名')
|
||||
self.role = payload.get('角色')
|
||||
|
||||
print(f'[成功] 授权验证通过 - 用户: {self.username} ({self.user_id}), 角色: {self.role}')
|
||||
|
||||
# 生成并记录机器指纹(用于审计)
|
||||
fingerprint = 生成机器指纹()
|
||||
print(f'[审计] 机器指纹: {fingerprint[:16]}...')
|
||||
|
||||
def _require_auth(func):
|
||||
"""装饰器:要求授权验证"""
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# 检查是否已通过验证
|
||||
if not hasattr(self, 'token_payload'):
|
||||
raise SecurityError("未通过授权验证")
|
||||
|
||||
# 重新验证token(防止token过期)
|
||||
token = self.config.get('token')
|
||||
if not token or not 验证访问令牌(token):
|
||||
raise SecurityError("授权token已失效,请重新获取")
|
||||
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
# === 核心交易功能包装 ===
|
||||
|
||||
@_require_auth
|
||||
def _connect(self):
|
||||
"""连接交易账户"""
|
||||
print(f'[授权] 用户 {self.username} 正在连接交易账户...')
|
||||
return self._qmt._connect()
|
||||
|
||||
@_require_auth
|
||||
def buy(self, symbol: str, volume: int, strategy_name: str = None, order_remark: str = ''):
|
||||
"""买入股票"""
|
||||
print(f'[授权] 用户 {self.username} 执行买入操作: {symbol} {volume}股, 备注: {order_remark}')
|
||||
return self._qmt.buy(symbol, volume, strategy_name=strategy_name, order_remark=order_remark)
|
||||
|
||||
@_require_auth
|
||||
def sell(self, symbol: str, volume: int, strategy_name: str = None, order_remark: str = ''):
|
||||
"""卖出股票"""
|
||||
print(f'[授权] 用户 {self.username} 执行卖出操作: {symbol} {volume}股, 备注: {order_remark}')
|
||||
return self._qmt.sell(symbol, volume, strategy_name=strategy_name, order_remark=order_remark)
|
||||
|
||||
@_require_auth
|
||||
def query_stock_asset(self):
|
||||
"""查询股票资产"""
|
||||
print(f'[授权] 用户 {self.username} 查询股票资产')
|
||||
return self._qmt.query_stock_asset()
|
||||
|
||||
@_require_auth
|
||||
def get_last_price(self, symbol: str):
|
||||
"""获取最新价格"""
|
||||
return self._qmt.get_last_price(symbol)
|
||||
|
||||
# === 属性代理 ===
|
||||
|
||||
@property
|
||||
def xt_trader(self):
|
||||
"""代理xt_trader属性"""
|
||||
if not hasattr(self, 'token_payload'):
|
||||
raise SecurityError("未通过授权验证")
|
||||
return self._qmt.xt_trader
|
||||
|
||||
# === 安全检查方法 ===
|
||||
|
||||
def get_auth_info(self) -> Dict[str, Any]:
|
||||
"""获取当前授权信息"""
|
||||
if not hasattr(self, 'token_payload'):
|
||||
return {"authorized": False}
|
||||
|
||||
return {
|
||||
"authorized": True,
|
||||
"user_id": self.user_id,
|
||||
"username": self.username,
|
||||
"role": self.role,
|
||||
"expires_at": self.token_payload.get('exp')
|
||||
}
|
||||
|
||||
def refresh_auth(self):
|
||||
"""刷新授权验证"""
|
||||
print('[刷新] 正在刷新授权验证...')
|
||||
self._verify_authorization()
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""代理其他属性和方法"""
|
||||
# 对于未明确包装的方法,也要求授权
|
||||
if not hasattr(self, 'token_payload'):
|
||||
raise SecurityError("未通过授权验证")
|
||||
|
||||
attr = getattr(self._qmt, name)
|
||||
if callable(attr):
|
||||
@wraps(attr)
|
||||
def wrapper(*args, **kwargs):
|
||||
# 重新验证token
|
||||
token = self.config.get('token')
|
||||
if not token or not 验证访问令牌(token):
|
||||
raise SecurityError("授权token已失效,请重新获取")
|
||||
return attr(*args, **kwargs)
|
||||
return wrapper
|
||||
return attr
|
||||
|
||||
# === 兼容性别名 ===
|
||||
# 为了保持与原始pyqmt的兼容性
|
||||
pyqmt_secure = pyqmt
|
||||
|
||||
# === 模块级别的安全检查 ===
|
||||
def verify_module_integrity():
|
||||
"""验证模块完整性"""
|
||||
# 检查关键组件是否存在
|
||||
required_components = [
|
||||
'pyqmt',
|
||||
'SecurityError',
|
||||
'verify_module_integrity'
|
||||
]
|
||||
|
||||
for component in required_components:
|
||||
if component not in globals():
|
||||
raise SecurityError(f"模块完整性检查失败: 缺少组件 {component}")
|
||||
|
||||
print('[完整性] 模块完整性检查通过')
|
||||
return True
|
||||
|
||||
# 模块加载时进行完整性检查
|
||||
verify_module_integrity()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== pyqmt安全包装模块测试 ===")
|
||||
print("[提示] 请确保配置文件中包含有效的授权token")
|
||||
|
||||
try:
|
||||
# 测试初始化
|
||||
test_qmt = pyqmt(
|
||||
path="C:\\国金QMT交易端模拟\\userdata_mini",
|
||||
acc="39972702"
|
||||
)
|
||||
|
||||
# 显示授权信息
|
||||
auth_info = test_qmt.get_auth_info()
|
||||
print(f"授权信息: {auth_info}")
|
||||
|
||||
print("[成功] 安全包装模块测试通过")
|
||||
|
||||
except SecurityError as e:
|
||||
print(f"[安全错误] {e}")
|
||||
except Exception as e:
|
||||
print(f"[错误] {e}")
|
||||
+1015881
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,394 @@
|
||||
# 导入pandas库,用于数据处理和分析,简称pd
|
||||
import pandas as pd
|
||||
# 导入requests库,用于发送HTTP网络请求
|
||||
import requests
|
||||
# 导入json库,用于处理JSON格式的数据
|
||||
import json
|
||||
# 导入warnings库,用于处理警告信息
|
||||
import warnings
|
||||
# 导入yaml库,用于读取配置文件
|
||||
import yaml
|
||||
# 导入os库,用于文件路径操作
|
||||
import os
|
||||
# 忽略所有警告信息,让程序运行更清爽
|
||||
warnings.filterwarnings(action='ignore')
|
||||
# 定义雪球数据获取类
|
||||
class xueqie_data:
|
||||
# 类的初始化方法,创建对象时自动调用
|
||||
def __init__(self, cookie_list=None, config=None):
|
||||
'''
|
||||
雪球数据获取类的初始化方法
|
||||
参数说明:
|
||||
cookie_list: 雪球网站的登录凭证列表,用于模拟用户登录
|
||||
config: 配置信息字典
|
||||
'''
|
||||
print('[初始化] 正在初始化雪球数据获取工具...')
|
||||
|
||||
# 保存配置信息
|
||||
self.config = config if config else load_config()
|
||||
|
||||
# 从配置文件或参数获取cookie列表
|
||||
if cookie_list is not None:
|
||||
self.cookie_list = cookie_list
|
||||
print('[参数] 使用传入的cookie列表')
|
||||
else:
|
||||
# 从配置文件读取cookie列表
|
||||
self.cookie_list = self.config.get('cookie列表', [])
|
||||
print('[配置] 从配置文件读取cookie列表')
|
||||
|
||||
print('[加载] 已加载{}个身份验证信息'.format(len(self.cookie_list)))
|
||||
|
||||
# 创建一个空的DataFrame用于存储获取到的数据
|
||||
self.df=pd.DataFrame()
|
||||
print('[完成] 雪球数据获取工具初始化完成')
|
||||
# 定义获取HTTP请求头的方法
|
||||
def get_headers(self,cookie=''):
|
||||
'''
|
||||
构造访问雪球网站时需要的HTTP请求头信息
|
||||
参数说明:
|
||||
cookie: 用户的登录凭证字符串
|
||||
返回值: 包含所有必要头信息的字典
|
||||
'''
|
||||
# 构造HTTP请求头字典,模拟真实浏览器访问
|
||||
headers={
|
||||
# 告诉服务器可以接受任何类型的响应内容
|
||||
'Accept':'*/*',
|
||||
# 告诉服务器支持的压缩格式,用于减少传输数据量
|
||||
'Accept-Encoding':'gzip, deflate, br, zstd',
|
||||
# 设置语言偏好:首选中文,其次英文
|
||||
'Accept-Language':'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
||||
# 保持HTTP连接,提高效率
|
||||
'Connection':'keep-alive',
|
||||
# 用户的登录凭证,最重要的认证信息
|
||||
'Cookie':cookie,
|
||||
# 目标网站的域名
|
||||
'Host':'xueqiu.com',
|
||||
# 告诉服务器请求来源页面,防止被识别为爬虫
|
||||
'Referer':'https://xueqiu.com/P/ZH3223683',
|
||||
# 浏览器安全相关的头信息,模拟Chrome/Edge浏览器
|
||||
'Sec-Ch-Ua':'"Chromium";v="124", "Microsoft Edge";v="124", "Not-A.Brand";v="99"',
|
||||
# 表示不是移动设备
|
||||
'Sec-Ch-Ua-Mobile':'?0',
|
||||
# 操作系统平台信息
|
||||
'Sec-Ch-Ua-Platform':"Windows",
|
||||
# 请求目标类型
|
||||
'Sec-Fetch-Dest':'empty',
|
||||
# 请求模式,CORS表示跨域请求
|
||||
'Sec-Fetch-Mode':'cors',
|
||||
# 请求来源,same-origin表示同源请求
|
||||
'Sec-Fetch-Site':'same-origin',
|
||||
# 用户代理字符串,模拟Edge浏览器,让服务器认为是真实用户
|
||||
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
|
||||
# 表示这是一个AJAX请求
|
||||
'X-Requested-With':'XMLHttpRequest'
|
||||
}
|
||||
# 返回构造好的请求头字典
|
||||
return headers
|
||||
# 定义获取最近调仓记录的方法
|
||||
def get_the_latest_move(self,rb_id=''):
|
||||
'''
|
||||
获取投资组合的最近一次调仓记录
|
||||
参数说明:
|
||||
rb_id: 调仓记录的ID,用于获取特定的调仓信息
|
||||
返回值: 包含调仓记录的DataFrame,如果失败则返回空DataFrame
|
||||
API地址示例: https://xueqiu.com/cubes/rebalancing/show_origin.json?rb_id=164306198&cube_symbol=ZH3223683
|
||||
'''
|
||||
# 雪球API的URL地址,用于获取调仓详情
|
||||
url='https://xueqiu.com/cubes/rebalancing/show_origin.json?'
|
||||
# 获取HTTP请求头,但这里没有传入cookie(可能是个bug)
|
||||
headers=self.get_headers()
|
||||
# 构造请求参数
|
||||
params={
|
||||
'rb_id':rb_id, # 调仓记录ID
|
||||
'cube_symbol':self.assembly_id # 投资组合ID(注意:这里使用了未定义的属性)
|
||||
}
|
||||
# 使用try-except处理可能的网络错误
|
||||
try:
|
||||
# 发送GET请求获取数据
|
||||
print('[获取] 正在获取最新调仓信息...')
|
||||
res=requests.get(url=url,headers=headers,params=params)
|
||||
# 将响应转换为JSON格式
|
||||
text=res.json()
|
||||
|
||||
# 检查是否有错误码
|
||||
if 'error_code' in text:
|
||||
print('[错误] 获取最新调仓失败: {}'.format(text.get('error_description', '未知错误')))
|
||||
return pd.DataFrame()
|
||||
|
||||
# 检查API返回的状态
|
||||
stats=text['rebalancing']['status']
|
||||
# 如果请求成功
|
||||
if stats=='success':
|
||||
print('[成功] 最新调仓数据获取成功')
|
||||
# 提取调仓历史记录
|
||||
result=text['rebalancing']['rebalancing_histories']
|
||||
# 转换为DataFrame格式
|
||||
df=pd.DataFrame(result)
|
||||
# <<< 关键:过滤未成交/已取消的单子 >>>
|
||||
print(f'[过滤] 已过滤掉 {len(result) - len(df)} 条未成交/已取消的调仓记录')
|
||||
df = df[df['price'].notna()]
|
||||
# 将时间戳转换为可读的日期时间格式(毫秒级时间戳)
|
||||
df['updated_at']=pd.to_datetime(df['updated_at'],unit='ms')
|
||||
print('[数据] 成功获取{}条最新调仓记录'.format(len(df)))
|
||||
return df
|
||||
else:
|
||||
# 如果请求失败,打印错误信息
|
||||
print('[失败] 最新调仓数据获取失败')
|
||||
print('[响应] 服务器响应: {}'.format(text))
|
||||
print('[建议] 建议检查身份验证信息或稍后重试')
|
||||
# 返回空的DataFrame
|
||||
df=pd.DataFrame()
|
||||
return df
|
||||
# 捕获所有异常
|
||||
except Exception as e:
|
||||
# 打印异常信息
|
||||
print('[异常] 获取最新调仓数据时发生错误: {}'.format(str(e)))
|
||||
print('[建议] 建议检查网络连接或身份验证信息')
|
||||
# 返回空的DataFrame
|
||||
df=pd.DataFrame()
|
||||
return df
|
||||
# 定义获取历史调仓记录的方法
|
||||
def get_hist_move(self,assembly_id=None):
|
||||
'''
|
||||
获取投资组合的历史调仓记录
|
||||
参数说明:
|
||||
assembly_id: 投资组合ID,如果为None则从配置文件读取
|
||||
返回值: 包含所有历史调仓记录的DataFrame
|
||||
API地址示例: https://xueqiu.com/cubes/rebalancing/history.json?cube_symbol=ZH3223683&count=20&page=1
|
||||
'''
|
||||
# 如果没有传入组合ID,从组合配置中读取
|
||||
if assembly_id is None:
|
||||
portfolio_configs = self.config.get('组合配置', [])
|
||||
if portfolio_configs:
|
||||
assembly_id = portfolio_configs[0].get('组合ID', 'ZH3361149')
|
||||
else:
|
||||
assembly_id = 'ZH3361149' # 修正为实际数据中的组合ID
|
||||
print(f'[配置] 从配置文件读取组合ID: {assembly_id}')
|
||||
# 初始化cookie计数器,用于跟踪尝试了第几个cookie
|
||||
j=1
|
||||
# 状态标志,True表示还需要继续尝试,False表示已经成功获取数据
|
||||
stats=True
|
||||
# 遍历所有可用的cookie,尝试获取数据
|
||||
for cookie in self.cookie_list:
|
||||
# 如果还没有成功获取数据,继续尝试
|
||||
if stats:
|
||||
# 雪球历史调仓API的URL地址
|
||||
url='https://xueqiu.com/cubes/rebalancing/history.json?'
|
||||
# 使用当前cookie构造请求头
|
||||
headers=self.get_headers(cookie=cookie)
|
||||
# 构造请求参数
|
||||
params={
|
||||
'cube_symbol':assembly_id, # 投资组合ID
|
||||
'count': '50', # 每页返回50条记录
|
||||
'page': '1', # 获取第1页数据
|
||||
}
|
||||
# 发送GET请求
|
||||
res=requests.get(url=url,headers=headers,params=params)
|
||||
# 将响应转换为JSON格式
|
||||
text=res.json()
|
||||
|
||||
# 获取响应中的所有键名,用于检查是否有错误
|
||||
keys_list=list(set(text.keys()))
|
||||
# 如果响应中没有'error_code'键,说明请求成功
|
||||
if 'error_code' not in keys_list:
|
||||
# 保存原始数据到JSON文件,便于分析数据结构
|
||||
import json
|
||||
with open('原始数据.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(text, f, indent=2, ensure_ascii=False)
|
||||
print('[保存] 雪球原始数据已保存至: 原始数据.json')
|
||||
|
||||
# 将调仓列表转换为DataFrame
|
||||
df=pd.DataFrame(text['list'])
|
||||
# 创建空的DataFrame用于存储所有调仓记录
|
||||
data=pd.DataFrame()
|
||||
# 遍历每个调仓操作及其历史记录
|
||||
for idx, rebalancing in df.iterrows():
|
||||
histories = rebalancing['rebalancing_histories']
|
||||
|
||||
# 将每个调仓历史转换为DataFrame
|
||||
df1 = pd.DataFrame(histories)
|
||||
print(f'[过滤] 已过滤掉 {len(histories) - len(df1)} 条未成交/已取消的调仓记录')
|
||||
# <<< 关键修改:过滤已取消或未成交的单子 >>>
|
||||
df1 = df1[df1['price'].notna()] # 过滤掉 price 为 None 的已取消/未成交单
|
||||
|
||||
# 添加组合ID和组合名字信息
|
||||
if len(df1) > 0:
|
||||
df1['组合ID'] = assembly_id
|
||||
# 获取组合名字
|
||||
portfolio_name = self.get_portfolio_name(assembly_id)
|
||||
df1['组合名字'] = portfolio_name
|
||||
|
||||
# 将所有调仓记录合并到一个DataFrame中
|
||||
data=pd.concat([data,df1],ignore_index=True)
|
||||
print('[成功] 第{}个身份验证成功,开始获取数据...'.format(j))
|
||||
# 设置状态为False,表示已经成功获取数据,不需要再尝试其他cookie
|
||||
stats=False
|
||||
else:
|
||||
# 如果有错误,打印错误信息
|
||||
print('[失败] 第{}个身份验证失败: {}'.format(j, text.get('error_description', '未知错误')))
|
||||
print('[重试] 正在尝试下一个身份验证...')
|
||||
else:
|
||||
# 如果前面已经成功获取数据,跳过后续cookie
|
||||
print('[跳过] 第{}个身份验证跳过(已获取到数据)'.format(j))
|
||||
stats=False
|
||||
# cookie计数器加1
|
||||
j+=1
|
||||
# 检查最终状态
|
||||
if stats:
|
||||
# 如果所有cookie都失败了,这里什么都不做
|
||||
pass
|
||||
else:
|
||||
# 这个打印信息有误,应该是"cookie 有可用的"
|
||||
print('[完成] 数据获取成功,已找到可用的身份验证')
|
||||
# 使用try-except处理可能的变量未定义错误
|
||||
try:
|
||||
# 检查data变量是否存在且有数据
|
||||
if data.shape[0]>=0:
|
||||
data=data
|
||||
else:
|
||||
# 如果没有数据,返回空DataFrame
|
||||
data=pd.DataFrame()
|
||||
except:
|
||||
# 如果data变量未定义,创建空DataFrame
|
||||
data=pd.DataFrame()
|
||||
# 返回获取到的历史调仓数据
|
||||
return data
|
||||
|
||||
def get_portfolio_name(self, assembly_id):
|
||||
"""
|
||||
根据组合ID获取组合名字
|
||||
"""
|
||||
try:
|
||||
if self.config:
|
||||
portfolio_configs = self.config.get('组合配置', [])
|
||||
for config in portfolio_configs:
|
||||
if config.get('组合ID') == assembly_id:
|
||||
return config.get('组合名字', assembly_id)
|
||||
return assembly_id # 如果找不到配置,返回组合ID
|
||||
except Exception as e:
|
||||
print(f'[警告] 获取组合名字失败: {e}')
|
||||
return assembly_id
|
||||
|
||||
# 定义列名翻译映射字典
|
||||
def get_column_translation():
|
||||
'''
|
||||
返回英文列名到中文列名的映射字典
|
||||
'''
|
||||
column_mapping = {
|
||||
'id': '记录ID',
|
||||
'rebalancing_id': '调仓ID',
|
||||
'组合ID': '组合ID',
|
||||
'stock_id': '股票ID',
|
||||
'stock_name': '股票名称',
|
||||
'stock_symbol': '股票代码',
|
||||
'volume': '持仓数量',
|
||||
'price': '价格',
|
||||
'net_value': '净值',
|
||||
'weight': '当前权重',
|
||||
'target_weight': '目标权重',
|
||||
'prev_weight': '前期权重',
|
||||
'prev_target_weight': '前期目标权重',
|
||||
'prev_weight_adjusted': '前期调整权重',
|
||||
'prev_volume': '前期数量',
|
||||
'prev_price': '前期价格',
|
||||
'prev_net_value': '前期净值',
|
||||
'proactive': '主动调仓',
|
||||
'created_at': '创建时间',
|
||||
'updated_at': '更新时间',
|
||||
'target_volume': '目标数量',
|
||||
'prev_target_volume': '前期目标数量'
|
||||
}
|
||||
return column_mapping
|
||||
|
||||
# 定义配置文件读取函数
|
||||
def load_config():
|
||||
'''
|
||||
从YAML配置文件读取参数
|
||||
返回配置字典
|
||||
'''
|
||||
config_file = '参数设置.yaml'
|
||||
try:
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
print(f'[配置] 成功加载配置文件: {config_file}')
|
||||
return config
|
||||
else:
|
||||
print(f'[警告] 配置文件不存在: {config_file}')
|
||||
print('[默认] 使用默认配置')
|
||||
return {'组合ID': 'ZH3361149'}
|
||||
except Exception as e:
|
||||
print(f'[错误] 读取配置文件失败: {str(e)}')
|
||||
print('[默认] 使用默认配置')
|
||||
return {'组合ID': 'ZH3361149'}
|
||||
|
||||
|
||||
|
||||
# 主程序入口,当直接运行此文件时执行
|
||||
if __name__=='__main__':
|
||||
'''
|
||||
雪球投资组合数据获取程序
|
||||
'''
|
||||
print('[启动] 雪球跟单数据获取程序启动...')
|
||||
print('[初始化] 正在初始化数据获取工具...')
|
||||
|
||||
# 从配置文件获取组合配置
|
||||
config = load_config()
|
||||
|
||||
# 获取组合ID列表
|
||||
portfolio_configs = config.get('组合配置', [])
|
||||
if portfolio_configs:
|
||||
assembly_ids = [config.get('组合ID') for config in portfolio_configs if config.get('组合ID')]
|
||||
else:
|
||||
assembly_ids = ['ZH3361149'] # 默认组合ID(修正为实际数据中的组合ID)
|
||||
print(f'[配置] 使用组合ID: {assembly_ids}')
|
||||
|
||||
# 创建雪球数据获取对象
|
||||
trader = xueqie_data(config=config)
|
||||
|
||||
# 合并所有组合的数据
|
||||
all_data = pd.DataFrame()
|
||||
|
||||
for assembly_id in assembly_ids:
|
||||
print(f'[处理] 正在获取组合 {assembly_id} 的数据...')
|
||||
df = trader.get_hist_move(assembly_id=assembly_id)
|
||||
if len(df) > 0:
|
||||
all_data = pd.concat([all_data, df], ignore_index=True)
|
||||
print(f'[成功] 组合 {assembly_id} 获取到 {len(df)} 条记录')
|
||||
else:
|
||||
print(f'[警告] 组合 {assembly_id} 未获取到数据')
|
||||
|
||||
df = all_data
|
||||
# 打印当前进度,让用户了解数据获取状态
|
||||
if len(df) > 0:
|
||||
print('[成功] 数据获取成功!共获取{}条调仓记录'.format(len(df)))
|
||||
print('[保存] 正在保存数据到CSV文件...')
|
||||
|
||||
# 获取列名翻译映射
|
||||
column_mapping = get_column_translation()
|
||||
|
||||
# 翻译列名为中文
|
||||
df_chinese = df.copy()
|
||||
df_chinese.columns = [column_mapping.get(col, col) for col in df.columns]
|
||||
|
||||
# 保存为临时CSV文件,避免覆盖交易系统处理后的数据
|
||||
df_chinese.to_csv('./辅助文件/雪球原始数据.csv', index=False, encoding='utf-8-sig')
|
||||
print('[完成] 雪球原始数据已保存到:雪球原始数据.csv(中文列名)')
|
||||
print('[提示] 交易系统将读取此文件并处理后保存到数据.csv')
|
||||
|
||||
# # 同时保存原始英文列名版本供参考
|
||||
# df.to_csv('数据_英文列名.csv', index=False, encoding='utf-8-sig')
|
||||
# print('[完成] 原始数据已保存到:数据_英文列名.csv(英文列名)')
|
||||
|
||||
else:
|
||||
print('[警告] 本次未获取到数据,可能是网络问题或身份验证失效')
|
||||
|
||||
print('\n[完成] 所有数据获取任务完成!')
|
||||
print('[文件] 请查看工作目录下的 CSV 文件')
|
||||
print(' - 雪球原始数据.csv(雪球原始数据,中文列名版本)')
|
||||
print(' - 数据.csv(交易系统处理后的完整数据)')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user