273 lines
15 KiB
Python
273 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""页面的本机预览服务 (只给开发机看样式用, 不进容器、不连库、不进代码指纹)。
|
|
|
|
用途: 改 app/web/static/index.html 之后, 不部署就能在浏览器里看效果。它只做几件事:
|
|
静态托管页面; 假登录 (交易员加系统管理员两个角色); 给顶栏、持仓表、建仓候选栏、个股详情抽屉、
|
|
评析报告抽屉、运维视图「组合操作」页喂固定的假数据。其余 /api/* 一律回「预览无后端」,
|
|
页面照常挂载, 顶部会有一条错误提示, 不影响看样式。
|
|
|
|
怎么跑 (开发机, 需要一个装了 fastapi 与 uvicorn 的 Python, 用项目 requirements.txt 建的虚拟环境即可):
|
|
|
|
~/venvs/pms/bin/python docs/dev_preview/mock_preview.py
|
|
浏览器开 http://127.0.0.1:38199/ 看主页面
|
|
浏览器开 http://127.0.0.1:38199/#detail=300627.SZ 直达个股详情抽屉
|
|
|
|
两个坑:
|
|
一, 仓库若放在 macOS 的「文稿」目录下, 由别的程序 (例如 Claude 桌面版的预览面板) 拉起的进程可能没有
|
|
读这个目录的权限, 表现为页面 404 或读不到文件。办法是把 app/web/static 整个拷到一个临时目录,
|
|
再用环境变量 PMS_PREVIEW_STATIC 指过去:
|
|
cp -R app/web/static /tmp/pms_static && PMS_PREVIEW_STATIC=/tmp/pms_static python docs/dev_preview/mock_preview.py
|
|
改了页面要重新拷一次。
|
|
二, 假数据的键要跟着接口走。交易员视图的持仓表只显示 total_qty 大于零的行; 顶栏读 /health、/api/overview、
|
|
/api/tech/status、/api/macro/status、/api/dispatch-mode; 运维视图「组合操作」页读 /api/params。
|
|
页面新读了哪个接口, 就在这里补一个同形状的假应答。
|
|
|
|
本文件放在 docs/ 下是有意的: 代码指纹只覆盖 app/、scripts/、config/ 三个目录的 .py, 放这里不会让
|
|
服务器上的 make stale 报「镜像与工作树不一致」。
|
|
"""
|
|
import os
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = os.environ.get("PMS_PREVIEW_STATIC") or os.path.abspath(os.path.join(HERE, "..", "..", "app", "web", "static"))
|
|
SAMPLE_REVIEW = os.path.join(HERE, "sample_review.md")
|
|
app = FastAPI()
|
|
app.mount("/static", StaticFiles(directory=ROOT), name="static")
|
|
|
|
|
|
@app.get("/")
|
|
def index():
|
|
return FileResponse(os.path.join(ROOT, "index.html"))
|
|
|
|
|
|
@app.get("/api/me")
|
|
def me():
|
|
return {"ok": True, "user": {"phone": "", "username": "预览"}, "roles": ["交易员", "系统管理员"]}
|
|
|
|
|
|
RESEARCH = {
|
|
"ts_code": "300750.SZ",
|
|
"company": {"overall": "好", "valuation": "便宜", "period": "2026Q2",
|
|
"line": "质地好(回报好、盈余中、成长好),估值便宜,置信中",
|
|
"invalidation": "下一期财报看M24净利率、M26经营现金流对净利润、M63收入同比、M64净利润同比",
|
|
"doubt_hard": False, "report_url": "https://example.invalid/report/300750", "text": ""},
|
|
"fund": {"stance": "看多", "fact": "质地好", "mark": None, "priced_in": False, "valuation": "便宜",
|
|
"overall": "好", "no_read_why": None},
|
|
"tech": {"stance": "看空", "strength": "强", "phase": "趋势空", "confirm": None, "sar_side": "空",
|
|
"sar_value": 344.446, "sar_flip_days": 18, "choppy": False,
|
|
"reason": "SAR 空且多空布林线未在多头区:下行", "data_date": 20260911,
|
|
"latest": {"data_date": 20260911,
|
|
"boll": {"upper": 352.1, "mid": 338.0, "lower": 323.9, "bw_pct": 8.3, "pos": 0.12,
|
|
"state": "贴下轨", "squeeze": False},
|
|
"bbi": {"bbi": 341.2, "pos": -0.6, "state": "空头区", "dist_pct": -1.2},
|
|
"sar": {"value": 344.446, "side": "空", "flip_days": 18, "dist_pct": 2.1},
|
|
"quality": "OK", "reanchored": True},
|
|
"flips": 1, "bars": 25},
|
|
"timing": {"stance": "中性", "nightly": "WATCH", "trade_date": "20260911", "flip_at": None},
|
|
"consensus": {"direction": "中性", "votes": {"fund": "看多", "tech": "看空", "timing": "中性"},
|
|
"strength": None, "reason": "基本面看多、技术面看空、择时中性 → 中性",
|
|
"route": "观察", "route_reason": "等技术面转向"},
|
|
"sources": {"company": "ok", "fund": "ok", "tech": "ok", "timing": "ok", "consensus": "ok"},
|
|
}
|
|
|
|
|
|
@app.get("/api/research/{code}")
|
|
def research(code: str):
|
|
return {"ok": True, "data": {**RESEARCH, "ts_code": code}}
|
|
|
|
|
|
@app.get("/api/upstream/signals/{code}")
|
|
def signals(code: str):
|
|
return {"ok": True, "data": {
|
|
"ok": True, "ts_code": code,
|
|
"timeline": [
|
|
{"time": "10:53", "src": "alert", "src_label": "盘中告警", "cat": "money_flow",
|
|
"cat_label": "资金流强度", "direction": "看涨", "level": "INFO",
|
|
"reason": "money_flow_in_intensity", "stale": True, "dup": 1, "ts_from": "trigger"},
|
|
],
|
|
"snapshot": {"metrics": [{"direction": "看涨", "window_net": 432000000.0, "z_dd": 6.143}], "mr": {}},
|
|
"sources": {"intraday": {"ok": True}, "alerts": {"ok": True}, "sell_actions": {"ok": True},
|
|
"metrics": {"ok": True}, "mr": {"ok": True}},
|
|
"truncated": {},
|
|
}}
|
|
|
|
|
|
@app.get("/api/ledger")
|
|
def ledger(ts_code: str = "", limit: int = 40):
|
|
import datetime
|
|
today = datetime.date.today().isoformat()
|
|
return {"ok": True, "data": [
|
|
{"created_at": f"{today} 09:41:12", "action": "OPEN", "verdict": "REJECT", "arbiter": "system",
|
|
"reason": "系统放弃:研究证据走弱(逻辑存疑),自动放弃;明日重新评估"},
|
|
]}
|
|
|
|
|
|
@app.get("/api/proposals")
|
|
def proposals(status: str = "WAIT_USER", limit: int = 100):
|
|
return {"ok": True, "data": []}
|
|
|
|
|
|
# ---- 持仓表 (综合分析列三枚标签), 照 155 上 10:53 读回的三票造几行 ----
|
|
def _pos(code, name, qty, cost, price, dims, cushion="POSITIVE", cpct=0.052, frozen="NONE", target=None):
|
|
mv = qty * price
|
|
return {
|
|
"ts_code": code, "name": name, "qty": qty, "total_qty": qty, "sellable_qty": qty, "avg_cost": cost, "price": price,
|
|
"status": "HOLDING", "frozen_reason": frozen, "user_target_price": target,
|
|
"price_ok": True, "market_value": mv, "pct_of_scale": mv / 2000000.0,
|
|
"cushion_state": cushion, "cushion_pct": cpct, "neg_cushion_days": 0,
|
|
"user_stop_price": None,
|
|
"entry": {"why": "光模块需求上行"}, "logic_now": {"state": "INTACT", "text": "证据仍在"},
|
|
"tech": {"stance": dims["tech"] or "无读数", "phase": dims["tech_phase"]},
|
|
"dims": dims, "lots": 1,
|
|
}
|
|
|
|
|
|
POSITIONS = [
|
|
_pos("300627.SZ", "华测导航", 1200, 41.2, 43.5,
|
|
{"fund": "看多", "fund_fact": "质地好", "tech": "看空", "tech_phase": "转空", "timing": "中性", "nightly": "WATCH"}),
|
|
_pos("688281.SH", "华秦科技", 300, 158.0, 151.2,
|
|
{"fund": "中性", "fund_fact": "质地中", "tech": "看空", "tech_phase": "转空", "timing": "中性", "nightly": "WATCH"},
|
|
cushion="NEGATIVE", cpct=-0.043),
|
|
_pos("002463.SZ", "沪电股份", 200, 115.25, 118.9,
|
|
{"fund": None, "fund_fact": "没有买方评析", "tech": "中性", "tech_phase": "收口等待", "timing": "中性", "nightly": "WATCH"}),
|
|
_pos("002179.SZ", "中航光电", 500, 52.3, 55.1,
|
|
{"fund": None, "fund_fact": "没有买方评析", "tech": None, "tech_phase": None, "timing": "看多", "nightly": "BUY"},
|
|
frozen="MANUAL", target=60.0),
|
|
_pos("002709.SZ", "天赐材料", 800, 24.1, 22.7,
|
|
{"fund": None, "fund_fact": "没有买方评析", "tech": "看空", "tech_phase": "趋势空", "timing": None, "nightly": "DROPPED"},
|
|
cushion="NEGATIVE", cpct=-0.058),
|
|
_pos("600279.SH", "重庆港", 2000, 6.1, 6.2,
|
|
{"fund": None, "fund_fact": "没有买方评析", "tech": None, "tech_phase": None, "timing": None, "nightly": None}),
|
|
]
|
|
|
|
|
|
@app.get("/api/strategies")
|
|
def strategies(include_archived: bool = False):
|
|
return {"ok": True, "strategies": [
|
|
{"strategy_id": "STR_20260908_300627SZ_1", "ts_code": "300627.SZ", "type": "GRID", "status": "ACTIVE",
|
|
"autonomy": "auto", "params": {"lower": 38.0, "upper": 46.0, "center": 42.0},
|
|
"state": {"filled_levels": {"1": 200, "2": 200}, "invested": 16800.0}, "archived_at": None,
|
|
"buy_paused": None, "created_at": "2026-09-08 09:40:00"},
|
|
{"strategy_id": "STR_20260910_002463SZ_1", "ts_code": "002463.SZ", "type": "TRAIL", "status": "PAUSED",
|
|
"autonomy": "auto", "params": {"trail_pct": 0.05}, "state": {"high_water": 121.4, "armed": False},
|
|
"archived_at": None, "buy_paused": None, "created_at": "2026-09-10 09:40:00"},
|
|
], "enabled": True, "auto": {"enabled": True, "ran_today": True, "last_scan": {"at": "2026-09-15 09:40:12"}}}
|
|
|
|
|
|
@app.get("/api/positions")
|
|
def positions():
|
|
return {"ok": True, "positions": POSITIONS, "stock_params": {},
|
|
"summary": {"total_scale": 2000000.0, "market_value": sum(p["market_value"] for p in POSITIONS)}}
|
|
|
|
|
|
# ---- 建仓候选栏: 计划行 + 只读处置快照 ----
|
|
PLAN_ROWS = [
|
|
{"rank": 1, "ts_code": "300308.SZ", "name": "中际旭创", "theme": "光模块与算力互联", "tier": "白名单"},
|
|
{"rank": 2, "ts_code": "300750.SZ", "name": "宁德时代", "theme": "动力电池", "tier": "白名单"},
|
|
{"rank": 3, "ts_code": "688981.SH", "name": "中芯国际", "theme": "晶圆代工与先进制程国产替代", "tier": "白名单"},
|
|
{"rank": 4, "ts_code": "002050.SZ", "name": "三花智控", "theme": "热管理与机器人执行器", "tier": "候补"},
|
|
{"rank": 5, "ts_code": "300660.SZ", "name": "江苏雷利", "theme": "微特电机", "tier": "候补"},
|
|
{"rank": 6, "ts_code": "603986.SH", "name": "兆易创新", "theme": "存储芯片", "tier": "候补"},
|
|
]
|
|
|
|
|
|
@app.get("/api/upstream/plan")
|
|
def plan(limit: int = 50):
|
|
return {"ok": True, "status": {"ok": True, "date": "2026-09-15", "plan_source": "snapshot", "gate_on": True, "age_tdays": 1, "fetched_at": __import__("time").time() - 3000, "heat_date": "2026-09-15"},
|
|
"rows": PLAN_ROWS, "candidates_raw": None}
|
|
|
|
|
|
@app.get("/api/upstream/plan-changes")
|
|
def plan_changes(limit: int = 50):
|
|
return {"ok": True, "changes": {"ok": True, "added": [], "removed": []}, "log": {}}
|
|
|
|
|
|
def _con(fund, overall, tech, phase, timing, direction, route):
|
|
return {"fund": fund, "overall": overall, "tech": tech, "phase": phase, "timing": timing,
|
|
"direction": direction, "route": route}
|
|
|
|
|
|
@app.get("/api/open-scan")
|
|
def open_scan():
|
|
return {"ok": True, "notes": ["名额 3/8", "可用资金 61.2 万"], "by_code": {
|
|
"300308.SZ": {"disp": "would", "why": "名额与资金充足,本轮将建底仓",
|
|
"consensus": _con("看多", "好", "看多", "趋势多", "看多", "看多", "放行")},
|
|
"300750.SZ": {"disp": "wait_tech", "why": "布林带仍在收口,等开口向上再评估",
|
|
"consensus": _con("看多", "好", "中性", "收口等待", "中性", "看多", "交人")},
|
|
"688981.SH": {"disp": "wait_timing", "why": "三票看多但择时看空,等择时转多",
|
|
"consensus": _con("看多", "好", "看多", "趋势多", "看空", "看多", "观察")},
|
|
"002050.SZ": {"disp": "wait_confirm", "why": "技术面已开口,等盘中站上均价且量比达标",
|
|
"consensus": _con("中性", "中", "看多", "开口向上", "中性", "看多", "放行")},
|
|
"300660.SZ": {"disp": "sys_decline", "why": "系统放弃:研究证据走弱(逻辑存疑),明日重新评估",
|
|
"consensus": _con("看空", "差", "中性", "收口等待", None, "中性", "跳过")},
|
|
}}
|
|
|
|
|
|
|
|
# ---- 2026-09-18: 顶栏标签、运维视图组合操作页、评析报告抽屉要用的几路 ----
|
|
@app.get("/health")
|
|
def health():
|
|
import datetime
|
|
return {"ok": True, "version": "preview", "now": datetime.datetime.now().strftime("%H:%M:%S"), "trade_day": True}
|
|
|
|
|
|
@app.get("/api/overview")
|
|
def overview():
|
|
mv = sum(p["market_value"] for p in POSITIONS)
|
|
return {"ok": True, "data": {"buy_halt": False, "exec_halt": False, "autonomy": "full", "scale": 2000000.0,
|
|
"portfolio_mv": mv, "portfolio_pct": mv / 2000000.0, "portfolio_cap": 0.8, "cap_room": 2000000.0 * 0.8 - mv,
|
|
"cash_source": "ws", "cash_avail": 612000.0, "cash_est": 600000.0, "names_count": len(POSITIONS), "max_names": 8,
|
|
"float_pnl": 12345.0, "solid_names": 2, "neg_names": 2, "sector_ready": True}}
|
|
|
|
|
|
@app.get("/api/params")
|
|
def params():
|
|
def p(k, v, t="float", d=""):
|
|
return {"key": k, "value": v, "type": t, "file_default": v, "source": "table", "desc": d, "updated_at": "2026-09-17 15:20:00"}
|
|
return {"ok": True, "data": {"params": [
|
|
p("PMS_TOTAL_SCALE", 2000000, "int", "总操作规模"), p("PMS_PORTFOLIO_CAP", 0.8, "float", "总仓上限"),
|
|
p("PMS_STOCK_CAP", 0.15, "float", "单股上限"), p("PMS_MAX_NAMES", 8, "int", "最多持仓"),
|
|
p("PMS_CASH_RESERVE", 0.05, "float", "预留现金"), p("PMS_DCA_MAX_RATIO", 0.5, "float", "最多补到首批的"),
|
|
p("PMS_DCA_TRIGGERS", "-0.08,-0.15", "str", "亏到多少开始评估"), p("PMS_DCA_DEEP_CONFIRM", True, "bool", "更深那档要确认"),
|
|
p("PMS_FILL_MAX_LOSS", -0.05, "float", "回落补足的止损"), p("PMS_AUTONOMY", "full", "str", "自主档位"),
|
|
]}}
|
|
|
|
|
|
@app.get("/api/tech/status")
|
|
def tech_status():
|
|
return {"ok": True, "data": {"enabled": True, "data_date": 20260917, "map": {"fresh": True}}}
|
|
|
|
|
|
@app.get("/api/macro/status")
|
|
def macro_status():
|
|
return {"ok": True, "enabled": True, "autonomy": "propose_only", "params": {"hot_th": 1.5, "cold_th": -1.5, "exit_band": 0.3},
|
|
"signals": [{"key": "MZ_20260918", "label": "大盘冷热", "value": 0.4, "zone": "NEUTRAL", "ymd": "2026-09-18",
|
|
"e_now": 0, "streak": 3, "history": [], "advice": None}]}
|
|
|
|
|
|
@app.get("/api/dispatch-mode")
|
|
def dispatch_mode():
|
|
return {"ok": True, "data": {"mode": "ws", "shadow": False}}
|
|
|
|
|
|
@app.get("/api/research/{code}/report")
|
|
def research_report(code: str):
|
|
md = open(SAMPLE_REVIEW, encoding="utf-8").read()
|
|
if code == "600279.SH":
|
|
return {"ok": False, "ts_code": code, "url": None, "source": "none", "markdown": "", "cached": False,
|
|
"fetched_at": None, "error": "这只票没有个股深度评析报告的链接(不在持仓也不在当日选股计划里)"}
|
|
return {"ok": True, "ts_code": code, "url": "http://192.168.16.178:8000/company/%s/review" % code, "source": "held",
|
|
"markdown": md, "cached": False, "fetched_at": "2026-09-18 11:50:00", "error": None}
|
|
|
|
|
|
@app.api_route("/api/{path:path}", methods=["GET", "POST"])
|
|
def catchall(path: str):
|
|
return JSONResponse({"ok": False, "error": "预览无后端"})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="127.0.0.1", port=38199)
|