Skip to content

Commit

Permalink
Feat: Add Games Analysis (#19)
Browse files Browse the repository at this point in the history
* Set final config for initla testing, confirmed scrapping running and owrking

* Fetch analyzable data from postgres

* Get trigram query working

* Add matcher to three way game events as well

* Get final ev tallies

* Minor formatting

* Display all ev results
  • Loading branch information
nigelnindodev authored Oct 21, 2023
1 parent 0e6f69c commit 84b7e60
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 12 deletions.
7 changes: 6 additions & 1 deletion src/config/orbit.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"name": "Tennis",
"betType": "Two Way",
"url": "https://www.orbitxch.com/customer/sport/2"
},
{
"name": "Basketball",
"betType": "Two Way",
"url": "https://www.orbitxch.com/customer/sport/7522"
}
]
}
}
95 changes: 95 additions & 0 deletions src/core/analysis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { getConfig } from "../..";
import { PostgresDataSourceSingleton } from "../../datastores/postgres";
import { ThreeWayGameEventEntity, TwoWayGameEventEntity } from "../../datastores/postgres/entities";
import { getAnalyzableTwoWayGames, getMatchingTwoWayGameEventsTrigram } from "../../datastores/postgres/queries/two_way_game_event";
import { Result } from "../../utils/types/result_type";
import { getAnalyzableThreeWayGames, getMatchingThreeWayGameEventsTrigram } from "../../datastores/postgres/queries/three_way_game_event";

const {logger} = getConfig();

export class BaseAnalyser {

private getWinnings(stake: number, oddsForEvent: number): number {
const totalWithdrawableOnWin = stake * oddsForEvent;
return totalWithdrawableOnWin - stake;
}

/**
* Gets the expected value of staking on an event.
* Positive and higher results are better;
* @param probabilityOfEvent "True" Probability between 0 and 1.
* @param oddsForEvent Odds by the bet provider for the event.
*/
protected getEventEvPercent(probabilityOfEvent: number, oddsForEvent: number): number {
const theoreticalStake = 10;
const evAsNumber = (this.getWinnings(theoreticalStake, oddsForEvent) * probabilityOfEvent) - (theoreticalStake * (1-probabilityOfEvent));
return evAsNumber; // TODO: Return as a percentage
}
/**
* Get two way game events that can be analyzed.
* Analyzable data is the data where the start event is greater than the current time.
*
* @param betType
*/
protected async getTwoWayGameEventData(): Promise<Result<TwoWayGameEventEntity[], Error>> {
const getPostgresDataSourceResult = await PostgresDataSourceSingleton.getInstance(getConfig());
if (getPostgresDataSourceResult.result === "error") {
const message = "Failed to get postgres data source when fetching two way game events for analysis";
logger.error(message);
return getPostgresDataSourceResult;
} else {
return {
result: "success",
value: await getAnalyzableTwoWayGames(getPostgresDataSourceResult.value)
};
}
}

/**
* Get three way game events that can be analyzed.
* Analyzable data is the data where the start event is greater than the current time.
*
* @param betType
*/
protected async getThreeWayGameEventData(): Promise<Result<ThreeWayGameEventEntity[], Error>> {
const getPostgresDataSourceResult = await PostgresDataSourceSingleton.getInstance(getConfig());
if (getPostgresDataSourceResult.result === "error") {
const message = "Failed to get postgres data source when fetching three way game events for analysis";
logger.error(message);
return getPostgresDataSourceResult;
} else {
return {
result: "success",
value: await getAnalyzableThreeWayGames(getPostgresDataSourceResult.value)
}
}
}

protected async getMatchingTwoWayGameEvents(gameEvent: TwoWayGameEventEntity): Promise<Result<TwoWayGameEventEntity[] | null, Error>> {
const getPostgresDataSourceResult = await PostgresDataSourceSingleton.getInstance(getConfig());
if (getPostgresDataSourceResult.result === "error") {
const message = "Failed to get postgres data source when fetching matching game events for analysis";
logger.error(message);
return getPostgresDataSourceResult;
} else {
return {
result: "success",
value: await getMatchingTwoWayGameEventsTrigram(getPostgresDataSourceResult.value, gameEvent)
};
}
}

protected async getMatchingThreeWayGameEvents(gameEvent: ThreeWayGameEventEntity): Promise<Result<ThreeWayGameEventEntity[] | null, Error>> {
const getPostgresDataSourceResult = await PostgresDataSourceSingleton.getInstance(getConfig());
if (getPostgresDataSourceResult.result === "error") {
const message = "Failed to get postgres data source when fetching matching game events for analysis";
logger.error(message);
return getPostgresDataSourceResult;
} else {
return {
result: "success",
value: await getMatchingThreeWayGameEventsTrigram(getPostgresDataSourceResult.value, gameEvent)
};
}
}
}
48 changes: 48 additions & 0 deletions src/core/analysis/three_way/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { BaseAnalyser } from "..";
import { getConfig } from "../../..";
import { ThreeWayGameEventEntity } from "../../../datastores/postgres/entities";

