deploy: push latest version to production
Some checks failed
2026 World Cup Quant Platform - Production Deployment / Code Quality, Security Gate & Testing (push) Failing after 4m12s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Gitea CD (push) Has been skipped

This commit is contained in:
QuantBot
2026-06-26 14:06:37 +08:00
parent 9a0fa72738
commit 64cae96d0d
16 changed files with 447 additions and 0 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ package-lock.json
.env .env
/ops/shared /ops/shared
/ops/*.log /ops/*.log
*/__pycache__

View File

@@ -0,0 +1,26 @@
# 爬蟲與資料來源政策
正式站目標是提供高時效、高可信度的世界盃投注研究資料,但所有來源都必須分級處理,避免把未授權資料當成主資料源。
## 來源分級
1. `active_api`:有授權 API、可機器讀取、可監控 quota 與錯誤率,例如 The Odds API。
2. `official_reference`:官方賽程、公告、比分與規則來源,例如 FIFA 官方網站。
3. `market_reference`:本地市場口徑核對來源,例如台灣運彩官方網站。
4. `news_reference`:新聞與事件訊號來源,例如 Google 新聞 RSS、NewsAPI、路透社、BBC、ESPN。
5. `manual_reference`:只能人工核對,不能自動高頻抓取。
## 台灣運彩使用原則
- `https://www.sportslottery.com.tw/` 可作為台灣市場投注語言、玩法名稱與盤口口徑參考。
- 未完成條款與 robots 檢查前,不做高頻爬取。
- 若未來要自動化導入必須加入節流、快取、錯誤退避、User-Agent 標示與資料來源標記。
- 台灣運彩資料不得覆蓋授權 odds API只能作市場口徑交叉檢查與前台繁體中文術語校準。
## 首頁推薦資料優先序
1. 授權賠率 API 與官方賽程。
2. 後端量化模型產生的勝率、期望值、模型優勢與倉位建議。
3. 新聞與事件訊號作風險調整。
4. 台灣運彩作台灣市場口徑參考。
5. 若資料源不健康,首頁必須顯示資料延遲或暫停推薦,不得硬推單。

View File

@@ -0,0 +1,29 @@
# 2026 世界盃官方資料種子與 ingestion 邊界
更新時間2026-06-14 Asia/Taipei
## 已落地
- 新增 `platform/backend/app/analytics/worldcup_seed.py`
- 以 upsert 方式建立 16 個 2026 世界盃主辦場館,加上 `待確認場館` fallback。
- 每筆場館包含FIFA 賽事期間場館名稱、城市、國家、海拔、時區。
- production compose 新增 `fifa2026-seed` 一次性服務,`backend``odds-worker` 會等待 seed 成功後啟動。
- `/analytics/source-health` 會回報盤口筆數、賽事數、場館數、高海拔場館數、最新盤口時間、worker 狀態。
## 資料來源基準
- FIFA 官方 2026 世界盃場館頁:`https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/world-cup-2026-stadiums-fifa-soccer-football-mexico-usa-canada`
- FIFA 官方 2026 世界盃賽程頁:`https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/match-schedule-fixtures-results-teams-stadiums`
- FIFA 官方賽程 PDF`https://digitalhub.fifa.com/m/1be9ce37eb98fcc5/original/FWC26-Match-Schedule_English.pdf`
## 邊界
- 場館資料可以穩定 seed因為官方 16 個 host city/stadium 結構已明確。
- 完整賽程不在本次硬寫,後續應以官方 schedule PDF/API parser 或授權資料源 ingestion 產生,不手動填入不完整賽程。
- 台灣運彩可作為台灣盤口參考源,但必須先確認 robots、條款與頻率限制不能無差別高頻爬取。
## 下一個 P0
- 建立官方賽程 parser把 104 場完整賽事寫入 `matches`
- 將 sportsbook 賽事與官方 schedule 做 team/time/venue reconciliation。
- 補上場館別模型權重,特別是 Mexico City / Guadalajara 高海拔影響。

View File

