From 13a8d8af91ef14db37b69a33a837d1346ef09554 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 6 Mar 2026 14:20:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BC=98=E5=8C=96=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=8C=87=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawler/__pycache__/db_merge.cpython-311.pyc | Bin 11439 -> 25266 bytes .../indicator_smooth.cpython-311.pyc | Bin 0 -> 3675 bytes crawler/db_merge.py | 32 ++++--- crawler/indicator_smooth.py | 83 ++++++++++++++++++ docs/INDICATORS_WALLSTREET_RETALIATION.md | 69 +++++++++++++++ src/components/WarMap.tsx | 33 ++++++- 6 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 crawler/__pycache__/indicator_smooth.cpython-311.pyc create mode 100644 crawler/indicator_smooth.py create mode 100644 docs/INDICATORS_WALLSTREET_RETALIATION.md diff --git a/crawler/__pycache__/db_merge.cpython-311.pyc b/crawler/__pycache__/db_merge.cpython-311.pyc index bb79ac2f690dbf80372f32fd91defeb4b5f83b5e..f80fec584d8267e76ddbdeec8e950d6c7927d32f 100644 GIT binary patch literal 25266 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 delta 4194 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 RF2Y;WWT(8G{IVuG;=cglYu*3= literal 0 HcmV?d00001 diff --git a/crawler/db_merge.py b/crawler/db_merge.py index 2bc7958..94acfae 100644 --- a/crawler/db_merge.py +++ b/crawler/db_merge.py @@ -14,6 +14,13 @@ from datetime import datetime from pathlib import Path from typing import Any, Dict, Optional +from crawler.indicator_smooth import ( + clamp as _indicator_clamp, + smooth_retaliation as _smooth_retaliation, + smooth_wall_street as _smooth_wall_street, + wall_street_should_append as _wall_street_should_append, +) + PROJECT_ROOT = Path(__file__).resolve().parent.parent DB_PATH = os.environ.get("DB_PATH", str(PROJECT_ROOT / "server" / "data.db")) @@ -26,8 +33,7 @@ MAX_DELTA_PER_MERGE = { "civilian_ships": 20, "airport_port": 10, } -# 反击情绪 / 华尔街:合理区间,避免爬虫单条提取 0 或 100 导致指标归零或打满 -RETALIATION_SMOOTH_WEIGHT = 0.6 # 当前值权重,1 - 此值为新值权重,使更新平滑 +# 反击情绪 / 华尔街:限幅与平滑见 crawler.indicator_smooth RETALIATION_HISTORY_MAX_ROWS = 300 # 反击历史条数上限,供前端曲线与回放使用 WALL_STREET_TREND_MAX_ROWS = 200 # 趋势表保留最近条数,避免无限增长 VALUE_CLAMP_MIN, VALUE_CLAMP_MAX = 1, 99 # 0/100 视为异常,写入前夹在 [1,99] @@ -206,17 +212,13 @@ def merge(extracted: Dict[str, Any], db_path: Optional[str] = None) -> bool: updated = True except Exception: pass - # retaliation:平滑更新,避免单条新闻 0/100 导致指标归零或打满 + # retaliation:由 indicator_smooth 计算平滑值 + 单步变化上限,避免爬虫连续更新导致剧烈波动 if "retaliation" in extracted: r = extracted["retaliation"] - raw = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, int(r.get("value", 50)))) + raw = _indicator_clamp(int(r.get("value", 50))) row = conn.execute("SELECT value FROM retaliation_current WHERE id = 1").fetchone() current = int(row[0]) if row else 50 - current = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, current)) - new_val = round( - RETALIATION_SMOOTH_WEIGHT * current + (1 - RETALIATION_SMOOTH_WEIGHT) * raw - ) - new_val = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, new_val)) + new_val = _smooth_retaliation(raw, current) ts = (r.get("time") or datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z"))[:25] conn.execute("INSERT OR REPLACE INTO retaliation_current (id, value) VALUES (1, ?)", (new_val,)) conn.execute("INSERT INTO retaliation_history (time, value) VALUES (?, ?)", (ts, new_val)) @@ -227,13 +229,19 @@ def merge(extracted: Dict[str, Any], db_path: Optional[str] = None) -> bool: (n_ret - RETALIATION_HISTORY_MAX_ROWS,), ) updated = True - # wall_street_trend:限幅后写入,并保留最近 N 条避免表无限增长 + # wall_street_trend:由 indicator_smooth 与上一点平滑 + 最小写入间隔,抑制密集报道导致的锯齿 if "wall_street" in extracted: w = extracted["wall_street"] raw = int(w.get("value", 50)) - val = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, raw)) ts = (w.get("time") or datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z"))[:25] - conn.execute("INSERT INTO wall_street_trend (time, value) VALUES (?, ?)", (ts, val)) + last_row = conn.execute( + "SELECT time, value FROM wall_street_trend ORDER BY time DESC LIMIT 1" + ).fetchone() + last_time = last_row[0] if last_row else None + last_val = int(last_row[1]) if last_row else None + if _wall_street_should_append(last_time, ts): + val = _smooth_wall_street(raw, last_val) + conn.execute("INSERT INTO wall_street_trend (time, value) VALUES (?, ?)", (ts, val)) n = conn.execute("SELECT COUNT(*) FROM wall_street_trend").fetchone()[0] if n > WALL_STREET_TREND_MAX_ROWS: conn.execute( diff --git a/crawler/indicator_smooth.py b/crawler/indicator_smooth.py new file mode 100644 index 0000000..6664ce6 --- /dev/null +++ b/crawler/indicator_smooth.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" +华尔街财团投入指数 & 反击情绪指数:从爬虫实时数据计算稳定指标,抑制单条报道导致的剧烈波动。 +供 db_merge.merge() 调用,写入同一批 DB 表,前端契约不变。 +""" +from typing import Optional, Tuple + +VALUE_CLAMP_MIN = 1 +VALUE_CLAMP_MAX = 99 + +# 华尔街:与上一点平滑,新点权重 +WALL_STREET_NEW_WEIGHT = 0.35 # raw 权重;1 - 此值 = 上一点权重,越大曲线越平滑 +# 华尔街:两次写入最小间隔(分钟),避免短时间多条报道造成密集锯齿 +WALL_STREET_MIN_INTERVAL_MINUTES = 20 + +# 反击情绪:当前值权重(1 - 此值 = 新 raw 权重) +RETALIATION_CURRENT_WEIGHT = 0.8 +# 反击情绪:单次更新相对当前值的最大变化幅度(绝对值) +RETALIATION_MAX_STEP = 5 + + +def clamp(value: int) -> int: + return max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, int(value))) + + +def smooth_wall_street( + raw_value: int, + last_value: Optional[int], + *, + new_weight: float = WALL_STREET_NEW_WEIGHT, +) -> int: + """ + 华尔街投入指数:用上一点做平滑,避免单条报道 30/80 导致曲线骤变。 + 若尚无上一点,直接使用限幅后的 raw。 + """ + raw = clamp(raw_value) + if last_value is None: + return raw + w = 1.0 - new_weight + return clamp(round(w * last_value + new_weight * raw)) + + +def wall_street_should_append( + last_time_iso: Optional[str], + new_time_iso: str, + min_interval_minutes: int = WALL_STREET_MIN_INTERVAL_MINUTES, +) -> bool: + """ + 是否应追加一条华尔街趋势点。若与上一条间隔不足 min_interval_minutes 则跳过, + 减少因爬虫短时间多篇报道导致的密集锯齿。 + """ + if not last_time_iso: + return True + try: + from datetime import datetime + last = datetime.fromisoformat(last_time_iso.replace("Z", "+00:00")) + new = datetime.fromisoformat(new_time_iso.replace("Z", "+00:00")) + delta_min = (new - last).total_seconds() / 60 + return delta_min >= min_interval_minutes + except Exception: + return True + + +def smooth_retaliation( + raw_value: int, + current_value: int, + *, + current_weight: float = RETALIATION_CURRENT_WEIGHT, + max_step: int = RETALIATION_MAX_STEP, +) -> int: + """ + 反击情绪指数:先与当前值平滑,再限制单步变化幅度,避免连续多条报道导致快速漂移或抖动。 + """ + raw = clamp(raw_value) + cur = clamp(current_value) + smoothed = round(current_weight * cur + (1.0 - current_weight) * raw) + smoothed = clamp(smoothed) + # 单步变化上限 + delta = smoothed - cur + if abs(delta) > max_step: + step = max_step if delta > 0 else -max_step + smoothed = clamp(cur + step) + return smoothed diff --git a/docs/INDICATORS_WALLSTREET_RETALIATION.md b/docs/INDICATORS_WALLSTREET_RETALIATION.md new file mode 100644 index 0000000..6d17f43 --- /dev/null +++ b/docs/INDICATORS_WALLSTREET_RETALIATION.md @@ -0,0 +1,69 @@ +# 华尔街财团投入指数 & 反击情绪指数:更新逻辑与波动说明 + +## 一、数据流概览 + +``` +爬虫提取 (extractor_ai / extractor_dashscope) + → retaliation_sentiment / wall_street_value (0–100,按单篇报道) + → db_merge.merge(extracted) + → SQLite: retaliation_current, retaliation_history, wall_street_trend + → server/situationData.js getSituation() + → 前端: iranForces.retaliationSentiment(History), usForces.wallStreetInvestmentTrend + → 组件: RetaliationGauge, InvestmentTrendChart;回放: useReplaySituation 插值 +``` + +## 二、当前写入逻辑(crawler/db_merge.py) + +### 1. 华尔街财团投入指数 (wall_street_trend) + +- **表**:`wall_street_trend(id, time, value)`,前端用整表做 `wallStreetInvestmentTrend` 折线。 +- **写入**:每次爬虫产出 `extracted["wall_street"]` 时: + - 对 `value` 做限幅 `[1, 99]`,**不做任何平滑**; + - 直接 `INSERT INTO wall_street_trend (time, value) VALUES (?, ?)`; + - 表保留最近 `WALL_STREET_TREND_MAX_ROWS`(200)条。 +- **波动原因**: + - 每条报道一个点,爬虫频繁时点很密; + - 不同报道提取值差异大(如 30 / 80 / 45),曲线会剧烈锯齿; + - 无“与上一点平滑”,无时间间隔限制,易受单条 0/100 或异常值影响(虽已夹到 1–99)。 + +### 2. 反击情绪指数 (retaliation_current + retaliation_history) + +- **表**:`retaliation_current(id=1, value)` 当前值;`retaliation_history(id, time, value)` 历史曲线。 +- **写入**:每次爬虫产出 `extracted["retaliation"]` 时: + - 当前值平滑:`new_val = 0.6 * current + 0.4 * raw`,再夹到 [1, 99]; + - `retaliation_current` 更新为该 `new_val`; + - `retaliation_history` 追加一条 `(time, new_val)`; + - 历史表保留最近 `RETALIATION_HISTORY_MAX_ROWS`(300)条。 +- **波动原因**: + - 多条新闻短时间连续写入时,每次都用新的 raw 更新 current,连续多步 0.6/0.4 仍会快速漂移; + - history 每写一次就一个点,点过密且 raw 差异大时折线仍会明显抖动; + - 单步无“最大变化幅度”限制,极端 raw 仍会导致单次跳动较大。 + +## 三、爬虫侧产出形态 + +- **extractor_ai / extractor_dashscope**:仅当报道涉及对应维度时才输出: + - `retaliation_sentiment`:0–100; + - `wall_street_value`:0–100。 +- 每条报道独立一个值,多篇报道会多次调用 `merge()`,因此**波动确实主要由爬虫数据更新频率和单条取值差异导致**。 + +## 四、稳定化思路(计算模块) + +1. **华尔街** + - 与**上一点**做平滑再写入:例如 `value = alpha * last_value + (1-alpha) * raw`,再限幅; + - 可选:仅当距上一条时间超过一定间隔(如 15–30 分钟)才 INSERT,减少密集点带来的锯齿。 + +2. **反击情绪** + - 加强平滑:例如提高当前值权重(0.8 * current + 0.2 * raw); + - 单步变化上限:例如 `new_val` 相对 `current` 最多 ±N(如 5)点; + - 历史记录:可对 history 做同样限幅或间隔写入,避免曲线过密抖动。 + +实现位置:**crawler/indicator_smooth.py**,在 `db_merge.merge()` 中调用,仍写回现有表,前端与 API 契约不变。 + +### 已实现参数(可调) + +| 指标 | 参数 | 默认 | 说明 | +|------|------|------|------| +| 华尔街 | `WALL_STREET_NEW_WEIGHT` | 0.35 | 新 raw 权重,越小曲线越平滑 | +| 华尔街 | `WALL_STREET_MIN_INTERVAL_MINUTES` | 20 | 两笔趋势最小间隔(分钟),不足则本条不写入 | +| 反击情绪 | `RETALIATION_CURRENT_WEIGHT` | 0.8 | 当前值权重,越大越平滑 | +| 反击情绪 | `RETALIATION_MAX_STEP` | 5 | 单次相对当前值最大变化幅度(点) | diff --git a/src/components/WarMap.tsx b/src/components/WarMap.tsx index acc7d2b..cd496c1 100644 --- a/src/components/WarMap.tsx +++ b/src/components/WarMap.tsx @@ -318,6 +318,9 @@ export function WarMap() { if (isMobile) setLegendOpen(false) }, []) + /** 移动端打击面板收纳:默认展开,可手动收纳以腾出地图 */ + const [strikePanelOpen, setStrikePanelOpen] = useState(true) + const { usNaval, usBaseOp, usBaseDamaged, usBaseAttacked, labelsGeoJson } = useMemo(() => { const naval: GeoJSON.Feature[] = [] const op: GeoJSON.Feature[] = [] @@ -1195,8 +1198,23 @@ export function WarMap() { return (
- {/* 图例 - 全部=主视角+所有动画,实时=主视角+仅最新进展 */} -
+ {/* 打击交互面板:移动端可收纳,默认展开;桌面端始终显示 */} +
+ {/* 移动端收纳后:展开按钮 */} + + {/* 面板主体:移动端收纳时隐藏 */} +
基地 @@ -1296,6 +1314,17 @@ export function WarMap() { > 库尔德武装 + {/* 移动端收纳按钮 */} + +
{/* 右侧图例模块:可收纳悬浮按钮 */}