changes
Deploy Iddaai Backend / build-and-deploy (push) Successful in 42s

This commit is contained in:
2026-05-20 10:10:28 +03:00
parent 1d4aa36602
commit 9481ad7094
9 changed files with 103 additions and 25 deletions
+56 -5
View File
@@ -60,6 +60,51 @@ from models.calibration import get_calibrator
class CouponMixin: class CouponMixin:
def _prefilter_match_ids(self, match_ids: List[str], limit: int = 15) -> List[str]:
"""
40+ maç gelirse hepsini analiz etmek çok yavaş.
DB'den hızlıca en kaliteli limit adet maçı seç:
- Odds verisi olan maçlar önce
- football_ai_features'da gerçek ELO'su olan maçlar
- Yüksek lig güvenilirliği
"""
if len(match_ids) <= limit:
return match_ids
try:
with psycopg2.connect(self.dsn) as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("""
SELECT
m.id,
COUNT(oc.db_id) AS odds_count,
COALESCE(f.home_elo, 1500) AS home_elo,
lr.reliability_score
FROM matches m
LEFT JOIN odd_categories oc ON oc.match_id = m.id
LEFT JOIN football_ai_features f ON f.match_id = m.id
LEFT JOIN team_elo_ratings ter_h ON ter_h.team_id = m.home_team_id
LEFT JOIN (
SELECT league_id, AVG(home_elo) AS reliability_score
FROM football_ai_features
GROUP BY league_id
) lr ON lr.league_id = m.league_id
WHERE m.id = ANY(%s)
GROUP BY m.id, f.home_elo, lr.reliability_score
ORDER BY
COUNT(oc.db_id) DESC,
COALESCE(f.home_elo, 1500) DESC
LIMIT %s
""", (match_ids, limit))
rows = cur.fetchall()
filtered = [r["id"] for r in rows]
# Eğer DB'den yeterli gelmediyse kalanları ekle
remaining = [m for m in match_ids if m not in filtered]
return filtered + remaining[:max(0, limit - len(filtered))]
except Exception as e:
print(f"⚠️ Prefilter failed, using original list: {e}")
return match_ids[:limit]
def build_coupon( def build_coupon(
self, self,
match_ids: List[str], match_ids: List[str],
@@ -70,15 +115,21 @@ class CouponMixin:
strategy_name = (strategy or "BALANCED").upper() strategy_name = (strategy or "BALANCED").upper()
strategy_config = { strategy_config = {
"SAFE": {"max_matches": 4, "min_conf": 66.0}, "SAFE": {"max_matches": 4, "min_conf": 66.0, "prefilter": 12},
"BALANCED": {"max_matches": 5, "min_conf": 58.0}, "BALANCED": {"max_matches": 5, "min_conf": 58.0, "prefilter": 15},
"AGGRESSIVE": {"max_matches": 8, "min_conf": 52.0}, "AGGRESSIVE": {"max_matches": 8, "min_conf": 52.0, "prefilter": 20},
"VALUE": {"max_matches": 8, "min_conf": 48.0}, "VALUE": {"max_matches": 8, "min_conf": 48.0, "prefilter": 20},
"MIRACLE": {"max_matches": 10, "min_conf": 44.0}, "MIRACLE": {"max_matches": 10, "min_conf": 44.0, "prefilter": 25},
} }
cfg = strategy_config.get(strategy_name, strategy_config["BALANCED"]) cfg = strategy_config.get(strategy_name, strategy_config["BALANCED"])
max_allowed = max_matches if max_matches is not None else cfg["max_matches"] max_allowed = max_matches if max_matches is not None else cfg["max_matches"]
min_conf = min_confidence if min_confidence is not None else cfg["min_conf"] min_conf = min_confidence if min_confidence is not None else cfg["min_conf"]
prefilter_limit = cfg["prefilter"]
# Çok fazla maç gelirse önce hızlı prefilter uygula
if len(match_ids) > prefilter_limit:
print(f"🔍 Prefiltering {len(match_ids)}{prefilter_limit} matches for {strategy_name} coupon")
match_ids = self._prefilter_match_ids(match_ids, prefilter_limit)
candidates: List[Dict[str, Any]] = [] candidates: List[Dict[str, Any]] = []
rejected: List[Dict[str, Any]] = [] rejected: List[Dict[str, Any]] = []
@@ -0,0 +1,2 @@
ALTER TABLE "leagues" ADD COLUMN IF NOT EXISTS "sort_order" INTEGER;
CREATE INDEX IF NOT EXISTS "leagues_sort_order_idx" ON "leagues"("sort_order");
+1
View File
@@ -30,6 +30,7 @@ model League {
competitionSlug String? @map("competition_slug") competitionSlug String? @map("competition_slug")
code String? code String?
logoUrl String? @map("logo_url") logoUrl String? @map("logo_url")
sortOrder Int? @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
country Country? @relation(fields: [countryId], references: [id]) country Country? @relation(fields: [countryId], references: [id])
liveMatches LiveMatch[] liveMatches LiveMatch[]
@@ -251,6 +251,10 @@ export class FeederPersistenceService {
if (existingLeague) { if (existingLeague) {
// If exists with different ID, use existing ID to prevent constraint errors // If exists with different ID, use existing ID to prevent constraint errors
finalLeagueId = existingLeague.id; finalLeagueId = existingLeague.id;
// Update sortOrder if changed
if (league.sortOrder !== undefined) {
await tx.$executeRaw`UPDATE leagues SET sort_order = ${league.sortOrder} WHERE id = ${finalLeagueId}`;
}
} else { } else {
// Create new league // Create new league
await tx.league.create({ await tx.league.create({
@@ -261,8 +265,11 @@ export class FeederPersistenceService {
sport: sport, sport: sport,
competitionSlug: league.competitionSlug, competitionSlug: league.competitionSlug,
logoUrl: `/uploads/competitions/${finalLeagueId}.png`, logoUrl: `/uploads/competitions/${finalLeagueId}.png`,
}, } as any,
}); });
if (league.sortOrder !== undefined) {
await tx.$executeRaw`UPDATE leagues SET sort_order = ${league.sortOrder} WHERE id = ${finalLeagueId}`;
}
} }
} }
+5
View File
@@ -323,6 +323,11 @@ export class FeederService {
return; return;
} }
// Inject sortOrder from mackolik competition order (key position = display order)
Object.keys(data.competitions).forEach((compId, idx) => {
(data.competitions[compId] as Competition).sortOrder = idx;
});
// Filter matches with iddaa code and deduplicate // Filter matches with iddaa code and deduplicate
const rawMatches = Object.values( const rawMatches = Object.values(
data.matches, data.matches,
+1
View File
@@ -77,6 +77,7 @@ export interface Competition {
id: string; id: string;
name: string; name: string;
competitionSlug: string; competitionSlug: string;
sortOrder?: number;
country: { country: {
id: string; id: string;
name: string; name: string;
+18 -7
View File
@@ -320,10 +320,11 @@ export class MatchesService {
const leagueId = match.leagueId || "unknown"; const leagueId = match.leagueId || "unknown";
if (!leaguesMap.has(leagueId)) { if (!leaguesMap.has(leagueId)) {
leaguesMap.set(leagueId, { const entry: any = {
id: leagueId, id: leagueId,
name: match.league?.name || "Unknown League", name: match.league?.name || "Unknown League",
code: match.league?.code || undefined, code: match.league?.code || undefined,
_league: match.league, // for sortOrder access
country: { country: {
id: match.league?.country?.id || "", id: match.league?.country?.id || "",
name: match.league?.country?.name || "", name: match.league?.country?.name || "",
@@ -333,7 +334,8 @@ export class MatchesService {
}, },
sport: sport, sport: sport,
matches: [], matches: [],
}); };
leaguesMap.set(leagueId, entry);
} }
const league = leaguesMap.get(leagueId)!; const league = leaguesMap.get(leagueId)!;
@@ -397,12 +399,21 @@ export class MatchesService {
} }
return Array.from(leaguesMap.values()).sort((a, b) => { return Array.from(leaguesMap.values()).sort((a, b) => {
const aIdx = this.topLeagueIds.indexOf(a.id); // 1. top_leagues.json sırası (sabit öncelik listesi)
const bIdx = this.topLeagueIds.indexOf(b.id); const aTop = this.topLeagueIds.indexOf(a.id);
const aPriority = aIdx === -1 ? 999 : aIdx; const bTop = this.topLeagueIds.indexOf(b.id);
const bPriority = bIdx === -1 ? 999 : bIdx; const aTopPriority = aTop === -1 ? 999 : aTop;
const bTopPriority = bTop === -1 ? 999 : bTop;
if (aTopPriority !== bTopPriority) return aTopPriority - bTopPriority;
if (aPriority !== bPriority) return aPriority - bPriority; // 2. Mackolik'ten gelen sortOrder (feeder her gün güncelliyor)
const leagueA = (a as any)._league as any;
const leagueB = (b as any)._league as any;
const aSortOrder = leagueA?.sortOrder ?? 9999;
const bSortOrder = leagueB?.sortOrder ?? 9999;
if (aSortOrder !== bSortOrder) return aSortOrder - bSortOrder;
// 3. Alfabetik fallback
return (a.name || "").localeCompare(b.name || ""); return (a.name || "").localeCompare(b.name || "");
}); });
} }
@@ -1194,13 +1194,13 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
} }
} }
// Direct HTTP mode // Direct HTTP mode — coupon needs longer timeout (many matches)
try { try {
const response = await this.aiEngineClient.post("/smart-coupon", { const response = await this.aiEngineClient.post("/smart-coupon", {
match_ids: matchIds, match_ids: matchIds,
strategy, strategy,
...options, ...options,
}); }, { timeout: 300000 }); // 5 dakika
return response.data; return response.data;
} catch (error: unknown) { } catch (error: unknown) {
const message = const message =
+9 -9
View File
@@ -1,25 +1,25 @@
[ [
"482ofyysbdbeoxauk19yg7tdt", "482ofyysbdbeoxauk19yg7tdt",
"2o9svokc5s7diish3ycrzk7jm", "2o9svokc5s7diish3ycrzk7jm",
"7af85xa75vozt2l4hzi6ryts7",
"4oogyu6o156iphvdvphwpck10",
"4c1nfi2j1m731hcay25fcgndq",
"c7b8o53flg36wbuevfzy3lb10",
"2kwbbcootiqqgmrzs6o5inle5", "2kwbbcootiqqgmrzs6o5inle5",
"34pl8szyvrbwcmfkuocjm3r6t", "34pl8szyvrbwcmfkuocjm3r6t",
"6by3h89i2eykc341oz7lv1ddd",
"1r097lpxe0xn03ihb7wi98kao", "1r097lpxe0xn03ihb7wi98kao",
"dm5ka0os1e3dxcp3vh05kmp33", "dm5ka0os1e3dxcp3vh05kmp33",
"6by3h89i2eykc341oz7lv1ddd",
"akmkihra9ruad09ljapsm84b3", "akmkihra9ruad09ljapsm84b3",
"8yi6ejjd1zudcqtbn07haahg6",
"4zwgbb66rif2spcoeeol2motx",
"7ntvbsyq31jnzoqoa8850b9b8", "7ntvbsyq31jnzoqoa8850b9b8",
"4w7x0s5gfs5abasphlha5de8k",
"3is4bkgf3loxv9qfg3hm8zfqb", "3is4bkgf3loxv9qfg3hm8zfqb",
"8ey0ww2zsosdmwr8ehsorh6t7",
"722fdbecxzcq9788l6jqclzlw", "722fdbecxzcq9788l6jqclzlw",
"8ey0ww2zsosdmwr8ehsorh6t7",
"e21cf135btr8t3upw0vl6n6x0", "e21cf135btr8t3upw0vl6n6x0",
"e0lck99w8meo9qoalfrxgo33o",
"581t4mywybx21wcpmpykhyzr3", "581t4mywybx21wcpmpykhyzr3",
"5c96g1zm7vo5ons9c42uy2w3r", "5c96g1zm7vo5ons9c42uy2w3r",
"4zwgbb66rif2spcoeeol2motx", "4zwgbb66rif2spcoeeol2motx",
"4oogyu6o156iphvdvphwpck10", "8yi6ejjd1zudcqtbn07haahg6",
"4c1nfi2j1m731hcay25fcgndq", "4w7x0s5gfs5abasphlha5de8k",
"c7b8o53flg36wbuevfzy3lb10" "e0lck99w8meo9qoalfrxgo33o"
] ]