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;
|