const {logger} = getConfig();

export class ThreeWayAnalyzer extends BaseAnalyser {
public async getData() {
const gameEventsWithEv: {clubAWinEv: number, clubBWinEv: number, drawEv: number, event: ThreeWayGameEventEntity}[] = [];

const getEventDataResult = await this.getThreeWayGameEventData();

if (getEventDataResult.result === "error") {
logger.error("Error while fetching event data: ", getEventDataResult.value.message);
return;
}

const results = await getEventDataResult.value.map(async event => {
const getMatchingEventsResult = await this.getMatchingThreeWayGameEvents(event);
logger.info("Event: ", event);
logger.info("Matching events: ", getMatchingEventsResult);

if (getMatchingEventsResult.result === "success" && getMatchingEventsResult.value !== null) {
getMatchingEventsResult.value.forEach(gameEvent => {
const clubAWinTrueProbability = (1 / event.odds_a_win);
const clubBWinTrueProbability = (1 / event.odds_b_win);
const drawTrueProbability = (1 / event.odds_draw);

const clubAWinEv = this.getEventEvPercent(clubAWinTrueProbability, gameEvent.odds_a_win);
const clubBWinEv = this.getEventEvPercent(clubBWinTrueProbability, gameEvent.odds_b_win);
const drawEv = this.getEventEvPercent(drawTrueProbability, gameEvent.odds_draw);

gameEventsWithEv.push({
clubAWinEv,
clubBWinEv,
drawEv,
event: gameEvent
});
});
}
});

await Promise.all(results);
gameEventsWithEv.forEach(eventWithEv => {
logger.info("Game event with EV: ", eventWithEv);
});
}
}
45 changes: 45 additions & 0 deletions src/core/analysis/two_way/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { BaseAnalyser } from "..";
import { getConfig } from "../../..";
import { TwoWayGameEventEntity } from "../../../datastores/postgres/entities";

const {logger} = getConfig();

export class TwoWayAnalyser extends BaseAnalyser {
public async getData(): Promise<void> {
const gameEventsWithEv: {clubAWinEv: Number, clubBWinEv: number, event: TwoWayGameEventEntity}[] = [];

const getEventDataResult = await this.getTwoWayGameEventData();

if (getEventDataResult.result === "error") {
logger.error("Error while fetching event data: ", getEventDataResult.value.message);
return;
}

const results = await getEventDataResult.value.map(async event => {
const getMatchingEventsResult = await this.getMatchingTwoWayGameEvents(event);
logger.info("Event: ", event);
logger.info("Matching events: ", getMatchingEventsResult);

if (getMatchingEventsResult.result === "success" && getMatchingEventsResult.value !== null) {
getMatchingEventsResult.value.forEach(gameEvent => {
const clubAWinTrueProbability = (1 / event.odds_a_win);
const clubBWinTrueProbability = (1 / event.odds_b_win);

const clubAWinEv = this.getEventEvPercent(clubAWinTrueProbability, gameEvent.odds_a_win);
const clubBWinEv = this.getEventEvPercent(clubBWinTrueProbability, gameEvent.odds_b_win);

gameEventsWithEv.push({
clubAWinEv,
clubBWinEv,
event: gameEvent
});
});
}
});

await Promise.all(results);
gameEventsWithEv.forEach(eventWithEv => {
logger.info("Game event with EV: ", eventWithEv);
});
}
}
4 changes: 3 additions & 1 deletion src/datastores/postgres/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,6 @@ export class ThreeWayGameEventEntity {

@Column("timestamptz", {nullable: false, default: () => "CURRENT_TIMESTAMP", onUpdate: "CURRENT_TIMESTAMP"})
updated_at_utc: Date
}
}

