From 9481ad70945c2440e9eb641fc352357bcbc61099 Mon Sep 17 00:00:00 2001 From: Fahri Can Date: Wed, 20 May 2026 10:10:28 +0300 Subject: [PATCH] changes --- ai-engine/services/orchestrator/coupon.py | 61 +++++++++++++++++-- .../migration.sql | 2 + prisma/schema.prisma | 1 + .../feeder/feeder-persistence.service.ts | 9 ++- src/modules/feeder/feeder.service.ts | 5 ++ src/modules/feeder/feeder.types.ts | 1 + src/modules/matches/matches.service.ts | 25 +++++--- .../predictions/predictions.service.ts | 4 +- top_leagues.json | 20 +++--- 9 files changed, 103 insertions(+), 25 deletions(-) create mode 100644 prisma/migrations/20260520000000_add_league_sort_order/migration.sql diff --git a/ai-engine/services/orchestrator/coupon.py b/ai-engine/services/orchestrator/coupon.py index 18c7390..33da215 100644 --- a/ai-engine/services/orchestrator/coupon.py +++ b/ai-engine/services/orchestrator/coupon.py @@ -60,6 +60,51 @@ from models.calibration import get_calibrator 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( self, match_ids: List[str], @@ -70,15 +115,21 @@ class CouponMixin: strategy_name = (strategy or "BALANCED").upper() strategy_config = { - "SAFE": {"max_matches": 4, "min_conf": 66.0}, - "BALANCED": {"max_matches": 5, "min_conf": 58.0}, - "AGGRESSIVE": {"max_matches": 8, "min_conf": 52.0}, - "VALUE": {"max_matches": 8, "min_conf": 48.0}, - "MIRACLE": {"max_matches": 10, "min_conf": 44.0}, + "SAFE": {"max_matches": 4, "min_conf": 66.0, "prefilter": 12}, + "BALANCED": {"max_matches": 5, "min_conf": 58.0, "prefilter": 15}, + "AGGRESSIVE": {"max_matches": 8, "min_conf": 52.0, "prefilter": 20}, + "VALUE": {"max_matches": 8, "min_conf": 48.0, "prefilter": 20}, + "MIRACLE": {"max_matches": 10, "min_conf": 44.0, "prefilter": 25}, } cfg = strategy_config.get(strategy_name, strategy_config["BALANCED"]) 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"] + 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]] = [] rejected: List[Dict[str, Any]] = [] diff --git a/prisma/migrations/20260520000000_add_league_sort_order/migration.sql b/prisma/migrations/20260520000000_add_league_sort_order/migration.sql new file mode 100644 index 0000000..3b1f935 --- /dev/null +++ b/prisma/migrations/20260520000000_add_league_sort_order/migration.sql @@ -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"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0059557..d0cfe62 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,6 +30,7 @@ model League { competitionSlug String? @map("competition_slug") code String? logoUrl String? @map("logo_url") + sortOrder Int? @map("sort_order") createdAt DateTime @default(now()) @map("created_at") country Country? @relation(fields: [countryId], references: [id]) liveMatches LiveMatch[] diff --git a/src/modules/feeder/feeder-persistence.service.ts b/src/modules/feeder/feeder-persistence.service.ts index 9668d4f..74cd862 100755 --- a/src/modules/feeder/feeder-persistence.service.ts +++ b/src/modules/feeder/feeder-persistence.service.ts @@ -251,6 +251,10 @@ export class FeederPersistenceService { if (existingLeague) { // If exists with different ID, use existing ID to prevent constraint errors 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 { // Create new league await tx.league.create({ @@ -261,8 +265,11 @@ export class FeederPersistenceService { sport: sport, competitionSlug: league.competitionSlug, logoUrl: `/uploads/competitions/${finalLeagueId}.png`, - }, + } as any, }); + if (league.sortOrder !== undefined) { + await tx.$executeRaw`UPDATE leagues SET sort_order = ${league.sortOrder} WHERE id = ${finalLeagueId}`; + } } } diff --git a/src/modules/feeder/feeder.service.ts b/src/modules/feeder/feeder.service.ts index 687ebd0..db543ac 100755 --- a/src/modules/feeder/feeder.service.ts +++ b/src/modules/feeder/feeder.service.ts @@ -323,6 +323,11 @@ export class FeederService { 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 const rawMatches = Object.values( data.matches, diff --git a/src/modules/feeder/feeder.types.ts b/src/modules/feeder/feeder.types.ts index 03fff71..8e31365 100755 --- a/src/modules/feeder/feeder.types.ts +++ b/src/modules/feeder/feeder.types.ts @@ -77,6 +77,7 @@ export interface Competition { id: string; name: string; competitionSlug: string; + sortOrder?: number; country: { id: string; name: string; diff --git a/src/modules/matches/matches.service.ts b/src/modules/matches/matches.service.ts index 5078813..cc27959 100755 --- a/src/modules/matches/matches.service.ts +++ b/src/modules/matches/matches.service.ts @@ -320,10 +320,11 @@ export class MatchesService { const leagueId = match.leagueId || "unknown"; if (!leaguesMap.has(leagueId)) { - leaguesMap.set(leagueId, { + const entry: any = { id: leagueId, name: match.league?.name || "Unknown League", code: match.league?.code || undefined, + _league: match.league, // for sortOrder access country: { id: match.league?.country?.id || "", name: match.league?.country?.name || "", @@ -333,7 +334,8 @@ export class MatchesService { }, sport: sport, matches: [], - }); + }; + leaguesMap.set(leagueId, entry); } const league = leaguesMap.get(leagueId)!; @@ -397,12 +399,21 @@ export class MatchesService { } return Array.from(leaguesMap.values()).sort((a, b) => { - const aIdx = this.topLeagueIds.indexOf(a.id); - const bIdx = this.topLeagueIds.indexOf(b.id); - const aPriority = aIdx === -1 ? 999 : aIdx; - const bPriority = bIdx === -1 ? 999 : bIdx; + // 1. top_leagues.json sırası (sabit öncelik listesi) + const aTop = this.topLeagueIds.indexOf(a.id); + const bTop = this.topLeagueIds.indexOf(b.id); + 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 || ""); }); } diff --git a/src/modules/predictions/predictions.service.ts b/src/modules/predictions/predictions.service.ts index 2de5a26..566708f 100755 --- a/src/modules/predictions/predictions.service.ts +++ b/src/modules/predictions/predictions.service.ts @@ -1194,13 +1194,13 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy { } } - // Direct HTTP mode + // Direct HTTP mode — coupon needs longer timeout (many matches) try { const response = await this.aiEngineClient.post("/smart-coupon", { match_ids: matchIds, strategy, ...options, - }); + }, { timeout: 300000 }); // 5 dakika return response.data; } catch (error: unknown) { const message = diff --git a/top_leagues.json b/top_leagues.json index d6584d3..1b2bf47 100755 --- a/top_leagues.json +++ b/top_leagues.json @@ -1,25 +1,25 @@ [ "482ofyysbdbeoxauk19yg7tdt", "2o9svokc5s7diish3ycrzk7jm", + "7af85xa75vozt2l4hzi6ryts7", + "4oogyu6o156iphvdvphwpck10", + "4c1nfi2j1m731hcay25fcgndq", + "c7b8o53flg36wbuevfzy3lb10", "2kwbbcootiqqgmrzs6o5inle5", "34pl8szyvrbwcmfkuocjm3r6t", + "6by3h89i2eykc341oz7lv1ddd", "1r097lpxe0xn03ihb7wi98kao", "dm5ka0os1e3dxcp3vh05kmp33", - "6by3h89i2eykc341oz7lv1ddd", "akmkihra9ruad09ljapsm84b3", - "8yi6ejjd1zudcqtbn07haahg6", - "4zwgbb66rif2spcoeeol2motx", "7ntvbsyq31jnzoqoa8850b9b8", - "4w7x0s5gfs5abasphlha5de8k", "3is4bkgf3loxv9qfg3hm8zfqb", - "8ey0ww2zsosdmwr8ehsorh6t7", "722fdbecxzcq9788l6jqclzlw", + "8ey0ww2zsosdmwr8ehsorh6t7", "e21cf135btr8t3upw0vl6n6x0", - "e0lck99w8meo9qoalfrxgo33o", "581t4mywybx21wcpmpykhyzr3", "5c96g1zm7vo5ons9c42uy2w3r", "4zwgbb66rif2spcoeeol2motx", - "4oogyu6o156iphvdvphwpck10", - "4c1nfi2j1m731hcay25fcgndq", - "c7b8o53flg36wbuevfzy3lb10" -] \ No newline at end of file + "8yi6ejjd1zudcqtbn07haahg6", + "4w7x0s5gfs5abasphlha5de8k", + "e0lck99w8meo9qoalfrxgo33o" +]