This commit is contained in:
@@ -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");
|
||||||
@@ -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}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
+10
-10
@@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user