Initial commit

This commit is contained in:
Yaser
2026-08-13 19:50:53 +03:30
commit 38084458fe
879 changed files with 95198 additions and 0 deletions

415
app/models/order.rb Normal file
View File

@@ -0,0 +1,415 @@
# encoding: UTF-8
# frozen_string_literal: true
require 'csv'
class Order < ApplicationRecord
belongs_to :market, required: true
belongs_to :member, required: true
attribute :uuid, :uuid if Rails.configuration.database_adapter.downcase != 'PostgreSQL'.downcase
# Error is raised in case market doesn't have enough volume to fulfill the Order.
InsufficientMarketLiquidity = Class.new(StandardError)
extend Enumerize
STATES = { pending: 0, wait: 100, done: 200, cancel: -100, reject: -200 }.freeze
enumerize :state, in: STATES, scope: true
TYPES = %w[market limit].freeze
THIRD_PARTY_ORDER_ACTION_TYPE = {
'submit_single' => 0,
'cancel_single' => 3,
'cancel_bulk' => 4
}.freeze
belongs_to :ask_currency, class_name: 'Currency', foreign_key: :ask
belongs_to :bid_currency, class_name: 'Currency', foreign_key: :bid
after_commit :trigger_event
validates :ord_type, :volume, :origin_volume, :locked, :origin_locked, presence: true
validates :price, numericality: { greater_than: 0 }, if: ->(order) { order.ord_type == 'limit' }
validates :origin_volume,
numericality: { greater_than: 0, greater_than_or_equal_to: ->(order){ order.market.min_amount } },
on: :create
validates :origin_volume, precision: { less_than_or_eq_to: ->(o) { o.market.amount_precision } },
if: ->(o) { o.origin_volume.present? }, on: :create
validate :market_order_validations, if: ->(order) { order.ord_type == 'market' }
validates :price, presence: true, if: :is_limit_order?
validates :price, precision: { less_than_or_eq_to: ->(o) { o.market.price_precision } },
if: ->(o) { o.price.present? }, on: :create
validates :price,
numericality: { less_than_or_equal_to: ->(order){ order.market.max_price }},
if: ->(order) { order.is_limit_order? && order.market.max_price.nonzero? },
on: :create
validates :price,
numericality: { greater_than_or_equal_to: ->(order){ order.market.min_price }},
if: :is_limit_order?, on: :create
attr_readonly :member_id,
:bid,
:ask,
:market_id,
:ord_type,
:origin_volume,
:origin_locked,
:created_at
PENDING = 'pending'
WAIT = 'wait'
DONE = 'done'
CANCEL = 'cancel'
REJECT = 'reject'
scope :done, -> { with_state(:done) }
scope :active, -> { with_state(:wait) }
scope :with_market, ->(market) { where(market_id: market) }
# Custom ransackers.
ransacker :state, formatter: proc { |v| STATES[v.to_sym] } do |parent|
parent.table[:state]
end
# Single Order can produce multiple Trades with different fee types (maker and taker).
# Since we can't predict fee types on order creation step and
# Market fees configuration can change we need to store fees on Order creation.
after_validation(on: :create, if: ->(o) { o.errors.blank? }) do
member.group_update(self)
trading_fee = TradingFee.for(group: member.group, market_id: market_id)
self.maker_fee = trading_fee.maker
self.taker_fee = trading_fee.taker
end
before_create do
self.uuid = UUID.generate if uuid.blank?
end
after_commit on: :create do
next unless ord_type == 'limit'
EventAPI.notify ['market', market_id, 'order_created'].join('.'), \
Serializers::EventAPI::OrderCreated.call(self)
end
after_commit on: :update do
next unless ord_type == 'limit'
event = case state
when 'cancel' then 'order_canceled'
when 'done' then 'order_completed'
else 'order_updated'
end
Serializers::EventAPI.const_get(event.camelize).call(self).tap do |payload|
EventAPI.notify ['market', market_id, event].join('.'), payload
end
end
class << self
def submit(id)
ActiveRecord::Base.transaction do
order = lock.find_by_id!(id)
return unless order.state == ::Order::PENDING
order.hold_account!.lock_funds!(order.locked)
order.record_submit_operations!
order.update!(state: ::Order::WAIT)
AMQP::Queue.enqueue(:matching, action: 'submit', order: order.to_matching_attributes)
end
rescue => e
order = find_by_id!(id)
order.update!(state: ::Order::REJECT) if order
raise e
end
def cancel(id)
order = lock.find_by_id!(id)
market_engine = order.market.engine
return unless order.state == ::Order::WAIT
return order.trigger_third_party_cancellation unless market_engine.peatio_engine?
ActiveRecord::Base.transaction do
order.hold_account!.unlock_funds!(order.locked)
order.record_cancel_operations!
order.update!(state: ::Order::CANCEL)
end
end
def trigger_bulk_cancel_third_party(engine_driver, filters = {})
AMQP::Queue.publish(engine_driver,
data: filters,
type: THIRD_PARTY_ORDER_ACTION_TYPE['cancel_bulk'])
end
def to_csv
attributes = %w[id market_id ord_type side price volume origin_volume avg_price trades_count state created_at updated_at]
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |order|
data = attributes[0...-2].map { |attr| order.send(attr) }
data += attributes[-2..-1].map { |attr| order.send(attr).iso8601 }
csv << data
end
end
end
end
def submit_order
return unless new_record?
self.locked = self.origin_locked = if ord_type == 'market' && side == 'buy'
[compute_locked * OrderBid::LOCKING_BUFFER_FACTOR, member_balance].min
else
compute_locked
end
raise ::Account::AccountError unless member_balance >= locked
return trigger_third_party_creation unless market.engine.peatio_engine?
save!
AMQP::Queue.enqueue(:order_processor,
{ action: 'submit', order: attributes },
{ persistent: false })
end
def trigger_third_party_creation
return unless new_record?
self.uuid ||= UUID.generate
self.created_at ||= Time.now
AMQP::Queue.publish(market.engine.driver, data: as_json_for_third_party, type: THIRD_PARTY_ORDER_ACTION_TYPE['submit_single'])
end
def trigger_cancellation
market.engine.peatio_engine? ? trigger_internal_cancellation : trigger_third_party_cancellation
end
def trigger_internal_cancellation
AMQP::Queue.enqueue(:matching, action: 'cancel', order: to_matching_attributes)
end
def trigger_third_party_cancellation
AMQP::Queue.publish(market.engine.driver,
data: as_json_for_third_party,
type: THIRD_PARTY_ORDER_ACTION_TYPE['cancel_single'])
end
def trades
Trade.where('maker_order_id = ? OR taker_order_id = ?', id, id)
end
def funds_used
origin_locked - locked
end
def trigger_event
# skip market type orders, they should not appear on trading-ui
return unless ord_type == 'limit' || state == 'done'
::AMQP::Queue.enqueue_event('private', member&.uid, 'order', for_notify)
end
def side
self.class.name.underscore[-3, 3] == 'ask' ? 'sell' : 'buy'
end
# @deprecated Please use {#side} instead
def kind
self.class.name.underscore[-3, 3]
end
# @deprecated Please use {#created_at} instead
def at
created_at.to_i
end
def for_notify
{
id: id,
market: market_id,
kind: kind,
side: side,
ord_type: ord_type,
price: price&.to_s('F'),
avg_price: avg_price&.to_s('F'),
state: state,
origin_volume: origin_volume.to_s('F'),
remaining_volume: volume.to_s('F'),
executed_volume: (origin_volume - volume).to_s('F'),
at: at,
created_at: created_at.to_i,
updated_at: updated_at.to_i,
trades_count: trades_count
}
end
def to_matching_attributes
{ id: id,
market: market_id,
type: type[-3, 3].downcase.to_sym,
ord_type: ord_type,
volume: volume,
price: price,
locked: locked,
timestamp: created_at.to_i }
end
def as_json_for_events_processor
{ id: id,
member_id: member_id,
member_uid: member.uid,
ask: ask,
bid: bid,
type: type,
ord_type: ord_type,
price: price,
volume: volume,
origin_volume: origin_volume,
market_id: market_id,
maker_fee: maker_fee,
taker_fee: taker_fee,
locked: locked,
state: read_attribute_before_type_cast(:state) }
end
def as_json_for_third_party
{
uuid: uuid,
market_id: market_id,
member_uid: member.uid,
origin_volume: origin_volume,
volume: volume,
price: price,
side: type,
type: ord_type,
created_at: created_at.to_i
}
end
# @deprecated
def round_amount_and_price
self.price = market.round_price(price.to_d) if price
if volume
self.volume = market.round_amount(volume.to_d)
self.origin_volume = origin_volume.present? ? market.round_amount(origin_volume.to_d) : volume
end
end
def record_submit_operations!
transaction do
# Debit main fiat/crypto Liability account.
# Credit locked fiat/crypto Liability account.
Operations::Liability.transfer!(
amount: locked,
currency: currency,
reference: self,
from_kind: :main,
to_kind: :locked,
member_id: member_id
)
end
end
def record_cancel_operations!
transaction do
# Debit locked fiat/crypto Liability account.
# Credit main fiat/crypto Liability account.
Operations::Liability.transfer!(
amount: locked,
currency: currency,
reference: self,
from_kind: :locked,
to_kind: :main,
member_id: member_id
)
end
end
def is_limit_order?
ord_type == 'limit'
end
def member_balance
member.get_account(currency).balance
end
private
def market_order_validations
errors.add(:price, 'must not be present') if price.present?
end
FUSE = '0.9'.to_d
def estimate_required_funds(price_levels)
required_funds = Account::ZERO
expected_volume = volume
until expected_volume.zero? || price_levels.empty?
level_price, level_volume = price_levels.shift
v = [expected_volume, level_volume].min
required_funds += yield level_price, v
expected_volume -= v
end
raise InsufficientMarketLiquidity if expected_volume.nonzero?
required_funds
end
end
# == Schema Information
# Schema version: 20201125134745
#
# Table name: orders
#
# id :integer not null, primary key
# uuid :binary(16) not null
# remote_id :string(255)
# bid :string(10) not null
# ask :string(10) not null
# market_id :string(20) not null
# price :decimal(32, 16)
# volume :decimal(32, 16) not null
# origin_volume :decimal(32, 16) not null
# maker_fee :decimal(17, 16) default(0.0), not null
# taker_fee :decimal(17, 16) default(0.0), not null
# state :integer not null
# type :string(8) not null
# member_id :integer not null
# ord_type :string(30) not null
# locked :decimal(32, 16) default(0.0), not null
# origin_locked :decimal(32, 16) default(0.0), not null
# funds_received :decimal(32, 16) default(0.0)
# trades_count :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_orders_on_member_id (member_id)
# index_orders_on_state (state)
# index_orders_on_type_and_market_id (type,market_id)
# index_orders_on_type_and_member_id (type,member_id)
# index_orders_on_type_and_state_and_market_id (type,state,market_id)
# index_orders_on_type_and_state_and_member_id (type,state,member_id)
# index_orders_on_updated_at (updated_at)
# index_orders_on_uuid (uuid) UNIQUE
#