export type GameEventEntityTypes = TwoWayGameEventEntity | ThreeWayGameEventEntity;
2 changes: 1 addition & 1 deletion src/datastores/postgres/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class PostgresDataSourceSingleton {
private constructor() {}

public static async getInstance(config: Config): Promise<Result<DataSource, Error>> {
logger.trace("Opening Postgres TypeORM data source");
if (!PostgresDataSourceSingleton.dataSource) {
logger.info("Creating postgres data source");
const candidateDataSource = new DataSource({
type: "postgres",
host: config.postgresHost,
Expand Down
55 changes: 55 additions & 0 deletions src/datastores/postgres/queries/three_way_game_event/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import moment from "moment";

import { DataSource, InsertResult, UpdateResult } from "typeorm";
import { BetProviders } from "../../../../utils/types/common";
import { DbThreeWayGameEvent } from "../../../../utils/types/db";
Expand Down Expand Up @@ -27,6 +29,59 @@ export const getThreeWayGame = async (
.getOne();
};

/**
* Fetches games where have true probabilities. Current fetched from Orbit bet provider.
* Plans to add Pinnacle sports later.
* @param dataSource
* @returns
*/
export const getAnalyzableThreeWayGames = async (
dataSource: DataSource
): Promise<ThreeWayGameEventEntity[]> => {
const currentDate = moment().format();
return await dataSource.createQueryBuilder()
.select("three_way_game_event")
.from(ThreeWayGameEventEntity, "three_way_game_event")
.where("estimated_start_time_utc > :currentDate", {currentDate: currentDate})
.where("bet_provider_name = :betProviderName", {betProviderName: BetProviders.ORBIT})
.getMany();
};

/**
* Fetch game events that are similar to selected game event from provider.
* Excludes the same event from provider being picked.
* Returns null if there are no other matching events found.
* Adding null here to make it explicit that th no values can be expected.
* @param dataSource
* @param event
* @returns
*/
export const getMatchingThreeWayGameEventsTrigram = async (
dataSource: DataSource,
event: ThreeWayGameEventEntity
): Promise<ThreeWayGameEventEntity[] | null> => {
//@ts-ignore
const currentDate = moment().format();

const results = await dataSource.createQueryBuilder()
.select("three_way_game_event")
.from(ThreeWayGameEventEntity, "three_way_game_event")
//.where("estimated_start_time_utc > :currentDate", {currentDate: currentDate}) TODO: Add before final commit
.where("bet_provider_name != :betProviderName", {betProviderName: event.bet_provider_name})
.andWhere("similarity(club_a, :clubAName) > 0.2", {clubAName: event.club_a})
.andWhere("similarity(club_b, :clubBName) > 0.2", {clubBName: event.club_b})
.getMany();

logger.trace("clubA: ", event.club_a);
logger.trace("clubB: ", event.club_b);

if (results.length === 0) {
return null;
} else {
return results;
}
};

export const insertThreeWayGameEvent = async (
dataSource: DataSource,
data: DbThreeWayGameEvent
Expand Down
Loading

0 comments on commit 84b7e60

Please sign in to comment.