This commit is contained in:
Harry Stuart 2022-12-07 12:48:00 +11:00
parent 85fac76d8a
commit 2f118c5bcb
30 changed files with 2185 additions and 0 deletions

440
.gitignore vendored Normal file
View File

@ -0,0 +1,440 @@
Skip to content
Search or jump to…
Pull requests
Issues
Codespaces
Marketplace
Explore
@harrystuart
github
/
gitignore
Public
Code
Pull requests
350
Actions
Security
Insights
gitignore/VisualStudio.gitignore
@n0099
n0099 [VisualStudio.gitignore] remove a trailing space
Latest commit 491040e on Jan 27
History
165 contributors
@shiftkey@arcresu@aroben@bbodenmiller@HassanHashemi@haacked@niik@AArnott@sayedihashimi@saschanaz@bdougie@OsirisTerje
398 lines (319 sloc) 6.7 KB
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
Footer
© 2022 GitHub, Inc.
Footer navigation
Terms
Privacy
Security
Status
Docs
Contact GitHub
Pricing
API
Training
Blog
About

25
Arbitrage.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32228.430
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arbitrage", "Arbitrage\Arbitrage.csproj", "{04687387-8899-4BF1-83A6-03870E6AE918}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{04687387-8899-4BF1-83A6-03870E6AE918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04687387-8899-4BF1-83A6-03870E6AE918}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04687387-8899-4BF1-83A6-03870E6AE918}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04687387-8899-4BF1-83A6-03870E6AE918}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A93770F5-598D-47F7-A933-CBCDF691B476}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.8.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<Compile Update="ConsoleNotificationChannel.cs">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="configuration.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,291 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class BaseDataService : IDataService
{
private LadbrokesService mLadBrokesService { get; set; }
private UnibetService mUnibetService { get; set; }
private BookerkeeperEntityComparer mBookerkeeperEntityComparer { get; set; }
public BaseDataService()
{
mLadBrokesService = new LadbrokesService();
mUnibetService = new UnibetService();
mBookerkeeperEntityComparer = new BookerkeeperEntityComparer();
}
public async Task InitialiseAsync()
{
await mLadBrokesService.InitialiseAsync();
await mUnibetService.InitialiseAsync();
}
public async Task<Dictionary<Event, IEnumerable<BookkeeperEvent>>> ReconcileSportEventsAsync(Sport sport)
{
Task<IEnumerable<BookkeeperEvent>> ladbrokesEventsTask = mLadBrokesService.GetSportEventsAsync(sport);
Task<IEnumerable<BookkeeperEvent>> unibetEventsTask = mUnibetService.GetSportEventsAsync(sport);
Task.WaitAll(ladbrokesEventsTask, unibetEventsTask);
List<BookkeeperEvent> ladbrokesEvents = ladbrokesEventsTask.Result.ToList();
List<BookkeeperEvent> unibetEvents = unibetEventsTask.Result.ToList();
List<List<BookkeeperEvent>> allBookkeeperEvents = new List<List<BookkeeperEvent>>();
allBookkeeperEvents.Add(ladbrokesEvents);
allBookkeeperEvents.Add(unibetEvents);
IEnumerable<IEnumerable<BookkeeperEvent>> bookkeeperEventGroups = FindEquiavelentBookkeeperEventsInLists(allBookkeeperEvents);
Dictionary<Event, IEnumerable<BookkeeperEvent>> events = new Dictionary<Event, IEnumerable<BookkeeperEvent>>();
foreach (List<BookkeeperEvent> group in bookkeeperEventGroups)
{
events.Add(new Event()
{
Competition = group[0].Competition,
Name = group[0].Name,
Sport = sport,
Commencement = group[0].Commencement,
TMPUnibetEventId = group[1].BookkeeperEventId
}, group);
}
return events;
}
public async Task<Dictionary<Market, IEnumerable<BookkeeperMarket>>> ReconcileEventMarketsAsync(Event @event, IEnumerable<BookkeeperEvent> bookkeeperEvents)
{
if (@event.Name == "Tepatitlan FC vs CD Tapatio")
{
var v = "";
}
Task<IEnumerable<BookkeeperMarket>> ladbrokesMarketsTask = mLadBrokesService
.GetEventMarketsAsync(bookkeeperEvents.First(x => x.Bookkeeper == Bookkeeper.Ladbrokes));
Task<IEnumerable<BookkeeperMarket>> unibetMarketsTask = mUnibetService
.GetEventMarketsAsync(bookkeeperEvents.First(x => x.Bookkeeper == Bookkeeper.Unibet));
Task.WaitAll(ladbrokesMarketsTask, unibetMarketsTask);
List<BookkeeperMarket> ladbrokesMarkets = ladbrokesMarketsTask.Result.ToList();
List<BookkeeperMarket> unibetMarkets = unibetMarketsTask.Result.ToList();
List<List<BookkeeperMarket>> allBookkeeperMarkets = new List<List<BookkeeperMarket>>();
allBookkeeperMarkets.Add(ladbrokesMarkets);
allBookkeeperMarkets.Add(unibetMarkets);
IEnumerable<IEnumerable<BookkeeperMarket>> bookkeeperMarketGroups = FindEquiavelentBookkeeperMarketsInLists(allBookkeeperMarkets, @event);
Dictionary<Market, IEnumerable<BookkeeperMarket>> markets = new Dictionary<Market, IEnumerable<BookkeeperMarket>>();
foreach (List<BookkeeperMarket> group in bookkeeperMarketGroups)
{
markets.Add(new Market() { Name = group[0].Name }, group);
}
return markets;
}
// A market is only valid if all odds for all bookkeepers operating in that market can be reconciled
public async Task<IEnumerable<Odds>> ReconcileMarketOddsAsync(Event @event, IEnumerable<BookkeeperMarket> bookkeeperMarkets)
{
Task<IEnumerable<BookkeeperOdds>> ladbrokesOddsTask = mLadBrokesService
.GetMarketOddsAsync(@event, bookkeeperMarkets.First(x => x.Bookkeeper == Bookkeeper.Ladbrokes));
Task<IEnumerable<BookkeeperOdds>> unibetOddsTask = mUnibetService
.GetMarketOddsAsync(@event, bookkeeperMarkets.First(x => x.Bookkeeper == Bookkeeper.Unibet));
Task.WaitAll(ladbrokesOddsTask, unibetOddsTask);
List<BookkeeperOdds> ladbrokesOdds = ladbrokesOddsTask.Result.ToList();
List<BookkeeperOdds> unibetOdds = unibetOddsTask.Result.ToList();
List<KeyValuePair<Bookkeeper, List<BookkeeperOdds>>> bookkeeperOdds = new List<KeyValuePair<Bookkeeper, List<BookkeeperOdds>>>();
bookkeeperOdds.Add(new KeyValuePair<Bookkeeper, List<BookkeeperOdds>>(Bookkeeper.Ladbrokes, ladbrokesOdds));
bookkeeperOdds.Add(new KeyValuePair<Bookkeeper, List<BookkeeperOdds>>(Bookkeeper.Unibet, unibetOdds));
int numOddsPerBookkeeper = bookkeeperOdds.First().Value.Count;
foreach (List<BookkeeperOdds> odds in bookkeeperOdds.Select(x => x.Value))
{
if (odds.Count != numOddsPerBookkeeper)
{
return new List<Odds>();
}
}
List<List<BookkeeperOdds>> bookkeeperOddsGroups = new List<List<BookkeeperOdds>>();
for (int i = 0; i < bookkeeperOdds.Count - 1; i++)
{
List<BookkeeperOdds> bookkeeperOdds1 = bookkeeperOdds[i].Value;
List<BookkeeperOdds> bookkeeperOdds2 = bookkeeperOdds[i + 1].Value;
for (int m = 0; m < numOddsPerBookkeeper; m++)
{
bookkeeperOddsGroups.Add(new List<BookkeeperOdds>());
bool foundMatch = false;
for (int n = 0; n < numOddsPerBookkeeper; n++)
{
if (mBookerkeeperEntityComparer.OddsEqual(bookkeeperOdds1[m], bookkeeperOdds2[n], @event))
{
foundMatch = true;
bookkeeperOddsGroups[m].Add(bookkeeperOdds1[m]);
if (i == bookkeeperOdds.Count - 2)
{
bookkeeperOddsGroups[m].Add(bookkeeperOdds2[n]);
}
break;
}
}
if (!foundMatch)
{
return new List<Odds>();
}
}
}
List<Odds> standardisedOdds = new List<Odds>();
foreach (List<BookkeeperOdds> bookkeeperOddsGroup in bookkeeperOddsGroups)
{
standardisedOdds.Add(new Odds()
{
Outcome = bookkeeperOddsGroup[0].Outcome,
BookkeeperOdds = bookkeeperOddsGroup
});
}
return standardisedOdds;
}
private IEnumerable<IEnumerable<BookkeeperEvent>> FindEquiavelentBookkeeperEventsInLists(List<List<BookkeeperEvent>> bookkeeperEvents)
{
List<List<BookkeeperEvent>> groups = new List<List<BookkeeperEvent>>();
for (int i = 0; i < bookkeeperEvents.Count; i++)
{
for (int j = i + 1; j < bookkeeperEvents.Count; j++)
{
for (int m = 0; m < bookkeeperEvents[i].Count; m++)
{
for (int n = 0; n < bookkeeperEvents[j].Count; n++)
{
if (mBookerkeeperEntityComparer.EventsEqual(bookkeeperEvents[i][m], bookkeeperEvents[j][n]))
{
bool foundMatchToExistingGroup = false;
for (int k = 0; k < groups.Count; k++)
{
if (groups[k][0] == bookkeeperEvents[i][m]) // Works if this is reference based
{
foundMatchToExistingGroup = true;
groups[k].Add(bookkeeperEvents[j][n]);
break;
}
}
if (!foundMatchToExistingGroup)
{
groups.Add(new List<BookkeeperEvent>() { bookkeeperEvents[i][m], bookkeeperEvents[j][n] });
}
bookkeeperEvents[j].RemoveAt(n);
break;
}
}
}
}
}
return groups;
}
private IEnumerable<IEnumerable<BookkeeperMarket>> FindEquiavelentBookkeeperMarketsInLists(List<List<BookkeeperMarket>> bookkeeperMarkets, Event @event)
{
List<List<BookkeeperMarket>> groups = new List<List<BookkeeperMarket>>();
for (int i = 0; i < bookkeeperMarkets.Count; i++)
{
for (int j = i + 1; j < bookkeeperMarkets.Count; j++)
{
// Get all pairings with scores
List<KeyValuePair<(BookkeeperMarket, BookkeeperMarket), double>> bookkeeperMarketPairings = new List<KeyValuePair<(BookkeeperMarket, BookkeeperMarket), double>>();
for (int m = 0; m < bookkeeperMarkets[i].Count; m++)
{
for (int n = 0; n < bookkeeperMarkets[j].Count; n++)
{
double similarity = mBookerkeeperEntityComparer.CalculateMarketSimilarity(
bookkeeperMarkets[i][m], bookkeeperMarkets[j][n], @event);
if (similarity > 0.8)
{
bookkeeperMarketPairings.Add(new KeyValuePair<(BookkeeperMarket, BookkeeperMarket), double>(
(bookkeeperMarkets[i][m], bookkeeperMarkets[j][n]), similarity));
}
}
}
// Order by score and then by string length similarity
bookkeeperMarketPairings = bookkeeperMarketPairings.OrderByDescending(x => x.Value)
.ThenBy(x => Math.Abs(x.Key.Item1.Name.Length - x.Key.Item2.Name.Length))
.ToList();
foreach (var pairing in bookkeeperMarketPairings)
{
Console.WriteLine($"{pairing.Value}:{pairing.Key.Item1.Name}:{pairing.Key.Item2.Name}");
}
Console.WriteLine();
while (bookkeeperMarketPairings.Any())
{
(BookkeeperMarket bookkeeperMarket1, BookkeeperMarket bookkeeperMarket2) = bookkeeperMarketPairings.First().Key;
bool foundMatchToExistingGroup = false;
for (int k = 0; k < groups.Count; k++)
{
if (groups[k].Contains(bookkeeperMarket1)) // Works if this is reference based
{
foundMatchToExistingGroup = true;
groups[k].Add(bookkeeperMarket2);
break;
}
}
if (!foundMatchToExistingGroup)
{
groups.Add(new List<BookkeeperMarket>() { bookkeeperMarket1, bookkeeperMarket2 });
}
bookkeeperMarketPairings.RemoveAll(x => {
if (x.Key.Item1 == bookkeeperMarket1 ||
x.Key.Item1 == bookkeeperMarket2 ||
x.Key.Item2 == bookkeeperMarket1 ||
x.Key.Item2 == bookkeeperMarket2)
{
return true;
}
return false;
});
}
}
}
return groups;
}
}
}

