skills/asset-monitor/scripts/monitor.py

254 lines
7.6 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
资产异常波动日频监控脚本
使用方法:
python scripts/monitor.py
python scripts/monitor.py --notify
"""
from WindPy import w
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import os
import sys
# 监控资产配置
ASSET_CONFIG = {
"sw3_industry": {
"name": "申万三级行业",
"type": "sector",
"sectorid": "a39901011i000000", # 259个
},
"ashare_index": {
"name": "A股主要指数",
"type": "direct",
"codes": [
"000300.SH", "000905.SH", "000016.SH", "000852.SH",
"000001.SH", "399001.SZ", "399006.SZ", "000688.SH", "883985.WI"
],
},
"bond_index": {
"name": "中债指数",
"type": "direct",
"codes": [
"CBA00101.CS", "CBA00301.CS", "CBA00401.CS",
"CBA00501.CS", "CBA00601.CS"
],
},
"etf": {
"name": "主流ETF",
"type": "direct",
"codes": [
"510300.SH", "510500.SH", "510050.SH", "159915.SZ",
"588000.SH", "512480.SH", "515030.SH", "512760.SH"
],
},
"commodity": {
"name": "商品期货",
"type": "direct",
"codes": [
"AU00.SHF", "AG00.SHF", "CU00.SHF", "AL00.SHF",
"ZN00.SHF", "RB00.SHF", "SC00.INE", "TA00.CZC"
],
},
"global_index": {
"name": "全球指数",
"type": "direct",
"codes": [
"SPX.GI", "IXIC.GI", "DJI.GI", "VIX.GI",
"HSI.HI", "N225.GI", "KS11.GI", "GDAXI.GI", "FTSE.GI"
],
},
}
def analyze_asset(code, name, category, threshold_z=2.0, min_days=30):
"""分析单个资产的波动"""
try:
one_year_ago = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
today_str = datetime.now().strftime('%Y%m%d')
hist = w.wsd(code, "pct_chg", one_year_ago, today_str, "", usedf=True)
if hist[0] == 0 and len(hist[1]) > min_days:
returns = hist[1]['PCT_CHG'].dropna()
if len(returns) > min_days:
mean_ret = returns.mean()
std_ret = returns.std()
today_ret = returns.iloc[-1] if len(returns) > 0 else None
if today_ret is not None and std_ret > 0:
z_score = (today_ret - mean_ret) / std_ret
if abs(z_score) > threshold_z:
return {
'category': category,
'code': code,
'name': name,
'today_return': float(today_ret),
'z_score': float(z_score),
'direction': '大涨' if z_score > 0 else '大跌'
}
except Exception:
pass
return None
def monitor_sector(config, threshold_z=2.0):
"""监控板块类资产"""
print(f"\n[监控] {config['name']}")
today_str = datetime.now().strftime('%Y%m%d')
result = w.wset("sectorconstituent", f"date={today_str};sectorid={config['sectorid']}")
if result.ErrorCode != 0 or len(result.Data) < 2:
print(f" ⚠️ 未获取到数据")
return []
codes = result.Data[1]
names = result.Data[2]
print(f"{len(codes)} 个资产")
anomalies = []
for code, name in zip(codes, names):
result = analyze_asset(code, name, config['name'], threshold_z)
if result:
anomalies.append(result)
print(f" ⚠️ {name}: {result['today_return']:+.2f}% (Z={result['z_score']:+.2f})")
print(f" 发现 {len(anomalies)} 个异常")
return anomalies
def monitor_direct(config, threshold_z=2.0):
"""监控直接代码类资产"""
print(f"\n[监控] {config['name']} ({len(config['codes'])}个)")
# 获取名称
try:
result = w.wss(','.join(config['codes']), "sec_name", "", usedf=True)
name_map = dict(zip(result[1].index, result[1]['SEC_NAME'])) if result[0] == 0 else {}
except:
name_map = {code: code for code in config['codes']}
anomalies = []
for code in config['codes']:
name = name_map.get(code, code)
result = analyze_asset(code, name, config['name'], threshold_z)
if result:
anomalies.append(result)
print(f" ⚠️ {name}: {result['today_return']:+.2f}% (Z={result['z_score']:+.2f})")
print(f" 发现 {len(anomalies)} 个异常")
return anomalies
def run_monitoring(threshold_z=2.0):
"""运行完整监控"""
today = datetime.now()
print("="*70)
print(f"📊 资产异常波动监控")
print(f"时间: {today.strftime('%Y-%m-%d %H:%M')}")
print(f"Z值阈值: {threshold_z}")
print("="*70)
all_anomalies = []
for key, config in ASSET_CONFIG.items():
try:
if config['type'] == 'sector':
anomalies = monitor_sector(config, threshold_z)
else:
anomalies = monitor_direct(config, threshold_z)
all_anomalies.extend(anomalies)
except Exception as e:
print(f"{config['name']} 监控失败: {e}")
print(f"\n{'='*70}")
print(f"✅ 监控完成,共发现 {len(all_anomalies)} 个异常")
print(f"{'='*70}\n")
return all_anomalies
def generate_excel_report(anomalies, output_dir="output"):
"""生成 Excel 报告"""
if not anomalies:
return None
os.makedirs(output_dir, exist_ok=True)
df = pd.DataFrame(anomalies)
df = df.sort_values('z_score', key=abs, ascending=False)
today_str = datetime.now().strftime('%Y%m%d')
excel_path = os.path.join(output_dir, f"asset_anomaly_report_{today_str}.xlsx")
df.to_excel(excel_path, index=False, sheet_name='异常波动资产')
print(f"✅ Excel 报告: {excel_path}")
return excel_path
def generate_text_report(anomalies):
"""生成文本报告"""
if not anomalies:
return "📊 资产异常监控\n\n✅ 今日无异常资产。"
lines = [
"📊 资产异常波动报告",
f"报告时间: {datetime.now().strftime('%Y-%m-%d')}",
"",
f"共发现 {len(anomalies)} 个异常资产:",
"",
]
for i, item in enumerate(anomalies, 1):
emoji = "🚀" if item['z_score'] > 0 else "📉"
lines.append(
f"{i}. {emoji} {item['name']} ({item['category']})\n"
f" 涨跌幅: {item['today_return']:+.2f}% | Z值: {item['z_score']:+.2f}"
)
return '\n'.join(lines)
def main():
import argparse
parser = argparse.ArgumentParser(description='资产异常波动监控')
parser.add_argument('--threshold', type=float, default=2.0, help='Z-Score阈值')
parser.add_argument('--output', type=str, default='output', help='输出目录')
parser.add_argument('--notify', action='store_true', help='打印报告')
args = parser.parse_args()
# 连接 Wind
print("正在连接 Wind...")
w.start()
print("✅ Wind 连接成功\n")
try:
# 运行监控
anomalies = run_monitoring(threshold_z=args.threshold)
# 生成报告
if anomalies:
excel_path = generate_excel_report(anomalies, args.output)
if args.notify:
print("\n" + generate_text_report(anomalies))
else:
print("✅ 今日无异常资产")
finally:
w.stop()
print("\nWind 连接已断开")
if __name__ == "__main__":
main()