fix:优化参数指数
This commit is contained in:
Binary file not shown.
BIN
crawler/__pycache__/indicator_smooth.cpython-311.pyc
Normal file
BIN
crawler/__pycache__/indicator_smooth.cpython-311.pyc
Normal file
Binary file not shown.
@@ -14,6 +14,13 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
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
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
DB_PATH = os.environ.get("DB_PATH", str(PROJECT_ROOT / "server" / "data.db"))
|
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,
|
"civilian_ships": 20, "airport_port": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 反击情绪 / 华尔街:合理区间,避免爬虫单条提取 0 或 100 导致指标归零或打满
|
# 反击情绪 / 华尔街:限幅与平滑见 crawler.indicator_smooth
|
||||||
RETALIATION_SMOOTH_WEIGHT = 0.6 # 当前值权重,1 - 此值为新值权重,使更新平滑
|
|
||||||
RETALIATION_HISTORY_MAX_ROWS = 300 # 反击历史条数上限,供前端曲线与回放使用
|
RETALIATION_HISTORY_MAX_ROWS = 300 # 反击历史条数上限,供前端曲线与回放使用
|
||||||
WALL_STREET_TREND_MAX_ROWS = 200 # 趋势表保留最近条数,避免无限增长
|
WALL_STREET_TREND_MAX_ROWS = 200 # 趋势表保留最近条数,避免无限增长
|
||||||
VALUE_CLAMP_MIN, VALUE_CLAMP_MAX = 1, 99 # 0/100 视为异常,写入前夹在 [1,99]
|
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
|
updated = True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# retaliation:平滑更新,避免单条新闻 0/100 导致指标归零或打满
|
# retaliation:由 indicator_smooth 计算平滑值 + 单步变化上限,避免爬虫连续更新导致剧烈波动
|
||||||
if "retaliation" in extracted:
|
if "retaliation" in extracted:
|
||||||
r = extracted["retaliation"]
|
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()
|
row = conn.execute("SELECT value FROM retaliation_current WHERE id = 1").fetchone()
|
||||||
current = int(row[0]) if row else 50
|
current = int(row[0]) if row else 50
|
||||||
current = max(VALUE_CLAMP_MIN, min(VALUE_CLAMP_MAX, current))
|
new_val = _smooth_retaliation(raw, 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))
|
|
||||||
ts = (r.get("time") or datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z"))[:25]
|
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 OR REPLACE INTO retaliation_current (id, value) VALUES (1, ?)", (new_val,))
|
||||||
conn.execute("INSERT INTO retaliation_history (time, value) VALUES (?, ?)", (ts, 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,),
|
(n_ret - RETALIATION_HISTORY_MAX_ROWS,),
|
||||||
)
|
)
|
||||||
updated = True
|
updated = True
|
||||||
# wall_street_trend:限幅后写入,并保留最近 N 条避免表无限增长
|
# wall_street_trend:由 indicator_smooth 与上一点平滑 + 最小写入间隔,抑制密集报道导致的锯齿
|
||||||
if "wall_street" in extracted:
|
if "wall_street" in extracted:
|
||||||
w = extracted["wall_street"]
|
w = extracted["wall_street"]
|
||||||
raw = int(w.get("value", 50))
|
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]
|
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]
|
n = conn.execute("SELECT COUNT(*) FROM wall_street_trend").fetchone()[0]
|
||||||
if n > WALL_STREET_TREND_MAX_ROWS:
|
if n > WALL_STREET_TREND_MAX_ROWS:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
83
crawler/indicator_smooth.py
Normal file
83
crawler/indicator_smooth.py
Normal file
@@ -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
|
||||||
69
docs/INDICATORS_WALLSTREET_RETALIATION.md
Normal file
69
docs/INDICATORS_WALLSTREET_RETALIATION.md
Normal file
@@ -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 | 单次相对当前值最大变化幅度(点) |
|
||||||
@@ -318,6 +318,9 @@ export function WarMap() {
|
|||||||
if (isMobile) setLegendOpen(false)
|
if (isMobile) setLegendOpen(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
/** 移动端打击面板收纳:默认展开,可手动收纳以腾出地图 */
|
||||||
|
const [strikePanelOpen, setStrikePanelOpen] = useState(true)
|
||||||
|
|
||||||
const { usNaval, usBaseOp, usBaseDamaged, usBaseAttacked, labelsGeoJson } = useMemo(() => {
|
const { usNaval, usBaseOp, usBaseDamaged, usBaseAttacked, labelsGeoJson } = useMemo(() => {
|
||||||
const naval: GeoJSON.Feature<GeoJSON.Point>[] = []
|
const naval: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||||||
const op: GeoJSON.Feature<GeoJSON.Point>[] = []
|
const op: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||||||
@@ -1195,8 +1198,23 @@ export function WarMap() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative h-full w-full min-w-0">
|
<div ref={containerRef} className="relative h-full w-full min-w-0">
|
||||||
{/* 图例 - 全部=主视角+所有动画,实时=主视角+仅最新进展 */}
|
{/* 打击交互面板:移动端可收纳,默认展开;桌面端始终显示 */}
|
||||||
<div className="absolute bottom-2 left-2 z-10 flex flex-wrap gap-x-3 gap-y-1 rounded bg-black/70 px-2 py-1.5 text-[9px] sm:text-[10px]">
|
<div className="absolute bottom-2 left-2 z-10 flex flex-col items-start gap-1">
|
||||||
|
{/* 移动端收纳后:展开按钮 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sm:hidden flex items-center gap-1 rounded bg-black/70 px-2 py-1.5 text-[9px] text-white border border-white/20 hover:bg-black/85"
|
||||||
|
onClick={() => setStrikePanelOpen(true)}
|
||||||
|
style={{ display: strikePanelOpen ? 'none' : 'flex' }}
|
||||||
|
aria-label="展开打击面板"
|
||||||
|
>
|
||||||
|
<span className="text-cyan-300">打击</span>
|
||||||
|
<span className="opacity-70">▸</span>
|
||||||
|
</button>
|
||||||
|
{/* 面板主体:移动端收纳时隐藏 */}
|
||||||
|
<div
|
||||||
|
className={`flex flex-wrap gap-x-3 gap-y-1 rounded bg-black/70 px-2 py-1.5 text-[9px] sm:text-[10px] ${!strikePanelOpen ? 'max-sm:hidden' : ''}`}
|
||||||
|
>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" /> 基地
|
<span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" /> 基地
|
||||||
</span>
|
</span>
|
||||||
@@ -1296,6 +1314,17 @@ export function WarMap() {
|
|||||||
>
|
>
|
||||||
<span className="h-1.5 w-1.5 rounded-sm bg-[#E066FF]/80" /> 库尔德武装
|
<span className="h-1.5 w-1.5 rounded-sm bg-[#E066FF]/80" /> 库尔德武装
|
||||||
</button>
|
</button>
|
||||||
|
{/* 移动端收纳按钮 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sm:hidden shrink-0 self-center rounded p-0.5 text-white/70 hover:bg-white/10 hover:text-white"
|
||||||
|
onClick={() => setStrikePanelOpen(false)}
|
||||||
|
aria-label="收纳打击面板"
|
||||||
|
title="收纳面板以展示地图"
|
||||||
|
>
|
||||||
|
<span className="text-[10px]">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 右侧图例模块:可收纳悬浮按钮 */}
|
{/* 右侧图例模块:可收纳悬浮按钮 */}
|
||||||
<div className="absolute right-2 top-1/2 z-10 -translate-y-1/2">
|
<div className="absolute right-2 top-1/2 z-10 -translate-y-1/2">
|
||||||
|
|||||||
Reference in New Issue
Block a user