init push - laying out the project
103
.drone.yml
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: build-test-deploy
|
||||||
|
image_pull_secrets:
|
||||||
|
- registryLogins
|
||||||
|
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: amd64
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: notify_start
|
||||||
|
image: plugins/slack
|
||||||
|
settings:
|
||||||
|
webhook: https://team.wizewerx.tech/hooks/5eq841fropn3prt4kzgoaoprgr
|
||||||
|
channel: builds
|
||||||
|
template: >
|
||||||
|
{{uppercase repo.name}}:{{uppercase build.branch}}:{{build.number}} CI build started
|
||||||
|
- name: consumer
|
||||||
|
image: wizewerx/wizewerx-docker-compose
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
DOCKER_AUTH_CONFIG:
|
||||||
|
from_secret: registryLogins
|
||||||
|
commands:
|
||||||
|
- mkdir -p "/root/.docker" && echo $DOCKER_AUTH_CONFIG > "/root/.docker/config.json"
|
||||||
|
- export BM_COMPONENT=consumer
|
||||||
|
- export BUILD_TARGET=wizewerx/bettermail_${BM_COMPONENT}:${DRONE_COMMIT_BRANCH}
|
||||||
|
- docker pull $BUILD_TARGET
|
||||||
|
- docker build --cache-from $BUILD_TARGET --build-arg GEM_CACHE=$BUILD_TARGET -f ./consumer/docker/Dockerfile -t $BUILD_TARGET .
|
||||||
|
- docker push $BUILD_TARGET
|
||||||
|
- name: portal
|
||||||
|
image: wizewerx/wizewerx-docker-compose
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
DOCKER_AUTH_CONFIG:
|
||||||
|
from_secret: registryLogins
|
||||||
|
commands:
|
||||||
|
- mkdir -p "/root/.docker" && echo $DOCKER_AUTH_CONFIG > "/root/.docker/config.json"
|
||||||
|
- export BM_COMPONENT=portal
|
||||||
|
- export BUILD_TARGET=wizewerx/bettermail_${BM_COMPONENT}:${DRONE_COMMIT_BRANCH}
|
||||||
|
- docker pull $BUILD_TARGET
|
||||||
|
- docker build --cache-from $BUILD_TARGET --build-arg GEM_CACHE=$BUILD_TARGET -f ./consumer/docker/Dockerfile -t $BUILD_TARGET .
|
||||||
|
- docker push $BUILD_TARGET
|
||||||
|
- name: proxy
|
||||||
|
image: wizewerx/wizewerx-docker-compose
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
DOCKER_AUTH_CONFIG:
|
||||||
|
from_secret: registryLogins
|
||||||
|
commands:
|
||||||
|
- mkdir -p "/root/.docker" && echo $DOCKER_AUTH_CONFIG > "/root/.docker/config.json"
|
||||||
|
- export BM_COMPONENT=proxy
|
||||||
|
- export BUILD_TARGET=wizewerx/bettermail_${BM_COMPONENT}:${DRONE_COMMIT_BRANCH}
|
||||||
|
- docker pull $BUILD_TARGET
|
||||||
|
- docker build --cache-from $BUILD_TARGET --build-arg GEM_CACHE=$BUILD_TARGET -f ./consumer/docker/Dockerfile -t $BUILD_TARGET .
|
||||||
|
- docker push $BUILD_TARGET
|
||||||
|
- name: notifications
|
||||||
|
image: wizewerx/wizewerx-docker-compose
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
DOCKER_AUTH_CONFIG:
|
||||||
|
from_secret: registryLogins
|
||||||
|
commands:
|
||||||
|
- mkdir -p "/root/.docker" && echo $DOCKER_AUTH_CONFIG > "/root/.docker/config.json"
|
||||||
|
- export BM_COMPONENT=notifications
|
||||||
|
- export BUILD_TARGET=wizewerx/bettermail_${BM_COMPONENT}:${DRONE_COMMIT_BRANCH}
|
||||||
|
- docker pull $BUILD_TARGET
|
||||||
|
- docker build --cache-from $BUILD_TARGET --build-arg GEM_CACHE=$BUILD_TARGET -f ./consumer/docker/Dockerfile -t $BUILD_TARGET .
|
||||||
|
- docker push $BUILD_TARGET
|
||||||
|
- name: notify_end
|
||||||
|
image: plugins/slack
|
||||||
|
settings:
|
||||||
|
webhook: https://team.wizewerx.tech/hooks/5eq841fropn3prt4kzgoaoprgr
|
||||||
|
channel: builds
|
||||||
|
username: mike
|
||||||
|
template: >
|
||||||
|
{{uppercase repo.name}}:{{uppercase build.branch}}:{{build.number}}{{#success build.status}} succeeded. Good job. {{else}} failed. FIX THE BUILD!!!.{{/success}}
|
||||||
|
Build took {{since build.started}}
|
||||||
|
when:
|
||||||
|
status: [ success, failure ]
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
|
- production
|
||||||
|
depends_on:
|
||||||
|
- consumer
|
||||||
|
- portal
|
||||||
|
- proxy
|
||||||
|
- notifications
|
44
.gitignore
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||||
|
#
|
||||||
|
# If you find yourself ignoring temporary files generated by your text editor
|
||||||
|
# or operating system, you probably want to add a global ignore instead:
|
||||||
|
# git config --global core.excludesfile '~/.gitignore_global'
|
||||||
|
|
||||||
|
# Ignore bundler config.
|
||||||
|
/.bundle
|
||||||
|
/vendor
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
*.xcf
|
||||||
|
log/*
|
||||||
|
tmp/*
|
||||||
|
!/log/.keep
|
||||||
|
!/tmp/.keep
|
||||||
|
.idea/*
|
||||||
|
*/.idea/*
|
||||||
|
/public/uploads/*
|
||||||
|
/node_modules
|
||||||
|
app/assets/images/avatars/*
|
||||||
|
# Ignore Byebug command history file.
|
||||||
|
.byebug_history
|
||||||
|
/.vscode/*
|
||||||
|
# Ignore master key for decrypting credentials and more.
|
||||||
|
config/master.key
|
||||||
|
.env
|
||||||
|
# allow database.yml and secrets for gitlab ci pipeline build
|
||||||
|
#/config/database.yml
|
||||||
|
#/config/secrets.yml
|
||||||
|
|
||||||
|
#ignore avatars
|
||||||
|
public/assets/
|
||||||
|
|
||||||
|
public/packs
|
||||||
|
|
||||||
|
dev.env
|
||||||
|
.env*.*
|
||||||
|
storage
|
||||||
|
*.dmp
|
||||||
|
package-lock.json
|
||||||
|
out/
|
||||||
|
/spec/
|
||||||
|
/samples/
|
15
consumer/Gemfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
ruby '2.6.5'
|
||||||
|
gem 'dotenv-rails'
|
||||||
|
|
||||||
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
|
gem 'bootsnap', '>= 1.4.2', require: false
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'web-console', '>= 3.3.0'
|
||||||
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
||||||
|
gem 'spring'
|
||||||
|
gem 'spring-watcher-listen', '~> 2.0.0'
|
||||||
|
end
|
||||||
|
|
||||||
|
gem 'bunny'
|
15
notifications/Gemfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
ruby '2.6.5'
|
||||||
|
gem 'dotenv-rails'
|
||||||
|
|
||||||
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
|
gem 'bootsnap', '>= 1.4.2', require: false
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'web-console', '>= 3.3.0'
|
||||||
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
||||||
|
gem 'spring'
|
||||||
|
gem 'spring-watcher-listen', '~> 2.0.0'
|
||||||
|
end
|
||||||
|
|
||||||
|
gem 'bunny'
|
2
portal/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.env.*
|
||||||
|
.env*.*
|
26
portal/.eslintrc.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"extends": "wesbos",
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly"
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["react"],
|
||||||
|
"rules": {
|
||||||
|
"jsx-a11y/href-no-hash": "off",
|
||||||
|
"jsx-a11y/img-has-alt": "off",
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "off"
|
||||||
|
}
|
||||||
|
}
|
45
portal/.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||||
|
#
|
||||||
|
# If you find yourself ignoring temporary files generated by your text editor
|
||||||
|
# or operating system, you probably want to add a global ignore instead:
|
||||||
|
# git config --global core.excludesfile '~/.gitignore_global'
|
||||||
|
|
||||||
|
# Ignore bundler config.
|
||||||
|
/.bundle
|
||||||
|
/vendor
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
*.xcf
|
||||||
|
/log/*
|
||||||
|
/tmp/*
|
||||||
|
!/log/.keep
|
||||||
|
!/tmp/.keep
|
||||||
|
/.idea/*
|
||||||
|
/public/uploads/*
|
||||||
|
/node_modules
|
||||||
|
app/assets/images/avatars/*
|
||||||
|
# Ignore Byebug command history file.
|
||||||
|
.byebug_history
|
||||||
|
/.vscode/*
|
||||||
|
# Ignore master key for decrypting credentials and more.
|
||||||
|
/config/master.key
|
||||||
|
/.env
|
||||||
|
# allow database.yml and secrets for gitlab ci pipeline build
|
||||||
|
#/config/database.yml
|
||||||
|
#/config/secrets.yml
|
||||||
|
|
||||||
|
#ignore avatars
|
||||||
|
/public/assets/
|
||||||
|
|
||||||
|
/public/packs
|
||||||
|
|
||||||
|
/dev.env
|
||||||
|
/.env*.*
|
||||||
|
/storage
|
||||||
|
*.dmp
|
||||||
|
/brakeman.html
|
||||||
|
package-lock.json
|
||||||
|
/out/
|
||||||
|
/ssl/
|
||||||
|
/spec/
|
||||||
|
/samples/
|
1
portal/.rspec
Normal file
@ -0,0 +1 @@
|
|||||||
|
--require spec_helper
|
150
portal/.rubocop.yml
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
inherit_from: .rubocop_todo.yml
|
||||||
|
require:
|
||||||
|
- rubocop-performance
|
||||||
|
|
||||||
|
AllCops:
|
||||||
|
NewCops: enable
|
||||||
|
Exclude:
|
||||||
|
- 'tmp/**/*'
|
||||||
|
- 'vendor/**/*'
|
||||||
|
- config/**/*
|
||||||
|
- db/**/*
|
||||||
|
- bin/**/*
|
||||||
|
- node_modules/**/*
|
||||||
|
- yarn-cache/**/*
|
||||||
|
- Rakefile
|
||||||
|
- app/javascript
|
||||||
|
- 'client/**/*'
|
||||||
|
Style/OptionalBooleanParameter:
|
||||||
|
Enabled: false
|
||||||
|
Style/Documentation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Lint/SuppressedException:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/EmptyLinesAroundBlockBody:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/EmptyLinesAroundModuleBody:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/EmptyLinesAroundClassBody:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/EmptyLinesAroundMethodBody:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/AbcSize:
|
||||||
|
Enabled: false
|
||||||
|
Max: 20
|
||||||
|
Metrics/ModuleLength:
|
||||||
|
Enabled: false
|
||||||
|
Metrics/BlockLength:
|
||||||
|
Enabled: false
|
||||||
|
Metrics/CyclomaticComplexity:
|
||||||
|
Enabled: false
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Enabled: false
|
||||||
|
Metrics/ClassLength:
|
||||||
|
Enabled: false
|
||||||
|
Style/StructInheritance:
|
||||||
|
Enabled: false
|
||||||
|
Metrics/ParameterLists:
|
||||||
|
Enabled: false
|
||||||
|
Naming/MethodParameterName:
|
||||||
|
Enabled: false
|
||||||
|
Lint/RequireParentheses:
|
||||||
|
Enabled: false
|
||||||
|
Metrics/MethodLength:
|
||||||
|
Enabled: false
|
||||||
|
Max: 30
|
||||||
|
Lint/ShadowedException:
|
||||||
|
Enabled: false
|
||||||
|
Layout/LineLength:
|
||||||
|
Max: 200
|
||||||
|
IgnoredPatterns: [
|
||||||
|
'# index_*'
|
||||||
|
]
|
||||||
|
|
||||||
|
Style/RaiseArgs:
|
||||||
|
Enabled:
|
||||||
|
Style/ClassAndModuleChildren:
|
||||||
|
Enabled: false
|
||||||
|
Style/EmptyMethod:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/FrozenStringLiteralComment:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/BlockDelimiters:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/**/*'
|
||||||
|
|
||||||
|
Style/IfUnlessModifier:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RescueStandardError:
|
||||||
|
Enabled: false
|
||||||
|
Layout/SpaceAroundMethodCallOperator:
|
||||||
|
Enabled: false
|
||||||
|
Lint/RaiseException:
|
||||||
|
Enabled: false
|
||||||
|
Lint/StructNewOverride:
|
||||||
|
Enabled: false
|
||||||
|
Style/ExponentialNotation:
|
||||||
|
Enabled: false
|
||||||
|
Style/HashEachMethods:
|
||||||
|
Enabled: false
|
||||||
|
Style/HashTransformKeys:
|
||||||
|
Enabled: false
|
||||||
|
Style/HashTransformValues:
|
||||||
|
Enabled: false
|
||||||
|
Performance/StringInclude:
|
||||||
|
Enabled: false
|
||||||
|
Layout/EmptyLinesAroundAttributeAccessor:
|
||||||
|
Enabled: false
|
||||||
|
Lint/DeprecatedOpenSSLConstant:
|
||||||
|
Enabled: false
|
||||||
|
Lint/DuplicateElsifCondition:
|
||||||
|
Enabled: false
|
||||||
|
Lint/MixedRegexpCaptureTypes:
|
||||||
|
Enabled: false
|
||||||
|
Style/AccessorGrouping:
|
||||||
|
Enabled: false
|
||||||
|
Style/ArrayCoercion:
|
||||||
|
Enabled: false
|
||||||
|
Style/BisectedAttrAccessor:
|
||||||
|
Enabled: false
|
||||||
|
Style/CaseLikeIf:
|
||||||
|
Enabled: false
|
||||||
|
Style/HashAsLastArrayItem:
|
||||||
|
Enabled: false
|
||||||
|
Style/HashLikeCase:
|
||||||
|
Enabled: false
|
||||||
|
Style/RedundantAssignment:
|
||||||
|
Enabled: true
|
||||||
|
Style/RedundantFetchBlock:
|
||||||
|
Enabled: true
|
||||||
|
Style/RedundantFileExtensionInRequire:
|
||||||
|
Enabled: false
|
||||||
|
Style/RedundantRegexpCharacterClass:
|
||||||
|
Enabled: false
|
||||||
|
Style/RedundantRegexpEscape:
|
||||||
|
Enabled: false
|
||||||
|
Style/SlicingWithRange:
|
||||||
|
Enabled: true
|
||||||
|
Performance/AncestorsInclude:
|
||||||
|
Enabled: false
|
||||||
|
Performance/BigDecimalWithNumericArgument:
|
||||||
|
Enabled: false
|
||||||
|
Performance/RedundantSortBlock:
|
||||||
|
Enabled: false
|
||||||
|
Performance/RedundantStringChars:
|
||||||
|
Enabled: true
|
||||||
|
Performance/ReverseFirst:
|
||||||
|
Enabled: false
|
||||||
|
Performance/SortReverse:
|
||||||
|
Enabled: false
|
||||||
|
Performance/Squeeze:
|
||||||
|
Enabled: false
|
0
portal/.rubocop_todo.yml
Normal file
1
portal/.ruby-gemset
Normal file
@ -0,0 +1 @@
|
|||||||
|
betbeast
|
1
portal/.ruby-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
ruby-2.6.5
|
80
portal/Gemfile
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
git_source(:techgit) { |repo| "https://git.wizewerx.tech/#{repo}.git" }
|
||||||
|
# gem 'wizewerx_utils', techgit: 'wizewerx/wizewerx_utils', branch: 'master'
|
||||||
|
|
||||||
|
ruby '2.6.5'
|
||||||
|
gem 'dotenv-rails'
|
||||||
|
|
||||||
|
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||||
|
gem 'rails', '~> 6.0.3', '>= 6.0.3.4'
|
||||||
|
# Use postgresql as the database for Active Record
|
||||||
|
gem 'pg', '>= 0.18', '< 2.0'
|
||||||
|
# Use Puma as the app server
|
||||||
|
gem 'puma', '~> 4.1'
|
||||||
|
# Use SCSS for stylesheets
|
||||||
|
gem 'sass-rails', '>= 6'
|
||||||
|
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
|
||||||
|
gem 'webpacker', '~> 4.0'
|
||||||
|
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
|
||||||
|
gem 'turbolinks', '~> 5'
|
||||||
|
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
||||||
|
gem 'jbuilder', '~> 2.7'
|
||||||
|
# Use Redis adapter to run Action Cable in production
|
||||||
|
# gem 'redis', '~> 4.0'
|
||||||
|
# Use Active Model has_secure_password
|
||||||
|
# gem 'bcrypt', '~> 3.1.7'
|
||||||
|
gem 'devise', techgit: 'foss/devise', tag: 'v4.8.1'
|
||||||
|
# Use Active Storage variant
|
||||||
|
# gem 'image_processing', '~> 1.2'
|
||||||
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
|
gem 'bootsnap', '>= 1.4.2', require: false
|
||||||
|
# this is used for asset precompile for docker image build to avoid database failure, wrap in production group
|
||||||
|
# ---------------------------------------------------
|
||||||
|
gem 'activerecord-nulldb-adapter'
|
||||||
|
# ---------------------------------------------------
|
||||||
|
#
|
||||||
|
#
|
||||||
|
gem 'public_activity'
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
||||||
|
gem 'byebug', platforms: %i[mri mingw x64_mingw]
|
||||||
|
gem 'rubocop', require: false
|
||||||
|
gem 'rubocop-performance', require: false
|
||||||
|
gem 'rubocop-rails'
|
||||||
|
gem 'rubocop-rspec'
|
||||||
|
# testing support
|
||||||
|
gem 'rspec-rails', '~> 4.0.1'
|
||||||
|
gem 'factory_bot_rails'
|
||||||
|
gem 'capybara'
|
||||||
|
gem 'database_cleaner'
|
||||||
|
gem 'faker'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'annotate'
|
||||||
|
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
|
||||||
|
gem 'listen', '~> 3.2'
|
||||||
|
gem 'web-console', '>= 3.3.0'
|
||||||
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
||||||
|
gem 'spring'
|
||||||
|
gem 'spring-watcher-listen', '~> 2.0.0'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||||
|
gem 'pry'
|
||||||
|
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
|
||||||
|
gem 'stripe'
|
||||||
|
gem 'feature'
|
||||||
|
gem 'omniauth', '~> 1.9.1'
|
||||||
|
gem 'omniauth-keycloak', '~> 1.2.1'
|
||||||
|
gem 'keycloak', '3.2.1'
|
||||||
|
gem 'mini_magick'
|
||||||
|
gem "aws-sdk-s3", require: false
|
||||||
|
gem 'sidekiq'
|
||||||
|
gem "sidekiq-cron", "1.8.0"
|
||||||
|
gem 'ledermann-rails-settings'
|
||||||
|
gem 'appsignal'
|
||||||
|
gem 'httparty'
|
||||||
|
gem 'pagy', '~> 3.5'
|
||||||
|
gem 'activerecord-import'
|
446
portal/Gemfile.lock
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
GIT
|
||||||
|
remote: https://git.wizewerx.tech/foss/devise.git
|
||||||
|
revision: ffecd4caaca700883a042561538f84507ac1edfc
|
||||||
|
tag: v4.8.1
|
||||||
|
specs:
|
||||||
|
devise (4.8.1)
|
||||||
|
bcrypt (~> 3.0)
|
||||||
|
orm_adapter (~> 0.1)
|
||||||
|
railties (>= 4.1.0)
|
||||||
|
responders
|
||||||
|
warden (~> 1.2.3)
|
||||||
|
|
||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
actioncable (6.0.6)
|
||||||
|
actionpack (= 6.0.6)
|
||||||
|
nio4r (~> 2.0)
|
||||||
|
websocket-driver (>= 0.6.1)
|
||||||
|
actionmailbox (6.0.6)
|
||||||
|
actionpack (= 6.0.6)
|
||||||
|
activejob (= 6.0.6)
|
||||||
|
activerecord (= 6.0.6)
|
||||||
|
activestorage (= 6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
mail (>= 2.7.1)
|
||||||
|
actionmailer (6.0.6)
|
||||||
|
actionpack (= 6.0.6)
|
||||||
|
actionview (= 6.0.6)
|
||||||
|
activejob (= 6.0.6)
|
||||||
|
mail (~> 2.5, >= 2.5.4)
|
||||||
|
rails-dom-testing (~> 2.0)
|
||||||
|
actionpack (6.0.6)
|
||||||
|
actionview (= 6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
rack (~> 2.0, >= 2.0.8)
|
||||||
|
rack-test (>= 0.6.3)
|
||||||
|
rails-dom-testing (~> 2.0)
|
||||||
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
|
actiontext (6.0.6)
|
||||||
|
actionpack (= 6.0.6)
|
||||||
|
activerecord (= 6.0.6)
|
||||||
|
activestorage (= 6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
nokogiri (>= 1.8.5)
|
||||||
|
actionview (6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
builder (~> 3.1)
|
||||||
|
erubi (~> 1.4)
|
||||||
|
rails-dom-testing (~> 2.0)
|
||||||
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
|
activejob (6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
globalid (>= 0.3.6)
|
||||||
|
activemodel (6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
activerecord (6.0.6)
|
||||||
|
activemodel (= 6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
activerecord-import (1.0.8)
|
||||||
|
activerecord (>= 3.2)
|
||||||
|
activerecord-nulldb-adapter (0.8.0)
|
||||||
|
activerecord (>= 5.2.0, < 7.1)
|
||||||
|
activestorage (6.0.6)
|
||||||
|
actionpack (= 6.0.6)
|
||||||
|
activejob (= 6.0.6)
|
||||||
|
activerecord (= 6.0.6)
|
||||||
|
marcel (~> 1.0)
|
||||||
|
activesupport (6.0.6)
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
i18n (>= 0.7, < 2)
|
||||||
|
minitest (~> 5.1)
|
||||||
|
tzinfo (~> 1.1)
|
||||||
|
zeitwerk (~> 2.2, >= 2.2.2)
|
||||||
|
addressable (2.8.1)
|
||||||
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
aes_key_wrap (1.1.0)
|
||||||
|
annotate (3.2.0)
|
||||||
|
activerecord (>= 3.2, < 8.0)
|
||||||
|
rake (>= 10.4, < 14.0)
|
||||||
|
appsignal (3.1.5)
|
||||||
|
rack
|
||||||
|
ast (2.4.2)
|
||||||
|
aws-eventstream (1.2.0)
|
||||||
|
aws-partitions (1.650.0)
|
||||||
|
aws-sdk-core (3.164.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
|
aws-sigv4 (~> 1.1)
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
aws-sdk-kms (1.58.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
|
aws-sigv4 (~> 1.1)
|
||||||
|
aws-sdk-s3 (1.116.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.4)
|
||||||
|
aws-sigv4 (1.5.2)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
bcrypt (3.1.18)
|
||||||
|
bindata (2.4.13)
|
||||||
|
bindex (0.8.1)
|
||||||
|
bootsnap (1.13.0)
|
||||||
|
msgpack (~> 1.2)
|
||||||
|
builder (3.2.4)
|
||||||
|
byebug (11.1.3)
|
||||||
|
capybara (3.36.0)
|
||||||
|
addressable
|
||||||
|
matrix
|
||||||
|
mini_mime (>= 0.1.3)
|
||||||
|
nokogiri (~> 1.8)
|
||||||
|
rack (>= 1.6.0)
|
||||||
|
rack-test (>= 0.6.3)
|
||||||
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
|
xpath (~> 3.2)
|
||||||
|
coderay (1.1.3)
|
||||||
|
concurrent-ruby (1.1.10)
|
||||||
|
connection_pool (2.3.0)
|
||||||
|
crass (1.0.6)
|
||||||
|
database_cleaner (2.0.1)
|
||||||
|
database_cleaner-active_record (~> 2.0.0)
|
||||||
|
database_cleaner-active_record (2.0.1)
|
||||||
|
activerecord (>= 5.a)
|
||||||
|
database_cleaner-core (~> 2.0.0)
|
||||||
|
database_cleaner-core (2.0.1)
|
||||||
|
diff-lcs (1.5.0)
|
||||||
|
domain_name (0.5.20190701)
|
||||||
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
|
dotenv (2.8.1)
|
||||||
|
dotenv-rails (2.8.1)
|
||||||
|
dotenv (= 2.8.1)
|
||||||
|
railties (>= 3.2)
|
||||||
|
erubi (1.11.0)
|
||||||
|
et-orbi (1.2.7)
|
||||||
|
tzinfo
|
||||||
|
factory_bot (6.2.1)
|
||||||
|
activesupport (>= 5.0.0)
|
||||||
|
factory_bot_rails (6.2.0)
|
||||||
|
factory_bot (~> 6.2.0)
|
||||||
|
railties (>= 5.0.0)
|
||||||
|
faker (2.22.0)
|
||||||
|
i18n (>= 1.8.11, < 2)
|
||||||
|
faraday (2.6.0)
|
||||||
|
faraday-net_http (>= 2.0, < 3.1)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-follow_redirects (0.3.0)
|
||||||
|
faraday (>= 1, < 3)
|
||||||
|
faraday-net_http (3.0.1)
|
||||||
|
feature (1.4.0)
|
||||||
|
ffi (1.15.5)
|
||||||
|
fugit (1.7.1)
|
||||||
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
|
raabro (~> 1.4)
|
||||||
|
globalid (1.0.0)
|
||||||
|
activesupport (>= 5.0)
|
||||||
|
hashie (5.0.0)
|
||||||
|
http-accept (1.7.0)
|
||||||
|
http-cookie (1.0.5)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httparty (0.20.0)
|
||||||
|
mime-types (~> 3.0)
|
||||||
|
multi_xml (>= 0.5.2)
|
||||||
|
i18n (1.12.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
jbuilder (2.11.5)
|
||||||
|
actionview (>= 5.0.0)
|
||||||
|
activesupport (>= 5.0.0)
|
||||||
|
jmespath (1.6.1)
|
||||||
|
json (2.3.0)
|
||||||
|
json-jwt (1.16.1)
|
||||||
|
activesupport (>= 4.2)
|
||||||
|
aes_key_wrap
|
||||||
|
bindata
|
||||||
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
|
jwt (2.2.1)
|
||||||
|
keycloak (3.2.1)
|
||||||
|
json (= 2.3.0)
|
||||||
|
jwt (= 2.2.1)
|
||||||
|
rest-client (= 2.1.0)
|
||||||
|
ledermann-rails-settings (2.5.0)
|
||||||
|
activerecord (>= 4.2)
|
||||||
|
listen (3.7.1)
|
||||||
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
|
loofah (2.19.0)
|
||||||
|
crass (~> 1.0.2)
|
||||||
|
nokogiri (>= 1.5.9)
|
||||||
|
mail (2.7.1)
|
||||||
|
mini_mime (>= 0.1.1)
|
||||||
|
marcel (1.0.2)
|
||||||
|
matrix (0.4.2)
|
||||||
|
method_source (1.0.0)
|
||||||
|
mime-types (3.4.1)
|
||||||
|
mime-types-data (~> 3.2015)
|
||||||
|
mime-types-data (3.2022.0105)
|
||||||
|
mini_magick (4.11.0)
|
||||||
|
mini_mime (1.1.2)
|
||||||
|
mini_portile2 (2.8.0)
|
||||||
|
minitest (5.16.3)
|
||||||
|
msgpack (1.6.0)
|
||||||
|
multi_json (1.15.0)
|
||||||
|
multi_xml (0.6.0)
|
||||||
|
netrc (0.11.0)
|
||||||
|
nio4r (2.5.8)
|
||||||
|
nokogiri (1.13.9)
|
||||||
|
mini_portile2 (~> 2.8.0)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.13.9-x86_64-linux)
|
||||||
|
racc (~> 1.4)
|
||||||
|
oauth2 (1.4.11)
|
||||||
|
faraday (>= 0.17.3, < 3.0)
|
||||||
|
jwt (>= 1.0, < 3.0)
|
||||||
|
multi_json (~> 1.3)
|
||||||
|
multi_xml (~> 0.5)
|
||||||
|
rack (>= 1.2, < 4)
|
||||||
|
omniauth (1.9.2)
|
||||||
|
hashie (>= 3.4.6)
|
||||||
|
rack (>= 1.6.2, < 3)
|
||||||
|
omniauth-keycloak (1.2.1)
|
||||||
|
json-jwt (~> 1.12)
|
||||||
|
omniauth (~> 1.9.0)
|
||||||
|
omniauth-oauth2 (~> 1.6.0)
|
||||||
|
omniauth-oauth2 (1.6.0)
|
||||||
|
oauth2 (~> 1.1)
|
||||||
|
omniauth (~> 1.9)
|
||||||
|
orm_adapter (0.5.0)
|
||||||
|
pagy (3.14.0)
|
||||||
|
parallel (1.22.1)
|
||||||
|
parser (3.1.2.1)
|
||||||
|
ast (~> 2.4.1)
|
||||||
|
pg (1.4.4)
|
||||||
|
pry (0.14.1)
|
||||||
|
coderay (~> 1.1)
|
||||||
|
method_source (~> 1.0)
|
||||||
|
public_activity (2.0.2)
|
||||||
|
actionpack (>= 5.0.0)
|
||||||
|
activerecord (>= 5.0)
|
||||||
|
i18n (>= 0.5.0)
|
||||||
|
railties (>= 5.0.0)
|
||||||
|
public_suffix (5.0.0)
|
||||||
|
puma (4.3.12)
|
||||||
|
nio4r (~> 2.0)
|
||||||
|
raabro (1.4.0)
|
||||||
|
racc (1.6.0)
|
||||||
|
rack (2.2.4)
|
||||||
|
rack-proxy (0.7.4)
|
||||||
|
rack
|
||||||
|
rack-test (2.0.2)
|
||||||
|
rack (>= 1.3)
|
||||||
|
rails (6.0.6)
|
||||||
|
actioncable (= 6.0.6)
|
||||||
|
actionmailbox (= 6.0.6)
|
||||||
|
actionmailer (= 6.0.6)
|
||||||
|
actionpack (= 6.0.6)
|
||||||
|
actiontext (= 6.0.6)
|
||||||
|
actionview (= 6.0.6)
|
||||||
|
activejob (= 6.0.6)
|
||||||
|
activemodel (= 6.0.6)
|
||||||
|
activerecord (= 6.0.6)
|
||||||
|
activestorage (= 6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
bundler (>= 1.3.0)
|
||||||
|
railties (= 6.0.6)
|
||||||
|
sprockets-rails (>= 2.0.0)
|
||||||
|
rails-dom-testing (2.0.3)
|
||||||
|
activesupport (>= 4.2.0)
|
||||||
|
nokogiri (>= 1.6)
|
||||||
|
rails-html-sanitizer (1.4.3)
|
||||||
|
loofah (~> 2.3)
|
||||||
|
railties (6.0.6)
|
||||||
|
actionpack (= 6.0.6)
|
||||||
|
activesupport (= 6.0.6)
|
||||||
|
method_source
|
||||||
|
rake (>= 0.8.7)
|
||||||
|
thor (>= 0.20.3, < 2.0)
|
||||||
|
rainbow (3.1.1)
|
||||||
|
rake (13.0.6)
|
||||||
|
rb-fsevent (0.11.2)
|
||||||
|
rb-inotify (0.10.1)
|
||||||
|
ffi (~> 1.0)
|
||||||
|
redis (4.8.0)
|
||||||
|
regexp_parser (2.6.0)
|
||||||
|
responders (3.0.1)
|
||||||
|
actionpack (>= 5.0)
|
||||||
|
railties (>= 5.0)
|
||||||
|
rest-client (2.1.0)
|
||||||
|
http-accept (>= 1.7.0, < 2.0)
|
||||||
|
http-cookie (>= 1.0.2, < 2.0)
|
||||||
|
mime-types (>= 1.16, < 4.0)
|
||||||
|
netrc (~> 0.8)
|
||||||
|
rexml (3.2.5)
|
||||||
|
rspec-core (3.11.0)
|
||||||
|
rspec-support (~> 3.11.0)
|
||||||
|
rspec-expectations (3.11.1)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.11.0)
|
||||||
|
rspec-mocks (3.11.2)
|
||||||
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
rspec-support (~> 3.11.0)
|
||||||
|
rspec-rails (4.0.2)
|
||||||
|
actionpack (>= 4.2)
|
||||||
|
activesupport (>= 4.2)
|
||||||
|
railties (>= 4.2)
|
||||||
|
rspec-core (~> 3.10)
|
||||||
|
rspec-expectations (~> 3.10)
|
||||||
|
rspec-mocks (~> 3.10)
|
||||||
|
rspec-support (~> 3.10)
|
||||||
|
rspec-support (3.11.1)
|
||||||
|
rubocop (1.37.1)
|
||||||
|
json (~> 2.3)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
parser (>= 3.1.2.1)
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
|
rexml (>= 3.2.5, < 4.0)
|
||||||
|
rubocop-ast (>= 1.23.0, < 2.0)
|
||||||
|
ruby-progressbar (~> 1.7)
|
||||||
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
|
rubocop-ast (1.23.0)
|
||||||
|
parser (>= 3.1.1.0)
|
||||||
|
rubocop-performance (1.15.0)
|
||||||
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
|
rubocop-ast (>= 0.4.0)
|
||||||
|
rubocop-rails (2.17.1)
|
||||||
|
activesupport (>= 4.2.0)
|
||||||
|
rack (>= 1.1)
|
||||||
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
|
rubocop-rspec (2.14.1)
|
||||||
|
rubocop (~> 1.33)
|
||||||
|
ruby-progressbar (1.11.0)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
sass-rails (6.0.0)
|
||||||
|
sassc-rails (~> 2.1, >= 2.1.1)
|
||||||
|
sassc (2.4.0)
|
||||||
|
ffi (~> 1.9)
|
||||||
|
sassc-rails (2.1.2)
|
||||||
|
railties (>= 4.0.0)
|
||||||
|
sassc (>= 2.0)
|
||||||
|
sprockets (> 3.0)
|
||||||
|
sprockets-rails
|
||||||
|
tilt
|
||||||
|
sidekiq (6.5.7)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
rack (~> 2.0)
|
||||||
|
redis (>= 4.5.0, < 5)
|
||||||
|
sidekiq-cron (1.8.0)
|
||||||
|
fugit (~> 1)
|
||||||
|
sidekiq (>= 4.2.1)
|
||||||
|
spring (2.1.1)
|
||||||
|
spring-watcher-listen (2.0.1)
|
||||||
|
listen (>= 2.7, < 4.0)
|
||||||
|
spring (>= 1.2, < 3.0)
|
||||||
|
sprockets (4.1.1)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
rack (> 1, < 3)
|
||||||
|
sprockets-rails (3.4.2)
|
||||||
|
actionpack (>= 5.2)
|
||||||
|
activesupport (>= 5.2)
|
||||||
|
sprockets (>= 3.0.0)
|
||||||
|
stripe (7.1.0)
|
||||||
|
thor (1.2.1)
|
||||||
|
thread_safe (0.3.6)
|
||||||
|
tilt (2.0.11)
|
||||||
|
turbolinks (5.2.1)
|
||||||
|
turbolinks-source (~> 5.2)
|
||||||
|
turbolinks-source (5.2.0)
|
||||||
|
tzinfo (1.2.10)
|
||||||
|
thread_safe (~> 0.1)
|
||||||
|
unf (0.1.4)
|
||||||
|
unf_ext
|
||||||
|
unf_ext (0.0.8.2)
|
||||||
|
unicode-display_width (2.3.0)
|
||||||
|
warden (1.2.9)
|
||||||
|
rack (>= 2.0.9)
|
||||||
|
web-console (4.2.0)
|
||||||
|
actionview (>= 6.0.0)
|
||||||
|
activemodel (>= 6.0.0)
|
||||||
|
bindex (>= 0.4.0)
|
||||||
|
railties (>= 6.0.0)
|
||||||
|
webpacker (4.3.0)
|
||||||
|
activesupport (>= 4.2)
|
||||||
|
rack-proxy (>= 0.6.1)
|
||||||
|
railties (>= 4.2)
|
||||||
|
websocket-driver (0.7.5)
|
||||||
|
websocket-extensions (>= 0.1.0)
|
||||||
|
websocket-extensions (0.1.5)
|
||||||
|
xpath (3.2.0)
|
||||||
|
nokogiri (~> 1.8)
|
||||||
|
zeitwerk (2.6.1)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
-darwin-21
|
||||||
|
ruby
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
activerecord-import
|
||||||
|
activerecord-nulldb-adapter
|
||||||
|
annotate
|
||||||
|
appsignal
|
||||||
|
aws-sdk-s3
|
||||||
|
bootsnap (>= 1.4.2)
|
||||||
|
byebug
|
||||||
|
capybara
|
||||||
|
database_cleaner
|
||||||
|
devise!
|
||||||
|
dotenv-rails
|
||||||
|
factory_bot_rails
|
||||||
|
faker
|
||||||
|
feature
|
||||||
|
httparty
|
||||||
|
jbuilder (~> 2.7)
|
||||||
|
keycloak (= 3.2.1)
|
||||||
|
ledermann-rails-settings
|
||||||
|
listen (~> 3.2)
|
||||||
|
mini_magick
|
||||||
|
omniauth (~> 1.9.1)
|
||||||
|
omniauth-keycloak (~> 1.2.1)
|
||||||
|
pagy (~> 3.5)
|
||||||
|
pg (>= 0.18, < 2.0)
|
||||||
|
pry
|
||||||
|
public_activity
|
||||||
|
puma (~> 4.1)
|
||||||
|
rails (~> 6.0.3, >= 6.0.3.4)
|
||||||
|
rspec-rails (~> 4.0.1)
|
||||||
|
rubocop
|
||||||
|
rubocop-performance
|
||||||
|
rubocop-rails
|
||||||
|
rubocop-rspec
|
||||||
|
sass-rails (>= 6)
|
||||||
|
sidekiq
|
||||||
|
sidekiq-cron (= 1.8.0)
|
||||||
|
spring
|
||||||
|
spring-watcher-listen (~> 2.0.0)
|
||||||
|
stripe
|
||||||
|
turbolinks (~> 5)
|
||||||
|
tzinfo-data
|
||||||
|
web-console (>= 3.3.0)
|
||||||
|
webpacker (~> 4.0)
|
||||||
|
|
||||||
|
RUBY VERSION
|
||||||
|
ruby 2.6.5p114
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.3.10
|
9
portal/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Betbeast
|
||||||
|
|
||||||
|
Betbeast is a private bets arbitration service.
|
||||||
|
|
||||||
|
## Setting up development
|
||||||
|
|
||||||
|
`$ bundle exec rails db:setup`
|
||||||
|
|
||||||
|
`$ bundle exec rails s`
|
6
portal/Rakefile
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||||
|
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||||
|
|
||||||
|
require_relative 'config/application'
|
||||||
|
|
||||||
|
Rails.application.load_tasks
|
2
portal/app/assets/config/manifest.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
//= link_tree ../images
|
||||||
|
//= link_directory ../stylesheets .css
|
0
portal/app/assets/images/.keep
Normal file
BIN
portal/app/assets/images/favicon.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
portal/app/assets/images/logo-white.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
portal/app/assets/images/logo.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
15
portal/app/assets/stylesheets/application.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
||||||
|
* listed below.
|
||||||
|
*
|
||||||
|
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
|
||||||
|
* vendor/assets/stylesheets directory can be referenced here using a relative path.
|
||||||
|
*
|
||||||
|
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
||||||
|
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
||||||
|
* files in this directory. Styles in this file should be added after the last require_* statement.
|
||||||
|
* It is generally better to create a new file per style scope.
|
||||||
|
*
|
||||||
|
*= require_tree .
|
||||||
|
*= require_self
|
||||||
|
*/
|
4
portal/app/channels/application_cable/channel.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module ApplicationCable
|
||||||
|
class Channel < ActionCable::Channel::Base
|
||||||
|
end
|
||||||
|
end
|
4
portal/app/channels/application_cable/connection.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module ApplicationCable
|
||||||
|
class Connection < ActionCable::Connection::Base
|
||||||
|
end
|
||||||
|
end
|
9
portal/app/channels/exchange_account_channel.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class ExchangeAccountChannel < ApplicationCable::Channel
|
||||||
|
def subscribed
|
||||||
|
stream_from "exchange_account:#{params[:id]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribed
|
||||||
|
# Any cleanup needed when channel is unsubscribed
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,3 @@
|
|||||||
|
class Api::AuthenticatedApiController < ActionController::Base
|
||||||
|
before_action :authenticate_user!
|
||||||
|
end
|
11
portal/app/controllers/api/v1/account_controller.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
class Api::V1::AccountController < Api::AuthenticatedApiController
|
||||||
|
def create
|
||||||
|
render json: { success: true} and return
|
||||||
|
end
|
||||||
|
|
||||||
|
# def is_domain_valid
|
||||||
|
# valid = !Helpbuild::Ecosystem.reserved_account_domains.include?(params[:domain])
|
||||||
|
# valid &= /[A-Za-z0-9-]{1,63}/.match(params[:domain]).present?
|
||||||
|
# render json: { success: valid} and return
|
||||||
|
# end
|
||||||
|
end
|
7
portal/app/controllers/api/v1/app_controller.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class Api::V1::AppController < Api::AuthenticatedApiController
|
||||||
|
skip_before_action :authenticate_user!, only: [:configuration]
|
||||||
|
def configuration
|
||||||
|
rtn = {exchange_accounts: ExchangeAccount.pluck(:id).sort}
|
||||||
|
render json: { success: true, config: rtn} and return
|
||||||
|
end
|
||||||
|
end
|
104
portal/app/controllers/api/v1/bets_controller.rb
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
class Api::V1::BetsController < Api::AuthenticatedApiController
|
||||||
|
include ApplicationHelper
|
||||||
|
include Pagy::Backend
|
||||||
|
skip_before_action :authenticate_user!, only: %i[add_placed_bet tips]
|
||||||
|
skip_before_action :verify_authenticity_token, only: %i[add_placed_bet tips]
|
||||||
|
|
||||||
|
def index
|
||||||
|
exchange_account = params[:exchange_account_id].blank? ? ExchangeAccount.mounted_account : ExchangeAccount.find(params[:exchange_account_id])
|
||||||
|
|
||||||
|
bets = filter_by_date_range(exchange_account.my_bets, 'created_at')
|
||||||
|
|
||||||
|
bets = bets.where(outcome: params[:outcome]) unless params[:outcome].blank?
|
||||||
|
bets = bets.where("exchange_event_name ILIKE '%%#{params[:event_name]}%%'") unless params[:event_name].blank?
|
||||||
|
|
||||||
|
ev_range = params[:expected_value]
|
||||||
|
if ev_range.present?
|
||||||
|
v_symbolize = JSON.parse(ev_range).symbolize_keys
|
||||||
|
bets = bets.where("expected_value >= #{v_symbolize[:min]} AND expected_value <= #{v_symbolize[:max]}")
|
||||||
|
end
|
||||||
|
# add additional filters later
|
||||||
|
#
|
||||||
|
summary = summary_dashboard(exchange_account, bets)
|
||||||
|
|
||||||
|
pagy, bets = pagy(bets)
|
||||||
|
|
||||||
|
pagy_json = pagy_metadata(pagy)
|
||||||
|
|
||||||
|
|
||||||
|
render json: { summary: summary, pagy: pagy_json, bets: bets.order(created_at: :desc).map(&:json_payload) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_placed_bet
|
||||||
|
# push a placed bet from an authenticated client
|
||||||
|
wpb = 'WEB_PLACED_BETS'
|
||||||
|
|
||||||
|
ex = ExchangeAccount.find_by(id: wpb)
|
||||||
|
stake = (params['stake'] || 0).to_f
|
||||||
|
executed_odds = (params['odds_accepted'] || 0).to_f
|
||||||
|
record = {
|
||||||
|
tip_provider_id: 'betburger',
|
||||||
|
tip_provider_bet_id: params['betburger_bet_id'],
|
||||||
|
tip_provider_percent: (params['value_bet_ev_percent'] || 0).to_f,
|
||||||
|
tip_provider_odds: (params['original_odds'] || 0).to_f,
|
||||||
|
team1: "Not Set",
|
||||||
|
team2: "Not Set",
|
||||||
|
exchange_id: params['bookmaker'],
|
||||||
|
exchange_event_name: params['match'],
|
||||||
|
exchange_market_details: {market: params['market'], selection: params['selection']},
|
||||||
|
stake: stake,
|
||||||
|
executed_odds: executed_odds,
|
||||||
|
expected_value: Bet.calculate_expected_value(stake, executed_odds),
|
||||||
|
original_json: params.to_json,
|
||||||
|
outcome: {'Bet placed': 'open', 'Odds changed': 'expired'}[params['placer_result']] || 'skipped',
|
||||||
|
log: [params['placer_result']],
|
||||||
|
}
|
||||||
|
ex.my_bets.create(record)
|
||||||
|
render json: { success: true }
|
||||||
|
end
|
||||||
|
|
||||||
|
def tips
|
||||||
|
id = params[:id]
|
||||||
|
resp = { success: false }
|
||||||
|
ts = id.nil? ? nil : TipSource.find_by(id: id)
|
||||||
|
if ts
|
||||||
|
resp[:success] = true
|
||||||
|
latest_source_data = ts.tip_source_data.latest
|
||||||
|
resp[:data] = latest_source_data.blank? ? {} : latest_source_data.data
|
||||||
|
else
|
||||||
|
resp[:message] = 'Unknown tip source requested'
|
||||||
|
end
|
||||||
|
render json: resp
|
||||||
|
end
|
||||||
|
|
||||||
|
def summary_dashboard(exchange_account, bets)
|
||||||
|
{
|
||||||
|
exchange_account: exchange_account.json_payload,
|
||||||
|
total_win_amount: exchange_account.total_won(bets),
|
||||||
|
total_lost_amount: exchange_account.total_lost(bets),
|
||||||
|
total_risked_amount: exchange_account.total_open(bets),
|
||||||
|
total_tips: bets.count,
|
||||||
|
total_tips_skipped: bets.skipped.count,
|
||||||
|
total_tips_processing: bets.processing.count,
|
||||||
|
total_tips_expired: bets.expired.count,
|
||||||
|
total_tips_ignored: bets.ignored.count,
|
||||||
|
total_tips_voided: bets.voided.count,
|
||||||
|
total_placed_bets: bets.placed_bets.count,
|
||||||
|
total_placed_bets_won: bets.won.count,
|
||||||
|
total_placed_bets_lost: bets.lost.count,
|
||||||
|
total_placed_bets_open: bets.open.count,
|
||||||
|
average_odds_won: odd_averages(bets.won),
|
||||||
|
average_odds_lost: odd_averages(bets.lost),
|
||||||
|
average_odds: odd_averages(bets),
|
||||||
|
running_since: exchange_account.my_bets.minimum(:created_at) || exchange_account.created_at,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def odd_averages(bets)
|
||||||
|
return 0 if bets.count.zero?
|
||||||
|
|
||||||
|
total_odds = 0
|
||||||
|
bets.map { |b| total_odds += b.tip_provider_odds.to_f }
|
||||||
|
(total_odds / bets.count).round(2)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,14 @@
|
|||||||
|
class Api::V1::ExchangeAccountsController < Api::AuthenticatedApiController
|
||||||
|
def update
|
||||||
|
exchange_account = ExchangeAccount.find(params[:exchange_account_id])
|
||||||
|
render json: { success: false} and return unless exchange_account
|
||||||
|
|
||||||
|
payload = {}
|
||||||
|
payload[:betting_enabled] = params['canBet'] unless params['canBet'].nil?
|
||||||
|
payload[:status] = params['isActive'] == true ? 'active' : 'inactive' unless params['isActive'].nil?
|
||||||
|
|
||||||
|
exchange_account.update(payload) unless payload.empty?
|
||||||
|
render json: { success: true} and return
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
13
portal/app/controllers/api/v1/users_controller.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class Api::V1::UsersController < Api::AuthenticatedApiController
|
||||||
|
skip_before_action :authenticate_user!, only: [ :auth]
|
||||||
|
|
||||||
|
def auth
|
||||||
|
unless user_signed_in?
|
||||||
|
render json: { success: false }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { success: true, user: current_user.json_payload}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
2
portal/app/controllers/application_controller.rb
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class ApplicationController < ActionController::Base
|
||||||
|
end
|
4
portal/app/controllers/pages_controller.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
class PagesController < ApplicationController
|
||||||
|
def index
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Users
|
||||||
|
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
|
def keycloakopenid
|
||||||
|
Rails.logger.debug(request.env['omniauth.auth'])
|
||||||
|
@user = User.find_or_create_from_auth_hash(request.env['omniauth.auth'])
|
||||||
|
sign_in_and_redirect @user, event: :authentication
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure
|
||||||
|
redirect_to root_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
25
portal/app/helpers/application_helper.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module ApplicationHelper
|
||||||
|
def render_activities(_activities)
|
||||||
|
'Activities'
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_date_range(records, field_string)
|
||||||
|
return records if field_string.blank?
|
||||||
|
|
||||||
|
field = params[field_string.to_sym]
|
||||||
|
|
||||||
|
return records unless field
|
||||||
|
|
||||||
|
ea_symbolize = JSON.parse(field).symbolize_keys
|
||||||
|
|
||||||
|
parsed_date_from = nil
|
||||||
|
parsed_date_to = nil
|
||||||
|
|
||||||
|
parsed_date_from = DateTime.parse(ea_symbolize[:from]) unless ea_symbolize[:from].nil?
|
||||||
|
parsed_date_to = DateTime.parse(ea_symbolize[:to]) unless ea_symbolize[:to].nil?
|
||||||
|
|
||||||
|
records = records.where("#{field_string} >= ? AND #{field_string} <= ?", parsed_date_from, parsed_date_to + 1.day) unless parsed_date_from.nil?
|
||||||
|
|
||||||
|
records
|
||||||
|
end
|
||||||
|
end
|
11
portal/app/helpers/mailer_style_helper.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module MailerStyleHelper
|
||||||
|
def mail_logo_style
|
||||||
|
"background: #222C36;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 10px 10px 0 0"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# reload!; ProjectMailer.notify_helper_for_join_request(ProjectHelper.first).deliver_now
|
14
portal/app/jobs/account_sync_and_reconciliation_job.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class AccountSyncAndReconciliationJob < ApplicationJob
|
||||||
|
queue_as :high
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
ExchangeAccount.active.each do | ea|
|
||||||
|
am = Integrations::Betfair::AccountManager.new(ea)
|
||||||
|
bm = Integrations::Betfair::BetManager.new(ea)
|
||||||
|
puts "Refreshing account balance on '#{ea.id}'"
|
||||||
|
am.refresh_account_balance
|
||||||
|
puts "Reconcile open bets on '#{ea.id}'"
|
||||||
|
bm.check_qualified_bet_outcome(ea.my_bets.open)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
8
portal/app/jobs/application_job.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class ApplicationJob < ActiveJob::Base
|
||||||
|
sidekiq_options retry: false
|
||||||
|
# Automatically retry jobs that encountered a deadlock
|
||||||
|
# retry_on ActiveRecord::Deadlocked
|
||||||
|
|
||||||
|
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||||
|
# discard_on ActiveJob::DeserializationError
|
||||||
|
end
|
63
portal/app/jobs/bet_placement_service.rb
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
class BetPlacementService < ApplicationJob
|
||||||
|
queue_as :high
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
# do not proceed if we have already procssed a bet for this tip
|
||||||
|
bet = args[:bet]
|
||||||
|
# get a valid betfair instance for the account we are using, halt if not valid.
|
||||||
|
exchange = Integrations::Betfair::BetManager.new(bet.exchange_account)
|
||||||
|
return unless exchange
|
||||||
|
|
||||||
|
last_step = 'Checking event id'
|
||||||
|
begin
|
||||||
|
# check this bet with the exchange and populate the event id if it is missing
|
||||||
|
|
||||||
|
unless bet.exchange_event_id
|
||||||
|
bet.exchange_event_id = exchange.bet_event(bet)
|
||||||
|
end
|
||||||
|
bet.placement_attempts += 1
|
||||||
|
|
||||||
|
# check the odds are still good at the exchange for that event
|
||||||
|
last_step = 'Checking odds'
|
||||||
|
prices_and_stakes = exchange.bet_odds(bet)
|
||||||
|
prices = prices_and_stakes[:prices]
|
||||||
|
|
||||||
|
bet.exchange_odds = prices_and_stakes
|
||||||
|
bet.outcome = 'expired'
|
||||||
|
tip_odds = bet.tip_provider_odds.to_f
|
||||||
|
|
||||||
|
# use tip odds or get closest match to tip odds offered by the exchange
|
||||||
|
last_step = 'Determining if odds optimal'
|
||||||
|
margin = tip_odds * bet.exchange_account.current_stake_strategy[:odds_margin]
|
||||||
|
max_tip_odds = tip_odds + margin
|
||||||
|
odds_to_execute_at = prices.min_by { |num| (max_tip_odds - num).abs }
|
||||||
|
raise 'Cannot determine optimal odds' unless odds_to_execute_at
|
||||||
|
if odds_to_execute_at.between?(tip_odds, tip_odds + margin)
|
||||||
|
last_step = 'Determining stake and placing bet'
|
||||||
|
odds_stake = prices_and_stakes[:stakes][odds_to_execute_at.to_s]
|
||||||
|
|
||||||
|
bet.executed_odds = odds_to_execute_at.to_s
|
||||||
|
# stake will always fill the max available or the limits of our stake management - whichever is the lower.
|
||||||
|
stake = bet.exchange_account.optimal_stake(bet, exchange.minimum_stake, odds_stake)
|
||||||
|
|
||||||
|
raise 'Optimal stake is zero. Not taking bet' if stake.zero?
|
||||||
|
|
||||||
|
bet.outcome = 'open'
|
||||||
|
bet.stake = stake
|
||||||
|
bet.expected_value = Bet.calculate_expected_value(stake, odds_to_execute_at)
|
||||||
|
if bet.exchange_account.can_bet?
|
||||||
|
bet.exchange_bet_id = exchange.place_bet(bet, stake)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
bet.log << '[Bet placement] Tip odds not found in latest prices, nudge too far out'
|
||||||
|
end
|
||||||
|
rescue Exception => e
|
||||||
|
x = "Placement #{bet.placement_attempts}--->Last step: #{last_step}. Error: #{e.message}. Skipping"
|
||||||
|
bet.log ||= []
|
||||||
|
bet.log << x
|
||||||
|
bet.outcome = e.message.include?('PERMISSION_DENIED') ? 'errored' : 'skipped'
|
||||||
|
ensure
|
||||||
|
bet.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
8
portal/app/jobs/clear_old_pulls_job.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class ClearOldPullsJob < ApplicationJob
|
||||||
|
queue_as :high
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
max_hours_to_keep = ENV['MAX_HISTORY_HOURS']&.to_i || 2
|
||||||
|
TipSourceData.where("created_at < ?", max_hours_to_keep.hours.ago).delete_all
|
||||||
|
end
|
||||||
|
end
|
10
portal/app/jobs/process_subscription_job.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class ProcessSubscriptionJob < ApplicationJob
|
||||||
|
queue_as :high
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
subscription = args[:subscription]
|
||||||
|
tsd = args[:tsd]
|
||||||
|
bb = Integrations::Betburger.new()
|
||||||
|
bb.process_subscription_tips(subscription: subscription, data: tsd)
|
||||||
|
end
|
||||||
|
end
|
11
portal/app/jobs/pull_event_markets_job.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
class PullEventMarketsJob < ApplicationJob
|
||||||
|
queue_as :medium
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT'])
|
||||||
|
raise 'No Betfair hunter account' unless ea
|
||||||
|
|
||||||
|
hunter = Integrations::Betfair::OpportunityHunter.new(ea)
|
||||||
|
hunter.event_markets_and_selections #refresh the markets to include
|
||||||
|
end
|
||||||
|
end
|
13
portal/app/jobs/pull_latest_odds_prices_job.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class PullLatestOddsPricesJob < ApplicationJob
|
||||||
|
queue_as :medium
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT'])
|
||||||
|
raise 'No Betfair hunter account' unless ea
|
||||||
|
|
||||||
|
hunter = Integrations::Betfair::OpportunityHunter.new(ea)
|
||||||
|
BetfairEventRunner.runners_for_open_events.each do | runner |
|
||||||
|
PullRunnerOddsJob.perform_later(hunter: hunter, runner: runner)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
9
portal/app/jobs/pull_runner_odds_job.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class PullRunnerOddsJob < ApplicationJob
|
||||||
|
queue_as :medium
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
hunter = params[:oh]
|
||||||
|
runner = params[:runner]
|
||||||
|
hunter.runner_odds(runner)
|
||||||
|
end
|
||||||
|
end
|
16
portal/app/jobs/pull_tips_job.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
class PullTipsJob < ApplicationJob
|
||||||
|
queue_as :high
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
ta = TipsterAccount.find_by(id: ENV['TIPSTER_ACCOUNT'])
|
||||||
|
bb = Integrations::Betburger.new(ta)
|
||||||
|
ta.tip_sources.active_sources.each do |ts|
|
||||||
|
tsd = bb.pull_and_save_tips(ts)
|
||||||
|
ts.source_subscriptions.each do |ss|
|
||||||
|
next unless ss.exchange_account.active?
|
||||||
|
|
||||||
|
ProcessSubscriptionJob.perform_later(subscription: ss, tsd: tsd)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
portal/app/jobs/pull_upcoming_events_job.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class PullUpcomingEventsJob < ApplicationJob
|
||||||
|
queue_as :medium
|
||||||
|
|
||||||
|
def perform(args = {})
|
||||||
|
ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT'])
|
||||||
|
raise 'No Betfair hunter account' unless ea
|
||||||
|
|
||||||
|
hunter = Integrations::Betfair::OpportunityHunter.new(ea)
|
||||||
|
top_of_hour = Time.now.beginning_of_hour
|
||||||
|
hunter.events_in_timeframe(from: top_of_hour, to: top_of_hour + 1.hour) #pull events
|
||||||
|
hunter.event_markets_and_selections #refresh the markets to include
|
||||||
|
end
|
||||||
|
end
|
6
portal/app/lib/general_helper.rb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
class GeneralHelper
|
||||||
|
|
||||||
|
def self.broadcast_to_account_channel(account_id, data)
|
||||||
|
ExchangeAccountChannel.broadcast_to(account_id, data)
|
||||||
|
end
|
||||||
|
end
|
940
portal/app/lib/integrations/betburger.rb
Normal file
@ -0,0 +1,940 @@
|
|||||||
|
module Integrations
|
||||||
|
class Betburger
|
||||||
|
include HTTParty
|
||||||
|
attr_reader :access_token
|
||||||
|
|
||||||
|
URIS = {
|
||||||
|
live: 'https://rest-api-lv.betburger.com/api/v1/valuebets/bot_pro_search',
|
||||||
|
prematch: 'https://rest-api-pr.betburger.com/api/v1/valuebets/bot_pro_search',
|
||||||
|
}.freeze.with_indifferent_access
|
||||||
|
|
||||||
|
def initialize(tipster_account=nil)
|
||||||
|
super()
|
||||||
|
tipster_account ||= TipsterAccount.find_by(id: ENV['TIPSTER_ACCOUNT'])
|
||||||
|
@access_token = tipster_account.apikey
|
||||||
|
end
|
||||||
|
|
||||||
|
def pull_and_save_tips(source)
|
||||||
|
url = URIS[source.source_type]
|
||||||
|
return unless url
|
||||||
|
|
||||||
|
response = self.class.post(url, { body: { per_page: 32, search_filter: source.filters, access_token: @access_token } })
|
||||||
|
source.tip_source_data.create(data: response)
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_subscription_tips(subscription:, data:,place_bets: true)
|
||||||
|
|
||||||
|
return unless subscription
|
||||||
|
|
||||||
|
tsd = data || subscription.tip_source_data.last
|
||||||
|
|
||||||
|
# tips already processed
|
||||||
|
return if subscription.subscription_runs.where(tip_source_data_id: tsd.id).exists?
|
||||||
|
|
||||||
|
json = tsd.data
|
||||||
|
return unless json['total_by_filter'].to_i.positive?
|
||||||
|
|
||||||
|
percentages = {}
|
||||||
|
vb_refs = json['source']['value_bets']
|
||||||
|
vb_refs.each do |vb_ref|
|
||||||
|
percentages[vb_ref['bet_id']] = { percent: vb_ref['percent'] }
|
||||||
|
end
|
||||||
|
bet_refs = json['bets']
|
||||||
|
stats = { non_valuebet: 0, unsupported_exchange: 0, duplicate: 0, processed: 0 }
|
||||||
|
bet_refs.each do |bf|
|
||||||
|
place_this_bet = place_bets
|
||||||
|
unless bf['is_value_bet'] == true
|
||||||
|
stats[:non_valuebet] += 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
exchange = exchanges[(bf['bookmaker_id']).to_s]
|
||||||
|
unless exchange
|
||||||
|
stats[:unsupported_exchange] += 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
tipster_event_id = bf['event_id']
|
||||||
|
|
||||||
|
log = []
|
||||||
|
bet_id = bf['id']
|
||||||
|
|
||||||
|
vb = populate_bet_hash(subscription, bet_id, bf, exchange, log, percentages, tipster_event_id)
|
||||||
|
# on live betting, strictly adhere to max_odds rule. Otherwise bet on all odds to generate learning data.
|
||||||
|
odds_within_range = vb[:tip_provider_odds].to_f.between?( subscription.exchange_account.current_stake_strategy[:min_odds_to_bet], subscription.exchange_account.current_stake_strategy[:max_odds_to_bet])
|
||||||
|
ev_within_range = vb[:tip_provider_percent].to_f.between?( subscription.exchange_account.current_stake_strategy[:min_ev], subscription.exchange_account.current_stake_strategy[:max_ev])
|
||||||
|
unless odds_within_range && ev_within_range
|
||||||
|
log << "[Parsing] Tip provider odds not within #{subscription.exchange_account.current_stake_strategy[:min_odds_to_bet]} and #{subscription.exchange_account.current_stake_strategy[:max_odds_to_bet]}" unless odds_within_range
|
||||||
|
log << "[Parsing] Tip provider EV not within #{subscription.exchange_account.current_stake_strategy[:min_ev]} and #{subscription.exchange_account.current_stake_strategy[:max_ev]}" unless ev_within_range
|
||||||
|
vb[:outcome] = 'ignored'
|
||||||
|
place_this_bet = false
|
||||||
|
end
|
||||||
|
|
||||||
|
vb[:log] = log
|
||||||
|
skip_duplicate_event_check = false
|
||||||
|
duplicates = subscription.exchange_account.my_bets.where(tip_provider_bet_id: bet_id)
|
||||||
|
if duplicates.exists?
|
||||||
|
stats[:duplicate] += 1
|
||||||
|
if duplicates.retryable_bets.count.positive?
|
||||||
|
bet = duplicates.retryable_bets.last #only supporting 1 duplicate!
|
||||||
|
bet.log = []
|
||||||
|
bet.update(vb)
|
||||||
|
skip_duplicate_event_check = true
|
||||||
|
else
|
||||||
|
next
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless skip_duplicate_event_check
|
||||||
|
next if subscription.exchange_account.my_bets.where(tip_provider_event_id: tipster_event_id).exists? && !subscription.exchange_account.allow_multiple_bets_per_event
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
bet ||= Bet.unscoped.create(vb)
|
||||||
|
|
||||||
|
if bet && place_this_bet && vb[:exchange_market_details]
|
||||||
|
BetPlacementService.perform_later(bet: bet)
|
||||||
|
end
|
||||||
|
rescue Exception => e
|
||||||
|
puts "Something weird happened: #{e.message}"
|
||||||
|
end
|
||||||
|
stats[:processed] += 1
|
||||||
|
end
|
||||||
|
subscription.subscription_runs.create(tip_source_data_id: tsd.id, log: ["Payload: #{bet_refs.size}. #{stats}"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def populate_bet_hash(subscription, bet_id, bf, exchange, log, percentages, tipster_event_id)
|
||||||
|
vb = { tip_provider_id: 'betburger', tip_provider_bet_id: bet_id }
|
||||||
|
vb[:exchange_id] = exchange
|
||||||
|
vb[:exchange_account_id] = subscription.exchange_account.id
|
||||||
|
vb[:tip_provider_event_id] = tipster_event_id
|
||||||
|
vb[:tip_provider_odds] = bf['koef'] # what the tip provider says the odds are from the exchange
|
||||||
|
vb[:exchange_event_id] = bf['bookmaker_event_direct_link'] # may be blank
|
||||||
|
vb[:team1] = bf['team1_name']
|
||||||
|
vb[:team2] = bf['team2_name']
|
||||||
|
vb[:period] = periods[bf['period_id'].to_s.to_sym] unless bf['period_id'].blank?
|
||||||
|
vb[:exchange_event_name] = vb[:team1]
|
||||||
|
vb[:exchange_event_name] += " v #{vb[:team2]}" unless vb[:team2].blank?
|
||||||
|
vb[:exchange_event_id] = bf['bookmaker_event_direct_link'] # may be blank
|
||||||
|
vb[:tip_provider_percent] = percentages[bet_id][:percent] # dunno what this is used for
|
||||||
|
tip_market_details = {}
|
||||||
|
tip_market_details[:market] = variations[bf['market_and_bet_type']&.to_s]
|
||||||
|
tip_market_details[:params] = bf['market_and_bet_type_param']&.to_s
|
||||||
|
vb[:tip_provider_market_details] = tip_market_details
|
||||||
|
exchange_market_details = map_exchange_market_details(exchange, tip_market_details, vb[:team1], vb[:team2])
|
||||||
|
vb[:original_json] = bf
|
||||||
|
if exchange_market_details
|
||||||
|
vb[:exchange_market_details] = exchange_market_details
|
||||||
|
else
|
||||||
|
vb[:outcome] = 'ignored'
|
||||||
|
log << '[Parsing] Unsupported market/selection'
|
||||||
|
end
|
||||||
|
vb
|
||||||
|
end
|
||||||
|
|
||||||
|
def exchanges
|
||||||
|
@exchanges ||= {
|
||||||
|
'11' => 'betfair'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def map_exchange_market_details(exchange, tip_market_details, team1, team2)
|
||||||
|
market_mapping = mapped_markets(exchange)
|
||||||
|
return unless market_mapping
|
||||||
|
|
||||||
|
rtn = market_mapping[tip_market_details[:market]]&.clone
|
||||||
|
return unless rtn
|
||||||
|
|
||||||
|
swaps = { 'Team1' => team1, 'Team2' => team2, '%s' => tip_market_details[:params]}
|
||||||
|
if tip_market_details[:params]
|
||||||
|
rounded = tip_market_details[:params].to_f.round
|
||||||
|
swaps['%rounded_s'] = "#{rounded.positive? ? '+':''}#{rounded}" if tip_market_details[:params]
|
||||||
|
swaps['%rounded_positive_s'] = "#{rounded.abs}"
|
||||||
|
swaps['%nearest_half_over_s'] = determine_nearest_half(tip_market_details[:params], 'over') if tip_market_details[:params]
|
||||||
|
swaps['%nearest_half_under_s'] = determine_nearest_half(tip_market_details[:params], 'under') if tip_market_details[:params]
|
||||||
|
end
|
||||||
|
|
||||||
|
swaps.keys.each do |k|
|
||||||
|
rtn[:market] = rtn[:market].sub(k, swaps[k])
|
||||||
|
rtn[:selection] = rtn[:selection].sub(k, swaps[k])
|
||||||
|
end
|
||||||
|
if rtn.key?(:handicap)
|
||||||
|
rtn[:handicap] = tip_market_details[:params].to_f
|
||||||
|
end
|
||||||
|
rtn
|
||||||
|
end
|
||||||
|
|
||||||
|
def determine_nearest_half(line, direction )
|
||||||
|
line_f = line.to_f - line.to_i
|
||||||
|
return line if line_f == 0.5
|
||||||
|
|
||||||
|
line_f = line.to_i + 0.5
|
||||||
|
line_f -= 1 if direction == 'over'
|
||||||
|
line_f += 1 if direction == 'under'
|
||||||
|
|
||||||
|
line_f.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def mapped_markets(key)
|
||||||
|
# this is ready for other exchanges
|
||||||
|
@mapped_markets ||= { 'betfair' => betfair_markets }
|
||||||
|
@mapped_markets[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
def betfair_markets
|
||||||
|
{
|
||||||
|
'Team1 Win' => { market: 'Match Odds', selection: 'Team1' },
|
||||||
|
'Team2 Win' => { market: 'Match Odds', selection: 'Team2' },
|
||||||
|
'X' => { market: 'Match Odds', selection: 'The Draw' },
|
||||||
|
'1' => { market: 'Match Odds', selection: 'Team1' },
|
||||||
|
'2' => { market: 'Match Odds', selection: 'Team2' },
|
||||||
|
'1X' => { market: 'Double Chance', selection: 'Home or Draw' },
|
||||||
|
'12' => { market: 'Double Chance', selection: 'Home or Away' },
|
||||||
|
'X2' => { market: 'Double Chance', selection: 'Draw or Away' },
|
||||||
|
'Total Over(%s)' => { market: 'Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' },
|
||||||
|
'Total Over(%s) for Team1' => { market: 'Team1 Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' },
|
||||||
|
'Total Over(%s) for Team2' => { market: 'Team2 Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' },
|
||||||
|
'Total Under(%s)' => { market: 'Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' },
|
||||||
|
'Total Under(%s) for Team1' => { market: 'Team1 Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' },
|
||||||
|
'Total Under(%s) for Team2' => { market: 'Team2 Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' },
|
||||||
|
'Asian Handicap1(%s)' => { market: 'Asian Handicap', selection: 'Team1' },
|
||||||
|
'Asian Handicap1(0.0)/Draw No Bet' => { market: 'Draw no Bet', selection: 'Team1' },
|
||||||
|
'Asian Handicap2(0.0)/Draw No Bet' => { market: 'Draw no Bet', selection: 'Team2' },
|
||||||
|
'Asian Handicap2(%s)' => { market: 'Asian Handicap', selection: 'Team2' },
|
||||||
|
'European Handicap1(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Team1 %rounded_s' },
|
||||||
|
'European Handicap2(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Team2 %rounded_s' },
|
||||||
|
'European HandicapX(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Draw' },
|
||||||
|
'Both to score' => { market: 'Both teams to score?', selection: 'Yes' },
|
||||||
|
'One scoreless' => { market: 'Both teams to score?', selection: 'No' },
|
||||||
|
'Only one to score' => { market: 'Both teams to score?', selection: 'No' },
|
||||||
|
'Odd' => { market: 'Total Goals Odd/Even', selection: 'Odd' },
|
||||||
|
'Even' => { market: 'Total Goals Odd/Even', selection: 'Even' },
|
||||||
|
'Mat' => { market: 'Total Goals Odd/Even', selection: 'Even' },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def variations
|
||||||
|
@variations ||=
|
||||||
|
{ '1' => 'Team1 Win',
|
||||||
|
'2' => 'Team2 Win',
|
||||||
|
'3' => 'Asian Handicap1(0.0)/Draw No Bet',
|
||||||
|
'4' => 'Asian Handicap2(0.0)/Draw No Bet',
|
||||||
|
'5' => 'European Handicap1(%s)',
|
||||||
|
'6' => 'European HandicapX(%s)',
|
||||||
|
'7' => 'European Handicap2(%s)',
|
||||||
|
'8' => 'Both to score',
|
||||||
|
'9' => 'One scoreless',
|
||||||
|
'10' => 'Only one to score',
|
||||||
|
'11' => '1',
|
||||||
|
'12' => 'X',
|
||||||
|
'13' => '2',
|
||||||
|
'14' => '1X',
|
||||||
|
'15' => 'X2',
|
||||||
|
'16' => '12',
|
||||||
|
'17' => 'Asian Handicap1(%s)',
|
||||||
|
'18' => 'Asian Handicap2(%s)',
|
||||||
|
'19' => 'Total Over(%s)',
|
||||||
|
'20' => 'Total Under(%s)',
|
||||||
|
'21' => 'Total Over(%s) for Team1',
|
||||||
|
'22' => 'Total Under(%s) for Team1',
|
||||||
|
'23' => 'Total Over(%s) for Team2',
|
||||||
|
'24' => 'Total Under(%s) for Team2',
|
||||||
|
'25' => 'Odd',
|
||||||
|
'26' => 'Even',
|
||||||
|
'27' => '1 - Yellow Cards',
|
||||||
|
'28' => 'X - Yellow Cards',
|
||||||
|
'29' => '2 - Yellow Cards',
|
||||||
|
'30' => '1X - Yellow Cards',
|
||||||
|
'31' => '12 - Yellow Cards',
|
||||||
|
'32' => 'X2 - Yellow Cards',
|
||||||
|
'33' => 'Asian Handicap1(%s) - Yellow Cards',
|
||||||
|
'34' => 'Asian Handicap2(%s) - Yellow Cards',
|
||||||
|
'35' => 'Total Over(%s) - Yellow Cards',
|
||||||
|
'36' => 'Total Under(%s) - Yellow Cards',
|
||||||
|
'37' => 'Total Under(%s) for Team1 - Yellow Cards',
|
||||||
|
'38' => 'Total Over(%s) for Team1 - Yellow Cards',
|
||||||
|
'39' => 'Total Under(%s) for Team2 - Yellow Cards',
|
||||||
|
'40' => 'Total Over(%s) for Team2 - Yellow Cards',
|
||||||
|
'41' => 'Even - Yellow Cards',
|
||||||
|
'42' => 'Odd - Yellow Cards',
|
||||||
|
'43' => '1 - Corners',
|
||||||
|
'44' => 'X - Corners',
|
||||||
|
'45' => '2 - Corners',
|
||||||
|
'46' => '1X - Corners',
|
||||||
|
'47' => '12 - Corners',
|
||||||
|
'48' => 'X2 - Corners',
|
||||||
|
'49' => 'Asian Handicap1(%s) - Corners',
|
||||||
|
'50' => 'Asian Handicap2(%s) - Corners',
|
||||||
|
'51' => 'Total Over(%s) - Corners',
|
||||||
|
'52' => 'Total Under(%s) - Corners',
|
||||||
|
'53' => 'Total Under(%s) for Team1 - Corners',
|
||||||
|
'54' => 'Total Over(%s) for Team1 - Corners',
|
||||||
|
'55' => 'Total Under(%s) for Team2 - Corners',
|
||||||
|
'56' => 'Total Over(%s) for Team2 - Corners',
|
||||||
|
'57' => 'Odd - Corners',
|
||||||
|
'58' => 'Even - Corners',
|
||||||
|
'63' => 'Red card - yes',
|
||||||
|
'64' => 'No red card',
|
||||||
|
'65' => 'Penalty - yes',
|
||||||
|
'66' => 'No penalty',
|
||||||
|
'67' => 'Score (%s)',
|
||||||
|
'68' => 'Total Over(%s) - Tie Break',
|
||||||
|
'69' => 'Total Under(%s) - Tie Break',
|
||||||
|
'70' => 'Score (%s) - not',
|
||||||
|
'71' => 'Any substitute to score a goal - yes',
|
||||||
|
'72' => 'Any substitute to score a goal - no',
|
||||||
|
'73' => 'Team1 to win by exactly 1 goal - yes',
|
||||||
|
'74' => 'Team1 to win by exactly 1 goal - no',
|
||||||
|
'75' => 'Team2 to win by exactly 1 goal - yes',
|
||||||
|
'76' => 'Team2 to win by exactly 1 goal - no',
|
||||||
|
'77' => 'Team1 to win by exactly 2 goals - yes',
|
||||||
|
'78' => 'Team1 to win by exactly 2 goals - no',
|
||||||
|
'79' => 'Team1 to win by exactly 3 goals - yes',
|
||||||
|
'80' => 'Team1 to win by exactly 3 goals - no',
|
||||||
|
'81' => 'Team2 to win by exactly 2 goals - yes',
|
||||||
|
'82' => 'Team2 to win by exactly 2 goals - no',
|
||||||
|
'83' => 'Team2 to win by exactly 3 goals - yes',
|
||||||
|
'84' => 'Team2 to win by exactly 3 goals - no',
|
||||||
|
'85' => 'Team1 to win to Nil - yes',
|
||||||
|
'86' => 'Team1 to win to Nil - no',
|
||||||
|
'87' => 'Team2 to win to Nil - yes',
|
||||||
|
'88' => 'Team2 to win to Nil - no',
|
||||||
|
'89' => 'Team1 to win either halves - yes',
|
||||||
|
'90' => 'Team1 to win either halves - no',
|
||||||
|
'91' => 'Team2 to win either halves - yes',
|
||||||
|
'92' => 'Team2 to win either halves - no',
|
||||||
|
'93' => 'Draw in either half - yes',
|
||||||
|
'94' => 'Draw in either half - no',
|
||||||
|
'95' => 'Team1 to win in both halves - yes',
|
||||||
|
'96' => 'Team1 to win in both halves - no',
|
||||||
|
'97' => 'Team2 to win in both halves - yes',
|
||||||
|
'98' => 'Team2 to win in both halves - no',
|
||||||
|
'99' => 'Team1 to win and Total Over 2.5 - yes',
|
||||||
|
'100' => 'Team1 to win and Total Over 2.5 - no',
|
||||||
|
'101' => 'Team1 to win and Total Under 2.5 - yes',
|
||||||
|
'102' => 'Team1 to win and Total Under 2.5 - no',
|
||||||
|
'103' => 'Team2 to win and Total Over 2.5 - yes',
|
||||||
|
'104' => 'Team2 to win and Total Over 2.5 - no',
|
||||||
|
'105' => 'Team2 to win and Total Under 2.5 - yes',
|
||||||
|
'106' => 'Team2 to win and Total Under 2.5 - no',
|
||||||
|
'107' => 'Draw in both half - yes',
|
||||||
|
'108' => 'Draw in both half - no',
|
||||||
|
'109' => 'Draw and Total Over 2.5 - yes',
|
||||||
|
'110' => 'Draw and Total Over 2.5 - no',
|
||||||
|
'111' => 'Draw and Total Under 2.5 - yes',
|
||||||
|
'112' => 'Draw and Total Under 2.5 - no',
|
||||||
|
'113' => 'Goals in both halves - yes',
|
||||||
|
'114' => 'Goals in both halves - no',
|
||||||
|
'115' => 'Team1 to score in both halves - yes',
|
||||||
|
'116' => 'Team1 to score in both halves - no',
|
||||||
|
'117' => 'Team2 to score in both halves - yes',
|
||||||
|
'118' => 'Team2 to score in both halves - no',
|
||||||
|
'119' => 'Double - yes',
|
||||||
|
'120' => 'Double - no',
|
||||||
|
'121' => 'Hattrick - yes',
|
||||||
|
'122' => 'Hattrick - no',
|
||||||
|
'123' => 'Own goal - yes',
|
||||||
|
'124' => 'Own goal - no',
|
||||||
|
'125' => 'Both halves > 1.5 goals - yes',
|
||||||
|
'126' => 'Both halves > 1.5 goals - no',
|
||||||
|
'127' => 'Both halves < 1.5 goals - yes',
|
||||||
|
'128' => 'Both halves < 1.5 goals - no',
|
||||||
|
'129' => 'Sets (%s)',
|
||||||
|
'130' => 'Sets (%s) - not',
|
||||||
|
'131' => 'Asian Handicap1(%s) - Sets',
|
||||||
|
'132' => 'Asian Handicap2(%s) - Sets',
|
||||||
|
'133' => 'Total Over(%s) - Sets',
|
||||||
|
'134' => 'Total Under(%s) - Sets',
|
||||||
|
'135' => 'Team1/Team1',
|
||||||
|
'136' => 'Team1/Team1 - no',
|
||||||
|
'137' => 'Team1/Draw',
|
||||||
|
'138' => 'Team1/Draw - no',
|
||||||
|
'139' => 'Team1/Team2',
|
||||||
|
'140' => 'Team1/Team2 - no',
|
||||||
|
'141' => 'Draw/Team1',
|
||||||
|
'142' => 'Draw/Team1 - no',
|
||||||
|
'143' => 'Draw/Draw',
|
||||||
|
'144' => 'Draw/Draw - no',
|
||||||
|
'145' => 'Draw/Team2',
|
||||||
|
'146' => 'Draw/Team2 - no',
|
||||||
|
'147' => 'Team2/Team1',
|
||||||
|
'148' => 'Team2/Team1 - no',
|
||||||
|
'149' => 'Team2/Draw',
|
||||||
|
'150' => 'Team2/Draw - no',
|
||||||
|
'151' => 'Team2/Team2',
|
||||||
|
'152' => 'Team2/Team2 - no',
|
||||||
|
'153' => 'Exact (%s)',
|
||||||
|
'154' => 'Exact (%s) - no',
|
||||||
|
'155' => 'Exact (%s) for Team1',
|
||||||
|
'156' => 'Exact (%s) for Team1 - no',
|
||||||
|
'157' => 'Exact (%s) for Team2',
|
||||||
|
'158' => 'Exact (%s) for Team2 - no',
|
||||||
|
'159' => 'More goals in the 1st half',
|
||||||
|
'160' => 'Equal goals in halves',
|
||||||
|
'161' => 'More goals in the 2nd half',
|
||||||
|
'162' => '1 half most goals (draw no bet)',
|
||||||
|
'163' => '2 half most goals (draw no bet)',
|
||||||
|
'164' => 'Team1 - 1st goal',
|
||||||
|
'165' => 'No goal',
|
||||||
|
'166' => 'Team2 - 1st goal',
|
||||||
|
'167' => 'Team1 - 1st goal (draw no bet)',
|
||||||
|
'168' => 'Team2 - 1st goal (draw no bet)',
|
||||||
|
'169' => 'Team1 - Last goal',
|
||||||
|
'170' => 'No goal',
|
||||||
|
'171' => 'Team2 - Last goal',
|
||||||
|
'172' => 'Team1 - Last goal (draw no bet)',
|
||||||
|
'173' => 'Team2 - Last goal (draw no bet)',
|
||||||
|
'174' => 'Total Over(%s) - Aces',
|
||||||
|
'175' => 'Total Under(%s) - Aces',
|
||||||
|
'176' => 'Total Over(%s) for Team1 - Aces',
|
||||||
|
'177' => 'Total Under(%s) for Team1 - Aces',
|
||||||
|
'178' => 'Total Over(%s) for Team2 - Aces',
|
||||||
|
'179' => 'Total Under(%s) for Team2 - Aces',
|
||||||
|
'180' => 'Total Over(%s) - Double Faults',
|
||||||
|
'181' => 'Total Under(%s) - Double Faults',
|
||||||
|
'182' => 'Total Over(%s) for Team1 - Double Faults',
|
||||||
|
'183' => 'Total Under(%s) for Team1 - Double Faults',
|
||||||
|
'184' => 'Total Over(%s) for Team2 - Double Faults',
|
||||||
|
'185' => 'Total Under(%s) for Team2 - Double Faults',
|
||||||
|
'186' => 'Total Over(%s) for Team1 - 1st Serve',
|
||||||
|
'187' => 'Total Under(%s) for Team1 - 1st Serve',
|
||||||
|
'188' => 'Total Over(%s) for Team2 - 1st Serve',
|
||||||
|
'189' => 'Total Under(%s) for Team2 - 1st Serve',
|
||||||
|
'190' => 'Asian Handicap1(%s) - Aces',
|
||||||
|
'191' => 'Asian Handicap2(%s) - Aces',
|
||||||
|
'192' => 'Asian Handicap1(%s) - Double Faults',
|
||||||
|
'193' => 'Asian Handicap2(%s) - Double Faults',
|
||||||
|
'194' => 'Asian Handicap1(%s) - 1st Serve',
|
||||||
|
'195' => 'Asian Handicap2(%s) - 1st Serve',
|
||||||
|
'196' => 'Player1 - 1st Ace',
|
||||||
|
'197' => 'Player2 - 1st Ace',
|
||||||
|
'198' => 'Player1 - 1st Double Fault',
|
||||||
|
'199' => 'Player2 - 1st Double Fault',
|
||||||
|
'200' => 'Player1 - 1st Break',
|
||||||
|
'201' => 'Player2 - 1st Break',
|
||||||
|
'202' => 'Player1 - 1st Break',
|
||||||
|
'203' => 'No break - 1st Break',
|
||||||
|
'204' => 'Player2 - 1st Break',
|
||||||
|
'205' => '6-0 Set - yes',
|
||||||
|
'206' => '6-0 Set - no',
|
||||||
|
'207' => 'Win From Behind - yes',
|
||||||
|
'208' => 'Win From Behind - no',
|
||||||
|
'209' => 'Exact (%s) - Sets',
|
||||||
|
'210' => 'Exact (%s) - Sets - no',
|
||||||
|
'211' => 'Team1 - 1st corner',
|
||||||
|
'212' => 'No corners',
|
||||||
|
'213' => 'Team2 - 1st corner',
|
||||||
|
'214' => 'Team1 - Last corner',
|
||||||
|
'215' => 'No corners',
|
||||||
|
'216' => 'Team2 - Last corner',
|
||||||
|
'217' => 'Team1 - 1st Yellow Card',
|
||||||
|
'218' => 'No Yellow Card',
|
||||||
|
'219' => 'Team2 - 1st Yellow Card',
|
||||||
|
'220' => 'Team1 - Last Yellow Card',
|
||||||
|
'221' => 'No Yellow Card',
|
||||||
|
'222' => 'Team2 - Last Yellow Card',
|
||||||
|
'223' => 'Team1 - 1st offside',
|
||||||
|
'224' => 'No offsides',
|
||||||
|
'225' => 'Team2 - 1st offside',
|
||||||
|
'226' => 'Team1 - Last offside',
|
||||||
|
'227' => 'No offsides',
|
||||||
|
'228' => 'Team2 - Last offside',
|
||||||
|
'229' => '1st substitution - 1 half',
|
||||||
|
'230' => '1st substitution - intermission',
|
||||||
|
'231' => '1st substitution - 2 half',
|
||||||
|
'232' => '1st goal - 1 half',
|
||||||
|
'233' => 'No goal',
|
||||||
|
'234' => '1st goal - 2 half',
|
||||||
|
'235' => 'Team1 - 1st subs',
|
||||||
|
'236' => 'Team2 - 1st subs',
|
||||||
|
'237' => 'Team1 - Last subs',
|
||||||
|
'238' => 'Team2 - Last subs',
|
||||||
|
'239' => '1 - Shots on goal',
|
||||||
|
'240' => 'X - Shots on goal',
|
||||||
|
'241' => '2 - Shots on goal',
|
||||||
|
'242' => '1X - Shots on goal',
|
||||||
|
'243' => '12 - Shots on goal',
|
||||||
|
'244' => 'X2 - Shots on goal',
|
||||||
|
'245' => 'Asian Handicap1(%s) - Shots on goal',
|
||||||
|
'246' => 'Asian Handicap2(%s) - Shots on goal',
|
||||||
|
'247' => 'Total Over(%s) - Shots on goal',
|
||||||
|
'248' => 'Total Under(%s) - Shots on goal',
|
||||||
|
'249' => 'Total Over(%s) for Team1 - Shots on goal',
|
||||||
|
'250' => 'Total Under(%s) for Team1 - Shots on goal',
|
||||||
|
'251' => 'Total Over(%s) for Team2 - Shots on goal',
|
||||||
|
'252' => 'Total Under(%s) for Team2 - Shots on goal',
|
||||||
|
'253' => 'Odd - Shots on goal',
|
||||||
|
'254' => 'Even - Shots on goal',
|
||||||
|
'255' => '1 - Fouls',
|
||||||
|
'256' => 'X - Fouls',
|
||||||
|
'257' => '2 - Fouls',
|
||||||
|
'258' => '1X - Fouls',
|
||||||
|
'259' => '12 - Fouls',
|
||||||
|
'260' => 'X2 - Fouls',
|
||||||
|
'261' => 'Asian Handicap1(%s) - Fouls',
|
||||||
|
'262' => 'Asian Handicap2(%s) - Fouls',
|
||||||
|
'263' => 'Total Over(%s) - Fouls',
|
||||||
|
'264' => 'Total Under(%s) - Fouls',
|
||||||
|
'265' => 'Total Over(%s) for Team1 - Fouls',
|
||||||
|
'266' => 'Total Under(%s) for Team1 - Fouls',
|
||||||
|
'267' => 'Total Over(%s) for Team2 - Fouls',
|
||||||
|
'268' => 'Total Under(%s) for Team2 - Fouls',
|
||||||
|
'269' => 'Odd - Fouls',
|
||||||
|
'270' => 'Even - Fouls',
|
||||||
|
'271' => '1 - Offsides',
|
||||||
|
'272' => 'X - Offsides',
|
||||||
|
'273' => '2 - Offsides',
|
||||||
|
'274' => '1X - Offsides',
|
||||||
|
'275' => '12 - Offsides',
|
||||||
|
'276' => 'X2 - Offsides',
|
||||||
|
'277' => 'Asian Handicap1(%s) - Offsides',
|
||||||
|
'278' => 'Asian Handicap2(%s) - Offsides',
|
||||||
|
'279' => 'Total Over(%s) - Offsides',
|
||||||
|
'280' => 'Total Under(%s) - Offsides',
|
||||||
|
'281' => 'Total Over(%s) for Team1 - Offsides',
|
||||||
|
'282' => 'Total Under(%s) for Team1 - Offsides',
|
||||||
|
'283' => 'Total Over(%s) for Team2 - Offsides',
|
||||||
|
'284' => 'Total Under(%s) for Team2 - Offsides',
|
||||||
|
'285' => 'Odd - Offsides',
|
||||||
|
'286' => 'Even - Offsides',
|
||||||
|
'287' => 'Team1 to Win From Behind - yes',
|
||||||
|
'288' => 'Team1 to Win From Behind - no',
|
||||||
|
'289' => 'Team2 to Win From Behind - yes',
|
||||||
|
'290' => 'Team2 to Win From Behind - no',
|
||||||
|
'291' => 'Both To Score and W1 - yes',
|
||||||
|
'292' => 'Both To Score and W1 - no',
|
||||||
|
'293' => 'Both To Score and W2 - yes',
|
||||||
|
'294' => 'Both To Score and W2 - no',
|
||||||
|
'295' => 'Both To Score and Draw - yes',
|
||||||
|
'296' => 'Both To Score and Draw - no',
|
||||||
|
'297' => 'Exact (%s) - Added time',
|
||||||
|
'298' => 'Exact (%s) - Added time - no',
|
||||||
|
'299' => 'Total Over(%s) - Added time',
|
||||||
|
'300' => 'Total Under(%s) - Added time',
|
||||||
|
'301' => 'Home No Bet - W2',
|
||||||
|
'302' => 'Home No Bet - Draw',
|
||||||
|
'303' => 'Home No Bet - No Draw',
|
||||||
|
'304' => 'Away No Bet - W1',
|
||||||
|
'305' => 'Away No Bet - Draw',
|
||||||
|
'306' => 'Away No Bet - No Draw',
|
||||||
|
'307' => 'Total Over(%s) - Subs',
|
||||||
|
'308' => 'Total Under(%s) - Subs',
|
||||||
|
'309' => 'Total Over(%s) for Team1 - Subs',
|
||||||
|
'310' => 'Total Under(%s) for Team1 - Subs',
|
||||||
|
'311' => 'Total Over(%s) for Team2 - Subs',
|
||||||
|
'312' => 'Total Under(%s) for Team1 - Subs',
|
||||||
|
'313' => '1 - Ball possession',
|
||||||
|
'314' => 'X - Ball possession',
|
||||||
|
'315' => '2 - Ball possession',
|
||||||
|
'316' => '1X - Ball possession',
|
||||||
|
'317' => '12 - Ball possession',
|
||||||
|
'318' => 'X2 - Ball possession',
|
||||||
|
'319' => 'Asian Handicap1(%s) - Ball possession',
|
||||||
|
'320' => 'Asian Handicap2(%s) - Ball possession',
|
||||||
|
'321' => 'Total Over(%s) for Team1 - Ball possession',
|
||||||
|
'322' => 'Total Under(%s) for Team1 - Ball possession',
|
||||||
|
'323' => 'Total Over(%s) for Team2 - Ball possession',
|
||||||
|
'324' => 'Total Under(%s) for Team2 - Ball possession',
|
||||||
|
'325' => 'Team1 - 1st corner',
|
||||||
|
'326' => 'Team2 - 1st corner',
|
||||||
|
'327' => 'Team1 - Last corner',
|
||||||
|
'328' => 'Team2 - Last corner',
|
||||||
|
'329' => 'Team1 - 1st Yellow Card',
|
||||||
|
'330' => 'Team2 - 1st Yellow Card',
|
||||||
|
'331' => 'Team1 - Last Yellow Card',
|
||||||
|
'332' => 'Team2 - Last Yellow Card',
|
||||||
|
'333' => 'Team1 - 1st offside',
|
||||||
|
'334' => 'Team2 - 1st offside',
|
||||||
|
'335' => 'Team1 - Last offside',
|
||||||
|
'336' => 'Team2 - Last offside',
|
||||||
|
'337' => 'Home No Bet - No W2',
|
||||||
|
'338' => 'Away No Bet - No W1',
|
||||||
|
'339' => '1X and Total Over 2.5 - yes',
|
||||||
|
'340' => '1X and Total Over 2.5 - no',
|
||||||
|
'341' => '1X and Total Under 2.5 - yes',
|
||||||
|
'342' => '1X and Total Under 2.5 - no',
|
||||||
|
'343' => 'X2 and Total Over 2.5 - yes',
|
||||||
|
'344' => 'X2 and Total Over 2.5 - no',
|
||||||
|
'345' => 'X2 and Total Under 2.5 - yes',
|
||||||
|
'346' => 'X2 and Total Under 2.5 - no',
|
||||||
|
'348' => 'Overtime - yes',
|
||||||
|
'351' => 'Overtime - no',
|
||||||
|
'354' => 'Score Draw - yes',
|
||||||
|
'357' => 'Score Draw - no',
|
||||||
|
'360' => 'Race to 2 - Team1',
|
||||||
|
'363' => 'Race to 2 - Team2',
|
||||||
|
'366' => 'Race to 2 - Team1',
|
||||||
|
'369' => 'Race to 2 - neither',
|
||||||
|
'372' => 'Race to 2 - Team2',
|
||||||
|
'375' => 'Race to 3 - Team1',
|
||||||
|
'378' => 'Race to 3 - Team2',
|
||||||
|
'381' => 'Race to 3 - Team1',
|
||||||
|
'384' => 'Race to 3 - neither',
|
||||||
|
'387' => 'Race to 3 - Team2',
|
||||||
|
'390' => 'Race to 4 - Team1',
|
||||||
|
'393' => 'Race to 4 - Team2',
|
||||||
|
'396' => 'Race to 4 - Team1',
|
||||||
|
'399' => 'Race to 4 - neither',
|
||||||
|
'402' => 'Race to 4 - Team2',
|
||||||
|
'405' => 'Race to 5 - Team1',
|
||||||
|
'408' => 'Race to 5 - Team2',
|
||||||
|
'411' => 'Race to 5 - Team1',
|
||||||
|
'414' => 'Race to 5 - neither',
|
||||||
|
'417' => 'Race to 5 - Team2',
|
||||||
|
'420' => 'Race to 10 - Team1',
|
||||||
|
'423' => 'Race to 10 - Team2',
|
||||||
|
'426' => 'Race to 10 - Team1',
|
||||||
|
'429' => 'Race to 10 - neither',
|
||||||
|
'432' => 'Race to 10 - Team2',
|
||||||
|
'435' => 'Race to 15 - Team1',
|
||||||
|
'438' => 'Race to 15 - Team2',
|
||||||
|
'441' => 'Race to 15 - Team1',
|
||||||
|
'444' => 'Race to 15 - neither',
|
||||||
|
'447' => 'Race to 15 - Team2',
|
||||||
|
'450' => 'Race to 20 - Team1',
|
||||||
|
'453' => 'Race to 20 - Team2',
|
||||||
|
'456' => 'Race to 20 - Team1',
|
||||||
|
'459' => 'Race to 20 - neither',
|
||||||
|
'462' => 'Race to 20 - Team2',
|
||||||
|
'467' => 'Team1 - Next goal (draw no bet)',
|
||||||
|
'470' => 'Team2 - Next goal (draw no bet)',
|
||||||
|
'473' => 'Team1 - Next goal',
|
||||||
|
'476' => 'No next goal',
|
||||||
|
'479' => 'Team2 - Next goal',
|
||||||
|
'481' => '1st - yes',
|
||||||
|
'484' => '1st - no',
|
||||||
|
'487' => '1st-3rd - yes',
|
||||||
|
'490' => '1st-3rd - no',
|
||||||
|
'493' => 'W1',
|
||||||
|
'496' => 'W2',
|
||||||
|
'499' => 'TO(%s) - Hits',
|
||||||
|
'502' => 'TU(%s) - Hits',
|
||||||
|
'505' => 'TO(%s) for Team1 - Hits',
|
||||||
|
'508' => 'TU(%s) for Team1 - Hits',
|
||||||
|
'511' => 'TO(%s) for Team2 - Hits',
|
||||||
|
'514' => 'TU(%s) for Team2 - Hits',
|
||||||
|
'517' => 'TO(%s) - Errors',
|
||||||
|
'520' => 'TU(%s) - Errors',
|
||||||
|
'523' => 'TO(%s) - Hits+Errors+Runs',
|
||||||
|
'526' => 'TU(%s) - Hits+Errors+Runs',
|
||||||
|
'529' => 'AH1(%s) - Hits',
|
||||||
|
'532' => 'AH2(%s) - Hits',
|
||||||
|
'535' => '1 - Hits',
|
||||||
|
'538' => 'X - Hits',
|
||||||
|
'541' => '2 - Hits',
|
||||||
|
'546' => 'Team1 Win - Kills',
|
||||||
|
'549' => 'Team2 Win - Kills',
|
||||||
|
'552' => 'Asian Handicap1(%s) - Kills',
|
||||||
|
'555' => 'Asian Handicap2(%s) - Kills',
|
||||||
|
'558' => 'Total Over(%s) - Kills',
|
||||||
|
'561' => 'Total Under(%s) - Kills',
|
||||||
|
'564' => 'Total Over(%s) for Team1 - Kills',
|
||||||
|
'567' => 'Total Under(%s) for Team1 - Kills',
|
||||||
|
'570' => 'Total Over(%s) for Team2 - Kills',
|
||||||
|
'573' => 'Total Under(%s) for Team2 - Kills',
|
||||||
|
'574' => 'W1 - 1st blood',
|
||||||
|
'575' => 'W2 - 1st blood',
|
||||||
|
'576' => 'W1 - 1st tower',
|
||||||
|
'577' => 'W2 - 1st tower',
|
||||||
|
'578' => 'W1 - 1st dragon',
|
||||||
|
'579' => 'W2 - 1st dragon',
|
||||||
|
'580' => 'W1 - 1st baron',
|
||||||
|
'581' => 'W2 - 1st baron',
|
||||||
|
'582' => 'W1 - 1st inhibitor',
|
||||||
|
'583' => 'W2 - 1st inhibitor',
|
||||||
|
'584' => 'W1 - 1st roshan',
|
||||||
|
'585' => 'W2 - 1st roshan',
|
||||||
|
'586' => 'Win pistol rounds - Yes',
|
||||||
|
'587' => 'Win pistol rounds - No',
|
||||||
|
'588' => 'TO(%s) - Duration',
|
||||||
|
'589' => 'TU(%s) - Duration',
|
||||||
|
'590' => 'TO(%s) - Barons',
|
||||||
|
'591' => 'TU(%s) - Barons',
|
||||||
|
'592' => 'TO(%s) - Inhibitors',
|
||||||
|
'593' => 'TU(%s) - Inhibitors',
|
||||||
|
'594' => 'TO(%s) - Towers',
|
||||||
|
'595' => 'TU(%s) - Towers',
|
||||||
|
'596' => 'TO(%s) - Dragons',
|
||||||
|
'597' => 'TU(%s) - Dragons',
|
||||||
|
'598' => 'TO(%s) - Roshans',
|
||||||
|
'599' => 'TU(%s) - Roshans',
|
||||||
|
'600' => 'TO(%s) for Team1 - Sets',
|
||||||
|
'601' => 'TO(%s) for Team1 - Sets',
|
||||||
|
'602' => 'TO(%s) for Team2 - Sets',
|
||||||
|
'603' => 'TO(%s) for Team2 - Sets',
|
||||||
|
'604' => 'W1 - Longest TD',
|
||||||
|
'605' => 'W2 - Longest TD',
|
||||||
|
'606' => 'W1 - Longest FG',
|
||||||
|
'607' => 'W2 - Longest FG',
|
||||||
|
'608' => 'Touchdown - Yes',
|
||||||
|
'609' => 'Touchdown - No',
|
||||||
|
'610' => 'Safety - Yes',
|
||||||
|
'611' => 'Safety - No',
|
||||||
|
'612' => 'First score TD - Yes',
|
||||||
|
'613' => 'First score TD - No',
|
||||||
|
'614' => 'Both teams 10 pts - Yes',
|
||||||
|
'615' => 'Both teams 10 pts - No',
|
||||||
|
'616' => 'Both teams 15 pts - Yes',
|
||||||
|
'617' => 'Both teams 15 pts - No',
|
||||||
|
'618' => 'Both teams 20 pts - Yes',
|
||||||
|
'619' => 'Both teams 20 pts - No',
|
||||||
|
'620' => 'Both teams 25 pts - Yes',
|
||||||
|
'621' => 'Both teams 25 pts - No',
|
||||||
|
'622' => 'Both teams 30 pts - Yes',
|
||||||
|
'623' => 'Both teams 30 pts - No',
|
||||||
|
'624' => 'Both teams 35 pts - Yes',
|
||||||
|
'625' => 'Both teams 35 pts - No',
|
||||||
|
'626' => 'Both teams 40 pts - Yes',
|
||||||
|
'627' => 'Both teams 40 pts - No',
|
||||||
|
'628' => 'Both teams 45 pts - Yes',
|
||||||
|
'629' => 'Both teams 45 pts - No',
|
||||||
|
'630' => 'Both teams 50 pts - Yes',
|
||||||
|
'631' => 'Both teams 50 pts - No',
|
||||||
|
'632' => 'Highest Scoring Quarter - 1st',
|
||||||
|
'633' => 'Highest Scoring Quarter - 2nd',
|
||||||
|
'634' => 'Highest Scoring Quarter - 3rd',
|
||||||
|
'635' => 'Highest Scoring Quarter - 4th',
|
||||||
|
'636' => 'Highest Scoring Quarter - Tie',
|
||||||
|
'637' => 'TO(%s) - Field Goals',
|
||||||
|
'638' => 'TU(%s) - Field Goals',
|
||||||
|
'639' => 'TO(%s) - Touchdowns',
|
||||||
|
'640' => 'TU(%s) - Touchdowns',
|
||||||
|
'641' => 'TO(%s) - Longest TD, distance',
|
||||||
|
'642' => 'TU(%s) - Longest TD, distance',
|
||||||
|
'643' => 'TO(%s) - Longest FG, distance',
|
||||||
|
'644' => 'TU(%s) - Longest FG, distance',
|
||||||
|
'645' => 'TO(%s) for Team1 - Field Goals',
|
||||||
|
'646' => 'TU(%s) for Team1 - Field Goals',
|
||||||
|
'647' => 'TO(%s) for Team2 - Field Goals',
|
||||||
|
'648' => 'TU(%s) for Team2 - Field Goals',
|
||||||
|
'649' => 'TO(%s) for Team1 - Touchdowns',
|
||||||
|
'650' => 'TU(%s) for Team1 - Touchdowns',
|
||||||
|
'651' => 'TO(%s) for Team2 - Touchdowns',
|
||||||
|
'652' => 'TU(%s) for Team2 - Touchdowns',
|
||||||
|
'653' => 'AH1(%s) - Maps',
|
||||||
|
'654' => 'AH2(%s) - Maps',
|
||||||
|
'655' => 'TO(%s) - Maps',
|
||||||
|
'656' => 'TU(%s) - Maps',
|
||||||
|
'657' => 'TO(%s) for Team1 - Maps',
|
||||||
|
'658' => 'TU(%s) for Team1 - Maps',
|
||||||
|
'659' => 'TO(%s) for Team2 - Maps',
|
||||||
|
'660' => 'TU(%s) for Team2 - Maps',
|
||||||
|
'661' => 'Maps (%s)',
|
||||||
|
'662' => 'Maps (%s) - not',
|
||||||
|
'663' => 'Exact (%s) - Maps',
|
||||||
|
'664' => 'Exact (%s) - Maps - no',
|
||||||
|
'665' => 'Win both pistol rounds - Yes',
|
||||||
|
'666' => 'Win both pistol rounds - No',
|
||||||
|
'667' => 'Team1 to win both pistol rounds - Yes',
|
||||||
|
'668' => 'Team1 to win both pistol rounds - No',
|
||||||
|
'669' => 'Team2 to win both pistol rounds - Yes',
|
||||||
|
'670' => 'Team1 to win both pistol rounds - No',
|
||||||
|
'671' => 'Team1 to win at least 1 set - Yes',
|
||||||
|
'672' => 'Team1 to win at least 1 set - No',
|
||||||
|
'673' => 'Team2 to win at least 1 set - Yes',
|
||||||
|
'674' => 'Team1 to win at least 1 set - No',
|
||||||
|
'675' => 'Team1 to win at least 1 map - Yes',
|
||||||
|
'676' => 'Team1 to win at least 1 map - No',
|
||||||
|
'677' => 'Team2 to win at least 1 map - Yes',
|
||||||
|
'678' => 'Team1 to win at least 1 map - No',
|
||||||
|
'679' => 'Both teams kill a dragon - Yes',
|
||||||
|
'680' => 'Both teams kill a dragon - No',
|
||||||
|
'681' => 'Both teams kill a baron - Yes',
|
||||||
|
'682' => 'Both teams kill a baron - No',
|
||||||
|
'683' => 'W1 - 1st Barracks',
|
||||||
|
'684' => 'W2 - 1st Barracks',
|
||||||
|
'685' => 'W1 - 1st Double Kill',
|
||||||
|
'686' => 'W2 - 1st Double Kill',
|
||||||
|
'687' => 'TO(%s) - Barracks',
|
||||||
|
'688' => 'TU(%s) - Barracks',
|
||||||
|
'689' => 'TO(%s) - Double Kills',
|
||||||
|
'690' => 'TU(%s) - Double Kills',
|
||||||
|
'691' => 'AH1(%s) - Barons',
|
||||||
|
'692' => 'AH2(%s) - Barons',
|
||||||
|
'693' => 'AH1(%s) - Dragons',
|
||||||
|
'694' => 'AH2(%s) - Dragons',
|
||||||
|
'695' => 'AH1(%s) - Towers/Turrets',
|
||||||
|
'696' => 'AH2(%s) - Towers/Turrets',
|
||||||
|
'697' => '1 - 3-points',
|
||||||
|
'698' => 'X - 3-points',
|
||||||
|
'699' => '2 - 3-points',
|
||||||
|
'700' => '1 - Rebounds',
|
||||||
|
'701' => 'X - Rebounds',
|
||||||
|
'702' => '2 - Rebounds',
|
||||||
|
'703' => '1 - Assists',
|
||||||
|
'704' => 'X - Assists',
|
||||||
|
'705' => '2 - Assists',
|
||||||
|
'706' => '1X - 3-points',
|
||||||
|
'707' => 'X2 - 3-points',
|
||||||
|
'708' => '12 - 3-points',
|
||||||
|
'709' => '1X - Rebounds',
|
||||||
|
'710' => 'X2 - Rebounds',
|
||||||
|
'711' => '12 - Rebounds',
|
||||||
|
'712' => '1X - Assists',
|
||||||
|
'713' => 'X2 - Assists',
|
||||||
|
'714' => '12 - Assists',
|
||||||
|
'715' => 'AH1(%s) - 3-points',
|
||||||
|
'716' => 'AH2(%s) - 3-points',
|
||||||
|
'717' => 'AH1(%s) - Rebounds',
|
||||||
|
'718' => 'AH2(%s) - Rebounds',
|
||||||
|
'719' => 'AH1(%s) - Assists',
|
||||||
|
'720' => 'AH2(%s) - Assists',
|
||||||
|
'721' => 'TO(%s) - 3-points',
|
||||||
|
'722' => 'TU(%s) - 3-points',
|
||||||
|
'723' => 'TO(%s) - Rebounds',
|
||||||
|
'724' => 'TU(%s) - Rebounds',
|
||||||
|
'725' => 'TO(%s) - Assists',
|
||||||
|
'726' => 'TU(%s) - Assists',
|
||||||
|
'727' => 'TO(%s) for Team1 - 3-points',
|
||||||
|
'728' => 'TU(%s) for Team1 - 3-points',
|
||||||
|
'729' => 'TO(%s) for Team1 - Rebounds',
|
||||||
|
'730' => 'TU(%s) for Team1 - Rebounds',
|
||||||
|
'731' => 'TO(%s) for Team1 - Assists',
|
||||||
|
'732' => 'TU(%s) for Team1 - Assists',
|
||||||
|
'733' => 'TO(%s) for Team2 - 3-points',
|
||||||
|
'734' => 'TU(%s) for Team2 - 3-points',
|
||||||
|
'735' => 'TO(%s) for Team2 - Rebounds',
|
||||||
|
'736' => 'TU(%s) for Team2 - Rebounds',
|
||||||
|
'737' => 'TO(%s) for Team2 - Assists',
|
||||||
|
'738' => 'TU(%s) for Team2 - Assists',
|
||||||
|
'739' => 'W1 - 180s',
|
||||||
|
'740' => 'W2 - 180s',
|
||||||
|
'741' => 'AH1(%s) - 180s',
|
||||||
|
'742' => 'AH2(%s) - 180s',
|
||||||
|
'743' => 'TO(%s) - 180s',
|
||||||
|
'744' => 'TU(%s) - 180s',
|
||||||
|
'745' => '1 - Cards',
|
||||||
|
'746' => 'X - Cards',
|
||||||
|
'747' => '2 - Cards',
|
||||||
|
'748' => '1 - Booking points',
|
||||||
|
'749' => 'X - Booking points',
|
||||||
|
'750' => '2 - Booking points',
|
||||||
|
'751' => '1X - Cards',
|
||||||
|
'752' => 'X2 - Cards',
|
||||||
|
'753' => '12 - Cards',
|
||||||
|
'754' => '1X - Booking points',
|
||||||
|
'755' => 'X2 - Booking points',
|
||||||
|
'756' => '12 - Booking points',
|
||||||
|
'757' => 'AH1(%s) - Cards',
|
||||||
|
'758' => 'AH2(%s) - Cards',
|
||||||
|
'759' => 'AH1(%s) - Booking points',
|
||||||
|
'760' => 'AH2(%s) - Booking points',
|
||||||
|
'761' => 'TO(%s) - Cards',
|
||||||
|
'762' => 'TU(%s) - Cards',
|
||||||
|
'763' => 'TO(%s) - Booking points',
|
||||||
|
'764' => 'TU(%s) - Booking points',
|
||||||
|
'765' => 'TO(%s) for Team1 - Cards',
|
||||||
|
'766' => 'TU(%s) for Team1 - Cards',
|
||||||
|
'767' => 'TO(%s) for Team1 - Booking points',
|
||||||
|
'768' => 'TU(%s) for Team1 - Booking points',
|
||||||
|
'769' => 'TO(%s) for Team2 - Cards',
|
||||||
|
'770' => 'TU(%s) for Team2 - Cards',
|
||||||
|
'771' => 'TO(%s) for Team2 - Booking points',
|
||||||
|
'772' => 'TU(%s) for Team2 - Booking points',
|
||||||
|
'773' => 'Odd - Cards',
|
||||||
|
'774' => 'Even - Cards',
|
||||||
|
'775' => 'Odd - Booking points',
|
||||||
|
'776' => 'Even - Booking points' }
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def periods
|
||||||
|
@periods ||=
|
||||||
|
{
|
||||||
|
'1': 'match (pairs)',
|
||||||
|
'2': 'with OT and SO',
|
||||||
|
'3': 'with OT',
|
||||||
|
'4': 'regular time', #match odds
|
||||||
|
'5': '1st',
|
||||||
|
'6': '2nd',
|
||||||
|
'7': '3rd',
|
||||||
|
'8': '4th',
|
||||||
|
'9': '5th',
|
||||||
|
'10': '1st half', #first
|
||||||
|
'13': '2nd half',
|
||||||
|
'14': '1 set, 01 game',
|
||||||
|
'15': '1 set, 02 game',
|
||||||
|
'16': '1 set, 03 game',
|
||||||
|
'17': '1 set, 04 game',
|
||||||
|
'18': '1 set, 05 game',
|
||||||
|
'19': '1 set, 06 game',
|
||||||
|
'20': '1 set, 07 game',
|
||||||
|
'21': '1 set, 08 game',
|
||||||
|
'22': '1 set, 09 game',
|
||||||
|
'23': '1 set, 10 game',
|
||||||
|
'24': '1 set, 11 game',
|
||||||
|
'25': '1 set, 12 game',
|
||||||
|
'26': '1 set, 13 game',
|
||||||
|
'44': '2 set, 01 game',
|
||||||
|
'45': '2 set, 02 game',
|
||||||
|
'46': '2 set, 03 game',
|
||||||
|
'47': '2 set, 04 game',
|
||||||
|
'48': '2 set, 05 game',
|
||||||
|
'49': '2 set, 06 game',
|
||||||
|
'50': '2 set, 07 game',
|
||||||
|
'51': '2 set, 08 game',
|
||||||
|
'52': '2 set, 09 game',
|
||||||
|
'53': '2 set, 10 game',
|
||||||
|
'54': '2 set, 11 game',
|
||||||
|
'55': '2 set, 12 game',
|
||||||
|
'56': '2 set, 13 game',
|
||||||
|
'57': '3 set, 01 game',
|
||||||
|
'58': '3 set, 02 game',
|
||||||
|
'59': '3 set, 03 game',
|
||||||
|
'60': '3 set, 04 game',
|
||||||
|
'61': '3 set, 05 game',
|
||||||
|
'62': '3 set, 06 game',
|
||||||
|
'63': '3 set, 07 game',
|
||||||
|
'64': '3 set, 08 game',
|
||||||
|
'65': '3 set, 09 game',
|
||||||
|
'66': '3 set, 10 game',
|
||||||
|
'67': '3 set, 11 game',
|
||||||
|
'68': '3 set, 12 game',
|
||||||
|
'71': '3 set, 13 game',
|
||||||
|
'76': '6th',
|
||||||
|
'78': '7th',
|
||||||
|
'86': 'regular time',
|
||||||
|
'92': 'regular time',
|
||||||
|
'93': '1st half',
|
||||||
|
'95': 'to qualify',
|
||||||
|
'96': '8th',
|
||||||
|
'97': '9th',
|
||||||
|
'113': '4 set, 01 game',
|
||||||
|
'114': '4 set, 02 game',
|
||||||
|
'115': '4 set, 03 game',
|
||||||
|
'116': '4 set, 04 game',
|
||||||
|
'117': '4 set, 05 game',
|
||||||
|
'118': '4 set, 06 game',
|
||||||
|
'119': '4 set, 07 game',
|
||||||
|
'120': '4 set, 08 game',
|
||||||
|
'121': '4 set, 09 game',
|
||||||
|
'122': '4 set, 10 game',
|
||||||
|
'123': '4 set, 11 game',
|
||||||
|
'124': '4 set, 12 game',
|
||||||
|
'125': '5 set, 01 game',
|
||||||
|
'126': '5 set, 02 game',
|
||||||
|
'127': '5 set, 03 game',
|
||||||
|
'128': '5 set, 04 game',
|
||||||
|
'129': '5 set, 05 game',
|
||||||
|
'130': '5 set, 06 game',
|
||||||
|
'131': '5 set, 07 game',
|
||||||
|
'132': '5 set, 08 game',
|
||||||
|
'133': '5 set, 09 game',
|
||||||
|
'134': '5 set, 10 game',
|
||||||
|
'156': '4 set, 13 game',
|
||||||
|
'159': '5 set, 11 game',
|
||||||
|
'161': '5 set, 12 game',
|
||||||
|
'169': '5 set, 13 game',
|
||||||
|
'223': 'regular time',
|
||||||
|
'243': '1st half',
|
||||||
|
'245': '2nd half',
|
||||||
|
'246': '2nd half',
|
||||||
|
'317': '1st half',
|
||||||
|
'318': '2nd half',
|
||||||
|
'4091': 'regular time', #match odds
|
||||||
|
'4094': '1st half'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
16
portal/app/lib/integrations/betfair/account_manager.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module Integrations
|
||||||
|
module Betfair
|
||||||
|
class AccountManager < Base
|
||||||
|
def refresh_account_balance
|
||||||
|
# go check the account balance and update the ExchangeAccount for this account
|
||||||
|
res = self.class.post("#{API_ACCOUNT_ENDPOINT}/getAccountFunds/", { headers: @connection.api_headers, data: {} })
|
||||||
|
if res['availableToBetBalance']
|
||||||
|
acc_bal = res['availableToBetBalance']
|
||||||
|
@account.update(account_balance: acc_bal, account_balance_last_checked: Time.now)
|
||||||
|
end
|
||||||
|
@account.account_balance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
40
portal/app/lib/integrations/betfair/base.rb
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
module Integrations
|
||||||
|
module Betfair
|
||||||
|
class Base
|
||||||
|
include HTTParty
|
||||||
|
|
||||||
|
API_BETTING_ENDPOINT = 'https://api.betfair.com/exchange/betting/rest/v1.0'.freeze
|
||||||
|
API_ACCOUNT_ENDPOINT = 'https://api.betfair.com/exchange/account/rest/v1.0'.freeze
|
||||||
|
|
||||||
|
def initialize(account_friendly_id)
|
||||||
|
@account = ExchangeAccount.find_by_id(account_friendly_id)
|
||||||
|
unless @account
|
||||||
|
puts "No exchange account for '#{account_friendly_id}'. Stopping"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.class.pem @account.ssl_pem
|
||||||
|
@connection = Integrations::Betfair::Connection.new(@account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def minimum_stake
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_events(filter)
|
||||||
|
body = { filter: filter }
|
||||||
|
self.class.post("#{API_BETTING_ENDPOINT}/listEvents/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
end
|
||||||
|
|
||||||
|
def debug_list_market_types
|
||||||
|
body = { filter: {} }
|
||||||
|
marketTypes = self.class.post("#{API_BETTING_ENDPOINT}/listMarketTypes/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
results = []
|
||||||
|
marketTypes.each do |market|
|
||||||
|
results << market['marketType']
|
||||||
|
end
|
||||||
|
File.write(Rails.root.join('samples/bf_market_types.json'), results.join("\n"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
173
portal/app/lib/integrations/betfair/bet_manager.rb
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
module Integrations
|
||||||
|
module Betfair
|
||||||
|
class BetManager < Base
|
||||||
|
def check_qualified_bet_outcome(bets)
|
||||||
|
bets_lookup = {}
|
||||||
|
market_ids = []
|
||||||
|
bets_by_bet_ids = []
|
||||||
|
reconciliation_limit = 20
|
||||||
|
bet_count = 0
|
||||||
|
bets.each do |bet|
|
||||||
|
bet_count += 1
|
||||||
|
next unless bet.exchange_market_details['market_id'] && bet.exchange_market_details['selection_id']
|
||||||
|
|
||||||
|
market_ids << bet.exchange_market_details['market_id']
|
||||||
|
bets_lookup[bet.exchange_market_details['market_id'].to_s] = { selection_id: bet.exchange_market_details['selection_id'], bet_id: bet.id }
|
||||||
|
bets_by_bet_ids << bet unless bet.exchange_bet_id.blank?
|
||||||
|
next unless (reconciliation_limit == market_ids.size) || (bet_count == bets.count)
|
||||||
|
|
||||||
|
reconcile_bets_by_markets(market_ids, bets_lookup) unless market_ids.empty?
|
||||||
|
market_ids = []
|
||||||
|
bets_lookup = {}
|
||||||
|
end
|
||||||
|
reconcile_bets_by_bet_ids(bets_by_bet_ids) unless bets_by_bet_ids.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reconcile_bets_by_markets(market_ids, bets_lookup)
|
||||||
|
outcomes = { LOSER: [], WINNER: [] }
|
||||||
|
body = { marketIds: market_ids }
|
||||||
|
markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketBook/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
markets.each do |market|
|
||||||
|
next unless market['status'] == 'CLOSED'
|
||||||
|
|
||||||
|
bet = bets_lookup[market['marketId']]
|
||||||
|
next unless bet
|
||||||
|
|
||||||
|
runners = market['runners']
|
||||||
|
runners.each do |runner|
|
||||||
|
next unless runner['selectionId'] == bet[:selection_id]
|
||||||
|
|
||||||
|
x_status = runner['status'].to_sym
|
||||||
|
if outcomes[x_status]
|
||||||
|
outcomes[x_status] << bet[:bet_id]
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ActiveRecord::Base.connection.execute("update bets set outcome = 'lost', outcome_value=stake where #{ActiveRecord::Base.sanitize_sql(['id in (?)', outcomes[:LOSER]])}") if outcomes[:LOSER].size.positive?
|
||||||
|
ActiveRecord::Base.connection.execute("update bets set outcome = 'won', outcome_value=expected_value where #{ActiveRecord::Base.sanitize_sql(['id in (?)', outcomes[:WINNER]])}") if outcomes[:WINNER].size.positive?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reconcile_bets_by_bet_ids(bets)
|
||||||
|
return unless bets.count.positive?
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
open_bet_ids = bets.pluck(:exchange_bet_id)
|
||||||
|
%w[SETTLED VOIDED CANCELLED].each do |status|
|
||||||
|
body = { betStatus: status, betIds: open_bet_ids }
|
||||||
|
r = self.class.post("#{API_BETTING_ENDPOINT}/listClearedOrders/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
orders = r['clearedOrders']
|
||||||
|
if status == 'SETTLED'
|
||||||
|
# the status updates are by betOutcome
|
||||||
|
orders.each do |o|
|
||||||
|
results[o['betOutcome']] ||= []
|
||||||
|
results[o['betOutcome']] << o['betId']
|
||||||
|
end
|
||||||
|
else
|
||||||
|
results[status] ||= []
|
||||||
|
bet_ids = orders.map { |o| o['betId'] }
|
||||||
|
results[status] = bet_ids
|
||||||
|
end
|
||||||
|
end
|
||||||
|
results.keys.each do |k|
|
||||||
|
next if results[k].blank?
|
||||||
|
|
||||||
|
Bet.unscoped.open.where(exchange_bet_id: results[k]).update(outcome: k.downcase)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def place_bet(bet, stake)
|
||||||
|
body = { marketId: bet.exchange_market_details['market_id'], customerRef: bet.tip_provider_bet_id }
|
||||||
|
body[:instructions] =
|
||||||
|
[{ orderType: 'LIMIT', side: 'BACK', selectionId: bet.exchange_market_details['selection_id'].to_s,
|
||||||
|
limitOrder: { timeInForce: 'FILL_OR_KILL', size: stake.to_s, price: bet.tip_provider_odds.to_s, persistenceType: 'LAPSE' } }]
|
||||||
|
r = self.class.post("#{API_BETTING_ENDPOINT}/placeOrders/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
|
||||||
|
success = r['status'] == 'SUCCESS'
|
||||||
|
if success
|
||||||
|
bet_id = r['instructionReports'][0]['betId']
|
||||||
|
return bet_id
|
||||||
|
end
|
||||||
|
error_code = r['errorCode']
|
||||||
|
raise "[Place bet] Placing bet failed: #{error_code}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def bet_event(bet, update_bet = false)
|
||||||
|
# return the event for this bet.
|
||||||
|
# easy if we have the event_id, else we have to search.
|
||||||
|
event_id = []
|
||||||
|
event_id << bet.exchange_event_id unless bet.exchange_event_id.blank?
|
||||||
|
events = list_events({ eventIds: event_id, textQuery: bet.exchange_event_name })
|
||||||
|
e_id = events[0]['event']['id'] if events.length.positive?
|
||||||
|
if e_id
|
||||||
|
bet.update(exchange_event_id: e_id) if update_bet
|
||||||
|
return e_id
|
||||||
|
end
|
||||||
|
raise '[bet_event] Error getting event id'
|
||||||
|
end
|
||||||
|
|
||||||
|
def bet_odds(bet)
|
||||||
|
event_market_selection_hash = event_market_selection(bet)
|
||||||
|
raise '[bet odds] - market not available' unless event_market_selection_hash['market_id']
|
||||||
|
raise '[bet odds] - selection not available' unless event_market_selection_hash['selection_id']
|
||||||
|
|
||||||
|
body = { marketId: event_market_selection_hash['market_id'], selectionId: event_market_selection_hash['selection_id'] }
|
||||||
|
body[:priceProjection] = { priceData: ['EX_ALL_OFFERS'], virtualise: true }
|
||||||
|
r = self.class.post("#{API_BETTING_ENDPOINT}/listRunnerBook/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
runners = r[0]['runners']
|
||||||
|
raise '[Bet odds] - cannot identify prices' unless runners
|
||||||
|
|
||||||
|
rs = runners.first
|
||||||
|
raise '[Bet odds] - cannot identify prices' unless rs && rs['ex'] && rs['ex']['availableToBack']
|
||||||
|
|
||||||
|
prices = []
|
||||||
|
stakes = {}
|
||||||
|
rs['ex']['availableToBack'].each do |ex|
|
||||||
|
prices << ex['price']
|
||||||
|
stakes[(ex['price']).to_s] = ex['size']
|
||||||
|
end
|
||||||
|
{ prices: prices, stakes: stakes, liquidity: r[0]["totalAvailable"] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def event_market_selection(bet)
|
||||||
|
if bet.exchange_event_id.blank?
|
||||||
|
bet_event(bet, true)
|
||||||
|
raise '[No event id]' if bet.exchange_event_id.blank?
|
||||||
|
end
|
||||||
|
body = { maxResults: 500, filter: { eventIds: [bet.exchange_event_id] }, marketProjection: %w[MARKET_DESCRIPTION RUNNER_DESCRIPTION RUNNER_METADATA] }
|
||||||
|
markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketCatalogue/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
m_details = bet.exchange_market_details
|
||||||
|
returned_markets = []
|
||||||
|
returned_selections = []
|
||||||
|
markets.each do |market|
|
||||||
|
returned_markets << market['marketName']
|
||||||
|
next unless m_details['market'].casecmp(market['marketName']).zero?
|
||||||
|
|
||||||
|
m_details['market_id'] = market['marketId']
|
||||||
|
fuzzy_match_runners = market['marketName'] == 'Match Odds'
|
||||||
|
runners = market['runners']
|
||||||
|
m_selection = m_details['selection']
|
||||||
|
runners.each do |runner|
|
||||||
|
returned_selections << runner['runnerName']
|
||||||
|
runner_name_matched = runner['runnerName'].casecmp(m_selection).zero?
|
||||||
|
if !runner_name_matched && fuzzy_match_runners
|
||||||
|
runner_name_matched = (runner['runnerName'].downcase.split & m_selection.downcase.split).length.positive?
|
||||||
|
end
|
||||||
|
next unless runner_name_matched
|
||||||
|
|
||||||
|
m_details['selection_id'] = runner['selectionId']
|
||||||
|
m_details['selection'] = runner['runnerName']
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
m_details['returned_markets'] = returned_markets unless m_details.key?('market_id')
|
||||||
|
m_details['returned_selections'] = returned_selections unless m_details.key?('selection_id')
|
||||||
|
|
||||||
|
bet.update(exchange_market_details: m_details)
|
||||||
|
m_details
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
39
portal/app/lib/integrations/betfair/connection.rb
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
module Integrations
|
||||||
|
module Betfair
|
||||||
|
class Connection
|
||||||
|
|
||||||
|
include HTTParty
|
||||||
|
def initialize(account)
|
||||||
|
@account = account
|
||||||
|
self.class.pem @account.ssl_pem
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_headers
|
||||||
|
{ 'X-Application' => @account.apikey, 'X-Authentication' => session_token, 'content-type' => 'application/json', 'accept' => 'application/json' }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def session_token
|
||||||
|
# if
|
||||||
|
puts 'Checking if session still fresh'
|
||||||
|
if @account.last_session_token_saved_at && @account.last_session_token_saved_at > 10.hours.ago
|
||||||
|
puts 'Returning cached session token'
|
||||||
|
return @account.last_session_token
|
||||||
|
end
|
||||||
|
|
||||||
|
puts 'Cache is stale or non-existent - getting fresh session key'
|
||||||
|
url = 'https://identitysso-cert.betfair.com/api/certlogin'
|
||||||
|
r = self.class.post(url, headers: { 'X-Application' => @account.apikey }, body: { username: @account.login_uid, password: @account.login_pass })
|
||||||
|
resp = JSON.parse(r)
|
||||||
|
if resp['loginStatus'] == 'SUCCESS'
|
||||||
|
@account.update(last_session_token: resp['sessionToken'], last_session_token_saved_at: Time.now)
|
||||||
|
return resp['sessionToken']
|
||||||
|
end
|
||||||
|
|
||||||
|
raise '[Betfair Session token] Cannot get session to Betfair'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
77
portal/app/lib/integrations/betfair/opportunity_hunter.rb
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
module Integrations
|
||||||
|
module Betfair
|
||||||
|
class OpportunityHunter < Base
|
||||||
|
def events_in_timeframe(from:, to:)
|
||||||
|
raise "Timeframe not set " unless from.present? && to.present?
|
||||||
|
|
||||||
|
timeframe = { from: from.iso8601, to: to.iso8601 }
|
||||||
|
filter = { marketStartTime: timeframe }
|
||||||
|
body = { filter: filter }
|
||||||
|
|
||||||
|
r = self.class.post("#{API_BETTING_ENDPOINT}/listEvents/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
events = []
|
||||||
|
r.each do |e|
|
||||||
|
ev = e['event']
|
||||||
|
events << BetfairEvent.new(event_id: ev['id'], event_name: ev['name'], event_start: DateTime.parse(ev['openDate']))
|
||||||
|
end
|
||||||
|
import_result = BetfairEvent.import(events, on_duplicate_key_ignore: true)
|
||||||
|
puts "#{import_result.ids.size} events added"
|
||||||
|
end
|
||||||
|
|
||||||
|
def event_markets_and_selections
|
||||||
|
batches = []
|
||||||
|
batch = 0
|
||||||
|
limit = 10
|
||||||
|
BetfairEvent.open.order(created_at: :desc).pluck(:event_id).each do |eid|
|
||||||
|
batches[batch] ||= []
|
||||||
|
if batches[batch].size < limit
|
||||||
|
batches[batch] << eid
|
||||||
|
else
|
||||||
|
batch += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
batches.each { |b| batch_event_runners b }
|
||||||
|
end
|
||||||
|
|
||||||
|
def runner_odds(runner)
|
||||||
|
body = { marketId: runner.market_id, selectionId: runner.selection_id }
|
||||||
|
body[:priceProjection] = { priceData: ['EX_BEST_OFFERS'], virtualise: true }
|
||||||
|
r = self.class.post("#{API_BETTING_ENDPOINT}/listRunnerBook/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
runners = r[0]['runners']
|
||||||
|
raise '[Odds] - cannot identify prices' unless runners
|
||||||
|
|
||||||
|
rs = runners.first
|
||||||
|
raise '[Odds] - cannot identify prices' unless rs && rs['ex'] && rs['ex']['availableToBack']
|
||||||
|
|
||||||
|
imports = []
|
||||||
|
rs['ex']['availableToBack'].each do |ex|
|
||||||
|
imports << BetfairRunnerOdd.new(betfair_event_runner_id: runner.id, odds: ex['price'], total_matched: 0, total_available: ex['size'], bet_type: 'back')
|
||||||
|
end
|
||||||
|
rs['ex']['availableToLay'].each do |ex|
|
||||||
|
imports << BetfairRunnerOdd.new(betfair_event_runner_id: runner.id, odds: ex['price'], total_matched: 0, total_available: ex['size'], bet_type: 'lay')
|
||||||
|
end
|
||||||
|
|
||||||
|
BetfairRunnerOdd.import(imports, on_duplicate_key_ignore: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def batch_event_runners(batch)
|
||||||
|
body = { maxResults: 1000 - (10 * (batch.size - 1)), filter: { eventIds: batch }, marketProjection: ['EVENT', 'RUNNER_DESCRIPTION'] }
|
||||||
|
markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketCatalogue/", { headers: @connection.api_headers, body: body.to_json })
|
||||||
|
import = []
|
||||||
|
markets.each do |market|
|
||||||
|
market_fragment = { event_id: market['event']['id'], market_id: market['marketId'], market_name: market['marketName'] }
|
||||||
|
runners = market['runners']
|
||||||
|
runners&.each do |runner|
|
||||||
|
rec = { selection_id: runner['selectionId'] || runner['runnerName'], selection_name: runner['runnerName'] }.merge(market_fragment)
|
||||||
|
import << BetfairEventRunner.new(rec)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
import_result = BetfairEventRunner.import(import, on_duplicate_key_ignore: true)
|
||||||
|
puts "#{import_result.ids.size} runners added"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
6
portal/app/lib/services/bet_outcome_service.rb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module Services
|
||||||
|
class BetOutcomeService
|
||||||
|
def bet_outcome(bet_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
44
portal/app/mailers/application_mailer.rb
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
class ApplicationMailer < ActionMailer::Base
|
||||||
|
default from: 'info@helpbuild.co'
|
||||||
|
helper MailerStyleHelper
|
||||||
|
layout 'mailer'
|
||||||
|
|
||||||
|
def try_delivering(_options = {})
|
||||||
|
yield
|
||||||
|
true
|
||||||
|
rescue EOFError,
|
||||||
|
IOError,
|
||||||
|
Errno::ECONNRESET,
|
||||||
|
Errno::ECONNABORTED,
|
||||||
|
Errno::EPIPE,
|
||||||
|
Errno::ETIMEDOUT,
|
||||||
|
Net::SMTPAuthenticationError,
|
||||||
|
Net::SMTPServerBusy,
|
||||||
|
Net::SMTPSyntaxError,
|
||||||
|
Net::SMTPUnknownError,
|
||||||
|
OpenSSL::SSL::SSLError => e
|
||||||
|
ExceptionHub.notify(e)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def deliver_mail(email, subject, reply_to = nil, to_name = '')
|
||||||
|
addresses = set_addresses_and_user(email: email, to_name: to_name)
|
||||||
|
reply_to = addresses[:from] if reply_to.nil?
|
||||||
|
try_delivering do
|
||||||
|
I18n.with_locale('en') do
|
||||||
|
mail(reply_to: reply_to, from: addresses[:from], to: addresses[:to], subject: "[Helpbuild] | #{subject}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_addresses_and_user(email: '', from_name: 'Helpbuild', to_name: '', from_email: nil)
|
||||||
|
addresses = {}
|
||||||
|
from_email ||= Rails.configuration.action_mailer[:default_options][:from]
|
||||||
|
addresses[:from] = %(#{from_name} <#{from_email}>)
|
||||||
|
addresses[:reply_to] = %(#{from_name} <#{from_email}>)
|
||||||
|
addresses[:to] = %("#{to_name}" <#{email}>)
|
||||||
|
addresses
|
||||||
|
end
|
||||||
|
end
|
76
portal/app/mailers/project_mailer.rb
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
class ProjectMailer < ApplicationMailer
|
||||||
|
layout 'project_mailer'
|
||||||
|
layout false, only: :send_project_invitation
|
||||||
|
|
||||||
|
def helper_join_request(builders, helper)
|
||||||
|
subject = I18n.t('project_join_request', project: helper.project.title)
|
||||||
|
@project_helper = helper
|
||||||
|
@project = helper.project
|
||||||
|
builders.each do |b|
|
||||||
|
@user = b.user
|
||||||
|
deliver_mail(@user.email, subject, reply_to = nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_helper_for_join_request(helper)
|
||||||
|
return if helper.builder?
|
||||||
|
subject = I18n.t('project_join_request_response', project: helper.project.title)
|
||||||
|
|
||||||
|
@user = helper.user
|
||||||
|
@project = helper.project
|
||||||
|
@status = helper.status.titlecase
|
||||||
|
|
||||||
|
deliver_mail(@user.email, subject, reply_to = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ask_request_to_helpers(helper, ask)
|
||||||
|
@user = helper.user
|
||||||
|
@project_helper = helper
|
||||||
|
@ask = ask
|
||||||
|
subject = I18n.t('ask_request_to_helpers', project: helper.project.title)
|
||||||
|
@project = helper.project
|
||||||
|
deliver_mail(helper.user.email, subject, reply_to = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_builder_ask_status(ask, status, project_helper)
|
||||||
|
@builder = ask.project_helper.user
|
||||||
|
@ask = ask
|
||||||
|
@status = status
|
||||||
|
@project_helper = project_helper
|
||||||
|
@project = ask.project
|
||||||
|
subject = I18n.t('ask_status_to_builder_subject', project: @project.title, ask: ask.title, status: status)
|
||||||
|
deliver_mail(@builder.email, subject, reply_to = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_helper_ask_status(ask, status, project_helper)
|
||||||
|
# @builder = ask.project_helper.user
|
||||||
|
# @ask = ask
|
||||||
|
# @status = status
|
||||||
|
# @project_helper = project_helper
|
||||||
|
# @project = ask.project
|
||||||
|
# subject = I18n.t('ask_status_to_builder_subject', project: @project.title, ask: ask.title, status: status)
|
||||||
|
# deliver_mail(project_helper.user.email, subject, reply_to = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_announcement(project_helper, announcement)
|
||||||
|
@user = project_helper.user
|
||||||
|
@announcement = announcement
|
||||||
|
@project = project_helper.project
|
||||||
|
subject = I18n.t('announcement_notification', project: announcement.project.title)
|
||||||
|
deliver_mail(project_helper.user.email, subject, reply_to = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_monthly_announcement(project)
|
||||||
|
emails = project.project_builders.map {|pb| pb.user.email }
|
||||||
|
subject = I18n.t('monthly_announcement_check', project: project.title)
|
||||||
|
@project = project
|
||||||
|
|
||||||
|
deliver_mail(emails.join(','), subject, reply_to = nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_project_invitation(email, project)
|
||||||
|
@project = project
|
||||||
|
subject = I18n.t('project_invitation_subject', project: project.title, builder_name: project.user.first_name)
|
||||||
|
deliver_mail(email, subject, reply_to = nil)
|
||||||
|
end
|
||||||
|
end
|
21
portal/app/mailers/user_mailer.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
class UserMailer < ApplicationMailer
|
||||||
|
def welcome_email user
|
||||||
|
@user = user
|
||||||
|
subject = I18n.t('welcome_email_subject')
|
||||||
|
deliver_mail(user.email, subject, reply_to = nil, user.fullname)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_token_purchase(user, tokens, amount, balance)
|
||||||
|
@user = user
|
||||||
|
subject = I18n.t('token_purchase_confirmation_subject')
|
||||||
|
@body = I18n.t('token_purchase_confirmation_body', tokens: tokens, amount: amount, balance: balance.to_i)
|
||||||
|
deliver_mail(user.email, subject, reply_to = nil, user.fullname)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_hourly_unread_activities(user_id, activities_hash)
|
||||||
|
@user = User.find_by(id: user_id)
|
||||||
|
@activities_hash = activities_hash
|
||||||
|
|
||||||
|
deliver_mail(@user.email, '[Helpbuild] Activity Digest Mail', reply_to = nil, @user.fullname)
|
||||||
|
end
|
||||||
|
end
|
15
portal/app/models/account.rb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: accounts
|
||||||
|
#
|
||||||
|
# id :uuid not null, primary key
|
||||||
|
# name :string
|
||||||
|
# contact_id :uuid not null
|
||||||
|
# subdomain :string not null
|
||||||
|
# style :text
|
||||||
|
# size :bigint default(10000)
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class Account < ApplicationRecord
|
||||||
|
end
|
3
portal/app/models/application_record.rb
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
|
self.abstract_class = true
|
||||||
|
end
|
82
portal/app/models/bet.rb
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: bets
|
||||||
|
#
|
||||||
|
# id :uuid not null, primary key
|
||||||
|
# tip_provider_id :string not null
|
||||||
|
# tip_provider_bet_id :string not null
|
||||||
|
# tip_provider_percent :float not null
|
||||||
|
# exchange_id :string not null
|
||||||
|
# exchange_event_name :string not null
|
||||||
|
# exchange_event_id :string
|
||||||
|
# tip_provider_odds :string
|
||||||
|
# expected_value :float
|
||||||
|
# exchange_bet_id :string
|
||||||
|
# stake :float default(0.0), not null
|
||||||
|
# outcome :string default("processing"), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# exchange_account_id :string
|
||||||
|
# tip_provider_event_id :string
|
||||||
|
# exchange_odds :string
|
||||||
|
# team1 :string not null
|
||||||
|
# team2 :string
|
||||||
|
# tip_provider_market_details :jsonb
|
||||||
|
# exchange_market_details :jsonb
|
||||||
|
# log :jsonb
|
||||||
|
# original_json :jsonb
|
||||||
|
# executed_odds :string
|
||||||
|
# placement_attempts :integer default(0)
|
||||||
|
# period :string
|
||||||
|
#
|
||||||
|
class Bet < ApplicationRecord
|
||||||
|
enum outcome: { processing: 'processing', won: 'won', lost: 'lost', open: 'open', expired: 'expired', skipped: 'skipped', ignored: 'ignored', errored: 'errored', voided: 'voided', cancelled: 'cancelled'}
|
||||||
|
scope :placed_bets, -> { where('outcome in (?)', %w[won lost open voided cancelled]) }
|
||||||
|
scope :retryable_bets, -> { where("outcome in ('expired', 'skipped')") }
|
||||||
|
|
||||||
|
default_scope { where(exchange_account_id: ENV['EXCHANGE_ACCOUNT']) }
|
||||||
|
|
||||||
|
belongs_to :exchange_account
|
||||||
|
include Loggable
|
||||||
|
def json_payload
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
exchange: exchange_id,
|
||||||
|
exchange_event_name: exchange_event_name,
|
||||||
|
created_at: updated_at,
|
||||||
|
exchange_odds: exchange_odds,
|
||||||
|
tip_provider_odds: tip_provider_odds,
|
||||||
|
expected_value: expected_value&.round(1),
|
||||||
|
stake: stake,
|
||||||
|
outcome: outcome,
|
||||||
|
market_identifier: tip_provider_market_details.to_s,
|
||||||
|
exchange_market_details: exchange_market_details.to_s,
|
||||||
|
bet_placement_type: bet_placement_type,
|
||||||
|
executed_odds: executed_odds,
|
||||||
|
ev_percent: tip_provider_percent.round(2),
|
||||||
|
event_scheduled_time: original_json['started_at'] ? Time.at(original_json['started_at']) : "",
|
||||||
|
log: show_log
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.calculate_expected_value(stake, odds)
|
||||||
|
(stake * odds) - stake
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def placed_bet?
|
||||||
|
open? || lost? || won? || voided?
|
||||||
|
end
|
||||||
|
|
||||||
|
def bet_placement_type
|
||||||
|
if placed_bet?
|
||||||
|
return 'Simulated' unless exchange_bet_id
|
||||||
|
|
||||||
|
return "Placed [#{exchange_bet_id}]"
|
||||||
|
end
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
5
portal/app/models/betfair_event.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class BetfairEvent < ApplicationRecord
|
||||||
|
has_many :betfair_event_runners, dependent: :delete_all, primary_key: :event_id
|
||||||
|
|
||||||
|
enum status: { open: 'open', closed: 'closed' }
|
||||||
|
end
|
6
portal/app/models/betfair_event_runner.rb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
class BetfairEventRunner < ApplicationRecord
|
||||||
|
belongs_to :betfair_event, foreign_key: :event_id
|
||||||
|
has_many :betfair_runner_odds, dependent: :delete_all
|
||||||
|
|
||||||
|
scope :runners_for_open_events, -> { joins("INNER JOIN betfair_events ON betfair_event_runners.event_id = betfair_events.event_id").where(betfair_events: { status: 'open' }) }
|
||||||
|
end
|
4
portal/app/models/betfair_runner_odd.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
class BetfairRunnerOdd < ApplicationRecord
|
||||||
|
belongs_to :betfair_event_runner
|
||||||
|
enum bet_type: {back: 'back', lay: 'lay'}
|
||||||
|
end
|
6
portal/app/models/concerns/latest.rb
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module Latest
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
included do
|
||||||
|
scope :latest, -> { order(created_at: :desc).first }
|
||||||
|
end
|
||||||
|
end
|
218
portal/app/models/exchange_account.rb
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: exchange_accounts
|
||||||
|
#
|
||||||
|
# id :string not null, primary key
|
||||||
|
# exchange_id :string not null
|
||||||
|
# exchange_name :string not null
|
||||||
|
# login_uid :string not null
|
||||||
|
# login_pass :string not null
|
||||||
|
# apikey :string not null
|
||||||
|
# contact_name :string not null
|
||||||
|
# contact_email :string not null
|
||||||
|
# account_balance :float default(10000.0)
|
||||||
|
# account_balance_last_checked :datetime
|
||||||
|
# ssl_pem :text
|
||||||
|
# allow_multiple_bets_per_event :boolean default(FALSE)
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# log :jsonb
|
||||||
|
# betting_enabled :boolean default(FALSE)
|
||||||
|
# last_session_token :string
|
||||||
|
# last_session_token_saved_at :datetime
|
||||||
|
# status :string default("inactive"), not null
|
||||||
|
# stake_strategy :jsonb
|
||||||
|
# last_log_time :datetime
|
||||||
|
#
|
||||||
|
class ExchangeAccount < ApplicationRecord
|
||||||
|
has_many :bets
|
||||||
|
has_many :source_subscriptions, dependent: :delete_all
|
||||||
|
has_many :tip_sources, through: :source_subscriptions
|
||||||
|
has_many :subscription_runs, through: :source_subscriptions
|
||||||
|
enum status: { active: 'active', inactive: 'inactive'}
|
||||||
|
|
||||||
|
DEFAULT_STRATEGY =
|
||||||
|
{
|
||||||
|
max_bankroll_per_bet: 0.2,
|
||||||
|
stake_sizing: 'MINIMUM', #[MINIMUM,FIXED, HYBRID, PROPORTIONAL]
|
||||||
|
min_odds_to_bet: 1.1,
|
||||||
|
max_odds_to_bet: 3.0,
|
||||||
|
min_ev: 2,
|
||||||
|
max_ev: 7,
|
||||||
|
fixed_stake_size: 3.0,
|
||||||
|
odds_margin: 0.1,
|
||||||
|
kelly_multiplier: 0.3,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
DEFAULT_HYBRID_STRATEGY =
|
||||||
|
{
|
||||||
|
max_bets: 10,
|
||||||
|
action_on_max_losses: 'REBASE', #[REBASE]
|
||||||
|
pause_in_minutes: 0,
|
||||||
|
rebase_status: :complete,
|
||||||
|
current_stake: 0.0,
|
||||||
|
last_account_balance: nil,
|
||||||
|
account_balance_checked_at: nil,
|
||||||
|
max_losses: 0.1
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def json_payload
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
exchange_name: exchange_name,
|
||||||
|
exchange_id: exchange_id,
|
||||||
|
contact_name: contact_name,
|
||||||
|
contact_email: contact_email,
|
||||||
|
account_balance: account_balance,
|
||||||
|
is_active: active?,
|
||||||
|
can_bet: can_bet?,
|
||||||
|
stake_strategy_name: stake_strategy_description,
|
||||||
|
log: subscription_runs.empty? ? nil : last_run.log,
|
||||||
|
last_log_time: subscription_runs.empty? ? updated_at : last_run.created_at,
|
||||||
|
stake_strategy_config: current_stake_strategy,
|
||||||
|
source_types: tip_sources.empty? ? 'None' : tip_sources.pluck(:source_type).join(' , ')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_run
|
||||||
|
subscription_runs&.latest
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.mounted_account
|
||||||
|
find_by_id(ENV['EXCHANGE_ACCOUNT'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.mounted_account_payload
|
||||||
|
return {} unless mounted_account
|
||||||
|
|
||||||
|
mounted_account.json_payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def optimal_stake(bet, exchange_minimum, stake_available)
|
||||||
|
return exchange_minimum if minimum_stake?
|
||||||
|
|
||||||
|
x = [stake_by_strategy(bet).round(1), stake_available].min
|
||||||
|
|
||||||
|
[x, exchange_minimum].max
|
||||||
|
end
|
||||||
|
|
||||||
|
# # account_balance will ultimately come from accounts model
|
||||||
|
def actual_account_balance
|
||||||
|
return account_balance if betting_enabled
|
||||||
|
|
||||||
|
simulated_account_balance
|
||||||
|
end
|
||||||
|
|
||||||
|
def simulated_account_balance
|
||||||
|
account_balance + total_won - (total_lost + total_open)
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_won(subset = my_bets)
|
||||||
|
subset.won.sum(:expected_value).round(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_lost(subset = my_bets)
|
||||||
|
subset.lost.sum(:stake).round(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_open(subset = my_bets)
|
||||||
|
subset.open.sum(:stake).round(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_bet?
|
||||||
|
return false unless betting_enabled
|
||||||
|
|
||||||
|
actual_account_balance > total_open
|
||||||
|
end
|
||||||
|
|
||||||
|
def my_bets
|
||||||
|
Bet.unscoped.where(exchange_account_id: id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_stake_strategy
|
||||||
|
if stake_strategy.blank?
|
||||||
|
update(stake_strategy: DEFAULT_STRATEGY)
|
||||||
|
end
|
||||||
|
@current_stake_strategy ||= stake_strategy.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
def minimum_stake?
|
||||||
|
stake_strategy_description == 'MINIMUM'
|
||||||
|
end
|
||||||
|
|
||||||
|
def stake_strategy_description
|
||||||
|
return current_stake_strategy[:stake_sizing] unless current_stake_strategy[:stake_sizing].blank?
|
||||||
|
|
||||||
|
'MINIMUM'
|
||||||
|
end
|
||||||
|
|
||||||
|
def stake_by_strategy(bet)
|
||||||
|
return classic_kelly_stake_sizing(bet) if current_stake_strategy[:stake_sizing] == 'KELLY'
|
||||||
|
return proportional_stake if current_stake_strategy[:stake_sizing] == 'PROPORTIONAL'
|
||||||
|
return current_stake_strategy[:fixed_stake_size] if current_stake_strategy[:stake_sizing] == 'FIXED'
|
||||||
|
return hybrid_stake_sizing if current_stake_strategy[:stake_sizing] == 'HYBRID'
|
||||||
|
|
||||||
|
0
|
||||||
|
end
|
||||||
|
|
||||||
|
# def kelly_stake_sizing(bet)
|
||||||
|
# proportional = proportional_stake
|
||||||
|
# multiplier = current_stake_strategy[:kelly_multiplier] || 0.3
|
||||||
|
# kelly_factor = bet.tip_provider_percent * multiplier
|
||||||
|
# stake = kelly_factor * proportional
|
||||||
|
#
|
||||||
|
# return 0 if stake.negative?
|
||||||
|
#
|
||||||
|
# [stake, proportional].min
|
||||||
|
# end
|
||||||
|
|
||||||
|
def classic_kelly_stake_sizing(bet)
|
||||||
|
multiplier = current_stake_strategy[:kelly_multiplier] || 0.3
|
||||||
|
max_per_bet = proportional_stake
|
||||||
|
kelly(odds: bet.executed_odds.to_f, probs: bet.tip_provider_percent, balance: actual_account_balance, multiplier: multiplier, per_bet_max: max_per_bet)
|
||||||
|
end
|
||||||
|
|
||||||
|
def kelly(odds:, probs:, balance: , multiplier:, per_bet_max:)
|
||||||
|
kelly_stake_percent = (((odds * probs) -1) / (odds - 1 ) * multiplier)/100
|
||||||
|
stake = kelly_stake_percent * balance
|
||||||
|
|
||||||
|
return 0 if stake.negative?
|
||||||
|
|
||||||
|
[per_bet_max, stake].min
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def hybrid_stake_sizing
|
||||||
|
hybrid_strategy = current_stake_strategy[:hybrid] || DEFAULT_HYBRID_STRATEGY.clone
|
||||||
|
last_based_account_balance = hybrid_strategy[:last_account_balance]
|
||||||
|
last_based_account_balance ||= rebase_hybrid_strategy
|
||||||
|
|
||||||
|
max_losses = hybrid_strategy[:max_losses]
|
||||||
|
max_allowable_losses = last_based_account_balance * max_losses
|
||||||
|
|
||||||
|
difference = actual_account_balance - last_based_account_balance
|
||||||
|
if difference.abs >= max_allowable_losses
|
||||||
|
rebase_hybrid_strategy
|
||||||
|
end
|
||||||
|
current_stake_strategy[:hybrid][:current_stake]
|
||||||
|
# raise "[Bet sizing] Betting on '#{id}' paused due to stake management rules"
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebase_hybrid_strategy
|
||||||
|
puts 'Rebasing hybrid stake because account balances have gained/loss at least the max'
|
||||||
|
hybrid = current_stake_strategy[:hybrid] || DEFAULT_HYBRID_STRATEGY.clone(freeze: false)
|
||||||
|
hybrid[:last_account_balance] = actual_account_balance
|
||||||
|
hybrid[:current_stake] = ((actual_account_balance * hybrid[:max_losses]) / hybrid[:max_bets]).round(2)
|
||||||
|
hybrid[:account_balance_checked_at] = Time.now
|
||||||
|
current_stake_strategy[:hybrid] = hybrid
|
||||||
|
update(stake_strategy: current_stake_strategy)
|
||||||
|
hybrid[:current_stake]
|
||||||
|
end
|
||||||
|
|
||||||
|
def proportional_stake
|
||||||
|
(current_stake_strategy[:max_bankroll_per_bet] * actual_account_balance)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
20
portal/app/models/loggable.rb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
module Loggable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def set_log(s)
|
||||||
|
log_this(s, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_this(s, append = true)
|
||||||
|
x = append ? log || [] : []
|
||||||
|
x << "[#{Time.now}]: #{s}"
|
||||||
|
update(log: x)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_log
|
||||||
|
x = log || []
|
||||||
|
return 'Nothing logged' if x.length.zero?
|
||||||
|
|
||||||
|
x.join(',')
|
||||||
|
end
|
||||||
|
end
|
17
portal/app/models/source_subscription.rb
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: source_subscriptions
|
||||||
|
#
|
||||||
|
# id :uuid not null, primary key
|
||||||
|
# exchange_account_id :string
|
||||||
|
# tip_source_id :uuid
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class SourceSubscription < ApplicationRecord
|
||||||
|
belongs_to :tip_source, dependent: :delete
|
||||||
|
belongs_to :exchange_account
|
||||||
|
|
||||||
|
has_many :subscription_runs
|
||||||
|
has_many :tip_source_data, through: :tip_source
|
||||||
|
end
|
17
portal/app/models/subscription_run.rb
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: subscription_runs
|
||||||
|
#
|
||||||
|
# id :uuid not null, primary key
|
||||||
|
# source_subscription_id :uuid
|
||||||
|
# tip_source_data_id :uuid
|
||||||
|
# log :jsonb
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class SubscriptionRun < ApplicationRecord
|
||||||
|
include Loggable
|
||||||
|
include Latest
|
||||||
|
belongs_to :tip_source_data
|
||||||
|
belongs_to :source_subscription
|
||||||
|
end
|
22
portal/app/models/tip_source.rb
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: tip_sources
|
||||||
|
#
|
||||||
|
# id :uuid not null, primary key
|
||||||
|
# tipster_account_id :string
|
||||||
|
# description :string
|
||||||
|
# source_type :string
|
||||||
|
# active :boolean default(FALSE)
|
||||||
|
# filters :jsonb
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class TipSource < ApplicationRecord
|
||||||
|
belongs_to :tipster_account, dependent: :delete
|
||||||
|
has_many :source_subscriptions, dependent: :delete_all
|
||||||
|
has_many :tip_source_data, class_name: 'TipSourceData'
|
||||||
|
has_many :subscription_runs, through: :source_subscriptions
|
||||||
|
|
||||||
|
scope :active_sources, -> { where(active: true)}
|
||||||
|
|
||||||
|
end
|
15
portal/app/models/tip_source_data.rb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: tip_source_data
|
||||||
|
#
|
||||||
|
# id :uuid not null, primary key
|
||||||
|
# tip_source_id :uuid
|
||||||
|
# data :jsonb
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class TipSourceData < ApplicationRecord
|
||||||
|
include Latest
|
||||||
|
belongs_to :tip_source, dependent: :delete
|
||||||
|
has_many :source_subscriptions
|
||||||
|
end
|
18
portal/app/models/tipster_account.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: tipster_accounts
|
||||||
|
#
|
||||||
|
# id :string not null, primary key
|
||||||
|
# tipster_name :string not null
|
||||||
|
# apikey :string not null
|
||||||
|
# filter_ids :string not null
|
||||||
|
# contact_name :string not null
|
||||||
|
# contact_email :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class TipsterAccount < ApplicationRecord
|
||||||
|
has_many :tip_sources
|
||||||
|
has_many :tip_source_data, through: :tip_sources, class_name: 'TipSourceData'
|
||||||
|
has_many :tip_source_data_processing_runs, through: :tip_source_data
|
||||||
|
end
|
82
portal/app/models/user.rb
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: users
|
||||||
|
#
|
||||||
|
# id :uuid not null, primary key
|
||||||
|
# first_name :string
|
||||||
|
# last_name :string
|
||||||
|
# email :string default(""), not null
|
||||||
|
# encrypted_password :string default(""), not null
|
||||||
|
# reset_password_token :string
|
||||||
|
# reset_password_sent_at :datetime
|
||||||
|
# remember_created_at :datetime
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# uid :string
|
||||||
|
# provider :string
|
||||||
|
#
|
||||||
|
class User < ApplicationRecord
|
||||||
|
# Include default devise modules. Others available are:
|
||||||
|
# :confirmable, :lockable, :timeoutable, :trackable and
|
||||||
|
devise :database_authenticatable, :registerable, :omniauthable
|
||||||
|
|
||||||
|
|
||||||
|
def name
|
||||||
|
"#{first_name} #{last_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.find_or_create_from_auth_hash(auth)
|
||||||
|
user = where(email: auth.info.email).first
|
||||||
|
|
||||||
|
unless user.present?
|
||||||
|
user = where(provider: auth.provider, uid: auth.uid).first_or_initialize
|
||||||
|
user.email = auth.info.email
|
||||||
|
user.password = Devise.friendly_token.first(8)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless user.provider.present?
|
||||||
|
user.provider = auth.provider
|
||||||
|
user.uid = auth.uid
|
||||||
|
end
|
||||||
|
|
||||||
|
user.first_name = auth.info.first_name
|
||||||
|
user.last_name = auth.info.last_name
|
||||||
|
|
||||||
|
user.save!
|
||||||
|
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
def fullname
|
||||||
|
"#{first_name} #{last_name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirmation_token
|
||||||
|
if read_attribute(:confirmation_token).nil?
|
||||||
|
self.confirmation_token = generate_confirmation_token
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
read_attribute(:confirmation_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_payload
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
email: email,
|
||||||
|
first_name: first_name,
|
||||||
|
last_name: last_name,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def admin?
|
||||||
|
%w[mike@wizewerx.com].include?(email)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_confirmation_token
|
||||||
|
token = Devise.friendly_token(10)
|
||||||
|
token = Devise.friendly_token(10) while User.where(confirmation_token: token).count.positive?
|
||||||
|
self.confirmation_token = token
|
||||||
|
end
|
||||||
|
end
|
19
portal/app/views/layouts/application.html.erb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>betbeast : taking a bit out of arbitrage</title>
|
||||||
|
<link href="<%= asset_path("favicon.png") %>" rel="shortcut icon"></link>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<!-- End Google Tag Manager -->
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
|
||||||
|
<%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id='app'><%= yield %></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
0
portal/app/views/pages/index.html.erb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<% @activities_hash.except(:all_activities).each do |k, activities| %>
|
||||||
|
<% if k.starts_with?('project_asks.') %>
|
||||||
|
<div style="border: 1px solid #eee; padding: 15px; margin-bottom: 15px;border-radius: 15px; background: #f3f3f3">
|
||||||
|
<h3 style="margin: 0 0 15px;">Activities</h3>
|
||||||
|
|
||||||
|
<% activities.each do |activity| %>
|
||||||
|
<%= render partial: 'partials/project_ask_digest', locals: { activity: activity } %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
83
portal/babel.config.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
module.exports = function(api) {
|
||||||
|
const validEnv = ['development', 'test', 'production'];
|
||||||
|
const currentEnv = api.env();
|
||||||
|
const isDevelopmentEnv = api.env('development');
|
||||||
|
const isProductionEnv = api.env('production');
|
||||||
|
const isTestEnv = api.env('test');
|
||||||
|
|
||||||
|
if (!validEnv.includes(currentEnv)) {
|
||||||
|
throw new Error(
|
||||||
|
`${'Please specify a valid `NODE_ENV` or ' +
|
||||||
|
'`BABEL_ENV` environment variables. Valid values are "development", ' +
|
||||||
|
'"test", and "production". Instead, received: '}${JSON.stringify(
|
||||||
|
currentEnv
|
||||||
|
)}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
isTestEnv && [
|
||||||
|
'@babel/preset-env',
|
||||||
|
{
|
||||||
|
targets: {
|
||||||
|
node: 'current',
|
||||||
|
},
|
||||||
|
modules: 'commonjs',
|
||||||
|
},
|
||||||
|
'@babel/preset-react',
|
||||||
|
],
|
||||||
|
(isProductionEnv || isDevelopmentEnv) && [
|
||||||
|
'@babel/preset-env',
|
||||||
|
{
|
||||||
|
forceAllTransforms: true,
|
||||||
|
useBuiltIns: 'entry',
|
||||||
|
corejs: 3,
|
||||||
|
modules: false,
|
||||||
|
exclude: ['transform-typeof-symbol'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'@babel/preset-react',
|
||||||
|
{
|
||||||
|
development: isDevelopmentEnv || isTestEnv,
|
||||||
|
useBuiltIns: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
].filter(Boolean),
|
||||||
|
plugins: [
|
||||||
|
'babel-plugin-macros',
|
||||||
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
|
isTestEnv && 'babel-plugin-dynamic-import-node',
|
||||||
|
'@babel/plugin-transform-destructuring',
|
||||||
|
['@babel/plugin-proposal-decorators', { legacy: true }],
|
||||||
|
['@babel/plugin-proposal-class-properties', { loose: false }],
|
||||||
|
[
|
||||||
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
|
{
|
||||||
|
useBuiltIns: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'@babel/plugin-transform-runtime',
|
||||||
|
{
|
||||||
|
helpers: false,
|
||||||
|
regenerator: true,
|
||||||
|
corejs: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'@babel/plugin-transform-regenerator',
|
||||||
|
{
|
||||||
|
async: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isProductionEnv && [
|
||||||
|
'babel-plugin-transform-react-remove-prop-types',
|
||||||
|
{
|
||||||
|
removeImport: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
].filter(Boolean),
|
||||||
|
};
|
||||||
|
};
|
118
portal/bin/bundle
Executable file
@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
#
|
||||||
|
# This file was generated by Bundler.
|
||||||
|
#
|
||||||
|
# The application 'bundle' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
|
||||||
|
m = Module.new do
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def invoked_as_script?
|
||||||
|
File.expand_path($0) == File.expand_path(__FILE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_var_version
|
||||||
|
ENV['BUNDLER_VERSION']
|
||||||
|
end
|
||||||
|
|
||||||
|
def cli_arg_version
|
||||||
|
return unless invoked_as_script? # don't want to hijack other binstubs
|
||||||
|
return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update`
|
||||||
|
|
||||||
|
bundler_version = nil
|
||||||
|
update_index = nil
|
||||||
|
ARGV.each_with_index do |a, i|
|
||||||
|
bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
|
||||||
|
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
|
||||||
|
|
||||||
|
bundler_version = Regexp.last_match(1)
|
||||||
|
update_index = i
|
||||||
|
end
|
||||||
|
bundler_version
|
||||||
|
end
|
||||||
|
|
||||||
|
def gemfile
|
||||||
|
gemfile = ENV['BUNDLE_GEMFILE']
|
||||||
|
return gemfile if gemfile && !gemfile.empty?
|
||||||
|
|
||||||
|
File.expand_path('../Gemfile', __dir__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lockfile
|
||||||
|
lockfile =
|
||||||
|
case File.basename(gemfile)
|
||||||
|
when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile)
|
||||||
|
else "#{gemfile}.lock"
|
||||||
|
end
|
||||||
|
File.expand_path(lockfile)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lockfile_version
|
||||||
|
return unless File.file?(lockfile)
|
||||||
|
|
||||||
|
lockfile_contents = File.read(lockfile)
|
||||||
|
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
|
||||||
|
|
||||||
|
Regexp.last_match(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def bundler_version
|
||||||
|
@bundler_version ||=
|
||||||
|
env_var_version || cli_arg_version ||
|
||||||
|
lockfile_version
|
||||||
|
end
|
||||||
|
|
||||||
|
def bundler_requirement
|
||||||
|
return "#{Gem::Requirement.default}.a" unless bundler_version
|
||||||
|
|
||||||
|
bundler_gem_version = Gem::Version.new(bundler_version)
|
||||||
|
|
||||||
|
requirement = bundler_gem_version.approximate_recommendation
|
||||||
|
|
||||||
|
return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.7.0')
|
||||||
|
|
||||||
|
requirement += '.a' if bundler_gem_version.prerelease?
|
||||||
|
|
||||||
|
requirement
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_bundler!
|
||||||
|
ENV['BUNDLE_GEMFILE'] ||= gemfile
|
||||||
|
|
||||||
|
activate_bundler
|
||||||
|
end
|
||||||
|
|
||||||
|
def activate_bundler
|
||||||
|
gem_error = activation_error_handling do
|
||||||
|
gem 'bundler', bundler_requirement
|
||||||
|
end
|
||||||
|
return if gem_error.nil?
|
||||||
|
|
||||||
|
require_error = activation_error_handling do
|
||||||
|
require 'bundler/version'
|
||||||
|
end
|
||||||
|
if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
|
||||||
|
exit 42
|
||||||
|
end
|
||||||
|
|
||||||
|
def activation_error_handling
|
||||||
|
yield
|
||||||
|
nil
|
||||||
|
rescue StandardError, LoadError => e
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
m.load_bundler!
|
||||||
|
|
||||||
|
load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script?
|
9
portal/bin/rails
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
begin
|
||||||
|
load File.expand_path('spring', __dir__)
|
||||||
|
rescue LoadError => e
|
||||||
|
raise unless e.message.include?('spring')
|
||||||
|
end
|
||||||
|
APP_PATH = File.expand_path('../config/application', __dir__)
|
||||||
|
require_relative '../config/boot'
|
||||||
|
require 'rails/commands'
|
9
portal/bin/rake
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
begin
|
||||||
|
load File.expand_path('spring', __dir__)
|
||||||
|
rescue LoadError => e
|
||||||
|
raise unless e.message.include?('spring')
|
||||||
|
end
|
||||||
|
require_relative '../config/boot'
|
||||||
|
require 'rake'
|
||||||
|
Rake.application.run
|
35
portal/bin/setup
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require 'fileutils'
|
||||||
|
|
||||||
|
# path to your application root.
|
||||||
|
APP_ROOT = File.expand_path('..', __dir__)
|
||||||
|
|
||||||
|
def system!(*args)
|
||||||
|
system(*args) || abort("\n== Command #{args} failed ==")
|
||||||
|
end
|
||||||
|
|
||||||
|
FileUtils.chdir APP_ROOT do
|
||||||
|
# This script is a way to setup or update your development environment automatically.
|
||||||
|
# This script is idempotent, so that you can run it at anytime and get an expectable outcome.
|
||||||
|
# Add necessary setup steps to this file.
|
||||||
|
|
||||||
|
puts '== Installing dependencies =='
|
||||||
|
system! 'gem install bundler --conservative'
|
||||||
|
system('bundle check') || system!('bundle install')
|
||||||
|
|
||||||
|
# Install JavaScript dependencies
|
||||||
|
system('bin/yarn')
|
||||||
|
|
||||||
|
puts "\n== Copying sample files =="
|
||||||
|
FileUtils.cp 'config/database.yml', 'config/database.yml' unless File.exist?('config/database.yml')
|
||||||
|
|
||||||
|
puts "\n== Preparing database =="
|
||||||
|
system! 'bin/rails db:create'
|
||||||
|
system! 'bin/rails db:migrate'
|
||||||
|
|
||||||
|
puts "\n== Removing old logs and tempfiles =="
|
||||||
|
system! 'bin/rails log:clear tmp:clear'
|
||||||
|
|
||||||
|
puts "\n== Restarting application server =="
|
||||||
|
system! 'bin/rails restart'
|
||||||
|
end
|
17
portal/bin/spring
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
# This file loads Spring without using Bundler, in order to be fast.
|
||||||
|
# It gets overwritten when you run the `spring binstub` command.
|
||||||
|
|
||||||
|
unless defined?(Spring)
|
||||||
|
require 'rubygems'
|
||||||
|
require 'bundler'
|
||||||
|
|
||||||
|
lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
|
||||||
|
spring = lockfile.specs.detect { |spec| spec.name == 'spring' }
|
||||||
|
if spring
|
||||||
|
Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
|
||||||
|
gem 'spring', spring.version
|
||||||
|
require 'spring/binstub'
|
||||||
|
end
|
||||||
|
end
|
18
portal/bin/webpack
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
ENV['RAILS_ENV'] ||= ENV['RACK_ENV'] || 'development'
|
||||||
|
ENV['NODE_ENV'] ||= 'development'
|
||||||
|
|
||||||
|
require 'pathname'
|
||||||
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
||||||
|
Pathname.new(__FILE__).realpath)
|
||||||
|
|
||||||
|
require 'bundler/setup'
|
||||||
|
|
||||||
|
require 'webpacker'
|
||||||
|
require 'webpacker/webpack_runner'
|
||||||
|
|
||||||
|
APP_ROOT = File.expand_path('..', __dir__)
|
||||||
|
Dir.chdir(APP_ROOT) do
|
||||||
|
Webpacker::WebpackRunner.run(ARGV)
|
||||||
|
end
|
18
portal/bin/webpack-dev-server
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
ENV['RAILS_ENV'] ||= ENV['RACK_ENV'] || 'development'
|
||||||
|
ENV['NODE_ENV'] ||= 'development'
|
||||||
|
|
||||||
|
require 'pathname'
|
||||||
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
||||||
|
Pathname.new(__FILE__).realpath)
|
||||||
|
|
||||||
|
require 'bundler/setup'
|
||||||
|
|
||||||
|
require 'webpacker'
|
||||||
|
require 'webpacker/dev_server_runner'
|
||||||
|
|
||||||
|
APP_ROOT = File.expand_path('..', __dir__)
|
||||||
|
Dir.chdir(APP_ROOT) do
|
||||||
|
Webpacker::DevServerRunner.run(ARGV)
|
||||||
|
end
|
9
portal/bin/yarn
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
APP_ROOT = File.expand_path('..', __dir__)
|
||||||
|
Dir.chdir(APP_ROOT) do
|
||||||
|
exec 'yarnpkg', *ARGV
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
warn 'Yarn executable was not detected in the system.'
|
||||||
|
warn 'Download Yarn at https://yarnpkg.com/en/docs/install'
|
||||||
|
exit 1
|
||||||
|
end
|
24
portal/client/packs/application.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import { fab } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { far } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import TimeAgo from 'javascript-time-ago';
|
||||||
|
import en from 'javascript-time-ago/locale/en.json';
|
||||||
|
|
||||||
|
import '../src/assets/style';
|
||||||
|
|
||||||
|
import EntryPoint from '../src/entry_point';
|
||||||
|
|
||||||
|
library.add(fas, fab, far);
|
||||||
|
|
||||||
|
TimeAgo.addDefaultLocale(en);
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const container = document.getElementById('app');
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<EntryPoint />);
|
||||||
|
});
|
5
portal/client/src/assets/images.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import MAIN_BACKGROUND_IMG from './images/main-bg.jpg';
|
||||||
|
import LOGO from './images/logo.png';
|
||||||
|
import LINKEDIN from './images/linkedin.png';
|
||||||
|
|
||||||
|
export { MAIN_BACKGROUND_IMG, LINKEDIN, LOGO };
|
BIN
portal/client/src/assets/images/iconmonstr-arrow-65-240.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
portal/client/src/assets/images/linkedin.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
portal/client/src/assets/images/logo.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
4
portal/client/src/assets/images/logo.svg
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
portal/client/src/assets/images/main-bg.jpg
Normal file
After Width: | Height: | Size: 109 KiB |
9
portal/client/src/assets/style.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'react-datetime/css/react-datetime.css';
|
||||||
|
import 'react-date-range/dist/styles.css';
|
||||||
|
import 'react-date-range/dist/theme/default.css';
|
||||||
|
import 'react-input-range/lib/css/index.css';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import 'react-confirm-alert/src/react-confirm-alert.css';
|
||||||
|
|
||||||
|
import './stylesheets/style.scss';
|
1210
portal/client/src/assets/stylesheets/style.scss
Normal file
6
portal/client/src/channels/consumer.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Action Cable provides the framework to deal with WebSockets in Rails.
|
||||||
|
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
|
||||||
|
|
||||||
|
import { createConsumer } from '@rails/actioncable';
|
||||||
|
|
||||||
|
export default createConsumer();
|
15
portal/client/src/data/axiosClient.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const axiosObj = () => {
|
||||||
|
const instance = axios.create({
|
||||||
|
headers: {
|
||||||
|
'cache-control': 'no-cache',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default axiosObj();
|
19
portal/client/src/data/concerns/filterable.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {action, observable} from 'mobx';
|
||||||
|
|
||||||
|
import Pagination from '../entities/pagination';
|
||||||
|
|
||||||
|
const Filterable = {
|
||||||
|
pagination: null,
|
||||||
|
|
||||||
|
initializeFilterable: action(function(params) {
|
||||||
|
this.pagination = new Pagination(
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Filterable;
|
8
portal/client/src/data/concerns/notfications.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
const notification = {
|
||||||
|
notifySuccess: message => toast(message, { type: 'success' }),
|
||||||
|
notifyError: message => toast(message, { type: 'error' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default notification;
|
67
portal/client/src/data/entities/base_entity.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
action,
|
||||||
|
computed,
|
||||||
|
extendObservable,
|
||||||
|
flow,
|
||||||
|
makeObservable,
|
||||||
|
observable,
|
||||||
|
} from 'mobx';
|
||||||
|
import { camelCase, isEmpty, map } from 'lodash';
|
||||||
|
|
||||||
|
import client from '../axiosClient';
|
||||||
|
import notification from '../concerns/notfications';
|
||||||
|
|
||||||
|
class BaseEntity {
|
||||||
|
/* eslint-disable */
|
||||||
|
store;
|
||||||
|
client;
|
||||||
|
@observable dirty = false;
|
||||||
|
@observable json = false;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
constructor(value, store) {
|
||||||
|
makeObservable(this);
|
||||||
|
|
||||||
|
this.store = store;
|
||||||
|
this.client = client;
|
||||||
|
this.json = value;
|
||||||
|
|
||||||
|
extendObservable(this, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get currentUser() {
|
||||||
|
return this.store.rootStore.userStore.currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
initialize(params) {
|
||||||
|
this.update(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
update(params, updateServer) {
|
||||||
|
map(
|
||||||
|
Object.keys(params),
|
||||||
|
function(k) {
|
||||||
|
this[camelCase(k)] = params[k];
|
||||||
|
}.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateServer) this.updateServer(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
destroy() {
|
||||||
|
if (isEmpty(this.store)) return;
|
||||||
|
|
||||||
|
this.store.records.splice(this.store.records.indexOf(this), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@flow
|
||||||
|
*updateServer(params) {
|
||||||
|
yield this.client.put(this.updateUrl, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseEntity;
|
37
portal/client/src/data/entities/bet.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { action, computed, makeObservable, observable } from 'mobx';
|
||||||
|
import BaseEntity from './base_entity';
|
||||||
|
|
||||||
|
class Bet extends BaseEntity {
|
||||||
|
/* eslint-disable */
|
||||||
|
id;
|
||||||
|
outcome;
|
||||||
|
exchange;
|
||||||
|
exchangeEventName;
|
||||||
|
exchangeOdds;
|
||||||
|
tipProviderOdds;
|
||||||
|
stake;
|
||||||
|
expectedValue;
|
||||||
|
marketIdentifier;
|
||||||
|
exchangeMarketDetails;
|
||||||
|
betPlacementType;
|
||||||
|
executedOdds;
|
||||||
|
evPercent;
|
||||||
|
eventScheduledTime;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
constructor(value, store) {
|
||||||
|
super(value, store);
|
||||||
|
|
||||||
|
makeObservable(this);
|
||||||
|
|
||||||
|
this.handleConstruction(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleConstruction(value) {
|
||||||
|
const val = { ...value };
|
||||||
|
this.initialize(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bet;
|
48
portal/client/src/data/entities/exchange_account.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { action, computed, flow, makeObservable, observable } from 'mobx';
|
||||||
|
import BaseEntity from './base_entity';
|
||||||
|
import client from '../axiosClient';
|
||||||
|
|
||||||
|
class ExchangeAccount extends BaseEntity {
|
||||||
|
/* eslint-disable */
|
||||||
|
id;
|
||||||
|
exchangeName;
|
||||||
|
exchangeId;
|
||||||
|
contactEmail;
|
||||||
|
contactName;
|
||||||
|
stakeStrategyName;
|
||||||
|
sourceTypes;
|
||||||
|
stakeStrategyConfig;
|
||||||
|
stakeStrategy;
|
||||||
|
lastLogTime;
|
||||||
|
@observable isActive;
|
||||||
|
@observable canBet;
|
||||||
|
log;
|
||||||
|
|
||||||
|
@observable accountBalance;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
constructor(value, store) {
|
||||||
|
super(value, store);
|
||||||
|
|
||||||
|
makeObservable(this);
|
||||||
|
|
||||||
|
this.handleConstruction(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleConstruction(value) {
|
||||||
|
const val = { ...value };
|
||||||
|
this.initialize(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@flow
|
||||||
|
*updateExchangeAccount(params) {
|
||||||
|
this.update(params);
|
||||||
|
yield client.put('/api/v1/exchange_account.json', {
|
||||||
|
exchange_account_id: this.id,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExchangeAccount;
|