@@ -0,0 +1,67 @@
# Gemini 工作內容完整盤點與接續推進報告
盤點日期2026-06-14Asia/Taipei
正式站目標https://2026fifa.wooo.work/
## 一、使用者目標
1. 全站內容與所有溝通都必須使用繁體中文。
2. 2026 世界盃賽事期間,首頁每天都要能看到專業分析後的投注推薦。
3. 推薦不能只是報牌,要包含模型勝率、市場隱含機率、期望值、模型優勢、信心分數、倉位建議與資料新鮮度。
4. 賽程、賠率、新聞、場地、資金流等資訊必須盡可能即時更新。
5. 正式環境以 `https://2026fifa.wooo.work/` 為準,不以本機頁面完成當作交付。
## 二、Gemini 已完成內容
### 平台骨架
- `platform/web`Next.js 15 前端包含首頁、賽事中心、每日作戰室、Kelly、回測、反向盤口、Proof of Yield、球員道具盤、投資組合等頁面。
- `platform/backend`FastAPI 後端,包含 `/analytics/*` API、WebSocket、Redis Pub/Sub、PostgreSQL/TimescaleDB 查詢。
- `platform/alerts`Redis 掃描與 Telegram 高價值投注警報 worker。
- `platform/deploy``ops`Docker、K3s、PM2、Nginx、healthcheck 與部署腳本。
### 量化與投注功能模組
- 期望值、Kelly、去水、Poisson、機器學習、反向盤口、裁判天候、球員道具盤、對沖、投資組合弱點、Proof of Yield。
- 資料庫 schema 已涵蓋 `matches``odds_history``smart_money_flow``value_bet_recommendations`、affiliate/copy-bet 等延伸表。
### 文件與產品方向
- `docs/professional-market-architecture.md` 定義 Gemini 路線資料分層、賠率矩陣、期望值偵測、Poisson、Monte Carlo、CLV。
- `docs/professional-data-reference.md` 定義官方、賠率、新聞、統計、天氣來源台帳。
## 三、主要缺口
1. 首頁原本呼叫每日作戰室資料,但缺少 Next API 代理 route導致高勝率推薦入口可能斷線。
2. 首頁原本連到 `/schedule`,但沒有實際賽程頁。
3. 文件提到新聞來源,但前台沒有新聞脈搏或新聞頁。
4. Daily Card 推薦邏輯過度樣板化,缺少市場隱含機率、模型優勢、信心分數與資料檢查點。
5. root `README.md` 仍描述舊 Express 緊急版,與目前 `platform/web``platform/backend` 主線不一致。
6. 部分頁面仍是展示骨架或樣本資料,例如回測、深度投注連結、部分 proof-of-yield 流程。
7. FIFA 官方賽程、The Odds API、生產資料庫、新聞權重與結算回寫還沒有完整閉環。
## 四、本次已推進
1. 新增每日作戰室 API 代理:`platform/web/app/api/analytics/daily-card/[targetDate]/route.ts`
2. 重做首頁為「世界盃即時投研主控台」,直接顯示資料健康、今日推薦、近期賽程與新聞脈搏。
3. 新增 Google 新聞 RSS 備援 API`platform/web/app/api/news/route.ts`
4. 新增新聞頁:`platform/web/app/news/page.tsx`
5. 新增完整賽程頁:`platform/web/app/schedule/page.tsx`
6. 強化推薦卡:顯示信心分數、風險類型、市場隱含機率、模型優勢、期望值、建議配置與資料檢查點。
7. 強化 Daily Card 後端:加入保守勝平負機率、平局機率、正期望值過濾、模型優勢過濾、相關性折減與倉位上限。
8. 將核心新增畫面內容改為繁體中文。
## 五、外部來源核對
- FIFA 官方賽程與比分入口https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/scores-fixtures
- The Odds API V4 文件https://the-odds-api.com/liveapi/guides/v4/
- The Odds API sport key 台帳https://the-odds-api.com/sports-odds-data/sports-apis.html
## 六、下一步 P0
1. 正式站部署後確認首頁是否能看到每日推薦與資料健康。
2. 把 FIFA 官方賽程正式灌入 `matches`,建立每日校準 job。
3. 確認 The Odds API production key、`soccer_fifa_world_cup` sport key、regions、markets 與 quota。
4. 建立 source registry platform 版,直接回傳來源健康、延遲、最後更新時間與錯誤原因。
5. 把 Proof of Yield 從展示帳本推到真實推薦 id、closing odds、賽果結算與 CLV。
6. 建立賽事期間高頻刷新:賽前 24 小時、賽前 3 小時、開賽後不同頻率的賠率與新聞刷新策略。

View File

