# encoding: UTF-8 # frozen_string_literal: true require 'peatio/influxdb' class Trade < ApplicationRecord # == Constants ============================================================ extend Enumerize ZERO = '0.0'.to_d # == Relationships ======================================================== belongs_to :market, required: true belongs_to :maker_order, class_name: 'Order', foreign_key: :maker_order_id, required: true belongs_to :taker_order, class_name: 'Order', foreign_key: :taker_order_id, required: true belongs_to :maker, class_name: 'Member', foreign_key: :maker_id, required: true belongs_to :taker, class_name: 'Member', foreign_key: :taker_id, required: true has_many :bonus # == Validations ========================================================== validates :price, :amount, :total, numericality: { greater_than_or_equal_to: 0.to_d } # == Scopes =============================================================== scope :h24, -> { where('created_at > ?', 24.hours.ago) } scope :with_market, ->(market) { where(market_id: market) } scope :last_month, -> { where('created_at > ?', 1.month.ago) } # == Callbacks ============================================================ after_validation(on: :create) do # Set taker type before creation self.taker_type = taker_order&.side end after_commit on: :create do EventAPI.notify ['market', market_id, 'trade_completed'].join('.'), \ Serializers::EventAPI::TradeCompleted.call(self) end before_save :rial_total! # before_create :rial_total! # == Class Methods ======================================================== class << self def to_csv attributes = %w[id price amount maker_order_id taker_order_id market_id maker_id taker_id total created_at updated_at] CSV.generate(headers: true) do |csv| csv << attributes all.each do |trade| data = attributes[0...-2].map { |attr| trade.send(attr) } data += attributes[-2..-1].map { |attr| trade.send(attr).iso8601 } csv << data end end end def public_from_influx(market, limit = 100, options = {}) trades_query = ['SELECT id, price, amount, total, taker_type, market, created_at FROM trades WHERE market=%{market}'] trades_query << 'AND taker_type=%{type}' if options[:type].present? trades_query << 'AND created_at>=%{start_time}' if options[:start_time].present? trades_query << 'AND created_at<=%{end_time}' if options[:end_time].present? trades_query << 'AND price=%{price_eq}' if options[:price_eq].present? trades_query << 'AND price>=%{price_gt}' if options[:price_gt].present? trades_query << 'AND price=%{price_lt}' if options[:price_lt].present? trades_query << 'ORDER BY desc' unless limit.to_i.zero? trades_query << 'LIMIT %{limit}' options.merge!(limit: limit) end Peatio::InfluxDB.client(keyshard: market).query trades_query.join(' '), params: options.merge(market: market) do |_name, _tags, points| return points.map(&:deep_symbolize_keys!) end end # Low, High, First, Last, sum total (amount * price), sum 24 hours amount and average 24 hours price calculated using VWAP ratio for 24 hours trades def market_ticker_from_influx(market) tickers_query = 'SELECT MIN(price), MAX(price), FIRST(price), LAST(price), SUM(total) AS volume, SUM(amount) AS amount, SUM(total) / SUM(amount) AS vwap FROM trades WHERE market=%{market} AND time > now() - 24h' Peatio::InfluxDB.client(keyshard: market).query tickers_query, params: { market: market } do |_name, _tags, points| return points.map(&:deep_symbolize_keys!).first end end def trade_from_influx_before_date(market, date) trades_query = 'SELECT id, price, amount, total, taker_type, market, created_at FROM trades WHERE market=%{market} AND created_at < %{date} ORDER BY DESC LIMIT 1 ' Peatio::InfluxDB.client(keyshard: market).query trades_query, params: { market: market, date: date.to_i } do |_name, _tags, points| return points.map(&:deep_symbolize_keys!).first end end def trade_from_influx_after_date(market, date) trades_query = 'SELECT id, price, amount, total, taker_type, market, created_at FROM trades WHERE market=%{market} AND created_at >= %{date} ORDER BY ASC LIMIT 1 ' Peatio::InfluxDB.client(keyshard: market).query trades_query, params: { market: market, date: date.to_i } do |_name, _tags, points| return points.map(&:deep_symbolize_keys!).first end end def nearest_trade_from_influx(market, date) res = trade_from_influx_before_date(market, date) res.blank? ? trade_from_influx_after_date(market, date) : res end end # == Instance Methods ===================================================== def irt_price rls / total end def rial_total! return self.rls = total if market.quote.code == 'irt' tether_market = Market.find_by(quote: 'irt', base: 'usdt') tether_price = tether_market.price_now self.rls = tether_price * self.total end def order_fee(order) maker_order_id == order.id ? order.maker_fee : order.taker_fee end def side(member) return unless member order_for_member(member).side end def order_for_member(member) return unless member if member.id == maker_id maker_order elsif member.id == taker_id taker_order end end def sell_order [maker_order, taker_order].find { |o| o.side == 'sell' } end def buy_order [maker_order, taker_order].find { |o| o.side == 'buy' } end def trigger_event ::AMQP::Queue.enqueue_event("private", maker.uid, "trade", for_notify(maker)) ::AMQP::Queue.enqueue_event("private", taker.uid, "trade", for_notify(taker)) ::AMQP::Queue.enqueue_event("public", market.id, "trades", {trades: [for_global]}) end def for_notify(member = nil) { id: id, price: price.to_s || ZERO, amount: amount.to_s || ZERO, total: total.to_s || ZERO, market: market.id, side: side(member), taker_type: taker_type, created_at: created_at.to_i, order_id: order_for_member(member).id } end def for_global { tid: id, taker_type: taker_type, date: created_at.to_i, price: price.to_s || ZERO, amount: amount.to_s || ZERO } end def record_complete_operations! transaction do record_liability_debit! record_liability_credit! record_liability_transfer! record_revenues! end end def revert_trade! transaction do revert_sell_side! revert_buy_side! revert_fees! end end def influx_data { values: { id: id, price: price, amount: amount, total: total, taker_type: taker_type, created_at: created_at.to_i }, tags: { market: market.id } } end def write_to_influx Peatio::InfluxDB.client(keyshard: market_id).write_point(self.class.table_name, influx_data, "ns") end private def record_liability_debit! seller_outcome = amount buyer_outcome = total # Debit locked fiat/crypto Liability account for member who created ask. Operations::Liability.debit!( amount: seller_outcome, currency: sell_order.outcome_currency, reference: self, kind: :locked, member_id: sell_order.member_id, ) # Debit locked fiat/crypto Liability account for member who created bid. Operations::Liability.debit!( amount: buyer_outcome, currency: buy_order.outcome_currency, reference: self, kind: :locked, member_id: buy_order.member_id, ) end def record_liability_credit! seller_income = total - total * order_fee(sell_order) buyer_income = amount - amount * order_fee(buy_order) # Credit main fiat/crypto Liability account for member who created ask. Operations::Liability.credit!( amount: buyer_income, currency: buy_order.income_currency, reference: self, kind: :main, member_id: buy_order.member_id ) # Credit main fiat/crypto Liability account for member who created bid. Operations::Liability.credit!( amount: seller_income, currency: sell_order.income_currency, reference: self, kind: :main, member_id: sell_order.member_id ) end def record_liability_transfer! # Unlock unused funds. [maker_order, taker_order].each do |order| if order.volume.zero? && !order.locked.zero? Operations::Liability.transfer!( amount: order.locked, currency: order.outcome_currency, reference: self, from_kind: :locked, to_kind: :main, member_id: order.member_id ) end end end def record_revenues! seller_fee = total * order_fee(sell_order) buyer_fee = amount * order_fee(buy_order) # Credit main fiat/crypto Revenue account. Operations::Revenue.credit!( amount: seller_fee, currency: sell_order.income_currency, reference: self, member_id: sell_order.member_id ) # Credit main fiat/crypto Revenue account. Operations::Revenue.credit!( amount: buyer_fee, currency: buy_order.income_currency, reference: self, member_id: buy_order.member_id ) end def revert_sell_side! seller_outcome = amount seller_income = total - total * order_fee(sell_order) # Revert Trade for Sell side # Debit main fiat/crypto Liability account for member who created bid. Operations::Liability.debit!( amount: seller_income, currency: sell_order.income_currency, reference: self, kind: :main, member_id: sell_order.member_id ) Account.find_by(currency_id: sell_order.income_currency.id, member_id: sell_order.member_id).sub_funds(seller_income) # Credit main fiat/crypto Liability account for member who created ask. Operations::Liability.credit!( amount: seller_outcome, currency: sell_order.outcome_currency, reference: self, kind: :main, member_id: sell_order.member_id ) Account.find_by(currency_id: sell_order.outcome_currency.id, member_id: sell_order.member_id).plus_funds(seller_outcome) end def revert_buy_side! buyer_outcome = total buyer_income = amount - amount * order_fee(buy_order) # Revert Trade for Buy side # Debit main fiat/crypto Liability account for member who created ask Operations::Liability.debit!( amount: buyer_income, currency: buy_order.income_currency, reference: self, kind: :main, member_id: buy_order.member_id ) Account.find_by(currency_id: buy_order.income_currency.id, member_id: buy_order.member_id).sub_funds(buyer_income) # Credit main fiat/crypto Liability account for member who created bid. Operations::Liability.credit!( amount: buyer_outcome, currency: buy_order.outcome_currency, reference: self, kind: :main, member_id: buy_order.member_id ) Account.find_by(currency_id: buy_order.outcome_currency.id, member_id: buy_order.member_id).plus_funds(buyer_outcome) end def revert_fees! seller_fee = total * order_fee(sell_order) buyer_fee = amount * order_fee(buy_order) # Revert Revenues Operations::Revenue.debit!( amount: seller_fee, currency: sell_order.income_currency, reference: self, member_id: sell_order.member_id ) Operations::Revenue.debit!( amount: buyer_fee, currency: buy_order.income_currency, reference: self, member_id: buy_order.member_id ) end end # == Schema Information # Schema version: 20210120133912 # # Table name: trades # # id :integer not null, primary key # price :decimal(32, 16) not null # amount :decimal(32, 16) not null # total :decimal(32, 16) default(0.0), not null # maker_order_id :integer not null # taker_order_id :integer not null # market_id :string(20) not null # maker_id :integer not null # taker_id :integer not null # taker_type :string(20) default(""), not null # created_at :datetime not null # updated_at :datetime not null # # Indexes # # index_trades_on_created_at (created_at) # index_trades_on_maker_id (maker_id) # index_trades_on_maker_order_id (maker_order_id) # index_trades_on_market_id_and_created_at (market_id,created_at) # index_trades_on_taker_id (taker_id) # index_trades_on_taker_order_id (taker_order_id) #