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:
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]] = []
@@ -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")
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[]
@@ -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}`;
}
}
}
+5
View File
@@ -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,
+1
View File
@@ -77,6 +77,7 @@ export interface Competition {
id: string;
name: string;
competitionSlug: string;
sortOrder?: number;
country: {
id: string;
name: string;
+18 -7
View File
@@ -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 || "");
});
}
@@ -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 =
+9 -9
View File
@@ -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"
"8yi6ejjd1zudcqtbn07haahg6",
"4w7x0s5gfs5abasphlha5de8k",
"e0lck99w8meo9qoalfrxgo33o"
]