Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add Games Analysis #19

Merged
merged 7 commits into from
Oct 21, 2023
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
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