完善后台管理功能:订阅管理增强、训练精度面板、支付记录查询
This commit is contained in:
+2
-2
@@ -181,9 +181,9 @@ POLYWEATHER_PAYMENT_RPC_URL=https://polygon-rpc.com
|
||||
POLYWEATHER_PAYMENT_RPC_URLS=https://polygon-rpc.com
|
||||
POLYWEATHER_PAYMENT_RECEIVER_CONTRACT=
|
||||
POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS=
|
||||
POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174
|
||||
POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359
|
||||
POLYWEATHER_PAYMENT_TOKEN_DECIMALS=6
|
||||
POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=[{"code":"usdc_e","symbol":"USDC.e","name":"USDC.e (PoS)","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS","is_default":true},{"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"}]
|
||||
POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=[{"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS","is_default":true},{"code":"usdt","symbol":"USDT","name":"USDT","address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"},{"code":"pusd","symbol":"pUSD","name":"Polymarket pUSD","address":"0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"}]
|
||||
POLYWEATHER_PAYMENT_CONFIRMATIONS=2
|
||||
POLYWEATHER_PAYMENT_INTENT_TTL_SEC=1800
|
||||
POLYWEATHER_PAYMENT_WALLET_CHALLENGE_TTL_SEC=600
|
||||
|
||||
@@ -36,7 +36,7 @@ METEOBLUE_API_KEY=
|
||||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
||||
POLYWEATHER_PAYMENT_RECEIVER_CONTRACT=
|
||||
POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS=
|
||||
POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=[{"code":"usdc_e","symbol":"USDC.e","name":"USDC.e (PoS)","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS","is_default":true},{"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"}]
|
||||
POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON=[{"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS","is_default":true},{"code":"usdt","symbol":"USDT","name":"USDT","address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"},{"code":"pusd","symbol":"pUSD","name":"Polymarket pUSD","address":"0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb","decimals":6,"receiver_contract":"YOUR_RECEIVER_ADDRESS"}]
|
||||
POLYWEATHER_PAYMENT_PLAN_CATALOG_JSON=
|
||||
|
||||
########################################
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import { RefreshCcw, X, Search } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { opsApi } from "@/lib/ops-api";
|
||||
@@ -13,12 +13,30 @@ import {
|
||||
|
||||
type GrowthPoint = { date: string; trial: number; paid: number; total: number; cumulative: number };
|
||||
|
||||
type SubRow = {
|
||||
id?: string;
|
||||
status?: string;
|
||||
plan_code?: string;
|
||||
source?: string;
|
||||
starts_at?: string;
|
||||
expires_at?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export function MembershipsPageClient() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [memberships, setMemberships] = useState<MembershipEntry[]>([]);
|
||||
const [growth, setGrowth] = useState<GrowthPoint[]>([]);
|
||||
const [filter, setFilter] = useState<"all" | "paid" | "trial">("all");
|
||||
|
||||
// Detail modal state
|
||||
const [detailEmail, setDetailEmail] = useState<string | null>(null);
|
||||
const [detailUserId, setDetailUserId] = useState<string>("");
|
||||
const [detailRows, setDetailRows] = useState<SubRow[]>([]);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailError, setDetailError] = useState<string>("");
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -47,6 +65,46 @@ export function MembershipsPageClient() {
|
||||
return code;
|
||||
};
|
||||
|
||||
const sourceLabel = (source?: string) => {
|
||||
if (!source) return "—";
|
||||
if (source === "payment_contract") return "链上支付";
|
||||
if (source === "ops_manual_grant") return "后台赠送";
|
||||
if (source === "signup_trial") return "注册体验";
|
||||
if (source === "weekly_reward") return "周奖励";
|
||||
return source;
|
||||
};
|
||||
|
||||
const statusBadge = (status?: string) => {
|
||||
if (status === "active") return <span className="inline-flex items-center rounded-full bg-emerald-500/15 px-2 py-0.5 text-[11px] font-medium text-emerald-400">active</span>;
|
||||
if (status === "expired") return <span className="inline-flex items-center rounded-full bg-red-500/15 px-2 py-0.5 text-[11px] font-medium text-red-400">expired</span>;
|
||||
if (status === "cancelled") return <span className="inline-flex items-center rounded-full bg-slate-500/15 px-2 py-0.5 text-[11px] font-medium text-slate-400">cancelled</span>;
|
||||
return <span className="inline-flex items-center rounded-full bg-slate-500/15 px-2 py-0.5 text-[11px] font-medium text-slate-400">{status ?? "—"}</span>;
|
||||
};
|
||||
|
||||
const openDetail = async (email: string) => {
|
||||
if (!email) return;
|
||||
setDetailEmail(email);
|
||||
setDetailLoading(true);
|
||||
setDetailError("");
|
||||
setDetailRows([]);
|
||||
setDetailUserId("");
|
||||
try {
|
||||
const data = await opsApi.userSubscriptions(email);
|
||||
setDetailUserId(data.user_id ?? "");
|
||||
setDetailRows(data.subscriptions ?? []);
|
||||
} catch (e: unknown) {
|
||||
setDetailError(e instanceof Error ? e.message : "查询失败");
|
||||
}
|
||||
setDetailLoading(false);
|
||||
};
|
||||
|
||||
const closeDetail = () => {
|
||||
setDetailEmail(null);
|
||||
setDetailRows([]);
|
||||
setDetailUserId("");
|
||||
setDetailError("");
|
||||
};
|
||||
|
||||
if (loading) return <div className="text-slate-400 animate-pulse">加载中...</div>;
|
||||
|
||||
return (
|
||||
@@ -154,7 +212,7 @@ export function MembershipsPageClient() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((m, i) => (
|
||||
<tr key={m.user_id || i} className="border-b border-white/5">
|
||||
<tr key={m.user_id || i} className="border-b border-white/5 hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-2.5 px-4">
|
||||
{m.is_trial ? (
|
||||
<span className="inline-flex items-center rounded-full bg-amber-500/15 px-2 py-0.5 text-[11px] font-medium text-amber-400">体验</span>
|
||||
@@ -162,7 +220,16 @@ export function MembershipsPageClient() {
|
||||
<span className="inline-flex items-center rounded-full bg-emerald-500/15 px-2 py-0.5 text-[11px] font-medium text-emerald-400">付费</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2.5 px-4 text-white">{m.email ?? "—"}</td>
|
||||
<td className="py-2.5 px-4">
|
||||
<button
|
||||
className="text-white hover:text-cyan-400 transition-colors inline-flex items-center gap-1.5 group"
|
||||
onClick={() => openDetail(m.email ?? "")}
|
||||
title="查看全部订阅记录"
|
||||
>
|
||||
{m.email ?? "—"}
|
||||
<Search className="h-3 w-3 opacity-0 group-hover:opacity-60 transition-opacity" />
|
||||
</button>
|
||||
</td>
|
||||
<td className="py-2.5 px-4">{planLabel(m.plan_code)}</td>
|
||||
<td className="py-2.5 px-4 text-slate-400 text-xs">{m.starts_at?.slice(0, 10) ?? "—"}</td>
|
||||
<td className="py-2.5 px-4 text-slate-400 text-xs">{m.expires_at?.slice(0, 10) ?? "—"}</td>
|
||||
@@ -177,6 +244,81 @@ export function MembershipsPageClient() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Subscription detail modal */}
|
||||
{detailEmail && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={closeDetail}>
|
||||
<div
|
||||
className="bg-[#0f172a] border border-white/10 rounded-2xl shadow-2xl w-full max-w-3xl max-h-[80vh] overflow-hidden flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">订阅记录详情</h2>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
{detailEmail}
|
||||
{detailUserId && <span className="ml-2 text-slate-500">ID: {detailUserId.slice(0, 8)}…</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={closeDetail} className="text-slate-400 hover:text-white transition-colors p-1 rounded-lg hover:bg-white/10">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="overflow-y-auto flex-1 px-6 py-4">
|
||||
{detailLoading && (
|
||||
<div className="text-slate-400 animate-pulse py-8 text-center">查询中...</div>
|
||||
)}
|
||||
{detailError && (
|
||||
<div className="text-red-400 py-8 text-center text-sm">{detailError}</div>
|
||||
)}
|
||||
{!detailLoading && !detailError && detailRows.length === 0 && (
|
||||
<div className="text-slate-500 py-8 text-center text-sm">未找到订阅记录</div>
|
||||
)}
|
||||
{!detailLoading && detailRows.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10 text-left text-slate-400">
|
||||
<th className="py-2.5 px-3 font-medium text-xs">状态</th>
|
||||
<th className="py-2.5 px-3 font-medium text-xs">方案</th>
|
||||
<th className="py-2.5 px-3 font-medium text-xs">来源</th>
|
||||
<th className="py-2.5 px-3 font-medium text-xs">起始</th>
|
||||
<th className="py-2.5 px-3 font-medium text-xs">到期</th>
|
||||
<th className="py-2.5 px-3 font-medium text-xs">创建</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detailRows.map((row, i) => {
|
||||
const isExpired = row.expires_at && new Date(row.expires_at) < new Date();
|
||||
return (
|
||||
<tr key={row.id ?? i} className={`border-b border-white/5 ${isExpired ? "opacity-50" : ""}`}>
|
||||
<td className="py-2 px-3">{statusBadge(row.status)}</td>
|
||||
<td className="py-2 px-3 text-white text-xs">{planLabel(row.plan_code)}</td>
|
||||
<td className="py-2 px-3 text-slate-400 text-xs">{sourceLabel(row.source)}</td>
|
||||
<td className="py-2 px-3 text-slate-400 text-xs font-mono">{row.starts_at?.slice(0, 19)?.replace("T", " ") ?? "—"}</td>
|
||||
<td className="py-2 px-3 text-xs font-mono">
|
||||
<span className={isExpired ? "text-red-400" : "text-emerald-400"}>
|
||||
{row.expires_at?.slice(0, 19)?.replace("T", " ") ?? "—"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500 text-xs font-mono">{row.created_at?.slice(0, 19)?.replace("T", " ") ?? "—"}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="text-xs text-slate-500 mt-3 px-1">
|
||||
共 {detailRows.length} 条记录 · 时间为 UTC
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,15 +18,38 @@ function StatRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface CityAccuracy {
|
||||
city_id: string;
|
||||
name: string;
|
||||
deb?: {
|
||||
hit_rate: number;
|
||||
mae: number;
|
||||
total_days: number;
|
||||
details_str: string;
|
||||
} | null;
|
||||
mu?: {
|
||||
mae: number;
|
||||
hit_rate: number;
|
||||
brier_score: number | null;
|
||||
total_days: number;
|
||||
details_str: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function TrainingPageClient() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [status, setStatus] = useState<SystemStatusPayload | null>(null);
|
||||
const [accuracy, setAccuracy] = useState<CityAccuracy[] | null>(null);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const s = await opsApi.systemStatus() as SystemStatusPayload;
|
||||
const [s, accData] = await Promise.all([
|
||||
opsApi.systemStatus() as Promise<SystemStatusPayload>,
|
||||
opsApi.trainingAccuracy().catch(() => ({ accuracy: [] }))
|
||||
]);
|
||||
setStatus(s);
|
||||
setAccuracy(accData.accuracy);
|
||||
} catch { /* */ }
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -110,6 +133,105 @@ export function TrainingPageClient() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>模型融合与预测准确率效果 (DEB & 概率引擎)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left text-slate-300">
|
||||
<thead className="text-xs uppercase bg-slate-800/50 text-slate-400">
|
||||
<tr>
|
||||
<th scope="col" className="px-4 py-3">城市</th>
|
||||
<th scope="col" className="px-4 py-3 text-center">DEB 结算命中</th>
|
||||
<th scope="col" className="px-4 py-3 text-center">DEB MAE</th>
|
||||
<th scope="col" className="px-4 py-3 text-center">DEB 天数</th>
|
||||
<th scope="col" className="px-4 py-3 text-center">概率 μ 结算命中</th>
|
||||
<th scope="col" className="px-4 py-3 text-center">概率 μ MAE</th>
|
||||
<th scope="col" className="px-4 py-3 text-center">Brier Score</th>
|
||||
<th scope="col" className="px-4 py-3 text-center">概率天数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{accuracy && accuracy.length > 0 ? (
|
||||
accuracy.map((row) => (
|
||||
<tr key={row.city_id} className="hover:bg-white/5 transition-colors">
|
||||
<td className="px-4 py-3 font-medium text-white capitalize">
|
||||
{row.name}
|
||||
<span className="text-xs text-slate-500 block font-mono">{row.city_id}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{row.deb ? (
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-semibold ${
|
||||
row.deb.hit_rate >= 80
|
||||
? "bg-green-500/15 text-green-400"
|
||||
: row.deb.hit_rate >= 60
|
||||
? "bg-yellow-500/15 text-yellow-400"
|
||||
: "bg-red-500/15 text-red-400"
|
||||
}`}>
|
||||
{row.deb.hit_rate.toFixed(0)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-600">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center font-mono">
|
||||
{row.deb ? `${row.deb.mae.toFixed(1)}°` : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-slate-400">
|
||||
{row.deb ? row.deb.total_days : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{row.mu ? (
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-semibold ${
|
||||
row.mu.hit_rate >= 80
|
||||
? "bg-green-500/15 text-green-400"
|
||||
: row.mu.hit_rate >= 60
|
||||
? "bg-yellow-500/15 text-yellow-400"
|
||||
: "bg-red-500/15 text-red-400"
|
||||
}`}>
|
||||
{row.mu.hit_rate.toFixed(0)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-600">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center font-mono">
|
||||
{row.mu ? `${row.mu.mae.toFixed(1)}°` : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center font-mono">
|
||||
{row.mu && row.mu.brier_score !== null ? (
|
||||
<span className={`${
|
||||
row.mu.brier_score <= 0.1
|
||||
? "text-green-400 font-bold"
|
||||
: row.mu.brier_score <= 0.25
|
||||
? "text-yellow-400"
|
||||
: "text-red-400"
|
||||
}`}>
|
||||
{row.mu.brier_score.toFixed(3)}
|
||||
</span>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-slate-400">
|
||||
{row.mu ? row.mu.total_days : "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-4 py-8 text-center text-slate-500">
|
||||
无有效准确率记录,请确认 daily_records.json 中已有历史结算数据。
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>真值历史浏览</CardTitle>
|
||||
@@ -124,3 +246,4 @@ export function TrainingPageClient() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,4 +105,25 @@ export const opsApi = {
|
||||
count: number;
|
||||
}>(`/api/ops/subscriptions/user?email=${encodeURIComponent(email)}`);
|
||||
},
|
||||
trainingAccuracy() {
|
||||
return opsFetch<{
|
||||
accuracy: Array<{
|
||||
city_id: string;
|
||||
name: string;
|
||||
deb?: {
|
||||
hit_rate: number;
|
||||
mae: number;
|
||||
total_days: number;
|
||||
details_str: string;
|
||||
} | null;
|
||||
mu?: {
|
||||
mae: number;
|
||||
hit_rate: number;
|
||||
brier_score: number | null;
|
||||
total_days: number;
|
||||
details_str: string;
|
||||
} | null;
|
||||
}>;
|
||||
}>("/api/ops/training/accuracy");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import json
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
@@ -33,7 +33,8 @@ ERC20_ABI = [
|
||||
# Source: Polymarket official developer docs (Polygon contract addresses)
|
||||
# https://docs.polymarket.com/developers/market-makers/setup
|
||||
DEFAULT_POLYMARKET_CONTRACTS: Dict[str, str] = {
|
||||
"USDC.e": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
||||
"USDC": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
|
||||
"pUSD": "0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb",
|
||||
"CTF": "0x4d97dcd97ec945f40cf65f87097ace5ea0476045",
|
||||
"CTF_EXCHANGE": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
|
||||
"NEG_RISK_CTF_EXCHANGE": "0xC5d563A36AE78145C45a50134d48A1215220f80a",
|
||||
|
||||
@@ -21,6 +21,7 @@ from web.services.ops_api import (
|
||||
list_ops_payments,
|
||||
search_ops_users,
|
||||
update_ops_config,
|
||||
get_ops_training_accuracy,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["ops"])
|
||||
@@ -160,3 +161,9 @@ async def ops_logs(
|
||||
@router.get("/api/ops/health-check")
|
||||
async def ops_health_check(request: Request):
|
||||
return get_ops_health_check(request)
|
||||
|
||||
|
||||
@router.get("/api/ops/training/accuracy")
|
||||
async def ops_training_accuracy(request: Request):
|
||||
return get_ops_training_accuracy(request)
|
||||
|
||||
|
||||
@@ -783,3 +783,56 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
||||
|
||||
all_ok = all(v.get("ok") for v in results.values())
|
||||
return {"ok": all_ok, "checked_at": __import__("datetime").datetime.utcnow().isoformat() + "Z", "services": results}
|
||||
|
||||
|
||||
def get_ops_training_accuracy(request: Request) -> Dict[str, Any]:
|
||||
_require_ops(request)
|
||||
from src.analysis.deb_algorithm import get_deb_accuracy, get_mu_accuracy
|
||||
from src.data_collection.city_registry import CITY_REGISTRY
|
||||
|
||||
accuracy_data = []
|
||||
for city_id, info in CITY_REGISTRY.items():
|
||||
name = info.get("name") or city_id
|
||||
|
||||
# Calculate DEB accuracy
|
||||
deb_acc = get_deb_accuracy(city_id)
|
||||
deb_payload = None
|
||||
if deb_acc:
|
||||
deb_payload = {
|
||||
"hit_rate": deb_acc[0],
|
||||
"mae": deb_acc[1],
|
||||
"total_days": deb_acc[2],
|
||||
"details_str": deb_acc[3]
|
||||
}
|
||||
|
||||
# Calculate Mu accuracy
|
||||
mu_acc = get_mu_accuracy(city_id)
|
||||
mu_payload = None
|
||||
if mu_acc:
|
||||
mu_payload = {
|
||||
"mae": mu_acc[0],
|
||||
"hit_rate": mu_acc[1],
|
||||
"brier_score": mu_acc[2],
|
||||
"total_days": mu_acc[3],
|
||||
"details_str": mu_acc[4]
|
||||
}
|
||||
|
||||
if deb_payload or mu_payload:
|
||||
accuracy_data.append({
|
||||
"city_id": city_id,
|
||||
"name": name,
|
||||
"deb": deb_payload,
|
||||
"mu": mu_payload
|
||||
})
|
||||
|
||||
# Sort by total days of DEB or Mu
|
||||
accuracy_data.sort(
|
||||
key=lambda x: max(
|
||||
x["deb"]["total_days"] if x["deb"] else 0,
|
||||
x["mu"]["total_days"] if x["mu"] else 0
|
||||
),
|
||||
reverse=True
|
||||
)
|
||||
|
||||
return {"accuracy": accuracy_data}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user