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
5 changes: 5 additions & 0 deletions dashboard/backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `prisma/migrations/20260514000001_enhance_market_consensus_phase2/migration.sql`: Adds/backfills new `market_consensus` columns and creates an index on `market_type`.
- `src/services/market-consensus.service.ts`: `calculateConsensus()` now computes additional phase-2 market-truth metrics and persists them; added `identifyOutliers(gameId)` for per-market outlier discovery.
- `tests/market-consensus.service.test.ts`: New unit tests covering phase-2 spreads consensus calculations and outlier identification behavior.
- **Bookmaker Performance Analytics — Phase 2** (Issue: Bookmaker Analytics): Added persistent bookmaker analytics models and service calculations for value, sharpness, and reliability ranking.
- `prisma/schema.prisma`: Added `BookmakerAnalytics` and `BookmakerMovementEvent` models; linked movement events to `Game`.
- `prisma/migrations/20260514000002_add_bookmaker_analytics_phase2/migration.sql`: Creates `bookmaker_analytics` and `bookmaker_movement_events` tables, indexes, and FK constraints.
- `src/services/bookmaker-analytics.service.ts`: New `BookmakerAnalyticsService` with `calculateBookmakerMetrics(bookmaker)` and `rankBookmakers(criteria)` methods.
- `tests/bookmaker-analytics.service.test.ts`: Added unit tests for metric calculation/upsert behavior and criteria-based ranking.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
-- CreateTable
CREATE TABLE "bookmaker_analytics" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"bookmaker" VARCHAR(50) NOT NULL,
"average_clv_offered" DECIMAL(5,2) NOT NULL,
"best_odds_frequency" DECIMAL(5,2) NOT NULL,
"margin_vs_consensus" DECIMAL(5,2) NOT NULL,
"outlier_frequency" DECIMAL(5,2) NOT NULL,
"first_mover_frequency" DECIMAL(5,2) NOT NULL,
"line_movement_lag" INTEGER NOT NULL,
"sharp_book_rating" INTEGER NOT NULL,
"market_efficiency" DECIMAL(5,2) NOT NULL,
"sports_covered" TEXT[],
"markets_covered" TEXT[],
"uptime_percentage" DECIMAL(5,2) NOT NULL,
"odds_update_frequency" INTEGER NOT NULL,
"average_odds_age" INTEGER NOT NULL,
"limit_profile" VARCHAR(20) NOT NULL,
"estimated_max_bet" DECIMAL(10,2),
"account_limit_reports" INTEGER NOT NULL,
"total_games_offered" INTEGER NOT NULL,
"total_markets_offered" INTEGER NOT NULL,
"average_margin" DECIMAL(5,2) NOT NULL,
"user_rating" DECIMAL(3,2),
"user_review_count" INTEGER NOT NULL DEFAULT 0,
"recommendation_score" INTEGER NOT NULL,
"calculated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "bookmaker_analytics_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "bookmaker_movement_events" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"game_id" UUID NOT NULL,
"market_type" VARCHAR(20) NOT NULL,
"detected_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"first_mover" VARCHAR(50) NOT NULL,
"first_move_time" TIMESTAMPTZ(6) NOT NULL,
"followers" JSONB NOT NULL,
"movement_size" DECIMAL(5,2) NOT NULL,

CONSTRAINT "bookmaker_movement_events_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "bookmaker_analytics_bookmaker_key" ON "bookmaker_analytics"("bookmaker");

-- CreateIndex
CREATE INDEX "bookmaker_analytics_best_odds_frequency_idx" ON "bookmaker_analytics"("best_odds_frequency");

-- CreateIndex
CREATE INDEX "bookmaker_analytics_sharp_book_rating_idx" ON "bookmaker_analytics"("sharp_book_rating");

-- CreateIndex
CREATE INDEX "bookmaker_movement_events_first_mover_idx" ON "bookmaker_movement_events"("first_mover");

-- CreateIndex
CREATE INDEX "bookmaker_movement_events_detected_at_idx" ON "bookmaker_movement_events"("detected_at");

-- AddForeignKey
ALTER TABLE "bookmaker_movement_events" ADD CONSTRAINT "bookmaker_movement_events_game_id_fkey" FOREIGN KEY ("game_id") REFERENCES "games"("id") ON DELETE CASCADE ON UPDATE CASCADE;
68 changes: 68 additions & 0 deletions dashboard/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ model Game {
playerStats PlayerGameStats[]
marketConsensus MarketConsensus[]
lineMovements LineMovement[]
bookmakerMovementEvents BookmakerMovementEvent[]
sharpMoneyIndicators SharpMoneyIndicator[]

@@index([commenceTime])
Expand Down Expand Up @@ -348,6 +349,73 @@ model LineMovement {
@@map("line_movements")
}

// Bookmaker-level performance analytics
model BookmakerAnalytics {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
bookmaker String @unique @db.VarChar(50)

// Odds quality metrics
averageCLVOffered Decimal @map("average_clv_offered") @db.Decimal(5, 2)
bestOddsFrequency Decimal @map("best_odds_frequency") @db.Decimal(5, 2)
marginVsConsensus Decimal @map("margin_vs_consensus") @db.Decimal(5, 2)
outlierFrequency Decimal @map("outlier_frequency") @db.Decimal(5, 2)

// Market making behavior
firstMoverFrequency Decimal @map("first_mover_frequency") @db.Decimal(5, 2)
lineMovementLag Int @map("line_movement_lag")
sharpBookRating Int @map("sharp_book_rating")
marketEfficiency Decimal @map("market_efficiency") @db.Decimal(5, 2)

// Coverage & reliability
sportsCovered String[] @map("sports_covered")
marketsCovered String[] @map("markets_covered")
uptimePercentage Decimal @map("uptime_percentage") @db.Decimal(5, 2)
oddsUpdateFrequency Int @map("odds_update_frequency")
averageOddsAge Int @map("average_odds_age")

// Limits & accessibility
limitProfile String @map("limit_profile") @db.VarChar(20)
estimatedMaxBet Decimal? @map("estimated_max_bet") @db.Decimal(10, 2)
accountLimitReports Int @map("account_limit_reports")

// Historical performance
totalGamesOffered Int @map("total_games_offered")
totalMarketsOffered Int @map("total_markets_offered")
averageMargin Decimal @map("average_margin") @db.Decimal(5, 2)

// Reputation
userRating Decimal? @map("user_rating") @db.Decimal(3, 2)
userReviewCount Int @default(0) @map("user_review_count")
recommendationScore Int @map("recommendation_score")

// Metadata
calculatedAt DateTime @default(now()) @map("calculated_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)

@@index([bestOddsFrequency])
@@index([sharpBookRating])
@@map("bookmaker_analytics")
}

// Bookmaker movement events (who moved first and how quickly others followed)
model BookmakerMovementEvent {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
gameId String @map("game_id") @db.Uuid
marketType String @map("market_type") @db.VarChar(20)
detectedAt DateTime @default(now()) @map("detected_at") @db.Timestamptz(6)

firstMover String @map("first_mover") @db.VarChar(50)
firstMoveTime DateTime @map("first_move_time") @db.Timestamptz(6)
followers Json
movementSize Decimal @map("movement_size") @db.Decimal(5, 2)

game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)

@@index([firstMover])
@@index([detectedAt])
@@map("bookmaker_movement_events")
}

// Users table (for OAuth2 authentication)
model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
Expand Down
Loading
Loading