Initial commit
This commit is contained in:
235
app/models/market.rb
Normal file
235
app/models/market.rb
Normal file
@@ -0,0 +1,235 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
# People exchange commodities in markets. Each market focuses on certain
|
||||
# commodity pair `{A, B}`. By convention, we call people exchange A for B
|
||||
# *sellers* who submit *ask* orders, and people exchange B for A *buyers*
|
||||
# who submit *bid* orders.
|
||||
#
|
||||
# ID of market is always in the form "#{B}#{A}". For example, in 'btcusd'
|
||||
# market, the commodity pair is `{btc, usd}`. Sellers sell out _btc_ for
|
||||
# _usd_, buyers buy in _btc_ with _usd_. _btc_ is the `base_unit`, while
|
||||
# _usd_ is the `quote_unit`.
|
||||
#
|
||||
# Given market BTCUSD.
|
||||
# Ask/Base currency/unit = BTC.
|
||||
# Bid/Quote currency/unit = USD.
|
||||
|
||||
class Market < ApplicationRecord
|
||||
|
||||
# == Constants ============================================================
|
||||
|
||||
# Since we use decimal with 16 digits fractional part for storing numbers in DB
|
||||
# sum of multipliers fractional parts must not be greater then 16.
|
||||
# In the worst situation we have 3 multipliers (price * amount * fee).
|
||||
# For fee we define static precision - 6. See TradingFee::FEE_PRECISION.
|
||||
# So 10 left for amount and price precision.
|
||||
DB_DECIMAL_PRECISION = 16
|
||||
FUNDS_PRECISION = 10
|
||||
TOP_POSITION = 1
|
||||
AVERAGE_NUMBER = 5
|
||||
|
||||
STATES = %w[enabled disabled hidden locked sale presale].freeze
|
||||
# enabled - user can view and trade.
|
||||
# disabled - none can trade, user can't view.
|
||||
# hidden - user can't view but can trade.
|
||||
# locked - user can view but can't trade.
|
||||
# sale - user can't view but can trade with market orders.
|
||||
# presale - user can't view and trade. Admin can trade.
|
||||
|
||||
# == Attributes ===========================================================
|
||||
|
||||
attr_readonly :base_unit, :quote_unit
|
||||
|
||||
# base_currency & quote_currency is preferred names instead of legacy
|
||||
# base_unit & quote_unit.
|
||||
# For avoiding DB migration and config we use alias as temporary solution.
|
||||
alias_attribute :base_currency, :base_unit
|
||||
alias_attribute :quote_currency, :quote_unit
|
||||
|
||||
# == Extensions ===========================================================
|
||||
|
||||
serialize :data, JSON unless ::Rails.configuration.database_support_json
|
||||
|
||||
include Helpers::ReorderPosition
|
||||
|
||||
# == Relationships ========================================================
|
||||
|
||||
has_one :base, class_name: 'Currency', foreign_key: :id, primary_key: :base_unit
|
||||
has_one :quote, class_name: 'Currency', foreign_key: :id, primary_key: :quote_unit
|
||||
belongs_to :engine, required: true
|
||||
|
||||
has_many :trading_fees, dependent: :delete_all
|
||||
|
||||
# == Validations ==========================================================
|
||||
|
||||
validate do
|
||||
if quote_currency == base_currency
|
||||
errors.add(:quote_currency, 'duplicates base currency')
|
||||
end
|
||||
end
|
||||
|
||||
validate on: :create do
|
||||
if ENV['MAX_MARKETS'].present? && Market.count >= ENV['MAX_MARKETS'].to_i
|
||||
errors.add(:max, 'Market limit has been reached')
|
||||
end
|
||||
|
||||
if Market.where(base_currency: quote_currency, quote_currency: base_currency).present? ||
|
||||
Market.where(base_currency: base_currency, quote_currency: quote_currency).present?
|
||||
errors.add(:base, "#{base_currency.upcase}, #{quote_currency.upcase} market already exists")
|
||||
end
|
||||
end
|
||||
|
||||
validates :id, uniqueness: { case_sensitive: false }, presence: true
|
||||
|
||||
validates :base_currency, :quote_currency, presence: true
|
||||
|
||||
validates :min_price, :max_price, precision: { less_than_or_eq_to: ->(m) { m.price_precision } }
|
||||
|
||||
validates :min_amount, precision: { less_than_or_eq_to: ->(m) { m.amount_precision } }
|
||||
|
||||
validates :position,
|
||||
presence: true,
|
||||
numericality: { greater_than_or_equal_to: TOP_POSITION, only_integer: true }
|
||||
|
||||
validates :amount_precision,
|
||||
:price_precision,
|
||||
numericality: { greater_than_or_equal_to: 0, only_integer: true }
|
||||
|
||||
validates :price_precision,
|
||||
numericality: {
|
||||
less_than_or_equal_to: ->(_m) { FUNDS_PRECISION }
|
||||
}
|
||||
|
||||
validates :amount_precision,
|
||||
numericality: {
|
||||
less_than_or_equal_to: ->(m) { FUNDS_PRECISION - m.price_precision }
|
||||
}
|
||||
|
||||
validates :base_currency, :quote_currency, inclusion: { in: -> (_) { Currency.codes } }
|
||||
|
||||
validates :min_price,
|
||||
presence: true,
|
||||
numericality: { greater_than_or_equal_to: ->(market) { market.min_price_by_precision } }
|
||||
validates :max_price,
|
||||
numericality: { allow_blank: true, greater_than_or_equal_to: ->(market) { market.min_price }},
|
||||
if: ->(market) { !market.max_price.zero? }
|
||||
|
||||
validates :min_amount,
|
||||
presence: true,
|
||||
numericality: { greater_than_or_equal_to: ->(market) { market.min_amount_by_precision } }
|
||||
|
||||
validates :state, inclusion: { in: STATES }
|
||||
|
||||
# == Scopes ===============================================================
|
||||
|
||||
scope :ordered, -> { order(position: :asc) }
|
||||
scope :active, -> { where(state: %i[enabled hidden]) }
|
||||
scope :enabled, -> { where(state: :enabled) }
|
||||
|
||||
# == Callbacks ============================================================
|
||||
|
||||
after_initialize :initialize_defaults, if: :new_record?
|
||||
before_validation(on: :create) { self.id = "#{base_currency}#{quote_currency}" }
|
||||
before_validation(on: :create) { self.position = Market.count + 1 unless position.present? }
|
||||
|
||||
after_commit { AMQP::Queue.enqueue(:matching, action: 'new', market: id) }
|
||||
after_commit :wipe_cache
|
||||
after_create { insert_position(self) }
|
||||
|
||||
before_update { update_position(self) if position_changed? }
|
||||
|
||||
# == Instance Methods =====================================================
|
||||
|
||||
|
||||
def price_now
|
||||
last_trades = Trade.public_from_influx(self.id, AVERAGE_NUMBER)
|
||||
prices = last_trades.inject(0) { |sum,x| sum + x[:price] }
|
||||
prices / AVERAGE_NUMBER
|
||||
end
|
||||
|
||||
|
||||
|
||||
def initialize_defaults
|
||||
self.data = {} if data.blank?
|
||||
end
|
||||
|
||||
def wipe_cache
|
||||
Rails.cache.delete_matched("markets*")
|
||||
end
|
||||
|
||||
def name
|
||||
"#{base_currency}/#{quote_currency}".upcase
|
||||
end
|
||||
|
||||
def underscore_name
|
||||
"#{base_currency.upcase}_#{quote_currency.upcase}"
|
||||
end
|
||||
|
||||
alias to_s name
|
||||
|
||||
def round_amount(d)
|
||||
d.round(amount_precision, BigDecimal::ROUND_DOWN)
|
||||
end
|
||||
|
||||
def round_price(d)
|
||||
d.round(price_precision, BigDecimal::ROUND_DOWN)
|
||||
end
|
||||
|
||||
def unit_info
|
||||
{ name: name, base_unit: base_currency, quote_unit: quote_currency }
|
||||
end
|
||||
|
||||
# min_amount_by_precision - is the smallest positive number which could be
|
||||
# rounded to value greater then 0 with precision defined by
|
||||
# Market #amount_precision. So min_amount_by_precision is the smallest amount
|
||||
# of order/trade for current market.
|
||||
# E.g.
|
||||
# market.amount_precision => 4
|
||||
# min_amount_by_precision => 0.0001
|
||||
#
|
||||
# market.amount_precision => 2
|
||||
# min_amount_by_precision => 0.01
|
||||
#
|
||||
def min_amount_by_precision
|
||||
0.1.to_d**amount_precision
|
||||
end
|
||||
|
||||
# See #min_amount_by_precision.
|
||||
def min_price_by_precision
|
||||
0.1.to_d**price_precision
|
||||
end
|
||||
|
||||
def engine_name=(engine_name)
|
||||
self.engine = Engine.find_by(name: engine_name)
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20200909083000
|
||||
#
|
||||
# Table name: markets
|
||||
#
|
||||
# id :string(20) not null, primary key
|
||||
# base_unit :string(10) not null
|
||||
# quote_unit :string(10) not null
|
||||
# engine_id :bigint not null
|
||||
# amount_precision :integer default(4), not null
|
||||
# price_precision :integer default(4), not null
|
||||
# min_price :decimal(32, 16) default(0.0), not null
|
||||
# max_price :decimal(32, 16) default(0.0), not null
|
||||
# min_amount :decimal(32, 16) default(0.0), not null
|
||||
# position :integer not null
|
||||
# data :json
|
||||
# state :string(32) default("enabled"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_markets_on_base_unit (base_unit)
|
||||
# index_markets_on_base_unit_and_quote_unit (base_unit,quote_unit) UNIQUE
|
||||
# index_markets_on_engine_id (engine_id)
|
||||
# index_markets_on_position (position)
|
||||
# index_markets_on_quote_unit (quote_unit)
|
||||
#
|
||||
Reference in New Issue
Block a user