"""拉取建材价格指数,更新 data.json 并刷新 index.html。""" import json import pandas as pd import akshare as ak df = ak.macro_china_construction_index() df['日期'] = pd.to_datetime(df['日期']) df = df.sort_values('日期').reset_index(drop=True) for w in [5, 10, 20, 60]: df[f'MA{w}'] = df['最新值'].rolling(w).mean().round(1) df[f'MA{w}_dir'] = df[f'MA{w}'].diff().apply(lambda x: 'up' if x > 0 else ('down' if x < 0 else 'flat')) latest = df.iloc[-1] signals = [] if latest['最新值'] > latest['MA5']: signals.append(('短期', '偏多', '价格 > MA5')) else: signals.append(('短期', '偏空', '价格 < MA5')) if latest['MA5'] > latest['MA20']: signals.append(('中期', '偏多', 'MA5 > MA20')) else: signals.append(('中期', '偏空', 'MA5 < MA20')) if latest['MA20'] > latest['MA60']: signals.append(('长期', '偏多', 'MA20 > MA60')) else: signals.append(('长期', '偏空', 'MA20 < MA60')) momentum_5d = latest['最新值'] - df.iloc[-6]['最新值'] signals.append(('动量(5日)', '偏多' if momentum_5d > 0 else '偏空', f"{momentum_5d:+d}")) bull_count = sum(1 for _, s, _ in signals if s == '偏多') if bull_count >= 3: overall = ('偏多', '#16a34a') elif bull_count <= 1: overall = ('偏空', '#dc2626') else: overall = ('震荡', '#d97706') summary = { 'date': latest['日期'].strftime('%Y-%m-%d'), 'value': int(latest['最新值']), 'change': round(float(latest['涨跌幅']), 4), 'chg_3m': round(float(latest['近3月涨跌幅']), 4), 'chg_6m': round(float(latest['近6月涨跌幅']), 4), 'chg_1y': round(float(latest['近1年涨跌幅']), 4), } mas = {} for w in [5, 10, 20, 60]: mas[f'MA{w}'] = { 'value': float(latest[f'MA{w}']), 'direction': latest[f'MA{w}_dir'], 'price_vs_ma': 'above' if latest['最新值'] > latest[f'MA{w}'] else 'below' } rows = [] for _, r in df.tail(30).iterrows(): rows.append({ 'date': r['日期'].strftime('%m-%d'), 'value': int(r['最新值']), 'chg': round(float(r['涨跌幅']), 4), 'MA5': None if pd.isna(r['MA5']) else round(float(r['MA5']), 1), 'MA20': None if pd.isna(r['MA20']) else round(float(r['MA20']), 1), 'MA60': None if pd.isna(r['MA60']) else round(float(r['MA60']), 1), }) def build_history(df): """构建历史全景数据:月度采样 + 周期标注 + 统计。""" # 月度采样(取每月最后一条有效记录) monthly = df.set_index('日期').resample('ME')['最新值'].last().dropna().reset_index() ma60_monthly = df.set_index('日期').resample('ME')['MA60'].last().dropna().reset_index() monthly_vals = [] for _, r in monthly.iterrows(): ma_row = ma60_monthly[ma60_monthly['日期'] == r['日期']] ma60_val = None if ma_row.empty or pd.isna(ma_row['MA60'].iloc[0]) else round(float(ma_row['MA60'].iloc[0]), 1) monthly_vals.append({ 'date': r['日期'].strftime('%Y-%m'), 'value': int(r['最新值']), 'ma60': ma60_val, }) # 历史统计 vals = df['最新值'] latest_val = float(vals.iloc[-1]) pct_rank = round((vals < latest_val).sum() / len(vals) * 100, 1) # 年度涨跌(排除不足 200 个交易日的首尾年) df['year'] = df['日期'].dt.year yearly = df.groupby('year')['最新值'].agg(['first', 'last', 'count']) yearly = yearly[yearly['count'] >= 200] yearly['chg'] = ((yearly['last'] - yearly['first']) / yearly['first'] * 100).round(1) down_years = int((yearly['chg'] < 0).sum()) ytd = float(yearly['chg'].iloc[-1]) if len(yearly) > 0 else 0 # 识别主要周期:用未取整的 MA60 找极值,间距至少 4 个月 ma60_raw = df['最新值'].rolling(60).mean().values # 未取整,保留精度 all_turns = [] # (date, value, type) min_gap = 80 # 交易日,约 4 个月 last_turn_idx = -999 for i in range(2, len(ma60_raw) - 2): if pd.isna(ma60_raw[i]): continue if ma60_raw[i] > ma60_raw[i - 1] and ma60_raw[i] > ma60_raw[i - 2] and ma60_raw[i] > ma60_raw[i + 1] and ma60_raw[i] > ma60_raw[i + 2]: if i - last_turn_idx >= min_gap: all_turns.append((df['日期'].iloc[i], float(ma60_raw[i]), 'peak')) last_turn_idx = i elif ma60_raw[i] < ma60_raw[i - 1] and ma60_raw[i] < ma60_raw[i - 2] and ma60_raw[i] < ma60_raw[i + 1] and ma60_raw[i] < ma60_raw[i + 2]: if i - last_turn_idx >= min_gap: all_turns.append((df['日期'].iloc[i], float(ma60_raw[i]), 'trough')) last_turn_idx = i # 构建周期(谷 → 下一个峰) cycles = [] for i, (t_date, t_val, t_type) in enumerate(all_turns): if t_type != 'trough': continue # 找下一个峰 for j in range(i + 1, len(all_turns)): if all_turns[j][2] == 'peak': p_date, p_val, _ = all_turns[j] gain = round((p_val - t_val) / t_val * 100, 1) if gain > 10: label = '' if t_date.year == 2015 or t_date.year == 2016: label = '供给侧改革' elif t_date.year == 2020: label = '疫情后刺激' cycles.append({ 'label': label, 'start': t_date.strftime('%Y-%m'), 'end': p_date.strftime('%Y-%m'), 'from': int(t_val), 'to': int(p_val), 'pct': gain, }) break # 当前反弹(用价格实际谷底,不是 MA60 谷底) # 找最近 2 年的价格最低点 recent_2y = df[df['日期'] >= df['日期'].iloc[-1] - pd.DateOffset(years=2)] trough_idx = recent_2y['最新值'].idxmin() trough_date = df.loc[trough_idx, '日期'] trough_val = float(df.loc[trough_idx, '最新值']) cur_gain = round((latest_val - trough_val) / trough_val * 100, 1) current_rebound = { 'label': '当前反弹', 'start': trough_date.strftime('%Y-%m'), 'end': None, 'from': int(trough_val), 'to': int(latest_val), 'pct': cur_gain, } # 均线结构 last = df.iloc[-1] ma20_vs_ma60 = 'above' if last['MA20'] > last['MA60'] else 'below' return { 'monthly': monthly_vals, 'stats': { 'mean': round(float(vals.mean()), 1), 'min': int(vals.min()), 'max': int(vals.max()), 'min_date': df.loc[vals.idxmin(), '日期'].strftime('%Y-%m'), 'max_date': df.loc[vals.idxmax(), '日期'].strftime('%Y-%m'), 'percentile': pct_rank, 'down_years': down_years, 'ytd_chg': round(ytd, 1), }, 'cycles': cycles, 'current_rebound': current_rebound, 'ma20_vs_ma60': ma20_vs_ma60, } output = { 'summary': summary, 'mas': mas, 'signals': [{'period': p, 'signal': s, 'detail': d} for p, s, d in signals], 'overall': {'signal': overall[0], 'color': overall[1]}, 'recent': rows, 'history': build_history(df), } with open('data.json', 'w') as f: json.dump(output, f, ensure_ascii=False, indent=2) SENTINEL = '/*__DATA_SENTINEL__*/{}' html = open('index_template.html').read() html = html.replace(SENTINEL, json.dumps(output, ensure_ascii=False)) open('index.html', 'w').write(html) print(f"刷新完成: {summary['date']} → {summary['value']} (日涨跌 {summary['change']:+.2f}%)") print(f"综合信号: {overall[0]}") print("信号明细:") for p, s, d in signals: print(f" {p}: {s} ({d})")