mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
feat: 盘面洞察收官 — AI 盘面复盘 + 板块主力资金日历
- AI 盘面复盘(市场情绪页):一键汇总当前情绪/量能/涨停梯队数据生成 摘要 Prompt,走 /llm/chat/async 异步任务 + 轮询,回复页内展示并经 _record_history 自动归档「AI 解读历史」 - 板块资金日历:FundFlowSampler 每交易日 14:45 后经 get_board_ranking(main_net_amount) 采样行业主力净流入 Top10, SentimentStore 新增 board_fund 表((date,rank) 幂等);口径为 "涨幅前 50 名中主力净流入最高 10 个"(逐板块 summary 太贵,非全市场 严格排序);/market/board-fund/history + 市场情绪页日历视图 - app.py lifespan 接线启停;路线图 ⑥⑩ 标记收官
This commit is contained in:
@@ -10,11 +10,11 @@
|
|||||||
| ② | **涨停生态 / 连板天梯** `/limitup` | 连板高度、首板/二板分布、炸板率、跌停 | 本地 vipdoc .day 文件(strength 扫描器同款读取器),close==涨停价 连续天数可回算 | ✅ 第二批 |
|
| ② | **涨停生态 / 连板天梯** `/limitup` | 连板高度、首板/二板分布、炸板率、跌停 | 本地 vipdoc .day 文件(strength 扫描器同款读取器),close==涨停价 连续天数可回算 | ✅ 第二批 |
|
||||||
| ① | **市场情绪时间线** `/sentiment` | 情绪处于冰点/回暖/高潮/退潮 | 涨跌家数、涨停跌停数逐分钟采样(新采样器 + sqlite) | ✅ 第三批 |
|
| ① | **市场情绪时间线** `/sentiment` | 情绪处于冰点/回暖/高潮/退潮 | 涨跌家数、涨停跌停数逐分钟采样(新采样器 + sqlite) | ✅ 第三批 |
|
||||||
| ⑨ | **市场宽度分时** `/sentiment` | 指数新高但上涨家数背离的顶部信号 | 依赖 ① 的采样器 | ✅ 第三批(随①) |
|
| ⑨ | **市场宽度分时** `/sentiment` | 指数新高但上涨家数背离的顶部信号 | 依赖 ① 的采样器 | ✅ 第三批(随①) |
|
||||||
| ⑥ | **板块资金日历** | 哪天钱涌向了哪个板块 | board summary 主力净额逐日采样 | ⏳ 后续批次 |
|
| ⑥ | **板块资金日历** `/sentiment` | 哪天钱涌向了哪个板块 | FundFlowSampler 每交易日 14:45 后采一次行业主力净流入 Top10(涨幅前 50 名口径) | ✅ 收官 |
|
||||||
| ⑤ | **板块相关性热力图** `/hotspots` | 哪些板块同涨同跌(抱团 vs 分散) | 热点滚动已缓存的 60 日涨跌矩阵求两两相关 | ✅ 第四批(热点滚动页内「相关性」视图) |
|
| ⑤ | **板块相关性热力图** `/hotspots` | 哪些板块同涨同跌(抱团 vs 分散) | 热点滚动已缓存的 60 日涨跌矩阵求两两相关 | ✅ 第四批(热点滚动页内「相关性」视图) |
|
||||||
| ③ | ~~异动雷达时间线~~ | ~~异动密度骤增 = 盘面转折点~~ | `/mac/unusual` 盘中数据源质量差(盘中几乎无记录),**整项取消**(页面/导航/端点已移除) | ❌ 已取消 |
|
| ③ | ~~异动雷达时间线~~ | ~~异动密度骤增 = 盘面转折点~~ | `/mac/unusual` 盘中数据源质量差(盘中几乎无记录),**整项取消**(页面/导航/端点已移除) | ❌ 已取消 |
|
||||||
| ⑧ | **量能仪表盘** `/sentiment` | 放量/缩量(两市累计成交 vs 5日同期均值) | 指数 5 分钟线现成 | ✅ 第四批(并入市场情绪页) |
|
| ⑧ | **量能仪表盘** `/sentiment` | 放量/缩量(两市累计成交 vs 5日同期均值) | 指数 5 分钟线现成 | ✅ 第四批(并入市场情绪页) |
|
||||||
| ⑩ | **AI 盘面早报/复盘** | 把以上所有数据"自动读"给你听 | LLM 管道 + ai-history 归档现成 | ⏳ 收尾(必须做) |
|
| ⑩ | **AI 盘面复盘** `/sentiment` | 把以上所有数据"自动读"给你听 | LLM 管道 + ai-history 归档现成 | ✅ 收官(情绪页「生成 AI 复盘」按钮,异步任务 + 自动归档) |
|
||||||
|
|
||||||
## 批次
|
## 批次
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,19 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
ex_client = None
|
ex_client = None
|
||||||
app.state.ex_client = ex_client
|
app.state.ex_client = ex_client
|
||||||
|
|
||||||
|
# --- 板块资金采样器(交易日 14:45 后记一次行业主力净流入排行,供资金日历) ---
|
||||||
|
app.state.fund_flow_sampler = None
|
||||||
|
if mac_client is not None:
|
||||||
|
try:
|
||||||
|
from easy_tdx.web.sentiment_sampler import FundFlowSampler
|
||||||
|
|
||||||
|
fund_sampler = FundFlowSampler(mac_client)
|
||||||
|
fund_sampler.start()
|
||||||
|
app.state.fund_flow_sampler = fund_sampler
|
||||||
|
logger.info("FundFlowSampler 已启动")
|
||||||
|
except Exception:
|
||||||
|
logger.warning("FundFlowSampler 启动失败 — 资金日历不可用", exc_info=True)
|
||||||
|
|
||||||
# --- 市场情绪采样器(交易时段每分钟落一条广度快照,供 /market/sentiment/*) ---
|
# --- 市场情绪采样器(交易时段每分钟落一条广度快照,供 /market/sentiment/*) ---
|
||||||
# 依赖标准 TDX 客户端(get_market_stat),mock 模式缩短间隔让曲线快速成形。
|
# 依赖标准 TDX 客户端(get_market_stat),mock 模式缩短间隔让曲线快速成形。
|
||||||
app.state.sentiment_sampler = None
|
app.state.sentiment_sampler = None
|
||||||
@@ -191,6 +204,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
# --- 停止板块资金采样器 ---
|
||||||
|
fund_svc = getattr(app.state, "fund_flow_sampler", None)
|
||||||
|
if fund_svc is not None:
|
||||||
|
try:
|
||||||
|
await fund_svc.stop()
|
||||||
|
except Exception:
|
||||||
|
logger.warning("FundFlowSampler stop failed", exc_info=True)
|
||||||
|
|
||||||
# --- 停止市场情绪采样器 ---
|
# --- 停止市场情绪采样器 ---
|
||||||
sampler_svc = getattr(app.state, "sentiment_sampler", None)
|
sampler_svc = getattr(app.state, "sentiment_sampler", None)
|
||||||
if sampler_svc is not None:
|
if sampler_svc is not None:
|
||||||
|
|||||||
@@ -195,6 +195,21 @@ async def limitup_history(
|
|||||||
return DictResponse.from_dict(payload)
|
return DictResponse.from_dict(payload)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/market/board-fund/history", response_model=DictResponse)
|
||||||
|
async def board_fund_history(
|
||||||
|
days: int = Query(15, ge=1, le=90, description="返回交易日数"),
|
||||||
|
) -> DictResponse:
|
||||||
|
"""行业主力净流入逐日排行(FundFlowSampler 每交易日 14:45 后采样一条)。
|
||||||
|
|
||||||
|
口径:涨幅前 50 名行业中主力净流入最高的 10 个(逐板块 summary 太贵,
|
||||||
|
非全市场严格排序)。数据需采样积累,页面空态有明示。
|
||||||
|
"""
|
||||||
|
from easy_tdx.web.sentiment_store import get_sentiment_store
|
||||||
|
|
||||||
|
days_rows = get_sentiment_store().list_fund_days(days)
|
||||||
|
return DictResponse.from_dict({"count": len(days_rows), "days": days_rows})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/fund-flow", response_model=DataFrameResponse)
|
@router.get("/fund-flow", response_model=DataFrameResponse)
|
||||||
async def fund_flow(
|
async def fund_flow(
|
||||||
market: str = Query(..., description="市场: SZ, SH"),
|
market: str = Query(..., description="市场: SZ, SH"),
|
||||||
|
|||||||
@@ -97,3 +97,81 @@ class SentimentSampler:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.samples += 1
|
self.samples += 1
|
||||||
|
|
||||||
|
|
||||||
|
class FundFlowSampler:
|
||||||
|
"""每日收盘前记录一次行业主力净流入排行(板块资金日历数据源)。
|
||||||
|
|
||||||
|
采样窗口:交易时段内 14:45 之后(临近收盘的净流入已基本定型),
|
||||||
|
每日只采一次(``latest_fund_date`` 幂等)。数据走 MAC
|
||||||
|
``get_board_ranking(sort_by="main_net_amount")``——该实现先按涨幅
|
||||||
|
取候选池再聚合 summary,因此口径是"涨幅前 ``top_n`` 名中主力净流入
|
||||||
|
最高的 ``keep`` 个行业",并非全市场严格排序(逐板块 summary 太贵)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: Any,
|
||||||
|
store: SentimentStore | None = None,
|
||||||
|
interval: float = 300.0,
|
||||||
|
top_n: int = 50,
|
||||||
|
keep: int = 10,
|
||||||
|
):
|
||||||
|
self._client = client
|
||||||
|
self._store = store or get_sentiment_store()
|
||||||
|
self._interval = interval
|
||||||
|
self._top_n = top_n
|
||||||
|
self._keep = keep
|
||||||
|
self._task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
if self._task is None or self._task.done():
|
||||||
|
self._task = asyncio.create_task(self._run())
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
if self._task is not None:
|
||||||
|
self._task.cancel()
|
||||||
|
try:
|
||||||
|
await self._task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
self._task = None
|
||||||
|
|
||||||
|
async def _run(self) -> None:
|
||||||
|
logger.info("FundFlowSampler 启动(间隔 %ss,交易日 14:45 后每日一条)", self._interval)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
now = datetime.now()
|
||||||
|
if is_trading_time(now) and (now.hour * 100 + now.minute) >= 1445:
|
||||||
|
await self._sample_once()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception: # noqa: BLE001 — 采样器永不退出
|
||||||
|
logger.warning("板块资金采样失败", exc_info=True)
|
||||||
|
await asyncio.sleep(self._interval)
|
||||||
|
|
||||||
|
async def _sample_once(self) -> None:
|
||||||
|
from easy_tdx.mac.enums import BoardType
|
||||||
|
|
||||||
|
today = int(datetime.now().strftime("%Y%m%d"))
|
||||||
|
if self._store.latest_fund_date() == today:
|
||||||
|
return # 当日已采样
|
||||||
|
df = await self._client.get_board_ranking(
|
||||||
|
board_type=BoardType.HY,
|
||||||
|
top_n=self._top_n,
|
||||||
|
sort_by="main_net_amount",
|
||||||
|
ascending=False,
|
||||||
|
)
|
||||||
|
if df is None or df.empty:
|
||||||
|
return
|
||||||
|
ranked = df.sort_values("main_net_amount", ascending=False).head(self._keep)
|
||||||
|
boards = [
|
||||||
|
{
|
||||||
|
"code": str(r["code"]),
|
||||||
|
"name": str(r.get("name", r["code"])),
|
||||||
|
"main_net": round(float(r["main_net_amount"]), 0),
|
||||||
|
}
|
||||||
|
for _, r in ranked.iterrows()
|
||||||
|
]
|
||||||
|
self._store.upsert_fund_day(today, boards)
|
||||||
|
logger.info("板块资金采样完成:%s,Top1 %s", today, boards[0]["name"] if boards else "-")
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ class SentimentStore:
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_samples_date ON samples(date)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_samples_date ON samples(date)")
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS board_fund (
|
||||||
|
date INTEGER NOT NULL, -- YYYYMMDD
|
||||||
|
rank INTEGER NOT NULL, -- 主力净流入名次(1 起)
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
main_net REAL NOT NULL, -- 主力净流入(元)
|
||||||
|
PRIMARY KEY (date, rank)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -94,6 +106,62 @@ class SentimentStore:
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def latest_fund_date(self) -> int:
|
||||||
|
"""最近有板块资金采样的交易日(YYYYMMDD),无数据返回 0。"""
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
row = conn.execute("SELECT MAX(date) AS d FROM board_fund").fetchone()
|
||||||
|
return int(row["d"] or 0)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def upsert_fund_day(self, date: int, boards: list[dict[str, Any]]) -> None:
|
||||||
|
"""覆盖写入某日行业主力净流入排行(rank 按列表顺序 1 起)。"""
|
||||||
|
with _write_lock:
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
conn.execute("DELETE FROM board_fund WHERE date = ?", (int(date),))
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO board_fund (date, rank, code, name, main_net) VALUES (?,?,?,?,?)",
|
||||||
|
[
|
||||||
|
(int(date), i + 1, str(b["code"]), str(b["name"]), float(b["main_net"]))
|
||||||
|
for i, b in enumerate(boards)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def list_fund_days(self, days: int = 15) -> list[dict[str, Any]]:
|
||||||
|
"""近 N 个有采样的交易日(降序),每日主力净流入排行。"""
|
||||||
|
conn = self._connect()
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT date, rank, code, name, main_net
|
||||||
|
FROM board_fund
|
||||||
|
WHERE date IN (
|
||||||
|
SELECT DISTINCT date FROM board_fund ORDER BY date DESC LIMIT ?
|
||||||
|
)
|
||||||
|
ORDER BY date DESC, rank
|
||||||
|
""",
|
||||||
|
(int(days),),
|
||||||
|
).fetchall()
|
||||||
|
grouped: dict[int, dict[str, Any]] = {}
|
||||||
|
for r in rows:
|
||||||
|
g = grouped.setdefault(int(r["date"]), {"date": int(r["date"]), "boards": []})
|
||||||
|
g["boards"].append(
|
||||||
|
{
|
||||||
|
"rank": int(r["rank"]),
|
||||||
|
"code": str(r["code"]),
|
||||||
|
"name": str(r["name"]),
|
||||||
|
"main_net": float(r["main_net"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return list(grouped.values())
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def day_samples(self, date: int) -> list[dict[str, Any]]:
|
def day_samples(self, date: int) -> list[dict[str, Any]]:
|
||||||
"""某交易日的全部分钟采样(按时间升序)。"""
|
"""某交易日的全部分钟采样(按时间升序)。"""
|
||||||
conn = self._connect()
|
conn = self._connect()
|
||||||
|
|||||||
@@ -748,6 +748,13 @@ export interface LimitUpHistoryRow {
|
|||||||
limit_down: number
|
limit_down: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 板块主力资金日历(GET /api/v1/market/board-fund/history,每日采样) ─────
|
||||||
|
|
||||||
|
export interface BoardFundDay {
|
||||||
|
date: number
|
||||||
|
boards: Array<{ rank: number; code: string; name: string; main_net: number }>
|
||||||
|
}
|
||||||
|
|
||||||
// ── Walk-Forward 样本外验证(v1.27 POST /backtest/wf/run/async)──────────────
|
// ── Walk-Forward 样本外验证(v1.27 POST /backtest/wf/run/async)──────────────
|
||||||
|
|
||||||
export interface WalkForwardWindow {
|
export interface WalkForwardWindow {
|
||||||
|
|||||||
@@ -7,14 +7,23 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|||||||
import echarts, { DOWN_COLOR, UP_COLOR } from '../echarts-setup'
|
import echarts, { DOWN_COLOR, UP_COLOR } from '../echarts-setup'
|
||||||
import {
|
import {
|
||||||
fetchBars,
|
fetchBars,
|
||||||
|
fetchBoardFundHistory,
|
||||||
|
fetchLimitUpEcology,
|
||||||
fetchLimitUpHistory,
|
fetchLimitUpHistory,
|
||||||
fetchMarketStat,
|
fetchMarketStat,
|
||||||
fetchSentimentHistory,
|
fetchSentimentHistory,
|
||||||
fetchSentimentToday,
|
fetchSentimentToday,
|
||||||
formatError,
|
formatError,
|
||||||
|
runLlmChatWithPolling,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { fmtAmount, fmtPctSigned } from '../format'
|
import { fmtAmount, fmtPctSigned } from '../format'
|
||||||
import type { LimitUpHistoryRow, MarketStat, SentimentDay, SentimentSample } from '../types'
|
import type {
|
||||||
|
BoardFundDay,
|
||||||
|
LimitUpHistoryRow,
|
||||||
|
MarketStat,
|
||||||
|
SentimentDay,
|
||||||
|
SentimentSample,
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
const today = ref<{ date: number; count?: number; samples: SentimentSample[] } | null>(null)
|
const today = ref<{ date: number; count?: number; samples: SentimentSample[] } | null>(null)
|
||||||
const histDays = ref<SentimentDay[]>([])
|
const histDays = ref<SentimentDay[]>([])
|
||||||
@@ -212,6 +221,7 @@ const volEl = ref<HTMLDivElement>()
|
|||||||
let volChart: echarts.ECharts | null = null
|
let volChart: echarts.ECharts | null = null
|
||||||
const volRatio = ref<number | null>(null)
|
const volRatio = ref<number | null>(null)
|
||||||
const volDate = ref('')
|
const volDate = ref('')
|
||||||
|
const fundDays = ref<BoardFundDay[]>([])
|
||||||
|
|
||||||
async function loadVolume() {
|
async function loadVolume() {
|
||||||
try {
|
try {
|
||||||
@@ -304,11 +314,91 @@ async function loadVolume() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadFund() {
|
||||||
|
try {
|
||||||
|
fundDays.value = await fetchBoardFundHistory(15)
|
||||||
|
} catch {
|
||||||
|
fundDays.value = [] // 资金日历独立降级
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let timer = 0
|
let timer = 0
|
||||||
|
|
||||||
|
// ── ⑩ AI 盘面复盘:自动汇总上方数据 → LLM 生成 → 自动归档「AI 解读历史」 ─────
|
||||||
|
|
||||||
|
const aiReply = ref('')
|
||||||
|
const aiBusy = ref(false)
|
||||||
|
const aiError = ref('')
|
||||||
|
const aiModel = ref('')
|
||||||
|
|
||||||
|
async function buildDigest(): Promise<string> {
|
||||||
|
const lines: string[] = []
|
||||||
|
const s = stat.value
|
||||||
|
if (s) {
|
||||||
|
const denom = Math.max(s.up_count + s.down_count, 1)
|
||||||
|
lines.push(
|
||||||
|
`上涨 ${s.up_count} 家 / 下跌 ${s.down_count} 家(上涨占比 ${((100 * s.up_count) / denom).toFixed(1)}%),` +
|
||||||
|
`涨停 ${s.limit_up_count} 家,跌停 ${s.limit_down_count} 家,两市成交 ${fmtAmount(s.total_amount)}。`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (volRatio.value !== null) {
|
||||||
|
lines.push(`量能:当日两市累计成交较近 5 日同期均值 ${fmtPctSigned(volRatio.value)}。`)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const eco = await fetchLimitUpEcology()
|
||||||
|
const sm = eco.summary
|
||||||
|
lines.push(`连板高度 ${sm.max_streak} 板(首板 ${sm.first_board}、二板 ${sm.second_board}、3 板以上 ${sm.plus3}),炸板率 ${sm.blown_rate ?? '-'}%。`)
|
||||||
|
} catch {
|
||||||
|
// 涨停生态不可用时跳过该维度
|
||||||
|
}
|
||||||
|
const lu = luHistory.value.slice(-5)
|
||||||
|
if (lu.length) {
|
||||||
|
lines.push(
|
||||||
|
`近 5 日涨停家数:${lu.map((r) => `${String(r.date).slice(4, 6)}-${String(r.date).slice(6, 8)} ${r.limit_up}`).join(';')}。`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const sampled = histDays.value.filter((d) => d.n >= 10).slice(-5)
|
||||||
|
if (sampled.length) {
|
||||||
|
lines.push(
|
||||||
|
`采样上涨占比:${sampled.map((d) => `${String(d.date).slice(4, 6)}-${String(d.date).slice(6, 8)} ${d.up_ratio}%`).join(';')}。`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (lines.length === 0) return ''
|
||||||
|
return `以下是最新的 A 股盘面数据摘要:\n${lines.join('\n')}\n\n` +
|
||||||
|
'请以资深市场分析师的口吻写一段 200~400 字的盘面复盘,依次覆盖:1) 市场情绪与赚钱效应;' +
|
||||||
|
'2) 量能特征(放量/缩量及其含义);3) 涨停梯队与炸板率反映的题材热度与分歧;4) 结尾一句风险提示。' +
|
||||||
|
'直接给观点和逻辑,不要复述数据。'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateReview() {
|
||||||
|
aiBusy.value = true
|
||||||
|
aiError.value = ''
|
||||||
|
aiReply.value = ''
|
||||||
|
try {
|
||||||
|
const digest = await buildDigest()
|
||||||
|
if (!digest) {
|
||||||
|
aiError.value = '暂无盘面数据可生成复盘'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const state = await runLlmChatWithPolling(digest)
|
||||||
|
if (state.status === 'failed') {
|
||||||
|
throw new Error(String((state as { error?: string }).error ?? 'AI 解读任务失败'))
|
||||||
|
}
|
||||||
|
const result = state.result as { reply?: string; model?: string }
|
||||||
|
aiReply.value = result.reply ?? ''
|
||||||
|
aiModel.value = result.model ?? ''
|
||||||
|
} catch (e) {
|
||||||
|
aiError.value = formatError(e)
|
||||||
|
} finally {
|
||||||
|
aiBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await load()
|
await load()
|
||||||
loadVolume()
|
loadVolume()
|
||||||
|
loadFund()
|
||||||
timer = window.setInterval(() => {
|
timer = window.setInterval(() => {
|
||||||
if (document.hidden) return
|
if (document.hidden) return
|
||||||
load()
|
load()
|
||||||
@@ -389,6 +479,41 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 板块主力资金日历 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="sec-title">行业主力资金 · 每日净流入 Top 10(交易日 14:45 后采样,需积累)</div>
|
||||||
|
<div class="card fund-card">
|
||||||
|
<div v-for="d in fundDays" :key="d.date" class="fund-row">
|
||||||
|
<span class="mono dim fund-date">{{ String(d.date).slice(4, 6) }}-{{ String(d.date).slice(6, 8) }}</span>
|
||||||
|
<span v-for="b in d.boards" :key="b.code" class="fund-chip mono">
|
||||||
|
{{ b.name }} <span class="up">+{{ (b.main_net / 1e8).toFixed(1) }}亿</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="fundDays.length === 0" class="empty-hint dim">
|
||||||
|
尚无采样:每个交易日的 14:45 后自动记录一次行业主力净流入排行(涨幅前 50 名口径),持续运行后日历成形。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 盘面复盘 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="sec-title-ai">
|
||||||
|
AI 盘面复盘
|
||||||
|
<button class="gen-btn" :disabled="aiBusy" @click="generateReview">
|
||||||
|
{{ aiBusy ? '生成中…(约 1~3 分钟)' : aiReply ? '重新生成' : '生成 AI 复盘' }}
|
||||||
|
</button>
|
||||||
|
<span v-if="aiModel" class="dim">{{ aiModel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card ai-card">
|
||||||
|
<div v-if="aiBusy" class="dim">模型基于上方情绪 / 量能 / 涨停数据生成中…</div>
|
||||||
|
<div v-else-if="aiError" class="up">{{ aiError }}</div>
|
||||||
|
<div v-else-if="aiReply" class="ai-reply">{{ aiReply }}</div>
|
||||||
|
<div v-else class="dim">
|
||||||
|
汇总本页情绪 / 量能 / 涨停数据交给已配置的模型生成复盘,自动归档到「AI 解读历史」。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 近 60 日情绪 -->
|
<!-- 近 60 日情绪 -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="sec-title">近 60 日 · 涨停/跌停家数(vipdoc 回补)与上涨占比(采样积累)</div>
|
<div class="sec-title">近 60 日 · 涨停/跌停家数(vipdoc 回补)与上涨占比(采样积累)</div>
|
||||||
@@ -501,6 +626,49 @@ onBeforeUnmount(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
.sec-title-ai {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.gen-btn {
|
||||||
|
font-size: 11.5px;
|
||||||
|
padding: 3px 12px;
|
||||||
|
}
|
||||||
|
.gen-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.fund-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.fund-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.fund-date {
|
||||||
|
width: 44px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.fund-chip {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.ai-card {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
.chart-card {
|
.chart-card {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
Reference in New Issue
Block a user