View File

@ -0,0 +1,167 @@
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class BookerkeeperEntityComparer
{
private BookerkeeperEntityComparerConfiguration mConfiguration;
public BookerkeeperEntityComparer()
{
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("configuration.json")
.Build();
mConfiguration = config.GetRequiredSection("bookerkeeperEntityComparer").Get<BookerkeeperEntityComparerConfiguration>();
}
public bool EventsEqual(BookkeeperEvent a, BookkeeperEvent b)
{
if (a == null || b == null)
{
return false;
}
if (a.Competition != null && b.Competition != null)
{
if (StringDistance.NumberOfSubstringMovementsWithLengthPenalty(a.Competition, b.Competition) > 0.3)
{
return false;
}
}
if (StringDistance.NumberOfSubstringMovementsWithLengthPenalty(a.Name, b.Name) > 0.3)
{
return false;
}
return true;
}
public double CalculateMarketSimilarity(BookkeeperMarket a, BookkeeperMarket b, Event @event)
{
if (a == null || b == null)
{
return 0;
}
if (mConfiguration.KnownCorrespondingMarketNames.ContainsKey(a.Bookkeeper) &&
mConfiguration.KnownCorrespondingMarketNames[a.Bookkeeper].ContainsKey(@event.Sport) &&
mConfiguration.KnownCorrespondingMarketNames[a.Bookkeeper][@event.Sport].ContainsKey(a.Name))
{
if (mConfiguration.KnownCorrespondingMarketNames[a.Bookkeeper][@event.Sport][a.Name].Contains(b.Name))
{
return 1;
}
return 1;
}
if (mConfiguration.KnownCorrespondingMarketNames.ContainsKey(b.Bookkeeper) &&
mConfiguration.KnownCorrespondingMarketNames[b.Bookkeeper].ContainsKey(@event.Sport) &&
mConfiguration.KnownCorrespondingMarketNames[b.Bookkeeper][@event.Sport].ContainsKey(b.Name))
{
if (mConfiguration.KnownCorrespondingMarketNames[b.Bookkeeper][@event.Sport][b.Name].Contains(a.Name))
{
return 1;
}
return 1;
}
double score = StringDistance.NumberOfSubstringMovementsWithLengthPenalty(a.Name, b.Name);
return 1 - score;
}
public bool MarketsEqual(BookkeeperMarket a, BookkeeperMarket b, Event @event)
{
if (a == null || b == null)
{
return false;
}
if (mConfiguration.KnownCorrespondingMarketNames.ContainsKey(a.Bookkeeper) &&
mConfiguration.KnownCorrespondingMarketNames[a.Bookkeeper].ContainsKey(@event.Sport) &&
mConfiguration.KnownCorrespondingMarketNames[a.Bookkeeper][@event.Sport].ContainsKey(a.Name))
{
if (mConfiguration.KnownCorrespondingMarketNames[a.Bookkeeper][@event.Sport][a.Name].Contains(b.Name)) {
return true;
}
return false;
}
if (mConfiguration.KnownCorrespondingMarketNames.ContainsKey(b.Bookkeeper) &&
mConfiguration.KnownCorrespondingMarketNames[b.Bookkeeper].ContainsKey(@event.Sport) &&
mConfiguration.KnownCorrespondingMarketNames[b.Bookkeeper][@event.Sport].ContainsKey(b.Name))
{
if (mConfiguration.KnownCorrespondingMarketNames[b.Bookkeeper][@event.Sport][b.Name].Contains(a.Name))
{
return true;
}
return false;
}
if (StringDistance.NumberOfSubstringMovementsWithLengthPenalty(a.Name, b.Name) > 0.3)
{
return false;
}
return true;
}
public bool OddsEqual(BookkeeperOdds a, BookkeeperOdds b, Event @event)
{
if (a == null || b == null)
{
return false;
}
if (mConfiguration.KnownCorrespondingOddsNames.ContainsKey(a.Bookkeeper) &&
mConfiguration.KnownCorrespondingOddsNames[a.Bookkeeper].ContainsKey(@event.Sport) &&
mConfiguration.KnownCorrespondingOddsNames[a.Bookkeeper][@event.Sport].ContainsKey(a.Outcome))
{
if (mConfiguration.KnownCorrespondingOddsNames[a.Bookkeeper][@event.Sport][a.Outcome].Contains(b.Outcome))
{
return true;
}
return false;
}
if (mConfiguration.KnownCorrespondingOddsNames.ContainsKey(b.Bookkeeper) &&
mConfiguration.KnownCorrespondingOddsNames[b.Bookkeeper].ContainsKey(@event.Sport) &&
mConfiguration.KnownCorrespondingOddsNames[b.Bookkeeper][@event.Sport].ContainsKey(b.Outcome))
{
if (mConfiguration.KnownCorrespondingOddsNames[b.Bookkeeper][@event.Sport][b.Outcome].Contains(a.Outcome))
{
return true;
}
return false;
}
if (a.Description != null && b.Description != null)
{
if (StringDistance.NumberOfSubstringMovementsWithLengthPenalty(a.Description, b.Description) > 0.3)
{
return false;
}
}
if (StringDistance.NumberOfSubstringMovementsWithLengthPenalty(a.Outcome, b.Outcome) > 0.3)
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class BookerkeeperEntityComparerConfiguration
{
public Dictionary<Bookkeeper, Dictionary<Sport, Dictionary<string, List<string>>>> KnownCorrespondingMarketNames { get; set; } = null!;
public Dictionary<Bookkeeper, Dictionary<Sport, Dictionary<string, List<string>>>> KnownCorrespondingOddsNames { get; set; } = null!;
}
}

14
Arbitrage/Bookkeeper.cs Normal file
View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public enum Bookkeeper
{
Ladbrokes,
Unibet
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class BookkeeperEvent
{
public Bookkeeper Bookkeeper { get; set; }
public string BookkeeperEventId { get; set; } = null!;
public string Name { get; set; } = null!;
public string? Competition { get; set; }
public DateTimeOffset? Commencement { get; set; }
public string? Url { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class BookkeeperMarket
{
public Bookkeeper Bookkeeper { get; set; }
public string BookkeeperMarketId { get; set; } = null!;
public string Name { get; set; } = null!;
public string? Url { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class BookkeeperOdds
{
public Bookkeeper Bookkeeper { get; set; }
public string BookkeeperOddsId { get; set; } = null!;
public string Outcome { get; set; } = null!;
public double Value { get; set; }
public string? Description { get; set; }
public string? Url { get; set; }
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public abstract class BookkeeperService
{
protected HttpClient mHttpClient;
protected readonly ILogger<BookkeeperService> mLogger;
public BookkeeperService()
{
var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter("System", LogLevel.Warning)
.AddConsole()
.AddEventLog();
});
mLogger = loggerFactory.CreateLogger<BookkeeperService>();
mHttpClient = new HttpClient();
}
public abstract Task<IEnumerable<BookkeeperEvent>> GetSportEventsAsync(Sport sport);
public abstract Task<IEnumerable<BookkeeperMarket>> GetEventMarketsAsync(BookkeeperEvent bookkeeperEvent);
public abstract Task<IEnumerable<BookkeeperOdds>> GetMarketOddsAsync(Event @event, BookkeeperMarket market);
}
}

38
Arbitrage/Calculator.cs Normal file
View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public static class Calculator
{
public static (double, IEnumerable<BookkeeperOdds>) FindBestArbitrage(Market market)
{
List<BookkeeperOdds> optimalBookkeeperOdds = FindBestOdds(market);
double arbitrage = optimalBookkeeperOdds.Select(x => x.Value).Sum(x => 1.0 / x);
return (arbitrage, optimalBookkeeperOdds);
}
private static List<BookkeeperOdds> FindBestOdds(Market market)
{
if (market.Odds == null)
{
throw new Exception($"Odds are null for market {market}.");
}
List<BookkeeperOdds> optimalBookkeeperOdds = new List<BookkeeperOdds>();
foreach (Odds odds in market.Odds)
{
BookkeeperOdds maxBookkeeperOdds = odds.BookkeeperOdds.MaxBy(x => x.Value);
optimalBookkeeperOdds.Add(maxBookkeeperOdds);
}
return optimalBookkeeperOdds;
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class ConsoleNotificationChannel : INotificationChannel
{
public bool Notify(Sport sport, Event @event, Market market, IEnumerable<BookkeeperMarket> bookkeeperMarkets, double arbitrage, IEnumerable<BookkeeperOdds> bestBookkeeperOdds)
{
Console.WriteLine(Enum.GetName(typeof(Sport), sport));
Console.WriteLine(@event.Name);
Console.WriteLine(market.Name);
Console.WriteLine(arbitrage);
Console.WriteLine(@event.Commencement.ToString());
foreach (BookkeeperMarket bookkeeperMarket in bookkeeperMarkets)
{
Console.WriteLine(bookkeeperMarket.Name);
}
foreach (BookkeeperOdds bookkeeperOdds in bestBookkeeperOdds)
{
Console.WriteLine(bookkeeperOdds.Url);
Console.WriteLine($"{Enum.GetName(typeof(Bookkeeper), bookkeeperOdds.Bookkeeper)}\t{bookkeeperOdds.Outcome}\t{bookkeeperOdds.Value}");
}
Console.WriteLine();
return true;
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.Extensions.Configuration;
namespace Arbitrage
{
public class DiscordNotificationChannel : INotificationChannel
{
private readonly DiscordSocketClient mClient;
private SocketTextChannel? mChannel;
public DiscordNotificationChannel()
{
mClient = new DiscordSocketClient();
}
public async Task InitialiseAsync()
{
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("configuration.json")
.Build();
string token = config.GetRequiredSection("discord").GetValue<string>("token");
await mClient.LoginAsync(TokenType.Bot, token);
await mClient.StartAsync();
ulong serverId = config.GetRequiredSection("discord").GetValue<ulong>("serverId");
ulong channelId = config.GetRequiredSection("discord").GetValue<ulong>("channelId");
SocketGuild? socketGuild = null;
int maxNumRetries = 100;
int numRetries = 0;
while (socketGuild == null)
{
if (numRetries >= maxNumRetries)
{
throw new Exception("Could not connect Discord bot.");
}
socketGuild = mClient.GetGuild(serverId);
await Task.Delay(50);
numRetries++;
}
mChannel = socketGuild.GetTextChannel(channelId);
}
public bool Notify(Sport sport, Event @event, Market market, IEnumerable<BookkeeperMarket> bookkeeperMarkets, double arbitrage, IEnumerable<BookkeeperOdds> bestBookkeeperOdds)
{
Thread.Sleep(2000);
if (mChannel == null)
{
throw new Exception("Discord channel not connected");
}
string longFormat = "0.00";
string header = "```yaml\nNEW ARBITRAGE OPPORTUNITY FOUND\n```";
string sportSection = $"**Sport** {Enum.GetName(typeof(Sport), sport)}";
string eventSection = $"**Event** {@event.Name}";
string startSection = $"**Start Time** {@event.Commencement.ToString()}";
string marketSection = $"**Market** {market.Name}";
string arbitrageSection = $"**Arbitrage** {arbitrage.ToString(longFormat)}";
string oddsSection = "__**Optimal Odds**__";
List<string> bookkeeperSections = new List<string>();
foreach (BookkeeperOdds bookkeeperOdds in bestBookkeeperOdds)
{
BookkeeperMarket bookkeeperMarket = bookkeeperMarkets.First(x => x.Bookkeeper == bookkeeperOdds.Bookkeeper);
string bookkeeperSection = $"**Bookie** {Enum.GetName(typeof(Bookkeeper), bookkeeperOdds.Bookkeeper)} **Market** {bookkeeperMarket.Name} **Outcome** {bookkeeperOdds.Outcome} **Odds** {bookkeeperOdds.Value.ToString(longFormat)} **Url** {bookkeeperOdds.Url}";
bookkeeperSections.Add(bookkeeperSection);
}
string content = header + "\n" + sportSection + "\n" + eventSection + "\n" + startSection + "\n" + marketSection + "\n" + arbitrageSection + "\n\n" + oddsSection + "\n\n";
foreach (string bookkeeperSection in bookkeeperSections)
{
content += bookkeeperSection + "\n";
}
mChannel.SendMessageAsync(content);
return true;
}
}
}

56
Arbitrage/Engine.cs Normal file
View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class Engine
{
private IDataService mDataService { get; set; }
private IEnumerable<INotificationChannel> mNotificationChannels { get; set; }
public Engine(IDataService dataService,
IEnumerable<INotificationChannel> notificationChannels)
{
mDataService = dataService;
mNotificationChannels = notificationChannels;
}
public async Task RunAsync(IEnumerable<Sport> sports)
{
await mDataService.InitialiseAsync();
foreach (Sport sport in sports)
{
Dictionary<Event, IEnumerable<BookkeeperEvent>> events = await mDataService.ReconcileSportEventsAsync(sport);
foreach ((Event @event, IEnumerable<BookkeeperEvent> bookkeeperEvents) in events)
{
Dictionary<Market, IEnumerable<BookkeeperMarket>> markets = await mDataService.ReconcileEventMarketsAsync(@event, bookkeeperEvents);
@event.Markets = markets.Keys;
foreach ((Market market, IEnumerable<BookkeeperMarket> bookkeeperMarkets) in markets)
{
IEnumerable<Odds> odds = await mDataService.ReconcileMarketOddsAsync(@event, bookkeeperMarkets);
market.Odds = odds;
if (odds.Any())
{
(double arbitrage, IEnumerable<BookkeeperOdds> bookkeeperOdds) = Calculator.FindBestArbitrage(market);
if (arbitrage < 1)
{
foreach (INotificationChannel notificationChannel in mNotificationChannels)
{
notificationChannel.Notify(sport, @event, market, bookkeeperMarkets, arbitrage, bookkeeperOdds);
}
}
}
}
}
}
}
}
}

18
Arbitrage/Event.cs Normal file
View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class Event
{
public string Name { get; set; } = null!;
public string? Competition { get; set; }
public Sport Sport { get; set; }
public IEnumerable<Market>? Markets { get; set; }
public DateTimeOffset? Commencement { get; set; }
public string TMPUnibetEventId { get; set; }
}
}

16
Arbitrage/IDataService.cs Normal file
View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public interface IDataService
{
public Task InitialiseAsync();
public Task<Dictionary<Event, IEnumerable<BookkeeperEvent>>> ReconcileSportEventsAsync(Sport sport);
public Task<Dictionary<Market, IEnumerable<BookkeeperMarket>>> ReconcileEventMarketsAsync(Event @event, IEnumerable<BookkeeperEvent> bookkeeperEvents);
public Task<IEnumerable<Odds>> ReconcileMarketOddsAsync(Event @event, IEnumerable<BookkeeperMarket> bookkeeperMarkets);
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public interface INotificationChannel
{
public bool Notify(Sport sport, Event @event, Market market, IEnumerable<BookkeeperMarket> bookkeeperMarkets, double arbitrage, IEnumerable<BookkeeperOdds> bestBookkeeperOdds);
}
}

View File

@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Extensions.Configuration;
using System.Globalization;
namespace Arbitrage
{
public class LadbrokesService : BookkeeperService
{
private LadbrokesServiceConfiguration mConfiguration { get; set; }
private dynamic? EventsListData { get; set; }
private List<dynamic> EventsData { get; set; } = new List<dynamic>();
public LadbrokesService()
{
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("configuration.json")
.Build();
mConfiguration = config.GetRequiredSection("ladbrokesService").Get<LadbrokesServiceConfiguration>();
}
public async Task InitialiseAsync()
{
UriBuilder uriBuilder = new UriBuilder(mConfiguration.BaseSiteUrl);
var httpResult = await mHttpClient.GetAsync(uriBuilder.ToString());
if (!httpResult.IsSuccessStatusCode)
{
mLogger.LogWarning($"Could not get visit Ladbrokes");
}
}
public override async Task<IEnumerable<BookkeeperEvent>> GetSportEventsAsync(Sport sport)
{
if (!mConfiguration.Categories.Select(x => x.Sport).Contains(sport))
{
mLogger.LogWarning($"Ladbroke servive cannot get {sport}.");
return new List<BookkeeperEvent>();
}
Dictionary<string, string> queryDictionary = new Dictionary<string, string>()
{
{"category_ids", $"[\"{mConfiguration.Categories.First(x => x.Sport == sport).Id}\"]"}
};
var queryString = new FormUrlEncodedContent(queryDictionary)
.ReadAsStringAsync().Result;
UriBuilder uriBuilder = new UriBuilder(mConfiguration.GetEventsUrl);
uriBuilder.Query = queryString;
var httpResult = await mHttpClient.GetAsync(uriBuilder.ToString());
if (!httpResult.IsSuccessStatusCode)
{
mLogger.LogWarning($"Could not get events from Ladbroke.");
return new List<BookkeeperEvent>();
}
dynamic? data = JsonConvert.DeserializeObject<dynamic>(await httpResult.Content.ReadAsStringAsync());
if (data == null)
{
mLogger.LogWarning("Could not deserialise Ladbrokes get events response content.");
return new List<BookkeeperEvent>();
}
EventsListData = data;
List<BookkeeperEvent> events = new List<BookkeeperEvent>();
foreach (var x in EventsListData["events"])
{
var obj = ((IEnumerable<dynamic>)x).First();
DateTimeOffset commencementTime = DateTimeOffset.Parse((string)obj["actual_start"].ToString()).ToUniversalTime();
string eventId = obj["id"].ToString();
string url = new Uri(new Uri(mConfiguration.BaseEventUrl), $"{sport}/nice/website/{eventId}").ToString(); //TODO: add proper URL
events.Add(new BookkeeperEvent()
{
Bookkeeper = Bookkeeper.Ladbrokes,
BookkeeperEventId = eventId,
Name = obj["name"].ToString(),
Competition = obj["competition"]?["name"]?.ToString(),
Commencement = commencementTime,
Url = url
});
}
return events;
}
public override async Task<IEnumerable<BookkeeperMarket>> GetEventMarketsAsync(BookkeeperEvent bookkeeperEvent)
{
Dictionary<string, string> queryDictionary = new Dictionary<string, string>()
{
{"id", bookkeeperEvent.BookkeeperEventId}
};
var queryString = new FormUrlEncodedContent(queryDictionary)
.ReadAsStringAsync().Result;
UriBuilder uriBuilder = new UriBuilder(mConfiguration.GetOddsUrl);
uriBuilder.Query = queryString;
var httpResult = await mHttpClient.GetAsync(uriBuilder.ToString());
if (!httpResult.IsSuccessStatusCode)
{
mLogger.LogWarning($"Could not get event from Ladbrokes for event {bookkeeperEvent.BookkeeperEventId}.");
return new List<BookkeeperMarket>();
}
dynamic? data = JsonConvert.DeserializeObject<dynamic>(await httpResult.Content.ReadAsStringAsync());
if (data == null)
{
mLogger.LogWarning($"Could not deserialise Ladbrokes get event for {bookkeeperEvent.BookkeeperEventId}.");
return new List<BookkeeperMarket>();
}
EventsData.Add(data);
List<BookkeeperMarket> markets = new List<BookkeeperMarket>();
foreach (var market in data["markets"])
{
markets.Add(new BookkeeperMarket()
{
Bookkeeper = Bookkeeper.Ladbrokes,
BookkeeperMarketId = market.Value.id,
Name = market.Value.name,
Url = bookkeeperEvent.Url
});
}
return markets;
}
public async override Task<IEnumerable<BookkeeperOdds>> GetMarketOddsAsync(Event @event, BookkeeperMarket bookkeeperMarket)
{
dynamic? eventDynamic = null;
List<string> entrantIds = new List<string>();
foreach (dynamic eventData in EventsData)
{
foreach (dynamic eventMarket in eventData["markets"])
{
if (eventMarket.Value.id == bookkeeperMarket.BookkeeperMarketId)
{
eventDynamic = eventData;
foreach (string entrantId in eventMarket.Value["entrant_ids"])
{
entrantIds.Add(entrantId);
}
}
}
}
if (eventDynamic == null)
{
throw new Exception($"EventsData does not contain data for market {bookkeeperMarket.BookkeeperMarketId}");
}
List<BookkeeperOdds> bookkeeperOdds = new List<BookkeeperOdds>();
foreach (string entrantId in entrantIds)
{
string name = eventDynamic["entrants"][entrantId]["name"].ToString();
dynamic price = ((IEnumerable<dynamic>)eventDynamic["prices"]).First(x => x.Name.Split(":")[0] == entrantId).Value["odds"];
double value = double.Parse(price["numerator"].ToString()) / double.Parse(price["denominator"].ToString());
bookkeeperOdds.Add(new BookkeeperOdds()
{
Bookkeeper = Bookkeeper.Ladbrokes,
BookkeeperOddsId = entrantId,
Outcome = name,
Value = value + 1,
Url = bookkeeperMarket.Url
});
}
return bookkeeperOdds;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class LadbrokesServiceConfiguration
{
public string BaseEventUrl { get; set; } = null!;
public string BaseSiteUrl { get; set; } = null!;
public string GetEventsUrl { get; set; } = null!;
public string GetOddsUrl { get; set; } = null!;
public ICollection<Category> Categories { get; set; } = null!;
public class Category
{
public string Id { get; set; } = null!;
public Sport Sport { get; set; }
}
}
}

14
Arbitrage/Market.cs Normal file
View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class Market
{
public string Name { get; set; } = null!;
public IEnumerable<Odds>? Odds { get; set; }
}
}

14
Arbitrage/Odds.cs Normal file
View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class Odds
{
public string Outcome { get; set; } = null!;
public IEnumerable<BookkeeperOdds> BookkeeperOdds { get; set; } = null!;
}
}

16
Arbitrage/Program.cs Normal file
View File

@ -0,0 +1,16 @@
using Arbitrage;
using Newtonsoft.Json;
using System.Reflection;
BaseDataService dataService = new BaseDataService();
ConsoleNotificationChannel consoleNotificationChannel = new ConsoleNotificationChannel();
DiscordNotificationChannel discordNotificationChannel = new DiscordNotificationChannel();
await discordNotificationChannel.InitialiseAsync();
Engine engine = new Engine(dataService, new List<INotificationChannel>() { consoleNotificationChannel, discordNotificationChannel });
while (true)
{
await engine.RunAsync(new List<Sport>() { Sport.AFL, Sport.Baseball, Sport.Soccer, Sport.Boxing, Sport.TableTennis, Sport.Cricket });
Thread.Sleep(900000);
}

18
Arbitrage/Sport.cs Normal file
View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public enum Sport
{
AFL,
TableTennis,
Boxing,
Baseball,
Soccer,
Cricket
}
}

174
Arbitrage/StringDistance.cs Normal file
View File

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public static class StringDistance
{
/// <summary>
/// Compute the distance between two strings.
/// </summary>
public static int LevenshteinDistance(string s, string t)
{
int n = s.Length;
int m = t.Length;
int[,] d = new int[n + 1, m + 1];
// Step 1
if (n == 0)
{
return m;
}
if (m == 0)
{
return n;
}
// Step 2
for (int i = 0; i <= n; d[i, 0] = i++)
{
}
for (int j = 0; j <= m; d[0, j] = j++)
{
}
// Step 3
for (int i = 1; i <= n; i++)
{
//Step 4
for (int j = 1; j <= m; j++)
{
// Step 5
int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
// Step 6
d[i, j] = Math.Min(
Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
d[i - 1, j - 1] + cost);
}
}
// Step 7
return d[n, m];
}
public static int LengthUnbiasedLevenshteinDistance(string s, string t)
{
int levenshteinDist = LevenshteinDistance(s, t);
return levenshteinDist - Math.Abs(s.Length - t.Length);
}
public static bool TryFindLongestCommonSubstringSlice(string str1, string str2, out int start, out int end)
{
int[,] num = new int[str1.Length, str2.Length];
int maxlen = 0;
int lastSubsBegin = 0;
StringBuilder subStrBuilder = new StringBuilder();
for (int i = 0; i < str1.Length; i++)
{
for (int j = 0; j < str2.Length; j++)
{
if (str1[i] != str2[j])
{
num[i, j] = 0;
}
else
{
if ((i == 0) || (j == 0))
num[i, j] = 1;
else
num[i, j] = 1 + num[i - 1, j - 1];
if (num[i, j] > maxlen)
{
maxlen = num[i, j];
int thisSubsBegin = i - num[i, j] + 1;
if (lastSubsBegin == thisSubsBegin)
{
subStrBuilder.Append(str1[i]);
}
else
{
lastSubsBegin = thisSubsBegin;
subStrBuilder.Length = 0;
subStrBuilder.Append(str1.Substring(lastSubsBegin, (i + 1) - lastSubsBegin));
}
}
}
}
}
if (maxlen > 0)
{
// Inclusive
start = lastSubsBegin;
end = lastSubsBegin + maxlen - 1;
return true;
}
else
{
start = 0;
end = 0;
return false;
}
}
public static double NumberOfSubstringMovementsWithLengthPenalty(string s, string t)
{
s = s.Trim().ToLower();
t = t.Trim().ToLower();
int sLen = s.Length;
int tLen = t.Length;
string shortestString;
string longestString;
if (sLen > tLen)
{
shortestString = t;
longestString = s;
}
else
{
shortestString = s;
longestString = t;
}
int shortestStringLen = shortestString.Length;
List<string> substrings = new List<string>();
while (shortestString.Length > 0)
{
int start;
int end;
if (TryFindLongestCommonSubstringSlice(shortestString, longestString, out start, out end))
{
substrings.Add(shortestString.Substring(start, end - start + 1));
shortestString = shortestString.Remove(start, end - start + 1).Trim();
}
else
{
substrings.Add(shortestString);
break;
}
}
if (substrings.Count == 1)
{
return 0;
}
return substrings.Count / (float)shortestStringLen;
}
}
}

263
Arbitrage/UnibetService.cs Normal file
View File

@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Arbitrage
{
public class UnibetService : BookkeeperService
{
private UnibetServiceConfiguration mConfiguration { get; set; }
private dynamic? OddsData { get; set; }
private Dictionary<string, List<string>> mEventParticipants { get; set; }
public UnibetService()
{
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("configuration.json")
.Build();
mConfiguration = config.GetRequiredSection("unibetService").Get<UnibetServiceConfiguration>();
mEventParticipants = new Dictionary<string, List<string>>();
}
public async Task InitialiseAsync()
{
UriBuilder uriBuilder = new UriBuilder(mConfiguration.BaseSiteUrl);
var httpResult = await mHttpClient.GetAsync(uriBuilder.ToString());
if (!httpResult.IsSuccessStatusCode)
{
mLogger.LogWarning($"Could not get visit Ladbrokes");
}
}
public override async Task<IEnumerable<BookkeeperEvent>> GetSportEventsAsync(Sport sport)
{
if (!mConfiguration.GetSportEventsUrls.Select(x => x.Key).Contains(sport))
{
mLogger.LogWarning($"Unibet not configured to retrieve {sport} events.");
return new List<BookkeeperEvent>();
}
UriBuilder uriBuilder = new UriBuilder(mConfiguration.GetSportEventsUrls[sport]);
var httpResult = await mHttpClient.GetAsync(uriBuilder.ToString());
if (!httpResult.IsSuccessStatusCode)
{
mLogger.LogWarning($"Could not get {sport} events from Unibet");
return new List<BookkeeperEvent>();
}
dynamic? data = JsonConvert.DeserializeObject<dynamic>(await httpResult.Content.ReadAsStringAsync());
if (data == null)
{
mLogger.LogWarning($"Could not deserialise Unibet events for {sport}");
return new List<BookkeeperEvent>();
}
List<BookkeeperEvent> events = new List<BookkeeperEvent>();
foreach (dynamic section in data["layout"]["sections"])
{
foreach (dynamic widget in section["widgets"])
{
foreach (dynamic group in widget["matches"]["groups"])
{
List<dynamic> eventDatas = GetEventsFromGroup(group);
foreach (dynamic eventData in eventDatas)
{
string eventId = eventData.id.ToString();
List<string> participantIds = new List<string>();
if (eventData.ContainsKey("participants"))
{
foreach (dynamic participant in eventData["participants"])
{
participantIds.Add(participant["name"].ToString());
}
}
DateTimeOffset commencementTime = DateTimeOffset.Parse((string)eventData["start"].ToString()).ToUniversalTime();
string url = new Uri(new Uri(mConfiguration.BaseEventUrl), eventId).ToString();
events.Add(new BookkeeperEvent()
{
Bookkeeper = Bookkeeper.Unibet,
BookkeeperEventId = eventId,
Name = eventData.name.ToString(),
Competition = string.Join(" ", ((IEnumerable<dynamic>)eventData.path).Select(x => x.name.ToString())),
Commencement = commencementTime,
Url = url
});
mEventParticipants.Add(eventId, participantIds);
}
}
}
}
return events;
}
private static List<dynamic> GetEventsFromGroup(dynamic group)
{
List<dynamic> events = new List<dynamic>();
dynamic subGroups = group["subGroups"];
if (subGroups == null)
{
foreach (dynamic @event in group["events"])
{
events.Add(@event["event"]);
}
return events;
}
foreach (dynamic subgroup in subGroups)
{
events.AddRange(GetEventsFromGroup(subgroup));
}
return events;
}
public override async Task<IEnumerable<BookkeeperMarket>> GetEventMarketsAsync(BookkeeperEvent bookkeeperEvent)
{
UriBuilder uriBuilder = new UriBuilder(new Uri(new Uri(mConfiguration.GetOddsUrl), bookkeeperEvent.BookkeeperEventId));
var httpResult = await mHttpClient.GetAsync(uriBuilder.ToString());
if (!httpResult.IsSuccessStatusCode)
{
mLogger.LogWarning($"Could not get Unibet markets for event {bookkeeperEvent.BookkeeperEventId}");
return new List<BookkeeperMarket>();
}
dynamic? data = JsonConvert.DeserializeObject<dynamic>(await httpResult.Content.ReadAsStringAsync());
if (data == null)
{
mLogger.LogWarning($"Could not deserialise Unibet markets for event {bookkeeperEvent.BookkeeperEventId}");
return new List<BookkeeperMarket>();
}
OddsData = data;
List<BookkeeperMarket> markets = new List<BookkeeperMarket>();
foreach (dynamic market in OddsData["betOffers"])
{
string marketName = market["betOfferType"]["name"];
if (market["criterion"] != null)
{
marketName += " " + market["criterion"]["label"];
}
dynamic firstOutcome = market["outcomes"][0];
if (firstOutcome.ContainsKey("line"))
{
float line;
bool didParse = float.TryParse(firstOutcome["line"].ToString(), out line);
if (didParse)
{
marketName += " " + (line / 1000.0).ToString("0.0");
}
}
markets.Add(new BookkeeperMarket()
{
Bookkeeper = Bookkeeper.Unibet,
BookkeeperMarketId = market.id,
Name = marketName,
Url = bookkeeperEvent.Url
});
}
return markets;
}
public async override Task<IEnumerable<BookkeeperOdds>> GetMarketOddsAsync(Event @event, BookkeeperMarket bookkeeperMarket)
{
if (OddsData == null)
{
throw new Exception("OddsData property is not populated.");
}
dynamic betOffer = ((IEnumerable<dynamic>)OddsData["betOffers"]).First(x => x.id == bookkeeperMarket.BookkeeperMarketId);
List<BookkeeperOdds> bookkeeperOdds = new List<BookkeeperOdds>();
foreach (dynamic outcome in betOffer["outcomes"])
{
if (!outcome.ContainsKey("odds"))
{
continue;
}
double value = double.Parse(outcome["odds"].ToString()) / 1000;
string outcomeString = outcome["label"].ToString();
List<int> validParticipantIndices = new List<int>();
if (mEventParticipants.Keys.Contains(@event.TMPUnibetEventId))
{
for (int i = 0; i < mEventParticipants[@event.TMPUnibetEventId].Count; i++)
{
validParticipantIndices.Add(i);
}
}
string newOutcomeString = outcomeString;
foreach (string c in outcomeString.ToList().Select(x => x.ToString()))
{
int representation = -1;
if (int.TryParse(c, out representation))
{
if (validParticipantIndices.Contains(representation - 1))
{
newOutcomeString = newOutcomeString.Replace(c, mEventParticipants[@event.TMPUnibetEventId][representation - 1]);
}
}
else if (c == "X")
{
newOutcomeString = newOutcomeString.Replace(c, "Draw");
}
}
bookkeeperOdds.Add(new BookkeeperOdds()
{
Bookkeeper = Bookkeeper.Unibet,
BookkeeperOddsId = outcome["id"],
Outcome = newOutcomeString,
Value = value,
Url = bookkeeperMarket.Url
});
}
return bookkeeperOdds;
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arbitrage
{
public class UnibetServiceConfiguration
{
public string BaseEventUrl { get; set; } = null!;
public string BaseSiteUrl { get; set; } = null!;
public string GetOddsUrl { get; set; } = null!;
public Dictionary<Sport, string> GetSportEventsUrls { get; set; } = null!;
}
}

View File

@ -0,0 +1,87 @@
{
"ladbrokesService": {
"baseSiteUrl": "https://www.ladbrokes.com.au",
"categories": [
{
"sport": "AFL",
"id": "23d497e6-8aab-4309-905b-9421f42c9bc5"
},
{
"sport": "TableTennis",
"id": "b92b2d14-10f7-46c7-8655-16eeed36ec4b"
},
{
"sport": "Boxing",
"id": "a8217d48-3257-402b-b3b5-9db706fdc1e0"
},
{
"sport": "Baseball",
"id": "02721435-4671-4cd0-98f7-15d41ee4103e"
},
{
"sport": "Soccer",
"id": "71955b54-62f6-4ac5-abaa-df88cad0aeef"
},
{
"sport": "Cricket",
"id": "94984918-dbac-432b-b420-c219ec9203f4"
}
],
"getEventsUrl": "https://api.ladbrokes.com.au/v2/sport/event-request",
"getOddsUrl": "https://api.ladbrokes.com.au/v2/sport/event-card",
"baseEventUrl": "https://www.ladbrokes.com.au/sports/"
},
"unibetService": {
"baseSiteUrl": "https://www.unibet.com.au",
"getOddsUrl": "https://o1-api.aws.kambicdn.com/offering/v2018/ubau/betoffer/event/",
"getSportEventsUrls": {
"TableTennis": "https://www.unibet.com.au/sportsbook-feeds/views/filter/table_tennis/all/matches",
"Boxing": "https://www.unibet.com.au/sportsbook-feeds/views/filter/boxing/all/matches",
"Baseball": "https://www.unibet.com.au/sportsbook-feeds/views/filter/baseball/all/matches",
"Soccer": "https://www.unibet.com.au/sportsbook-feeds/views/filter/football/all/matches",
"Cricket": "https://www.unibet.com.au/sportsbook-feeds/views/filter/cricket/all/matches"
},
"baseEventUrl": "https://www.unibet.com.au/betting/sports/event/"
},
"bookerkeeperEntityComparer": {
"knownCorrespondingMarketNames": {
"Ladbrokes": {
"Boxing": {
"Fight Betting": [
"Match"
]
}
},
"Unibet": {
"Boxing": {
"Match": [
"Fight Betting"
]
}
}
},
"knownCorrespondingOddsNames": {
"Ladbrokes": {
"Boxing": {
"Draw": [
"X"
],
"Draw or Technical Draw": [
"X"
]
}
},
"Unibet": {
"Boxing": {
"X": [
"Draw",
"Draw or Technical Draw"
]
}
}
}
},
"discord": {
}
}

View File

@ -0,0 +1,14 @@
{
"categories": [
{
"sport": "AFL",
"id": "23d497e6-8aab-4309-905b-9421f42c9bc5"
},
{
"sport": "TableTennis",
"id": "b92b2d14-10f7-46c7-8655-16eeed36ec4b"
}
],
"getEventsUrl": "https://api.ladbrokes.com.au/v2/sport/event-request",
"getOddsUrl": "https://api.ladbrokes.com.au/v2/sport/event-card"
}

View File

@ -0,0 +1,6 @@
{
"GetOddsUrl": "https://o1-api.aws.kambicdn.com/offering/v2018/ubau/betoffer/event/",
"GetSportEventsUrls": {
"TableTennis": "https://www.unibet.com.au/sportsbook-feeds/views/filter/table_tennis/all/matches"
}
}