自用策略初始提交

This commit is contained in:
cls
2025-11-06 10:26:02 +08:00
commit 8a7583f111
90 changed files with 1053065 additions and 0 deletions
+278
View File
@@ -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"机器指纹: {指纹}")
+423
View File
@@ -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()
+284
View File
@@ -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}")
File diff suppressed because it is too large Load Diff
+394
View File
@@ -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