@@ -0,0 +1,77 @@
# 正式環境頁面盤點與優化紀錄
盤點時間2026-06-14 02:45 Asia/Taipei
正式網址https://2026fifa.wooo.work/
## 一、正式站狀態
所有主要頁面 HTTP 狀態皆為 200
- `/`:首頁主控台
- `/daily-card`:每日作戰室
- `/schedule`:完整賽程表
- `/news`:即時新聞情報
- `/matches`:賽事中心
- `/odds`:市場指數比較
- `/sharp-money`:聰明錢追蹤
- `/models`:量化模型庫
- `/ml-edge`:機器學習優勢
- `/match-conditions`:裁判/天候模型
- `/rlm`:反向盤口雷達
- `/proof-of-yield`:公開收益驗證
- `/props`:球員道具盤
- `/kelly`:凱利配置
- `/backtesting`:策略回測
- `/deep-bet`:一鍵配置
- `/portfolio`:個人組合
- `/paywall`:付費牆
- `/offline`:離線頁
API 狀態:
- `/api/news`200Google 新聞 RSS 備援可用。
- `/api/analytics/matches`200目前只有少量賽事資料。
- `/api/analytics/daily-card/2026-06-14`:已由 500 修復為 200。
## 二、已完成優化
1. 正式站改跑平台版 Next.js + FastAPI + TimescaleDB + Redis不再只依賴舊 Express PM2 腳本。
2. 首頁改為「世界盃即時投研主控台」,直接顯示資料健康、今日推薦、近期賽程、新聞脈搏與專業模組入口。
3. 新增 `/schedule` 頁,修復原本入口存在但頁面不存在的問題。
4. 新增 `/news` 頁與 `/api/news`,以 Google 新聞 RSS 作無金鑰備援。
5. 修復 `/api/analytics/daily-card/[date]` Next route handler 型別與後端 `generate_daily_card` import 問題。
6. 移除 Daily Card 的假樣本賽事 fallback資料不足時不硬推單。
7. 補充台灣運彩 `https://www.sportslottery.com.tw/` 為台灣市場參考來源,僅作口徑核對,不作未授權高頻抓取。
8. 前台主要新增內容改為繁體中文,並開始清理英文模型術語。
## 三、目前未達標風險
1. 每日推薦目前可能顯示 0 組,原因是正式賠率資料與 `odds_history` 還沒有穩定支撐每日 Daily Card。
2. `/api/analytics/matches` 目前只有少量賽事,且場地仍顯示 Unknown官方賽程與場地資料未完整灌入。
3. 部分進階頁面仍是模型展示或手動輸入,不是全自動真資料看板。
4. Proof of Yield 尚未與真實推薦 id、closing odds、賽果結算完整閉環。
5. Next.js 15.0.3 build 時提示版本存在安全風險,需升級到修補版本。
6. 後端 `xgboost` 會拉取大型 NCCL 依賴,導致部署慢且映像偏重。
7. 多頁仍有 Next `themeColor` metadata warning已開始調整但需後續完全清理。
## 四、下一步 P0
1. 建立真實賽事與賠率 ingestionFIFA 官方賽程 + The Odds API `soccer_fifa_world_cup` + h2h/spreads/totals/btts。
2.`odds_history` 補齊主勝、平局、客勝與各 bookmaker 最新盤口,讓 Daily Card 有資料可算。
3. 建立來源健康 API每個來源最後更新時間、延遲、錯誤、資料筆數。
4. 首頁推薦只在資料源健康時顯示;資料不足時顯示「暫停推薦」而不是假推薦。
5. 將台灣運彩納入 `reference_only` crawler 規格,先做條款與頻率限制檢查。
## 2026-06-14 03:00 Asia/Taipei 正式環境覆核
正式網址:`https://2026fifa.wooo.work/`
已確認 HTTP 200首頁、每日作戰室、完整賽程表、即時新聞情報、賽事中心、賠率、聰明錢、模型庫、ML 優勢、場地條件、RLM、公開收益、球員道具盤、凱利、回測、深度下注、投資組合、付費牆、離線頁。
API 覆核:
- `/api/news`HTTP 200回傳 12 則新聞項目。
- `/api/analytics/matches`HTTP 200回傳 3 場賽事資料。
- `/api/analytics/daily-card/2026-06-14`HTTP 200`matched_matches=0``safe_singles=0``high_risk_singles=0`
目前判定:正式站已可用,但尚未達到最終商業目標。推薦引擎已避免在缺乏真實盤口與足夠賽事資料時硬產生投注建議;下一個 P0 必須補齊即時賠率、正式賽程、場地、台灣運彩參考盤與排程 ingestion才會穩定產出每天可檢查的高信心候選。

