236 lines
7.9 KiB
Ruby
236 lines
7.9 KiB
Ruby
# 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)
|
|
#
|