优化三层打分模型:短期引入幅度因子与量能确认,中期资金信号连续化,长期加入价格维度,新增波动率调整
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from . import contracts, fetcher, scorer, storage
|
from . import contracts, fetcher, scorer, storage
|
||||||
|
|
||||||
@@ -37,15 +38,19 @@ def run(ts_code: str, trade_date: Optional[str] = None) -> int:
|
|||||||
print(f"\n信号: {result.signal}")
|
print(f"\n信号: {result.signal}")
|
||||||
print("=" * 65)
|
print("=" * 65)
|
||||||
|
|
||||||
|
quadrant_names = {
|
||||||
|
"accumulation": "增仓上涨", "distribution": "增仓下跌",
|
||||||
|
"covering": "减仓上涨", "liquidation": "减仓下跌", "flat": "持仓持平",
|
||||||
|
}
|
||||||
print("\n[短期动力] 近7日逐日打分:")
|
print("\n[短期动力] 近7日逐日打分:")
|
||||||
print("-" * 65)
|
print("-" * 80)
|
||||||
for d in result.detail.short_details:
|
for d in result.detail.short_details:
|
||||||
tag = "增仓" if d["oi_chg"] > 0 else "减仓"
|
q = quadrant_names.get(d["quadrant"], d["quadrant"])
|
||||||
if abs(d["oi_chg"] / d["oi"]) < 0.01:
|
print(f" {d['trade_date']} {q} "
|
||||||
tag = "持平"
|
f"涨跌{d['price_chg_pct']*100:>+.2f}% "
|
||||||
price_dir = "涨" if d["close"] >= d["pre_close"] else "跌"
|
f"OI变化{d['oi_chg_pct']*100:>+.2f}% "
|
||||||
print(f" {d['trade_date']} {tag:>4} + {price_dir} "
|
f"量比{d['vol_ratio']:.2f} "
|
||||||
f"持仓{d['oi_chg']:>+8,.0f} 得分: {d['score']:>3}")
|
f"得分: {d['score']:>5.1f}")
|
||||||
|
|
||||||
md = result.detail.medium_detail
|
md = result.detail.medium_detail
|
||||||
print(f"\n[中期趋势] 明细:")
|
print(f"\n[中期趋势] 明细:")
|
||||||
@@ -53,14 +58,24 @@ def run(ts_code: str, trade_date: Optional[str] = None) -> int:
|
|||||||
print(f" 价格信号得分: {md['price_signal']:.1f}")
|
print(f" 价格信号得分: {md['price_signal']:.1f}")
|
||||||
print(f" 增仓上涨天数: {md['long_up_days']} 天")
|
print(f" 增仓上涨天数: {md['long_up_days']} 天")
|
||||||
print(f" 增仓下跌天数: {md['long_down_days']} 天")
|
print(f" 增仓下跌天数: {md['long_down_days']} 天")
|
||||||
print(f" 资金意愿得分: {md['fund_signal']} 分")
|
print(f" 资金意愿得分: {md['fund_signal']:.1f} 分")
|
||||||
|
|
||||||
ld = result.detail.long_detail
|
ld = result.detail.long_detail
|
||||||
print(f"\n[长期结构] 明细:")
|
print(f"\n[长期结构] 明细:")
|
||||||
|
print(f" OI趋势得分: {ld['oi_score']:.1f} (权重 60%)")
|
||||||
|
print(f" 价格趋势得分: {ld['price_score']:.1f} (权重 40%)")
|
||||||
|
print(f" 30日价格收益: {ld['price_return_30d_pct']:+.2f}%")
|
||||||
|
print(f" 30日前收盘价: {ld['price_before_30d']:.2f}")
|
||||||
print(f" 近30日日均持仓: {ld['avg_oi']:,.0f}")
|
print(f" 近30日日均持仓: {ld['avg_oi']:,.0f}")
|
||||||
print(f" 30日前持仓量: {ld['oi_before']:,.0f}")
|
print(f" 30日前持仓量: {ld['oi_before']:,.0f}")
|
||||||
print(f" 持仓变化幅度: {ld['change_pct']:+.2f}%")
|
print(f" 持仓变化幅度: {ld['change_pct']:+.2f}%")
|
||||||
|
|
||||||
|
vd = result.detail.volatility
|
||||||
|
print(f"\n[波动率调整]")
|
||||||
|
print(f" 日波动率(30d std): {vd['daily_vol_pct']*100:.2f}%")
|
||||||
|
print(f" ATR%: {vd['atr_pct']*100:.2f}%")
|
||||||
|
print(f" 惩罚系数: {vd['vol_penalty']:.3f}")
|
||||||
|
|
||||||
print(f"\n[OK] 数据已持久化到 PostgreSQL")
|
print(f"\n[OK] 数据已持久化到 PostgreSQL")
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class ScoreDetail:
|
|||||||
short_details: list = field(default_factory=list)
|
short_details: list = field(default_factory=list)
|
||||||
medium_detail: dict = field(default_factory=dict)
|
medium_detail: dict = field(default_factory=dict)
|
||||||
long_detail: dict = field(default_factory=dict)
|
long_detail: dict = field(default_factory=dict)
|
||||||
|
volatility: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -5,44 +5,79 @@ import pandas as pd
|
|||||||
from .models import ScoreDetail, ScoreResult
|
from .models import ScoreDetail, ScoreResult
|
||||||
|
|
||||||
|
|
||||||
def _daily_short_score(row: pd.Series) -> int:
|
def _daily_short_score(row: pd.Series, avg_vol_7d: float) -> dict:
|
||||||
"""单日短期动力打分。"""
|
"""单日短期动力打分(连续值 + 幅度因子 + 量能确认)。"""
|
||||||
oi = float(row["oi"])
|
oi = float(row["oi"])
|
||||||
oi_chg = float(row["oi_chg"])
|
oi_chg = float(row["oi_chg"])
|
||||||
close = float(row["close"])
|
close = float(row["close"])
|
||||||
pre_close = float(row["pre_close"])
|
pre_close = float(row["pre_close"])
|
||||||
|
vol = float(row.get("vol", 0))
|
||||||
|
|
||||||
oi_change_pct = abs(oi_chg / oi) if oi != 0 else 0
|
oi_chg_pct = oi_chg / oi if oi != 0 else 0.0
|
||||||
|
price_chg_pct = (close - pre_close) / pre_close if pre_close != 0 else 0.0
|
||||||
price_up = close >= pre_close
|
price_up = close >= pre_close
|
||||||
|
|
||||||
if oi_change_pct < 0.01:
|
|
||||||
return 60 if price_up else 40
|
|
||||||
|
|
||||||
oi_increasing = oi_chg > 0
|
oi_increasing = oi_chg > 0
|
||||||
if oi_increasing and price_up:
|
|
||||||
return 100
|
# 象限基础分
|
||||||
if oi_increasing and not price_up:
|
if abs(oi_chg_pct) < 0.01:
|
||||||
return 0
|
base = 60.0 if price_up else 40.0
|
||||||
if not oi_increasing and price_up:
|
quadrant = "flat"
|
||||||
return 70
|
elif oi_increasing and price_up:
|
||||||
return 30
|
base = 75.0
|
||||||
|
quadrant = "accumulation"
|
||||||
|
elif oi_increasing and not price_up:
|
||||||
|
base = 25.0
|
||||||
|
quadrant = "distribution"
|
||||||
|
elif not oi_increasing and price_up:
|
||||||
|
base = 65.0
|
||||||
|
quadrant = "covering"
|
||||||
|
else:
|
||||||
|
base = 20.0
|
||||||
|
quadrant = "liquidation"
|
||||||
|
|
||||||
|
# 幅度加成:OI 变化率封顶 5%,价格涨跌幅封顶 3%
|
||||||
|
oi_mag = min(1.0, abs(oi_chg_pct) / 0.05)
|
||||||
|
price_mag = min(1.0, abs(price_chg_pct) / 0.03)
|
||||||
|
|
||||||
|
if quadrant in ("accumulation", "liquidation"):
|
||||||
|
boost = (oi_mag + price_mag) / 2.0 * 20.0
|
||||||
|
elif quadrant == "flat":
|
||||||
|
boost = price_mag * 10.0
|
||||||
|
else:
|
||||||
|
boost = 0.0
|
||||||
|
|
||||||
|
# 量能确认
|
||||||
|
vol_ratio = vol / avg_vol_7d if avg_vol_7d > 0 else 1.0
|
||||||
|
vol_factor = 0.9 + 0.2 * min(vol_ratio, 1.5)
|
||||||
|
|
||||||
|
score = max(0.0, min(100.0, (base + boost) * vol_factor))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"trade_date": str(row["trade_date"]),
|
||||||
|
"close": close,
|
||||||
|
"pre_close": pre_close,
|
||||||
|
"oi": oi,
|
||||||
|
"oi_chg": oi_chg,
|
||||||
|
"oi_chg_pct": round(float(oi_chg_pct), 4),
|
||||||
|
"price_chg_pct": round(float(price_chg_pct), 4),
|
||||||
|
"vol": float(vol),
|
||||||
|
"vol_ratio": round(float(vol_ratio), 2),
|
||||||
|
"quadrant": quadrant,
|
||||||
|
"score": round(float(score), 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def calc_short_term(df: pd.DataFrame, window: int = 7) -> tuple[float, list]:
|
def calc_short_term(df: pd.DataFrame, window: int = 7) -> tuple[float, list]:
|
||||||
recent = df.iloc[-window:].copy()
|
recent = df.iloc[-window:].copy()
|
||||||
|
avg_vol_7d = float(recent["vol"].mean()) if "vol" in recent.columns else 0.0
|
||||||
|
|
||||||
scores = []
|
scores = []
|
||||||
details = []
|
details = []
|
||||||
for _, row in recent.iterrows():
|
for _, row in recent.iterrows():
|
||||||
score = _daily_short_score(row)
|
detail = _daily_short_score(row, avg_vol_7d)
|
||||||
scores.append(score)
|
scores.append(detail["score"])
|
||||||
details.append({
|
details.append(detail)
|
||||||
"trade_date": str(row["trade_date"]),
|
|
||||||
"close": float(row["close"]),
|
|
||||||
"pre_close": float(row["pre_close"]),
|
|
||||||
"oi": float(row["oi"]),
|
|
||||||
"oi_chg": float(row["oi_chg"]),
|
|
||||||
"score": score,
|
|
||||||
})
|
|
||||||
return sum(scores) / len(scores), details
|
return sum(scores) / len(scores), details
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +101,7 @@ def calc_medium_term(df: pd.DataFrame, window: int = 15) -> tuple[float, dict]:
|
|||||||
else:
|
else:
|
||||||
long_down += 1
|
long_down += 1
|
||||||
|
|
||||||
fund_score = 80 if long_up > long_down else (20 if long_up < long_down else 50)
|
fund_score = 50.0 + (long_up - long_down) / window * 50.0
|
||||||
score = price_score * 0.6 + fund_score * 0.4
|
score = price_score * 0.6 + fund_score * 0.4
|
||||||
|
|
||||||
detail = {
|
detail = {
|
||||||
@@ -74,7 +109,8 @@ def calc_medium_term(df: pd.DataFrame, window: int = 15) -> tuple[float, dict]:
|
|||||||
"price_signal": round(price_score, 1),
|
"price_signal": round(price_score, 1),
|
||||||
"long_up_days": long_up,
|
"long_up_days": long_up,
|
||||||
"long_down_days": long_down,
|
"long_down_days": long_down,
|
||||||
"fund_signal": fund_score,
|
"fund_signal": round(fund_score, 1),
|
||||||
|
"window": window,
|
||||||
}
|
}
|
||||||
return score, detail
|
return score, detail
|
||||||
|
|
||||||
@@ -84,26 +120,31 @@ def calc_long_term(df: pd.DataFrame, window: int = 30) -> tuple[float, dict]:
|
|||||||
raise ValueError(f"数据不足,需要至少 {window + 1} 行")
|
raise ValueError(f"数据不足,需要至少 {window + 1} 行")
|
||||||
|
|
||||||
recent_oi = df.iloc[-window:]["oi"]
|
recent_oi = df.iloc[-window:]["oi"]
|
||||||
avg_oi = recent_oi.mean()
|
avg_oi = float(recent_oi.mean())
|
||||||
oi_before = float(df.iloc[-window - 1]["oi"])
|
oi_before = float(df.iloc[-window - 1]["oi"])
|
||||||
|
|
||||||
change_pct = (avg_oi - oi_before) / oi_before if oi_before != 0 else 0
|
oi_change_pct = (avg_oi - oi_before) / oi_before if oi_before != 0 else 0.0
|
||||||
|
|
||||||
if change_pct > 0.10:
|
# OI 趋势分 (60%)
|
||||||
score = 90
|
oi_score = max(0.0, min(100.0, 50.0 + oi_change_pct * 250))
|
||||||
elif change_pct > 0.05:
|
|
||||||
score = 70
|
# 价格趋势分 (40%)
|
||||||
elif change_pct > -0.05:
|
close_now = float(df.iloc[-1]["close"])
|
||||||
score = 50
|
price_before = float(df.iloc[-window - 1]["close"])
|
||||||
elif change_pct > -0.10:
|
price_return_30d = (close_now - price_before) / price_before if price_before != 0 else 0.0
|
||||||
score = 30
|
price_score = max(0.0, min(100.0, 50.0 + price_return_30d * 200))
|
||||||
else:
|
|
||||||
score = 10
|
score = oi_score * 0.6 + price_score * 0.4
|
||||||
|
|
||||||
detail = {
|
detail = {
|
||||||
"avg_oi": round(float(avg_oi), 0),
|
"avg_oi": round(avg_oi, 0),
|
||||||
"oi_before": round(oi_before, 0),
|
"oi_before": round(oi_before, 0),
|
||||||
"change_pct": round(change_pct * 100, 2),
|
"change_pct": round(oi_change_pct * 100, 2),
|
||||||
|
"oi_score": round(oi_score, 1),
|
||||||
|
"price_score": round(price_score, 1),
|
||||||
|
"price_return_30d_pct": round(price_return_30d * 100, 2),
|
||||||
|
"price_before_30d": round(price_before, 2),
|
||||||
|
"window": window,
|
||||||
}
|
}
|
||||||
return score, detail
|
return score, detail
|
||||||
|
|
||||||
@@ -139,7 +180,31 @@ def score_daily(df: pd.DataFrame, trade_date: Optional[str] = None) -> ScoreResu
|
|||||||
medium, medium_detail = calc_medium_term(df, 15)
|
medium, medium_detail = calc_medium_term(df, 15)
|
||||||
long_, long_detail = calc_long_term(df, 30)
|
long_, long_detail = calc_long_term(df, 30)
|
||||||
|
|
||||||
composite = short * 0.4 + medium * 0.35 + long_ * 0.25
|
# 波动率调整
|
||||||
|
recent_30 = df.iloc[-30:].copy()
|
||||||
|
recent_30["ret"] = recent_30["close"].pct_change()
|
||||||
|
daily_vol = float(recent_30["ret"].std())
|
||||||
|
|
||||||
|
recent_30["tr"] = recent_30.apply(
|
||||||
|
lambda r: max(
|
||||||
|
r["high"] - r["low"],
|
||||||
|
abs(r["high"] - r["pre_close"]),
|
||||||
|
abs(r["low"] - r["pre_close"]),
|
||||||
|
),
|
||||||
|
axis=1,
|
||||||
|
)
|
||||||
|
atr = float(recent_30["tr"].mean())
|
||||||
|
avg_close_30 = float(recent_30["close"].mean())
|
||||||
|
atr_pct = (atr / avg_close_30) if avg_close_30 else 0.0
|
||||||
|
|
||||||
|
vol_ref = 0.015
|
||||||
|
if daily_vol <= vol_ref:
|
||||||
|
vol_penalty = 1.0
|
||||||
|
else:
|
||||||
|
vol_penalty = max(0.85, 1.0 - (daily_vol - vol_ref) * 10)
|
||||||
|
|
||||||
|
composite_raw = short * 0.4 + medium * 0.35 + long_ * 0.25
|
||||||
|
composite = round(composite_raw * vol_penalty, 1)
|
||||||
signal = _interpret(composite)
|
signal = _interpret(composite)
|
||||||
|
|
||||||
return ScoreResult(
|
return ScoreResult(
|
||||||
@@ -157,5 +222,10 @@ def score_daily(df: pd.DataFrame, trade_date: Optional[str] = None) -> ScoreResu
|
|||||||
short_details=short_details,
|
short_details=short_details,
|
||||||
medium_detail=medium_detail,
|
medium_detail=medium_detail,
|
||||||
long_detail=long_detail,
|
long_detail=long_detail,
|
||||||
|
volatility={
|
||||||
|
"daily_vol_pct": round(float(daily_vol), 4),
|
||||||
|
"atr_pct": round(float(atr_pct), 4),
|
||||||
|
"vol_penalty": round(float(vol_penalty), 4),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export interface ShortDetail {
|
|||||||
oi: number
|
oi: number
|
||||||
oi_chg: number
|
oi_chg: number
|
||||||
score: number
|
score: number
|
||||||
|
oi_chg_pct: number
|
||||||
|
price_chg_pct: number
|
||||||
|
vol: number
|
||||||
|
vol_ratio: number
|
||||||
|
quadrant: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediumDetail {
|
export interface MediumDetail {
|
||||||
@@ -15,18 +20,31 @@ export interface MediumDetail {
|
|||||||
long_up_days: number
|
long_up_days: number
|
||||||
long_down_days: number
|
long_down_days: number
|
||||||
fund_signal: number
|
fund_signal: number
|
||||||
|
window: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LongDetail {
|
export interface LongDetail {
|
||||||
avg_oi: number
|
avg_oi: number
|
||||||
oi_before: number
|
oi_before: number
|
||||||
change_pct: number
|
change_pct: number
|
||||||
|
oi_score: number
|
||||||
|
price_score: number
|
||||||
|
price_return_30d_pct: number
|
||||||
|
price_before_30d: number
|
||||||
|
window: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolatilityDetail {
|
||||||
|
daily_vol_pct: number
|
||||||
|
atr_pct: number
|
||||||
|
vol_penalty: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScoreDetail {
|
export interface ScoreDetail {
|
||||||
short_details?: ShortDetail[]
|
short_details?: ShortDetail[]
|
||||||
medium_detail?: MediumDetail
|
medium_detail?: MediumDetail
|
||||||
long_detail?: LongDetail
|
long_detail?: LongDetail
|
||||||
|
volatility?: VolatilityDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Score {
|
export interface Score {
|
||||||
|
|||||||
@@ -34,10 +34,32 @@ watch(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const quadrantTag = (q: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
accumulation: 'success',
|
||||||
|
distribution: 'warning',
|
||||||
|
covering: 'info',
|
||||||
|
liquidation: 'danger',
|
||||||
|
flat: '',
|
||||||
|
}
|
||||||
|
return map[q] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const quadrantLabel = (q: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
accumulation: '增仓涨',
|
||||||
|
distribution: '增仓跌',
|
||||||
|
covering: '减仓涨',
|
||||||
|
liquidation: '减仓跌',
|
||||||
|
flat: '持平',
|
||||||
|
}
|
||||||
|
return map[q] ?? q
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-drawer v-model="visible" title="打分明细" :size="isMobile ? '92%' : '640px'" destroy-on-close>
|
<el-drawer v-model="visible" title="打分明细" :size="isMobile ? '92%' : '680px'" destroy-on-close>
|
||||||
<div v-loading="loading" v-if="score">
|
<div v-loading="loading" v-if="score">
|
||||||
<el-descriptions :column="isMobile ? 1 : 2" border>
|
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||||
<el-descriptions-item label="品种">{{ parseTsCode(score.ts_code).symbol }}</el-descriptions-item>
|
<el-descriptions-item label="品种">{{ parseTsCode(score.ts_code).symbol }}</el-descriptions-item>
|
||||||
@@ -60,20 +82,47 @@ watch(
|
|||||||
|
|
||||||
<h4 class="section">短期 7 日逐日打分</h4>
|
<h4 class="section">短期 7 日逐日打分</h4>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<el-table :data="score.detail?.short_details ?? []" size="small" border class="detail-table">
|
<el-table
|
||||||
|
:data="score.detail?.short_details ?? []"
|
||||||
|
size="small"
|
||||||
|
border
|
||||||
|
class="detail-table"
|
||||||
|
max-height="400"
|
||||||
|
>
|
||||||
<el-table-column prop="trade_date" label="日期" width="100" />
|
<el-table-column prop="trade_date" label="日期" width="100" />
|
||||||
<el-table-column prop="close" label="收盘" />
|
<el-table-column prop="close" label="收盘" width="70" />
|
||||||
<el-table-column prop="pre_close" label="昨收" />
|
<el-table-column label="涨跌幅" width="80">
|
||||||
<el-table-column prop="oi" label="持仓" />
|
<template #default="{ row }">
|
||||||
<el-table-column prop="oi_chg" label="持仓变化" />
|
<span :style="{ color: row.price_chg_pct >= 0 ? '#e4393c' : '#1ca11c' }">
|
||||||
<el-table-column prop="score" label="单日得分" />
|
{{ ((row.price_chg_pct ?? 0) * 100).toFixed(2) }}%
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="OI变化%" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ ((row.oi_chg_pct ?? 0) * 100).toFixed(2) }}%
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="量比" width="65">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.vol_ratio ?? '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="象限" width="85">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="quadrantTag(row.quadrant)" size="small">
|
||||||
|
{{ quadrantLabel(row.quadrant) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="score" label="得分" width="65" />
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="section">中期(15d)细节</h4>
|
<h4 class="section">中期(15d)细节</h4>
|
||||||
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.medium_detail">
|
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.medium_detail">
|
||||||
<el-descriptions-item label="价格收益率">
|
<el-descriptions-item label="价格收益率">
|
||||||
{{ (score.detail.medium_detail.price_return_pct * 100).toFixed(2) }}%
|
{{ score.detail.medium_detail.price_return_pct.toFixed(2) }}%
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="价格信号分">
|
<el-descriptions-item label="价格信号分">
|
||||||
{{ score.detail.medium_detail.price_signal.toFixed(2) }}
|
{{ score.detail.medium_detail.price_signal.toFixed(2) }}
|
||||||
@@ -85,20 +134,48 @@ watch(
|
|||||||
{{ score.detail.medium_detail.long_down_days }}
|
{{ score.detail.medium_detail.long_down_days }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="资金意愿分" :span="isMobile ? 1 : 2">
|
<el-descriptions-item label="资金意愿分" :span="isMobile ? 1 : 2">
|
||||||
{{ score.detail.medium_detail.fund_signal }}
|
{{ score.detail.medium_detail.fund_signal.toFixed(1) }}
|
||||||
|
<span class="formula-hint">(50 + (增仓涨 - 增仓跌)/{{ score.detail.medium_detail.window ?? 15 }} × 50)</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<h4 class="section">长期(30d)细节</h4>
|
<h4 class="section">长期(30d)细节</h4>
|
||||||
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.long_detail">
|
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.long_detail">
|
||||||
|
<el-descriptions-item label="OI 趋势分">
|
||||||
|
{{ score.detail.long_detail.oi_score?.toFixed(1) ?? '-' }}
|
||||||
|
<span class="formula-hint">(权重 60%)</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="价格趋势分">
|
||||||
|
{{ score.detail.long_detail.price_score?.toFixed(1) ?? '-' }}
|
||||||
|
<span class="formula-hint">(权重 40%)</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="30 日价格收益">
|
||||||
|
{{ score.detail.long_detail.price_return_30d_pct?.toFixed(2) ?? '-' }}%
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="30 日前收盘">
|
||||||
|
{{ score.detail.long_detail.price_before_30d?.toFixed(2) ?? '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="30 日均持仓">
|
<el-descriptions-item label="30 日均持仓">
|
||||||
{{ score.detail.long_detail.avg_oi.toFixed(0) }}
|
{{ score.detail.long_detail.avg_oi.toFixed(0) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="30 日前持仓">
|
<el-descriptions-item label="30 日前持仓">
|
||||||
{{ score.detail.long_detail.oi_before.toFixed(0) }}
|
{{ score.detail.long_detail.oi_before.toFixed(0) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="变化幅度" :span="isMobile ? 1 : 2">
|
<el-descriptions-item label="OI 变化幅度" :span="isMobile ? 1 : 2">
|
||||||
{{ (score.detail.long_detail.change_pct * 100).toFixed(2) }}%
|
{{ score.detail.long_detail.change_pct.toFixed(2) }}%
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<h4 class="section">波动率调整</h4>
|
||||||
|
<el-descriptions :column="isMobile ? 1 : 2" border v-if="score.detail?.volatility">
|
||||||
|
<el-descriptions-item label="日波动率(30d std)">
|
||||||
|
{{ (score.detail.volatility.daily_vol_pct * 100).toFixed(2) }}%
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="ATR%">
|
||||||
|
{{ (score.detail.volatility.atr_pct * 100).toFixed(2) }}%
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="波动率惩罚系数" :span="isMobile ? 1 : 2">
|
||||||
|
{{ score.detail.volatility.vol_penalty.toFixed(3) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,6 +190,11 @@ watch(
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
.detail-table {
|
.detail-table {
|
||||||
min-width: 520px;
|
min-width: 620px;
|
||||||
|
}
|
||||||
|
.formula-hint {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user