View File

@@ -0,0 +1,32 @@
# 專業投注玩法與推薦計算規則
更新時間2026-06-14 Asia/Taipei
## 已支援的推薦玩法
| 類型 | 市場 | 勝率模型 | 推薦條件 |
|---|---|---|---|
| 單關 | 勝平負 | Poisson xG score matrix | 勝率、EV、edge、倉位上限均通過 |
| 單關 | 大小球 2.5 | Poisson 總進球分布 | 正 EV 且市場隱含機率偏差明確 |
| 單關 | 雙方進球 BTTS | 雙隊至少一球分布 | 正 EV 且雙方 xG 支撐 |
| 小倉高賠 | 勝平負 / 大小球 / BTTS | 同上 | 更高 EV/edge 門檻,嚴格小倉位 |
| 跨場串關 | 2 串 1 | 單關勝率相乘後乘 0.92 | 兩腿不同賽事,重新計算組合 EV |
| 同場串關 | 同場不同市場 | 單關勝率相乘後乘 0.68 | 高 EV 門檻,小倉位上限 |
## 不硬推的玩法
以下玩法需要完整市場盤口線,不能只靠模型想像:
- 亞洲讓球:需要 handicap line例如 -0.25、-0.5、+0.75。
- 球隊進球數:需要 team total line。
- 正確比分:需要 correct score odds。
- 半場/全場:需要 HT/FT market odds。
- 角球、牌數、球員道具盤:需要對應事件源與市場賠率。
## 專業守則
- 沒有盤口,不產生投注推薦。
- 有模型勝率但沒有市場賠率,只能列入觀察,不列入可下注推薦。
- 串關不得直接把單關勝率相乘,必須套用相關性折減。
- 同場串關風險最高,預設只允許小倉位。
- 所有推薦都必須顯示勝率、目標賠率、市場隱含機率、edge、EV、建議單位與資料檢查。

View File

@@ -0,0 +1,33 @@
# 即時資料排程矩陣
更新時間2026-06-14 Asia/Taipei
## Production workers
| Worker | 頻率 | 職責 | 狀態寫入 |
|---|---:|---|---|
| `fifa2026-odds-worker` | 300 秒 | 賠率、比分、比賽狀態、完賽結果同步The Odds API + ESPN scoreboard 備援 | `ingestion:odds:last_run` |
| `fifa2026-news-worker` | 900 秒 | 世界盃新聞 RSS 快照更新 | `news:worldcup:latest`, `ingestion:news:last_run` |
| `fifa2026-fixtures-worker` | 21600 秒 | 完整賽程 fixtures JSON upsert可用 `FIXTURES_JSON_URL` 切換官方或授權來源 | `ingestion:fixtures:last_run` |
| `fifa2026-seed` | 部署時一次 | 場館、比分欄位、安全 upsert | Docker completed success |
## 前台讀取策略
- 首頁資料源健康卡讀 `/api/analytics/source-health`
- 新聞頁與首頁新聞讀 `/api/news`
- `/api/news` 優先讀 backend news snapshot沒有快照才 fallback Google News RSS。
- 賽事列表讀 `/api/analytics/matches`,包含繁中隊名、比分、狀態、場館。
- 每日推薦讀 `/api/analytics/daily-card/{date}`,包含勝平負、大小球、雙方進球、串關/同場候選。
## 資料來源策略
- `FIXTURES_JSON_URL` 預設為 `https://www.thestatsapi.com/world-cup/data/fixtures.json`
- 若取得 FIFA 官方 API 或授權 sports data feed直接替換 `FIXTURES_JSON_URL`worker 不需改程式。
- 台灣運彩 adapter 仍需確認 robots、條款與更新頻率限制後再接入。
## 還需要補強
- 台灣運彩合法參考盤 adapter。
- live minute / possession / red card 事件源。
- Worker failure alertTelegram/Email 告警。
- 資料延遲 SLA首頁顯示下一次更新時間與 stale 警示。

Binary file not shown.

View File

