增加关键位人工输入
This commit is contained in:
@@ -8,7 +8,7 @@ from pathlib import Path
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from fastapi import Depends, FastAPI, Form, HTTPException, Request, status
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
@@ -17,6 +17,14 @@ from starlette.middleware.sessions import SessionMiddleware
|
||||
from .config import Settings
|
||||
from .daily_report import DailyReportService
|
||||
from .gemma_client import OllamaGemmaClient
|
||||
from .key_monitor_service import KeyMonitorService
|
||||
from .key_sl_tp import (
|
||||
KEY_MONITOR_TYPES,
|
||||
normalize_monitor_type,
|
||||
normalize_sl_tp_mode,
|
||||
sl_tp_mode_label,
|
||||
stop_outside_pct_for_mode,
|
||||
)
|
||||
from .monitor import MonitorService
|
||||
from .notifier import WeComNotifier
|
||||
from .gate import GateClient
|
||||
@@ -33,6 +41,7 @@ from .storage import Storage
|
||||
LOGGER = logging.getLogger("onchain_scout.web")
|
||||
FIXED_BAR = "5m"
|
||||
DAILY_REPORT_JOB_ID = "daily_report_job"
|
||||
KEY_MONITOR_JOB_ID = "key_monitor_poll"
|
||||
FUNNEL_DISPLAY_HOURS_DEFAULT = 24.0
|
||||
FUNNEL_DISPLAY_HOURS_MIN = 1.0
|
||||
FUNNEL_DISPLAY_HOURS_MAX = 168.0
|
||||
@@ -194,6 +203,12 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
notifier=notifier,
|
||||
gemma_client=gemma_client,
|
||||
)
|
||||
key_monitor = KeyMonitorService(
|
||||
settings=settings,
|
||||
storage=storage,
|
||||
gate=gate_client,
|
||||
notifier=notifier,
|
||||
)
|
||||
daily_report = DailyReportService(
|
||||
settings=settings,
|
||||
storage=storage,
|
||||
@@ -206,6 +221,8 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
app.state.settings = settings
|
||||
app.state.storage = storage
|
||||
app.state.monitor = monitor
|
||||
app.state.key_monitor = key_monitor
|
||||
app.state.gate_client = gate_client
|
||||
app.state.scheduler = scheduler
|
||||
app.state.auth_user = settings.auth.username
|
||||
app.state.auth_password_hash = _hash_password(settings.auth.password)
|
||||
@@ -219,6 +236,15 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
await _ensure_runtime_defaults(storage)
|
||||
monitor.state.chart_bar = FIXED_BAR
|
||||
scheduler.add_job(monitor.run_cycle, "interval", seconds=settings.app.poll_interval_seconds, max_instances=1)
|
||||
if settings.key_monitor.enabled:
|
||||
scheduler.add_job(
|
||||
key_monitor.run_poll,
|
||||
"interval",
|
||||
seconds=settings.key_monitor.poll_interval_seconds,
|
||||
max_instances=1,
|
||||
id=KEY_MONITOR_JOB_ID,
|
||||
replace_existing=True,
|
||||
)
|
||||
dr = await _get_daily_report_settings(storage, settings)
|
||||
if dr["enabled"]:
|
||||
hh, mm = _parse_hhmm(str(dr["run_time_cn"]))
|
||||
@@ -242,7 +268,8 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
f"service_started_gate_usdt gemma={'on' if settings.gemma.enabled else 'off'} "
|
||||
f"proxy={'on ' + settings.proxy.url if settings.proxy.enabled else 'off'} "
|
||||
f"web_login={'on' if settings.auth.enabled else 'off'} "
|
||||
f"daily_report={'on' if settings.daily_report.enabled else 'off'}"
|
||||
f"daily_report={'on' if settings.daily_report.enabled else 'off'} "
|
||||
f"key_monitor={'on' if settings.key_monitor.enabled else 'off'}"
|
||||
),
|
||||
)
|
||||
LOGGER.info("Service started")
|
||||
@@ -314,6 +341,7 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
"intraday_settings": intraday,
|
||||
"gemma_enabled": settings.gemma.enabled,
|
||||
"gemma_model": settings.gemma.model,
|
||||
"key_monitor": settings.key_monitor.model_dump(),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -498,6 +526,173 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
)
|
||||
return JSONResponse({"ok": True, "symbol_blocklist_settings": await _get_symbol_blocklist_settings(storage)})
|
||||
|
||||
def _key_rule_text() -> str:
|
||||
km = settings.key_monitor
|
||||
return (
|
||||
f"周期 5m|突破K/确认K:倒数第2/第1根闭合K|量能:突破K量 > 前{km.volume_ma_bars}均量×{km.volume_ratio_min}|"
|
||||
f"计划RR须 > {km.min_planned_rr:g}|日成交额排名前{km.daily_volume_rank_max}|"
|
||||
f"箱体/收敛方案:标准突破(止损突破K外{km.standard_stop_outside_pct:g}%|止盈1×H)或 "
|
||||
f"趋势突破(止损突破K外{km.trend_stop_outside_pct:g}%|止盈手填)|"
|
||||
f"触发后企微+{'转发执行器' if km.forward_executor else '不转发执行器'}"
|
||||
)
|
||||
|
||||
@app.get("/api/key-monitors")
|
||||
async def api_key_monitors_list(_: None = Depends(require_login)) -> JSONResponse:
|
||||
rows = await storage.list_key_monitors()
|
||||
enriched = []
|
||||
for row in rows:
|
||||
preview = await key_monitor.preview_row(row)
|
||||
enriched.append({**row, "preview": preview})
|
||||
history = await storage.list_key_monitor_history(limit=200)
|
||||
return JSONResponse(
|
||||
{
|
||||
"active": enriched,
|
||||
"history": history,
|
||||
"rule_text": _key_rule_text(),
|
||||
"config": settings.key_monitor.model_dump(),
|
||||
}
|
||||
)
|
||||
|
||||
@app.post("/api/key-monitors")
|
||||
async def api_key_monitors_add(request: Request, _: None = Depends(require_login)) -> JSONResponse:
|
||||
body = await request.json()
|
||||
sym = _normalize_symbol_token(body.get("symbol"))
|
||||
if not sym:
|
||||
return JSONResponse({"ok": False, "detail": "invalid symbol"}, status_code=400)
|
||||
inst = gate_client.symbol_to_swap_inst_id(sym)
|
||||
monitor_type = normalize_monitor_type(body.get("monitor_type"))
|
||||
if monitor_type not in KEY_MONITOR_TYPES:
|
||||
return JSONResponse({"ok": False, "detail": "invalid monitor_type"}, status_code=400)
|
||||
direction = str(body.get("direction") or "").strip().lower()
|
||||
if direction not in ("long", "short"):
|
||||
return JSONResponse({"ok": False, "detail": "direction must be long or short"}, status_code=400)
|
||||
try:
|
||||
upper = float(body.get("upper"))
|
||||
lower = float(body.get("lower"))
|
||||
except (TypeError, ValueError):
|
||||
return JSONResponse({"ok": False, "detail": "upper/lower required"}, status_code=400)
|
||||
if upper <= lower:
|
||||
return JSONResponse({"ok": False, "detail": "upper must be > lower"}, status_code=400)
|
||||
sl_tp_mode = normalize_sl_tp_mode(body.get("sl_tp_mode"))
|
||||
manual_tp = body.get("manual_take_profit")
|
||||
manual_tp_f: float | None = None
|
||||
if sl_tp_mode == "trend_manual":
|
||||
try:
|
||||
manual_tp_f = float(manual_tp)
|
||||
except (TypeError, ValueError):
|
||||
return JSONResponse({"ok": False, "detail": "manual_take_profit required for trend"}, status_code=400)
|
||||
stop_pct = stop_outside_pct_for_mode(sl_tp_mode)
|
||||
if sl_tp_mode == "standard":
|
||||
stop_pct = float(settings.key_monitor.standard_stop_outside_pct)
|
||||
else:
|
||||
stop_pct = float(settings.key_monitor.trend_stop_outside_pct)
|
||||
be = 1 if str(body.get("breakeven_enabled") or "").lower() in ("1", "true", "on", "yes") else 0
|
||||
kid = await storage.add_key_monitor(
|
||||
symbol=sym,
|
||||
inst_id=inst,
|
||||
monitor_type=monitor_type,
|
||||
direction=direction,
|
||||
upper=upper,
|
||||
lower=lower,
|
||||
sl_tp_mode=sl_tp_mode,
|
||||
manual_take_profit=manual_tp_f,
|
||||
stop_outside_pct=stop_pct,
|
||||
breakeven_enabled=be,
|
||||
note=str(body.get("note") or "")[:500] or None,
|
||||
)
|
||||
await storage.add_log(
|
||||
"INFO",
|
||||
f"key_monitor_added id={kid} sym={sym} type={monitor_type} mode={sl_tp_mode} dir={direction}",
|
||||
)
|
||||
return JSONResponse({"ok": True, "id": kid})
|
||||
|
||||
@app.delete("/api/key-monitors/{kid}")
|
||||
async def api_key_monitors_delete(kid: int, _: None = Depends(require_login)) -> JSONResponse:
|
||||
row = await storage.get_key_monitor(kid)
|
||||
if not row:
|
||||
return JSONResponse({"ok": False, "detail": "not_found"}, status_code=404)
|
||||
await storage.finalize_key_monitor(
|
||||
row,
|
||||
close_reason="manual",
|
||||
last_alert_message=None,
|
||||
confirm_close=None,
|
||||
planned_sl=None,
|
||||
planned_tp=None,
|
||||
planned_rr=None,
|
||||
executor_signal_id=None,
|
||||
executor_status=None,
|
||||
checks=None,
|
||||
)
|
||||
await storage.add_log("INFO", f"key_monitor_manual_close id={kid} sym={row.get('symbol')}")
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
@app.delete("/api/key-monitors/history/{hid}")
|
||||
async def api_key_history_delete(hid: int, _: None = Depends(require_login)) -> JSONResponse:
|
||||
ok = await storage.delete_key_monitor_history(hid)
|
||||
if not ok:
|
||||
return JSONResponse({"ok": False, "detail": "not_found"}, status_code=404)
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
@app.get("/export/key_monitor_history.csv")
|
||||
async def export_key_monitor_history(
|
||||
days: int = 30,
|
||||
_: None = Depends(require_login),
|
||||
) -> StreamingResponse:
|
||||
days = max(1, min(365, int(days)))
|
||||
end_utc = datetime.utcnow()
|
||||
start_utc = end_utc - timedelta(days=days)
|
||||
rows = await storage.export_key_monitor_history_rows(start_utc=start_utc, end_utc=end_utc)
|
||||
import csv
|
||||
import io
|
||||
|
||||
buf = io.StringIO()
|
||||
head = [
|
||||
"id",
|
||||
"symbol",
|
||||
"monitor_type",
|
||||
"direction",
|
||||
"sl_tp_mode",
|
||||
"upper",
|
||||
"lower",
|
||||
"confirm_close",
|
||||
"planned_sl",
|
||||
"planned_tp",
|
||||
"planned_rr",
|
||||
"executor_signal_id",
|
||||
"executor_status",
|
||||
"close_reason",
|
||||
"closed_at",
|
||||
]
|
||||
w = csv.writer(buf)
|
||||
w.writerow(head)
|
||||
for r in rows:
|
||||
w.writerow(
|
||||
[
|
||||
r.get("id"),
|
||||
r.get("symbol"),
|
||||
r.get("monitor_type"),
|
||||
r.get("direction"),
|
||||
r.get("sl_tp_mode"),
|
||||
r.get("upper"),
|
||||
r.get("lower"),
|
||||
r.get("confirm_close"),
|
||||
r.get("planned_sl"),
|
||||
r.get("planned_tp"),
|
||||
r.get("planned_rr"),
|
||||
r.get("executor_signal_id"),
|
||||
r.get("executor_status"),
|
||||
r.get("close_reason"),
|
||||
r.get("closed_at"),
|
||||
]
|
||||
)
|
||||
day = datetime.utcnow().strftime("%Y%m%d")
|
||||
content = buf.getvalue().encode("utf-8-sig")
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": f'attachment; filename="key_monitor_history_{day}.csv"'},
|
||||
)
|
||||
|
||||
@app.get("/api/alerts")
|
||||
async def api_alerts(_: None = Depends(require_login)) -> JSONResponse:
|
||||
alerts = await storage.get_recent_alerts(limit=120)
|
||||
@@ -541,6 +736,7 @@ def create_app(settings: Settings) -> FastAPI:
|
||||
"url": settings.proxy.url if settings.proxy.enabled else "",
|
||||
},
|
||||
"order_executor": read_snapshot(settings),
|
||||
"key_monitor": settings.key_monitor.model_dump(),
|
||||
"watch_symbols": symbols,
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user