完善后台管理功能:订阅管理增强、训练精度面板、支付记录查询
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_RPC_URLS=https://polygon-rpc.com
|
||||||
POLYWEATHER_PAYMENT_RECEIVER_CONTRACT=
|
POLYWEATHER_PAYMENT_RECEIVER_CONTRACT=
|
||||||
POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS=
|
POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS=
|
||||||
POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174
|
POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359
|
||||||
POLYWEATHER_PAYMENT_TOKEN_DECIMALS=6
|
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_CONFIRMATIONS=2
|
||||||
POLYWEATHER_PAYMENT_INTENT_TTL_SEC=1800
|
POLYWEATHER_PAYMENT_INTENT_TTL_SEC=1800
|
||||||
POLYWEATHER_PAYMENT_WALLET_CHALLENGE_TTL_SEC=600
|
POLYWEATHER_PAYMENT_WALLET_CHALLENGE_TTL_SEC=600
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ METEOBLUE_API_KEY=
|
|||||||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
|
||||||
POLYWEATHER_PAYMENT_RECEIVER_CONTRACT=
|
POLYWEATHER_PAYMENT_RECEIVER_CONTRACT=
|
||||||
POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS=
|
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=
|
POLYWEATHER_PAYMENT_PLAN_CATALOG_JSON=
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { opsApi } from "@/lib/ops-api";
|
import { opsApi } from "@/lib/ops-api";
|
||||||
@@ -13,12 +13,30 @@ import {
|
|||||||
|
|
||||||
type GrowthPoint = { date: string; trial: number; paid: number; total: number; cumulative: number };
|
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() {
|
export function MembershipsPageClient() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [memberships, setMemberships] = useState<MembershipEntry[]>([]);
|
const [memberships, setMemberships] = useState<MembershipEntry[]>([]);
|
||||||
const [growth, setGrowth] = useState<GrowthPoint[]>([]);
|
const [growth, setGrowth] = useState<GrowthPoint[]>([]);
|
||||||
const [filter, setFilter] = useState<"all" | "paid" | "trial">("all");
|
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 () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -47,6 +65,46 @@ export function MembershipsPageClient() {
|
|||||||
return code;
|
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>;
|
if (loading) return <div className="text-slate-400 animate-pulse">加载中...</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +212,7 @@ export function MembershipsPageClient() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filtered.map((m, i) => (
|
{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">
|
<td className="py-2.5 px-4">
|
||||||
{m.is_trial ? (
|
{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>
|
<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>
|
<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>
|
||||||
<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">{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.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>
|
<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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</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() {
|
export function TrainingPageClient() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [status, setStatus] = useState<SystemStatusPayload | null>(null);
|
const [status, setStatus] = useState<SystemStatusPayload | null>(null);
|
||||||
|
const [accuracy, setAccuracy] = useState<CityAccuracy[] | null>(null);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
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);
|
setStatus(s);
|
||||||
|
setAccuracy(accData.accuracy);
|
||||||
} catch { /* */ }
|
} catch { /* */ }
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@@ -110,6 +133,105 @@ export function TrainingPageClient() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>真值历史浏览</CardTitle>
|
<CardTitle>真值历史浏览</CardTitle>
|
||||||
@@ -124,3 +246,4 @@ export function TrainingPageClient() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,4 +105,25 @@ export const opsApi = {
|
|||||||
count: number;
|
count: number;
|
||||||
}>(`/api/ops/subscriptions/user?email=${encodeURIComponent(email)}`);
|
}>(`/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 os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -33,7 +33,8 @@ ERC20_ABI = [
|
|||||||
# Source: Polymarket official developer docs (Polygon contract addresses)
|
# Source: Polymarket official developer docs (Polygon contract addresses)
|
||||||
# https://docs.polymarket.com/developers/market-makers/setup
|
# https://docs.polymarket.com/developers/market-makers/setup
|
||||||
DEFAULT_POLYMARKET_CONTRACTS: Dict[str, str] = {
|
DEFAULT_POLYMARKET_CONTRACTS: Dict[str, str] = {
|
||||||
"USDC.e": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
"USDC": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
|
||||||
|
"pUSD": "0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb",
|
||||||
"CTF": "0x4d97dcd97ec945f40cf65f87097ace5ea0476045",
|
"CTF": "0x4d97dcd97ec945f40cf65f87097ace5ea0476045",
|
||||||
"CTF_EXCHANGE": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
|
"CTF_EXCHANGE": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
|
||||||
"NEG_RISK_CTF_EXCHANGE": "0xC5d563A36AE78145C45a50134d48A1215220f80a",
|
"NEG_RISK_CTF_EXCHANGE": "0xC5d563A36AE78145C45a50134d48A1215220f80a",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from web.services.ops_api import (
|
|||||||
list_ops_payments,
|
list_ops_payments,
|
||||||
search_ops_users,
|
search_ops_users,
|
||||||
update_ops_config,
|
update_ops_config,
|
||||||
|
get_ops_training_accuracy,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(tags=["ops"])
|
router = APIRouter(tags=["ops"])
|
||||||
@@ -160,3 +161,9 @@ async def ops_logs(
|
|||||||
@router.get("/api/ops/health-check")
|
@router.get("/api/ops/health-check")
|
||||||
async def ops_health_check(request: Request):
|
async def ops_health_check(request: Request):
|
||||||
return get_ops_health_check(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())
|
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}
|
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