@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000';
type RouteContext = {
params: Promise<{ date: string }>;
};
export async function GET(_request: Request, { params }: RouteContext) {
const { date } = await params;
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
return NextResponse.json({ message: '日期格式必須為 YYYY-MM-DD' }, { status: 400 });
}
try {
const response = await fetch(`${ANALYTICS_BACKEND}/analytics/agent-daily-review/${encodeURIComponent(date)}`, {
method: 'GET',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
const message = await response.text();
return NextResponse.json({ message }, { status: response.status });
}
const data = (await response.json()) as Record<string, unknown>;
return NextResponse.json({ generated_at: new Date().toISOString(), ...data });
} catch (error) {
const message = error instanceof Error ? error.message : 'NemoTron 反方稽核暫時無法連線';
return NextResponse.json({ message }, { status: 502 });
}
}

View File

@@ -0,0 +1,24 @@
import { NextResponse } from 'next/server';
const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000';
export async function GET() {
try {
const response = await fetch(`${ANALYTICS_BACKEND}/analytics/agent-verification`, {
method: 'GET',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
const message = await response.text();
return NextResponse.json({ message }, { status: response.status });
}
const data = (await response.json()) as Record<string, unknown>;
return NextResponse.json({ generated_at: new Date().toISOString(), ...data });
} catch (error) {
const message = error instanceof Error ? error.message : 'AI 驗證服務暫時無法連線';
return NextResponse.json({ message }, { status: 502 });
}
}

View File

@@ -0,0 +1,43 @@
import { NextResponse } from 'next/server';
const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000';
export const dynamic = 'force-dynamic';
export async function GET(request: Request) {
const url = new URL(request.url);
const startDate = url.searchParams.get('start_date') || '2026-06-11';
const endDate = url.searchParams.get('end_date');
if (!/^\d{4}-\d{2}-\d{2}$/.test(startDate)) {
return NextResponse.json({ message: '開始日期格式必須為 YYYY-MM-DD' }, { status: 400 });
}
if (endDate && !/^\d{4}-\d{2}-\d{2}$/.test(endDate)) {
return NextResponse.json({ message: '結束日期格式必須為 YYYY-MM-DD' }, { status: 400 });
}
const params = new URLSearchParams({ start_date: startDate });
if (endDate) params.set('end_date', endDate);
try {
const response = await fetch(`${ANALYTICS_BACKEND}/analytics/daily-card-calendar?${params.toString()}`, {
method: 'GET',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const message = await response.text();
return NextResponse.json({ message }, { status: response.status });
}
const data = (await response.json()) as Record<string, unknown>;
return NextResponse.json(data);
} catch (error) {
const message = error instanceof Error ? error.message : '日期推薦摘要服務暫時無法連線';
return NextResponse.json({ message }, { status: 502 });
}
}

View File

@@ -0,0 +1,28 @@
import { NextResponse } from 'next/server';
const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000';
export const dynamic = 'force-dynamic';
export async function GET() {
try {
const response = await fetch(`${ANALYTICS_BACKEND}/analytics/daily-card-calendar/status`, {
method: 'GET',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const message = await response.text();
return NextResponse.json({ message }, { status: response.status });
}
const data = (await response.json()) as Record<string, unknown>;
return NextResponse.json(data);
} catch (error) {
const message = error instanceof Error ? error.message : '日期推薦摘要快取狀態暫時無法讀取';
return NextResponse.json({ message }, { status: 502 });
}
}

View File

@@ -0,0 +1,24 @@
import { NextResponse } from 'next/server';
const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000';
export async function GET() {
try {
const response = await fetch(`${ANALYTICS_BACKEND}/analytics/gemini-usage`, {
method: 'GET',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
const message = await response.text();
return NextResponse.json({ message }, { status: response.status });
}
const data = (await response.json()) as Record<string, unknown>;
return NextResponse.json({ generated_at: new Date().toISOString(), ...data });
} catch (error) {
const message = error instanceof Error ? error.message : 'Gemini 用量服務暫時無法連線';
return NextResponse.json({ message }, { status: 502 });
}
}

View File

@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000';
export async function GET(request: Request) {
const url = new URL(request.url);
const daysAhead = url.searchParams.get('days_ahead') || '2';
try {
const response = await fetch(`${ANALYTICS_BACKEND}/analytics/market-coverage?days_ahead=${encodeURIComponent(daysAhead)}`, {
method: 'GET',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const message = await response.text();
return NextResponse.json({ message }, { status: response.status });
}
const data = (await response.json()) as Record<string, unknown>;
return NextResponse.json(data);
} catch (error) {
const message = error instanceof Error ? error.message : '盤口覆蓋率暫時無法連線';
return NextResponse.json({ message }, { status: 502 });
}
}