页面收尾包:单票研究面接口补齐六块,抽屉四芯片、候选栏芯片、管理研究面列
评审发现六/七:研究面接口只给技术面一块,候选栏与管理视图缺研究面。补齐:
- GET /api/research/{ts_code} 补成六块:公司质地/基本面/技术面/择时/合议/各路来源状态。
持仓票的买方评析从逻辑状态映射取,候选票从当日计划主榜与观察档取(_research_st),
四块用 consensus_service.assemble 凑齐,company 块复用 logic_state_service.company_view。
任一路取不到按无读数弃权,sources 逐路标 ok/无读数/错误。
- 单票抽屉那一节改成「研究面 · 三源合议」四枚芯片(基本面/技术面/择时/合议),
按立场红涨绿跌、悬停看理由,下面保留技术面详细读数与评析报告链接。
- 候选栏每行加质地芯片与技术芯片:disposition_snapshot 把候选处置快照按代码挂上合议摘要
(consensus_seen 加 overall),前端用 conOf(code) 取。
- 管理视图持仓总览加研究面列,与「我的持仓」那列同款。
前端三处由两道页面守卫(wiring 204/324 对齐、enum 通过)与本地 uvicorn 浏览器冒烟
(页面完整挂载不白屏)兜;真实数据视觉判收在 155 部署后过目。第三十三批5例,
全量871例 ALL SUITES PASS。未推未部署(随盘中确认包下批一起)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
64863171f4
commit
2f64274d53
|
|
@ -516,7 +516,9 @@ def _attach_consensus(cands, out=None, now=None) -> None:
|
|||
rec = {"ts_code": code, "fund": fund.get("stance"),
|
||||
"tech": tech.get("stance"), "timing": tm.get("stance"),
|
||||
"direction": con.get("direction"), "route": con.get("route"),
|
||||
"phase": tech.get("phase")}
|
||||
"phase": tech.get("phase"),
|
||||
# 候选栏芯片用: 基本面质地档 (好/中/差), 从候选自带的买方评析取
|
||||
"overall": (c.get("company_review") or {}).get("overall")}
|
||||
if ikind:
|
||||
rec["intraday"] = ikind
|
||||
seen.append(rec)
|
||||
|
|
@ -705,9 +707,18 @@ def disposition_snapshot(now=None) -> dict:
|
|||
if s.get("why"):
|
||||
res["notes"].append(s["why"])
|
||||
else: # 单票原因; 不覆盖已判 would 的
|
||||
# 合议判观察的候选带 disp=wait_tech (等技术面开口), 其余不产出的按 deny。
|
||||
# 合议判观察的候选带 disp=wait_tech/wait_confirm, 其余不产出的按 deny。
|
||||
res["by_code"].setdefault(
|
||||
code, {"disp": s.get("disp") or "deny", "why": s.get("why") or "未产出候选"})
|
||||
# 候选栏三小块 (2026-09-14 页面收尾包): 每只候选的合议摘要, 从 consensus_seen 来。
|
||||
# 基本面立场与质地档 / 技术面立场与相位 / 合议方向与路由。挂到对应代码的 by_code 行上。
|
||||
for rec in (sink.get("consensus_seen") or []):
|
||||
code = rec.get("ts_code")
|
||||
if code and code in res["by_code"]:
|
||||
res["by_code"][code]["consensus"] = {
|
||||
"fund": rec.get("fund"), "overall": rec.get("overall"),
|
||||
"tech": rec.get("tech"), "phase": rec.get("phase"),
|
||||
"direction": rec.get("direction"), "route": rec.get("route")}
|
||||
return res
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -737,22 +737,60 @@ def api_tech_pull():
|
|||
return ok_logged("tech_pull", tech_service.pull_and_map)
|
||||
|
||||
|
||||
def _research_st(code):
|
||||
"""研究面的公司评析来源: 持仓票从逻辑状态映射取, 候选票从当日计划主榜与观察档取。
|
||||
返回 (逻辑状态样式的 dict 或 None, 来源标签)。取不到就当无读数弃权, 绝不折成看空。"""
|
||||
from app.services import logic_state_service, plan_feed
|
||||
try:
|
||||
st = logic_state_service.state_map().get(code)
|
||||
if st and (st.get("company_review") or {}).get("overall"):
|
||||
return st, "held"
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("[研究面] 取持仓逻辑状态失败 %s: %s", code, e)
|
||||
try:
|
||||
plan = plan_feed.get_plan()
|
||||
for r in (plan.get("main") or []) + (plan.get("observe") or []):
|
||||
if cs.normalize_code(r.get("ts_code") or "") == code and (r.get("company_review") or {}).get("overall"):
|
||||
return {"company_review": r.get("company_review"),
|
||||
"company_review_text": r.get("company_review_text")}, "candidate"
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("[研究面] 取候选计划失败 %s: %s", code, e)
|
||||
return None, "none"
|
||||
|
||||
|
||||
@app.get("/api/research/{ts_code}")
|
||||
def api_research(ts_code: str):
|
||||
"""单票研究面 · 三源合议 (点击时调, 只读, 绝不进轮询)。
|
||||
|
||||
工作包一先给技术面读数一块; 基本面评析、择时立场与三源合议由工作包二补 (现以 None 占位,
|
||||
页面按缺块处理)。代码归一后回。"""
|
||||
from app.services import tech_service
|
||||
"""单票研究面 · 三源合议 (点击时调, 只读, 绝不进轮询)。六块: 公司质地 / 基本面 / 技术面 /
|
||||
择时 / 合议 / 各路来源状态。任何一路取不到按无读数弃权 (设计原则二), 页面按缺块处理。"""
|
||||
from app.services import tech_service, consensus_service, logic_state_service
|
||||
|
||||
def _build(code):
|
||||
code = cs.normalize_code(code)
|
||||
t = tech_service.research_feed(code)
|
||||
return {"ts_code": code, "company": None, "tech": t.get("tech"),
|
||||
"tech_note": t.get("note") or t.get("error"),
|
||||
"consensus": None, "timing": None, "feed": None,
|
||||
"sources": {"tech": ("ok" if t.get("tech")
|
||||
else (t.get("error") or t.get("note") or "无读数"))}}
|
||||
st, src = _research_st(code)
|
||||
cr = (st or {}).get("company_review")
|
||||
company = logic_state_service.company_view(st) if st else None
|
||||
# 四块意见: assemble 用 company_review 算基本面、用映射算技术面、用昨夜定性算择时, 再合议。
|
||||
nightly = consensus_service.nightly_map([code]).get(code)
|
||||
tech_states = consensus_service.state_map()
|
||||
blocks = consensus_service.assemble({"ts_code": code, "company_review": cr},
|
||||
nightly=nightly, tech_states=tech_states)
|
||||
# 技术面详细读数 (点击时现读, 带最新一行与近日翻向次数); 缺则退回映射里的紧凑块。
|
||||
tfeed = tech_service.research_feed(code)
|
||||
tech = tfeed.get("tech") or blocks["tech"]
|
||||
tm = blocks["timing"]
|
||||
timing = {"stance": tm.get("stance"), "nightly": tm.get("nightly"),
|
||||
"trade_date": (nightly or {}).get("trade_date"),
|
||||
"flip_at": tm.get("intraday_flip_at")} # 转多时刻点击不回扫, 留昨夜定性带的
|
||||
fund = blocks["fund"]
|
||||
sources = {
|
||||
"company": ("ok" if company else ("无读数" if src != "none" else "不在持仓也不在候选")),
|
||||
"fund": ("ok" if fund.get("stance") != "无读数" else (fund.get("why") or "无读数")),
|
||||
"tech": ("ok" if tfeed.get("tech") else (tfeed.get("error") or tfeed.get("note") or "无读数")),
|
||||
"timing": ("ok" if tm.get("stance") != "无读数" else (tm.get("no_read_why") or "无读数")),
|
||||
"consensus": "ok",
|
||||
}
|
||||
return {"ts_code": code, "company": company, "fund": fund, "tech": tech,
|
||||
"timing": timing, "consensus": blocks["consensus"], "sources": sources}
|
||||
return ok(_build, ts_code)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1260,6 +1260,11 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
|||
<span class="nm">{{ r.name || nm(r.ts_code) }}</span>
|
||||
<span class="cat" v-if="r.theme">{{ r.theme }}</span>
|
||||
<span class="sp"></span>
|
||||
<!-- 三源合议两枚芯片 (2026-09-14 页面收尾包): 质地档 + 技术面立场·相位。数据从候选处置的合议摘要来。 -->
|
||||
<template v-if="conOf(r.ts_code)">
|
||||
<b v-if="conOf(r.ts_code).overall" :class="conOf(r.ts_code).overall==='好'?'clr-up':(conOf(r.ts_code).overall==='差'?'clr-down':'muted')" :title="'基本面 ' + (conOf(r.ts_code).fund||'无读数')" style="font-size:11.5px">质地{{ conOf(r.ts_code).overall }}</b>
|
||||
<b :class="techStanceCls(conOf(r.ts_code).tech)" :title="'技术面 ' + (conOf(r.ts_code).tech||'无读数') + ' · ' + (conOf(r.ts_code).phase||'—')" style="font-size:11.5px">{{ conOf(r.ts_code).tech || '技术—' }}</b>
|
||||
</template>
|
||||
<span class="disp" :class="dispOf(r.ts_code).cls" style="padding:0 6px">{{ dispOf(r.ts_code).label }}</span>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:6px;font-size:11.5px">
|
||||
|
|
@ -1724,6 +1729,18 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
|||
class="muted">今天没有读数</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 研究面列 (2026-09-14 页面收尾包): 与「我的持仓」那一列同款, 点技术面立场看研究面抽屉。 -->
|
||||
<el-table-column label="研究面" width="92">
|
||||
<template #default="s">
|
||||
<template v-if="s.row.tech">
|
||||
<b :class="techStanceCls(s.row.tech.stance)" style="cursor:pointer;text-decoration:underline dotted"
|
||||
@click.stop="openCodeSignals(s.row.ts_code)"
|
||||
:title="'技术面:' + s.row.tech.stance + '(' + (s.row.tech.phase||'') + ')\n' + (s.row.tech.reason||'') + '\n点一下看研究面'">{{ s.row.tech.stance }}</b>
|
||||
<div class="muted" style="font-size:11px" v-if="s.row.tech.phase">{{ s.row.tech.phase }}</div>
|
||||
</template>
|
||||
<span class="muted" v-else>无读数</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="做T" width="80">
|
||||
<template #default="s">
|
||||
<el-tag v-if="s.row.t0_enabled" size="small" type="warning" effect="dark">
|
||||
|
|
@ -2074,26 +2091,47 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
|||
</div>
|
||||
|
||||
<div class="cs-sect" v-if="techFeed || techFeedErr">
|
||||
<div class="h">研究面 · 技术面<span class="muted" style="font-weight:400" v-if="techFeed && techFeed.tech"> · 读数日 {{ techFeed.tech.data_date }}</span></div>
|
||||
<div class="muted" v-if="techFeedErr">读不到技术面:{{ techFeedErr }}</div>
|
||||
<div class="muted" v-else-if="!techFeed || !techFeed.tech">{{ (techFeed && techFeed.tech_note) || '这只票没有技术面读数' }}</div>
|
||||
<template v-else>
|
||||
<div class="cs-row">
|
||||
<span class="s">立场</span>
|
||||
<b :class="techStanceCls(techFeed.tech.stance)">{{ techFeed.tech.stance }}<span v-if="techFeed.tech.strength"> · {{ techFeed.tech.strength }}</span></b>
|
||||
<span v-if="techFeed.tech.phase">{{ techFeed.tech.phase }}</span>
|
||||
<span class="ab-stale" v-if="techFeed.tech.choppy">震荡市</span>
|
||||
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted" v-if="techFeed.tech.reason">{{ techFeed.tech.reason }}</div>
|
||||
<div class="h">研究面 · 三源合议<span class="muted" style="font-weight:400" v-if="techFeed && techFeed.tech && techFeed.tech.data_date"> · 技术面读数日 {{ techFeed.tech.data_date }}</span></div>
|
||||
<div class="muted" v-if="techFeedErr">读不到研究面:{{ techFeedErr }}</div>
|
||||
<template v-else-if="techFeed">
|
||||
<!-- 四枚芯片: 基本面 / 技术面 / 择时 / 合议。按立场红涨绿跌, 无读数灰; 悬停看一句理由。 -->
|
||||
<div class="cs-row" style="gap:10px;flex-wrap:wrap">
|
||||
<b :class="techStanceCls((techFeed.fund||{}).stance)" :title="(techFeed.fund||{}).fact || (techFeed.fund||{}).why || ('来源:' + ((techFeed.sources||{}).fund||'—'))">基本面 {{ (techFeed.fund||{}).stance || '无读数' }}<span class="muted" v-if="(techFeed.fund||{}).mark"> · {{ techFeed.fund.mark }}</span></b>
|
||||
<b :class="techStanceCls((techFeed.tech||{}).stance)" :title="(techFeed.tech||{}).reason || ('来源:' + ((techFeed.sources||{}).tech||'—'))">技术面 {{ (techFeed.tech||{}).stance || '无读数' }}<span class="muted" v-if="(techFeed.tech||{}).phase"> · {{ techFeed.tech.phase }}</span></b>
|
||||
<b :class="techStanceCls((techFeed.timing||{}).stance)" :title="'昨夜定性 ' + ((techFeed.timing||{}).nightly || '—') + '(' + ((techFeed.timing||{}).trade_date || '—') + ')'">择时 {{ (techFeed.timing||{}).stance || '无读数' }}</b>
|
||||
<b :class="techStanceCls((techFeed.consensus||{}).direction)" :title="(techFeed.consensus||{}).reason || ''">合议 {{ (techFeed.consensus||{}).direction || '—' }}<span class="muted" v-if="(techFeed.consensus||{}).strength"> · {{ techFeed.consensus.strength }}</span></b>
|
||||
</div>
|
||||
<div class="cs-row" v-if="techFeed.tech.latest">
|
||||
<span class="s">布林</span><span>{{ (techFeed.tech.latest.boll||{}).state || '—' }}</span>
|
||||
<span class="muted" v-if="(techFeed.tech.latest.boll||{}).squeeze">收口</span>
|
||||
<span class="s">多空布林线</span><span>{{ (techFeed.tech.latest.bbi||{}).state || '—' }}</span>
|
||||
<span class="s">SAR</span>
|
||||
<b :class="techFeed.tech.sar_side==='多'?'clr-up':'clr-down'">{{ techFeed.tech.sar_side || '—' }}</b>
|
||||
<span class="muted" v-if="techFeed.tech.sar_value!=null">止损位 <span class="mono">{{ techFeed.tech.sar_value }}</span></span>
|
||||
<span class="muted" v-if="techFeed.tech.sar_flip_days!=null">翻向 {{ techFeed.tech.sar_flip_days }} 天</span>
|
||||
<div class="cs-row" v-if="(techFeed.consensus||{}).route">
|
||||
<span class="s">路由</span><span>{{ techFeed.consensus.route }}</span>
|
||||
<span class="muted" v-if="techFeed.consensus.route_reason">{{ techFeed.consensus.route_reason }}</span>
|
||||
</div>
|
||||
<div class="cs-row" v-if="techFeed.company">
|
||||
<span class="s">质地</span><span>{{ techFeed.company.line }}</span>
|
||||
<a v-if="techFeed.company.report_url" :href="techFeed.company.report_url" target="_blank" rel="noopener">评析报告</a>
|
||||
<span class="ab-stale" v-if="techFeed.company.doubt_hard">硬疑点</span>
|
||||
</div>
|
||||
<div class="cs-row" v-if="(techFeed.company||{}).invalidation">
|
||||
<span class="s muted">失效条件</span><span class="muted">{{ techFeed.company.invalidation }}</span>
|
||||
</div>
|
||||
<template v-if="techFeed.tech">
|
||||
<div class="cs-row">
|
||||
<span class="s">技术面</span>
|
||||
<b :class="techStanceCls(techFeed.tech.stance)">{{ techFeed.tech.stance }}<span v-if="techFeed.tech.strength"> · {{ techFeed.tech.strength }}</span></b>
|
||||
<span v-if="techFeed.tech.phase">{{ techFeed.tech.phase }}</span>
|
||||
<span class="ab-stale" v-if="techFeed.tech.choppy">震荡市</span>
|
||||
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted" v-if="techFeed.tech.reason">{{ techFeed.tech.reason }}</div>
|
||||
</div>
|
||||
<div class="cs-row" v-if="techFeed.tech.latest">
|
||||
<span class="s">布林</span><span>{{ (techFeed.tech.latest.boll||{}).state || '—' }}</span>
|
||||
<span class="muted" v-if="(techFeed.tech.latest.boll||{}).squeeze">收口</span>
|
||||
<span class="s">多空布林线</span><span>{{ (techFeed.tech.latest.bbi||{}).state || '—' }}</span>
|
||||
<span class="s">SAR</span>
|
||||
<b :class="techFeed.tech.sar_side==='多'?'clr-up':'clr-down'">{{ techFeed.tech.sar_side || '—' }}</b>
|
||||
<span class="muted" v-if="techFeed.tech.sar_value!=null">止损位 <span class="mono">{{ techFeed.tech.sar_value }}</span></span>
|
||||
<span class="muted" v-if="techFeed.tech.sar_flip_days!=null">翻向 {{ techFeed.tech.sar_flip_days }} 天</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="muted" v-else>{{ (techFeed.sources||{}).tech || '这只票没有技术面读数' }}</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
|
@ -2888,6 +2926,9 @@ createApp({
|
|||
}
|
||||
return { cls: 'wait', label: '在盯', why: '' };
|
||||
}
|
||||
// 候选行的三源合议摘要 (2026-09-14 页面收尾包): 后端候选处置快照 by_code 里挂的 consensus 小块
|
||||
// (fund/overall/tech/phase/direction/route)。没有就返回 null, 芯片整段不显示。
|
||||
const conOf = (code) => ((openScan.value.by_code || {})[code] || {}).consensus || null;
|
||||
const denyToday = computed(() => (planRows.value || []).map(r => {
|
||||
const d = dispOf(r.ts_code);
|
||||
return d.cls === 'deny' ? { ts_code: r.ts_code, name: (r.name || nm(r.ts_code)), why: d.why } : null;
|
||||
|
|
@ -4287,7 +4328,7 @@ createApp({
|
|||
autoScanRes, autoScanBusy, autoScanPreview, stageTagType, autoInfo,
|
||||
openStrategy, validateStrategy, attachStrategy, setStrategyStatus, stratStateText, resumeBuy,
|
||||
showLedger, onPosExpand,
|
||||
openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, loadOpenScan, denyToday,
|
||||
openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, conOf, loadOpenScan, denyToday,
|
||||
commandsShown, strategiesShown, instrLive, showAllCommands, showAllStrategies, showAllInstr,
|
||||
cmdArchivable, stratArchivable, archiveRow,
|
||||
showOpLog, showSettings, watchCandidates, marketOpen, marketState, lastRefresh, autoRefresh,
|
||||
|
|
|
|||
|
|
@ -98,16 +98,19 @@
|
|||
取不到按等待/时段外开口进观察)/in_window 与 reask_rules 同源哨兵/synthesize
|
||||
与 _compact 带 boll_upper/接线突破重合议为放行且硬数字带 intraday/开口未确认
|
||||
路由改观察带 wait_confirm/总闸关掉 _intraday_cfg 为 None/参数登记与范围 (19 例)
|
||||
test_batch33_units.py 页面收尾包·单票研究面接口 (2026-09-14): _research_st 来源解析 (持仓/候选/都没有)/
|
||||
api_research 六块齐全且来源逐路标 ok / 无读数票 company 为 None 且来源标明。
|
||||
前端三处 (抽屉四芯片、候选栏芯片、管理研究面列) 由两道页面守卫与真机视觉判收兜 (5 例)
|
||||
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
|
||||
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
|
||||
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例)
|
||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
||||
目标价到价必定入队 (档位 full 也不自动卖) +
|
||||
用户设的止损价与目标价单独成列显示 (70 例)
|
||||
共 866 例
|
||||
共 871 例
|
||||
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
||||
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例到 820;
|
||||
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例、第三十批加三例到 846; 评审修订第三十一批加一例到 847; 09-14 盘中确认包第三十二批十九例, 现为 866)
|
||||
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例、第三十批加三例到 846; 评审修订第三十一批加一例到 847; 09-14 盘中确认包第三十二批十九例到 866; 页面收尾包第三十三批五例, 现为 871)
|
||||
任一子集失败即整体失败 (退出码 1)。
|
||||
|
||||
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
||||
|
|
@ -163,6 +166,7 @@ SUITES = ["test_core_units.py", "test_batch2_units.py", "test_batch3_units.py",
|
|||
"test_batch30_units.py",
|
||||
"test_batch31_units.py",
|
||||
"test_batch32_units.py",
|
||||
"test_batch33_units.py",
|
||||
"test_page_enum_guard.py",
|
||||
"test_page_wiring_guard.py",
|
||||
"test_wiring.py"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""页面收尾包 · 单票研究面接口 (2026-09-14)。后端离线单测, 不连库。
|
||||
|
||||
研究面接口补齐六块 (公司/基本面/技术面/择时/合议/来源), 页面抽屉四芯片、候选栏芯片、
|
||||
管理视图研究面列消费它。前端三处由两道页面守卫 (wiring/enum) 与真机视觉判收兜, 这里测后端:
|
||||
A 研究面来源解析 _research_st: 持仓票 / 候选票 / 都没有 三情形。
|
||||
B 研究面接口 api_research: 六块齐全且来源逐路标明; 无读数票 company 为 None、来源标不在持仓也不在候选。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.web import main # noqa: E402
|
||||
from app.services import (consensus_service, tech_service, # noqa: E402
|
||||
logic_state_service, plan_feed)
|
||||
|
||||
RESULTS = []
|
||||
|
||||
|
||||
def case(name):
|
||||
def deco(fn):
|
||||
RESULTS.append((name, fn))
|
||||
return fn
|
||||
return deco
|
||||
|
||||
|
||||
class _patch:
|
||||
"""按 (模块, 属性名, 替身) 列表打桩, with 结束自动还原。"""
|
||||
|
||||
def __init__(self, *specs):
|
||||
self.specs = specs
|
||||
self._saved = []
|
||||
|
||||
def __enter__(self):
|
||||
for mod, attr, fn in self.specs:
|
||||
self._saved.append((mod, attr, getattr(mod, attr)))
|
||||
setattr(mod, attr, fn)
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
for mod, attr, orig in reversed(self._saved):
|
||||
setattr(mod, attr, orig)
|
||||
return False
|
||||
|
||||
|
||||
# ================================================================ A 来源解析
|
||||
@case("A _research_st·持仓票 → held (逻辑状态映射有买方评析)")
|
||||
def _():
|
||||
with _patch((logic_state_service, "state_map",
|
||||
lambda: {"600000.SH": {"company_review": {"overall": "好"},
|
||||
"company_review_text": "x"}})):
|
||||
st, src = main._research_st("600000.SH")
|
||||
assert src == "held" and (st.get("company_review") or {}).get("overall") == "好"
|
||||
|
||||
|
||||
@case("A _research_st·候选票 → candidate (不在持仓, 当日计划主榜有)")
|
||||
def _():
|
||||
with _patch((logic_state_service, "state_map", lambda: {}),
|
||||
(plan_feed, "get_plan",
|
||||
lambda **k: {"main": [{"ts_code": "600001.SH",
|
||||
"company_review": {"overall": "中"},
|
||||
"company_review_text": "y"}], "observe": []})):
|
||||
st, src = main._research_st("600001.SH")
|
||||
assert src == "candidate" and (st.get("company_review") or {}).get("overall") == "中"
|
||||
|
||||
|
||||
@case("A _research_st·都没有 → none")
|
||||
def _():
|
||||
with _patch((logic_state_service, "state_map", lambda: {}),
|
||||
(plan_feed, "get_plan", lambda **k: {"main": [], "observe": []})):
|
||||
st, src = main._research_st("600002.SH")
|
||||
assert st is None and src == "none"
|
||||
|
||||
|
||||
# ================================================================ B 研究面接口
|
||||
def _mock_assemble(bull=True):
|
||||
if bull:
|
||||
return lambda row, **k: {"fund": {"stance": "看多", "fact": "质地好"},
|
||||
"tech": {"stance": "看多", "phase": "趋势多"},
|
||||
"timing": {"stance": "看多", "nightly": "BUY", "intraday_flip_at": None},
|
||||
"consensus": {"direction": "看多", "route": "放行", "reason": "三方看多"}}
|
||||
return lambda row, **k: {"fund": {"stance": "无读数", "why": "没有买方评析"},
|
||||
"tech": {"stance": "无读数"},
|
||||
"timing": {"stance": "无读数", "no_read_why": "没有昨夜结论"},
|
||||
"consensus": {"direction": "中性", "route": "跳过"}}
|
||||
|
||||
|
||||
@case("B api_research·六块齐全, 来源逐路标 ok")
|
||||
def _():
|
||||
with _patch(
|
||||
(logic_state_service, "state_map",
|
||||
lambda: {"600000.SH": {"company_review": {"overall": "好"}, "company_review_text": "x"}}),
|
||||
(logic_state_service, "company_view",
|
||||
lambda st: {"overall": "好", "line": "质地好(…)", "report_url": None}),
|
||||
(consensus_service, "nightly_map", lambda codes: {"600000.SH": {"verdict": "BUY", "trade_date": 20260911}}),
|
||||
(consensus_service, "state_map", lambda: {}),
|
||||
(consensus_service, "assemble", _mock_assemble(True)),
|
||||
(tech_service, "research_feed", lambda code: {"tech": {"stance": "看多", "phase": "趋势多", "latest": {}}}),
|
||||
):
|
||||
r = main.api_research("600000.SH")
|
||||
d = r["data"]
|
||||
for k in ("company", "fund", "tech", "timing", "consensus", "sources"):
|
||||
assert k in d, k
|
||||
assert d["consensus"]["direction"] == "看多"
|
||||
assert d["timing"]["trade_date"] == 20260911
|
||||
assert d["sources"]["company"] == "ok" and d["sources"]["consensus"] == "ok"
|
||||
assert d["sources"]["fund"] == "ok" and d["sources"]["tech"] == "ok"
|
||||
|
||||
|
||||
@case("B api_research·无读数票 → company 为 None, 来源标不在持仓也不在候选")
|
||||
def _():
|
||||
with _patch(
|
||||
(logic_state_service, "state_map", lambda: {}),
|
||||
(plan_feed, "get_plan", lambda **k: {"main": [], "observe": []}),
|
||||
(consensus_service, "nightly_map", lambda codes: {}),
|
||||
(consensus_service, "state_map", lambda: {}),
|
||||
(consensus_service, "assemble", _mock_assemble(False)),
|
||||
(tech_service, "research_feed", lambda code: {"tech": None, "note": "这只票没有技术面读数"}),
|
||||
):
|
||||
d = main.api_research("600002.SH")["data"]
|
||||
assert d["company"] is None
|
||||
assert d["sources"]["company"] == "不在持仓也不在候选"
|
||||
assert d["sources"]["fund"] != "ok" and d["sources"]["tech"] != "ok"
|
||||
|
||||
|
||||
def main_():
|
||||
ok = 0
|
||||
for name, fn in RESULTS:
|
||||
try:
|
||||
fn()
|
||||
ok += 1
|
||||
print(" ok " + name)
|
||||
except Exception:
|
||||
print(" FAIL " + name)
|
||||
traceback.print_exc()
|
||||
print("-" * 60)
|
||||
if ok == len(RESULTS):
|
||||
print("ALL PASS (%d cases)" % ok)
|
||||
return 0
|
||||
print("FAILED %d/%d" % (len(RESULTS) - ok, len(RESULTS)))
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main_())
|
||||
Loading…
Reference in New Issue