This commit is contained in:
@@ -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");
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface Competition {
|
||||
id: string;
|
||||
name: string;
|
||||
competitionSlug: string;
|
||||
sortOrder?: number;
|
||||
country: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
Reference in New Issue
Block a user