Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/backend/src/fantasy/fantasy.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export interface FantasyPlayer {
team: string | null;
injuryStatus: string | null;
fantasyPositions: string[];
/** Consensus market value from FantasyCalc for the league's format (dynasty/redraft, QB count, PPR). Null if unranked. */
/** Consensus market value from FantasyCalc redraft rankings for the league's QB count and PPR. Null if unranked. */
marketValue: number | null;
/** FantasyCalc rank among players at the same position. Null if unranked. */
positionRank: number | null;
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/fantasy/fantasy.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,7 @@ describe('FantasyService', () => {
roster_positions: ['QB', 'SUPER_FLEX', 'RB'],
scoring_settings: { rec: 1 },
}),
).toEqual({ isDynasty: true, numQbs: 2, ppr: 1 });
).toEqual({ isDynasty: false, numQbs: 2, ppr: 1 });

expect(
internals.resolveLeagueFormat({
Expand All @@ -1480,7 +1480,7 @@ describe('FantasyService', () => {
roster_positions: ['QB', 'QB', 'SUPER_FLEX', 'RB'],
scoring_settings: { rec: 0.25 },
}),
).toEqual({ isDynasty: true, numQbs: 2, ppr: 0.5 });
).toEqual({ isDynasty: false, numQbs: 2, ppr: 0.5 });
});

it('fetches and caches FantasyCalc player values scaled to the league format', async () => {
Expand Down Expand Up @@ -1528,7 +1528,7 @@ describe('FantasyService', () => {

expect(Axios.get).toHaveBeenCalledWith(
'https://api.fantasycalc.com/values/current',
expect.objectContaining({ params: { isDynasty: true, numQbs: 1, numTeams: 10, ppr: 0.5 }, timeout: 2000 }),
expect.objectContaining({ params: { isDynasty: false, numQbs: 1, numTeams: 10, ppr: 0.5 }, timeout: 2000 }),
);
expect(values.get('p1')).toEqual({
sleeperId: 'p1',
Expand All @@ -1548,7 +1548,7 @@ describe('FantasyService', () => {
const twelveTeamValues = await internals.getFantasyCalcValues(twelveTeamLeague);
expect(Axios.get).toHaveBeenCalledWith(
'https://api.fantasycalc.com/values/current',
expect.objectContaining({ params: { isDynasty: true, numQbs: 1, numTeams: 12, ppr: 0.5 }, timeout: 2000 }),
expect.objectContaining({ params: { isDynasty: false, numQbs: 1, numTeams: 12, ppr: 0.5 }, timeout: 2000 }),
);
expect(twelveTeamValues.get('p1')?.value).toBe(9100);
});
Expand Down
19 changes: 11 additions & 8 deletions packages/backend/src/fantasy/fantasy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,10 @@ export class FantasyService {
const numQbs = requestedQbs >= 2 ? 2 : 1;
const requestedPpr = league.scoring_settings?.rec;
const ppr = requestedPpr === undefined ? 0.5 : requestedPpr >= 0.75 ? 1 : requestedPpr >= 0.25 ? 0.5 : 0;
// Sleeper league format: 0 = redraft, 1 = keeper, 2 = dynasty. Keeper leagues value long-term assets
// similarly to dynasty, so both are treated as dynasty for valuation purposes.
const isDynasty = (league.settings?.type ?? 0) >= 1;
// Trade/waiver fairness cares about a player's current-season value, not long-term dynasty
// valuation, so FantasyCalc is always queried for redraft rankings regardless of the Sleeper
// league's keeper/dynasty settings.
const isDynasty = false;
Comment thread
sfreeman422 marked this conversation as resolved.
return { isDynasty, numQbs, ppr };
}

Expand All @@ -424,9 +425,11 @@ export class FantasyService {

/**
* Fetches consensus player trade values from the FantasyCalc API (https://fantasycalc.com/api-docs),
* matched to the league's format (dynasty/redraft, QB count, PPR). Values are keyed by Sleeper player ID
* so they can be merged directly onto `FantasyPlayer` records. Failures are non-fatal: trade/waiver
* recommendations still work without market values, just with less precise fairness signal.
* always using redraft rankings scaled to the league's QB count and PPR (dynasty valuation is
* intentionally not used, since trade/waiver fairness cares about current-season value). Values
* are keyed by Sleeper player ID so they can be merged directly onto `FantasyPlayer` records.
* Failures are non-fatal: trade/waiver recommendations still work without market values, just
* with less precise fairness signal.
*/
private async getFantasyCalcValues(league: SleeperLeague): Promise<Map<string, FantasyCalcPlayerValue>> {
const { isDynasty, numQbs, ppr } = this.resolveLeagueFormat(league);
Expand Down Expand Up @@ -1364,8 +1367,8 @@ export class FantasyService {
instructions:
'You are a fantasy football analyst. Return only valid JSON with keys teamHealth, tradeInsights, suggestions, waiverSuggestions, and lineupSummary. ' +
'teamHealth must assess the user roster relative to the supplied league with an integer percentage from 0 to 100 and a concise summary. ' +
"Each player object includes a marketValue (a FantasyCalc consensus trade value already scaled to this league's " +
'format, dynasty vs redraft, QB count, and PPR - higher is more valuable) and a positionRank (rank among players ' +
"Each player object includes a marketValue (a FantasyCalc consensus redraft trade value already scaled to this league's " +
'QB count and PPR format - higher is more valuable) and a positionRank (rank among players ' +
'at the same position, 1 is best). A null marketValue means the player is unranked by FantasyCalc or the market-value ' +
'feed was unavailable for this request; in either case fall back to position, roster needs, and matchup context instead ' +
'of value. ' +
Expand Down
Loading