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

View File

@@ -0,0 +1,28 @@
module Bench
module Barong_session
class Sessions
def initialize(trader:)
@trader = trader
end
def run!
config = YAML.load_file("#{Rails.root}/config/application.yml")
file = "#{Rails.root}/public/user_data.csv"
Kernel.puts "Creating members ..."
@members = Factories.create_list(:member, @trader)
Kernel.puts "sign up URL #{config["development"]["SIGN_UP_URL"]}"
@members.each do |member|
puts %x{http POST #{config["development"]["SIGN_UP_URL"]} email=#{member.email} password=#{config["development"]["SIGN_UP_PASSWORD"]}}
Kernel.puts "#{member.email} was requested for sign up with password=#{config["development"]["SIGN_UP_PASSWORD"]}"
end
CSV.open(file, 'w+', write_headers: true) do |csv|
@members.each do |member|
csv << %w{email}.map { |attr| member.send(attr) }
end
end
end
end
end
end

View File

@@ -0,0 +1,85 @@
# frozen_string_literal: true
module Bench
module Factories
class << self
def create(model, options = {})
"#{self.name}/#{model}"
.camelize
.constantize
.new(options)
.create
end
def create_list(model, number, options = {level: 2})
"#{self.name}/#{model}"
.camelize
.constantize
.new(options)
.create_list(number)
end
end
class Member
def initialize(options)
@options = options
end
def create
::Member.create!(construct_member)
end
def create_list(number)
Array.new(number) { create }
end
def construct_member
{ email: unique_email,
uid: "U#{Faker::Number.number(9)}",
level: 3,
role: 'member',
state: 'active' }.merge(@options)
end
def unique_email
@used_emails ||= ::Member.pluck(:email)
loop do
email = Faker::Internet.unique.email
unless @used_emails.include?(email)
@used_emails << email
return email
end
end
end
end
class Deposit
DEFAULT_DEPOSIT_AMOUNT = 1_000_000_000_000_000
def initialize(options)
@options = options
@currency = Currency.find(options[:currency_id])
end
def create
if @currency.fiat?
::Deposit.create!(construct_fiat_deposit).tap(&:charge!)
else
::Deposit.create!(construct_coin_deposit).tap { |d| d.with_lock { d.accept! } }
end
end
def construct_fiat_deposit
{ amount: DEFAULT_DEPOSIT_AMOUNT,
type: 'Deposits::Fiat' }.merge(@options)
end
def construct_coin_deposit
{ amount: DEFAULT_DEPOSIT_AMOUNT,
address: Faker::Blockchain::Bitcoin.address,
txid: Faker::Lorem.characters(64),
txout: 0,
type: 'Deposits::Coin' }.merge(@options)
end
end
end
end

View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
module Bench
module Helpers
def become_billionaire(member)
@currencies.each do |c|
Factories.create(:deposit, member_id: member.id, currency_id: c.id)
end
remove_locked
end
### for remove locked create in billioner method
def remove_locked
::Account.where.not(locked: 0).each do |re|
locked = re.locked
re.update(balance: locked, locked: 0)
end
end
def my_create_order(options)
Order.new(options)
.tap(&:round_amount_and_price)
.tap { |o| o.locked = o.origin_locked = o.compute_locked }
.tap { |o| o.hold_account!.lock_funds(o.locked) }
.tap(&:save)
end
end
end

View File

@@ -0,0 +1,60 @@
# frozen_string_literal: true
module Bench
module Injectors
class << self
def initialize_injector(config)
"#{self.name}/#{config[:injector]}"
.camelize
.constantize
.new(config)
end
end
class Base
attr_reader :config
def initialize(config)
@config = config
@number = config[:number].to_i
@step = config.fetch(:step, 1000).to_i
@markets = ::Market.where(id: config[:markets].split(',').map(&:squish).reject(&:blank?))
end
def generate!(members = nil)
@members = members || Member.all
@queue = Queue.new
Array.new(@number / @step) do
::Rails.logger.info { "Created orders: #{@queue.size}" }
ActiveRecord::Base.transaction do
Array.new(@step) do
create_order.tap { |o| @queue << o }
end
end
end
end
def pop
# Use non_blocking pop.
@queue.pop(true)
rescue ThreadError
# Return nil in case of empty queue.
nil
end
def size
@queue.size
end
def create_order
Order.new(construct_order)
.tap(&:round_amount_and_price)
.tap { |o| o.locked = o.origin_locked = o.compute_locked }
.tap { |o| o.hold_account!.lock_funds(o.locked) }
.tap(&:save)
end
end
end
end

View File

@@ -0,0 +1,37 @@
# frozen_string_literal: true
module Bench
module Injectors
class Bitfinex < Base
def initialize(config)
super
if config[:data_load_path].present?
@data = YAML.load_file(Rails.root.join(config[:data_load_path]))
@index = 0
end
end
private
def construct_order
@index = 0 if @data[@index].blank?
order_data = @data[@index]
price = order_data[1]
amount = order_data[2]
market = @markets.sample
type = amount > 0 ? 'OrderBid' : 'OrderAsk'
@index += 1
{ type: type,
state: Order::WAIT,
member: @members.sample,
market: market,
ask: market.base_unit,
bid: market.quote_unit,
ord_type: :limit,
price: price,
volume: amount.abs }
end
end
end
end

View File

@@ -0,0 +1,42 @@
# frozen_string_literal: true
module Bench
module Injectors
class Dummy < Base
extend Memoist
def initialize(config)
super
config.reverse_merge!(default_config)
%i[min_volume max_volume min_price max_price].each do |var|
instance_variable_set(:"@#{var}", config[var])
end
end
def construct_order(memberes = nil, order_type = nil)
market = @markets.sample
@members = memberes if memberes.present?
type = order_type || config.fetch(:side) { %w[OrderBid OrderAsk].sample }
{ type: type,
state: Order::PENDING,
member: @members.sample,
market: market,
ask: market.base_unit,
bid: market.quote_unit,
ord_type: :limit,
price: config.fetch(:price) { rand(@min_price..@max_price) },
volume: rand(@min_volume..@max_volume) }
end
private
def default_config
{ min_volume: 0.1,
max_volume: 1,
min_price: 0.5,
max_price: 2 }
end
memoize :default_config
end
end
end

View File

@@ -0,0 +1,181 @@
# frozen_string_literal: true
# TODO: Add Bench::Error and better errors processing.
# TODO: Add Bench::Report and extract all metrics to it.
module Bench
module Matching
class AMQP
# include Helpers
include ::Bench::Helpers
def initialize(config)
@config = config
@rmq_http_client = RabbitMQHTTP.default_client
@injector = Injectors.initialize_injector(@config[:orders])
@currencies = Currency.where(id: @config[:currencies].split(',').map(&:squish).reject(&:blank?))
# TODO: Print errors in the end of benchmark and include them into report.
@errors = []
end
def run_fee!
# TODO: Check if Matching daemon is running before start (use queue_info[:consumers]).
Kernel.puts "Creating members ..."
if Member.count == 0
@members = Factories.create_list(:member, @config[:traders])
else
@members = Member.all
end
Kernel.puts "Depositing funds ..."
@members.map(&method(:become_billionaire))
Kernel.puts "Generating orders by injector and saving them in db..."
# TODO: Add orders generation progress bar.
@config[:round].times do
@injector.generate!(@members)
@orders_number = @injector.size
Kernel.puts "Publishing messages to RabbitMQ..."
@matching_started_at = @publish_started_at = Time.now
# TODO: Add orders publishing progress bar.
publish_messages
end
@publish_finished_at = Time.now
Kernel.puts "Messages are published to RabbitMQ."
Kernel.puts "Waiting for order processing by matching daemon..."
wait_for_matching
@matching_finished_at = Time.now
end
def run!
# TODO: Check if Matching daemon is running before start (use queue_info[:consumers]).
Kernel.puts "Creating members ..."
if Member.count == 0
@members = Factories.create_list(:member, @config[:traders])
else
@members = Member.all
end
Kernel.puts "Depositing funds ..."
@members.map(&method(:become_billionaire))
Kernel.puts "Generating orders by injector and saving them in db..."
# TODO: Add orders generation progress bar.
@injector.generate!(@members)
@orders_number = @injector.size
Kernel.puts "Publishing messages to RabbitMQ..."
@matching_started_at = @publish_started_at = Time.now
# TODO: Add orders publishing progress bar.
publish_messages
@publish_finished_at = Time.now
Kernel.puts "Messages are published to RabbitMQ."
Kernel.puts "Waiting for order processing by matching daemon..."
wait_for_matching
@matching_finished_at = Time.now
end
def publish_messages
Array.new(@config[:threads]) do
Thread.new do
loop do
order = @injector.pop
break unless order
p Kernel.puts "user number #{order.member_id} and order number is #{order.id} maker fee = #{order.maker_fee} submiited order level is #{order.member.group} and trades = #{order.member.trades.last_month.inject(0){ |sum, x| sum + x.rls }}"
# ::AMQP::Queue.enqueue(:matching, action: 'submit', order: order.to_matching_attributes)
::AMQP::Queue.enqueue(:order_processor,
{ action: 'submit', order: order.to_matching_attributes },
{ persistent: false })
rescue StandardError => e
Kernel.puts e
@errors << e
end
end
end.map(&:join)
end
# TODO: Find better solution for getting message number in queue.
# E.g there is rabbitmqctl list_queues.
def wait_for_matching
last_log_time = Time.at(0)
queue_status_file = File.open(queue_status_file_path('matching'), 'a')
loop do
queue_status = matching_queue_status
break if queue_status[:messages].zero? &&
queue_status[:idle_since].present? &&
Time.parse("#{queue_status[:idle_since]} UTC") >= @publish_started_at
if last_log_time + 5 < Time.now
queue_status_file.puts(YAML.dump([queue_status.merge(timestamp: Time.now.iso8601).deep_stringify_keys]))
last_log_time = Time.now
end
sleep 0.5
end
end
# TODO: Add more useful metrics to result.
def result
@result ||=
begin
publish_ops = @orders_number / (@publish_finished_at - @publish_started_at)
matching_ops = @orders_number / (@matching_finished_at - @publish_started_at)
# TODO: Deal with calling iso8601(6) everywhere.
{ config: @config,
submit_publish: {
started_at: @publish_started_at.iso8601(6),
finished_at: @publish_started_at.iso8601(6),
operations: @orders_number,
ops: publish_ops
},
matching: {
finished_at: @matching_finished_at.iso8601(6),
operations: @orders_number,
ops: matching_ops,
started_at: @matching_started_at.iso8601(6)
}
}
end
end
def save_report
report_path = Rails.root.join(@config[:report_path])
FileUtils.mkpath(report_path)
report_name = "#{self.class.parent.name.demodulize.downcase}-"\
"#{self.class.name.humanize.demodulize}-#{@config[:orders][:injector]}-"\
"#{@config[:orders][:number]}-#{@publish_started_at.iso8601}.yml"
File.open(report_path.join(report_name), 'w') do |f|
f.puts YAML.dump(result.deep_stringify_keys)
end
end
private
# TODO: Use get queue by name.
# TODO: Use Faraday instead of RabbitMQ::HTTP::Client.
def matching_queue_status
@rmq_http_client.list_queues.find { |q| q[:name] == ::AMQP::Config.binding_queue(:matching).first }
end
def queue_status_file_path(name)
log_path = Rails.root.join(@config[:log_path])
FileUtils.mkpath(log_path)
log_path.join("#{name}-#{@publish_started_at.iso8601}.yml")
end
end
end
end

View File

@@ -0,0 +1,81 @@
# frozen_string_literal: true
# TODO: Add Bench::Error and better errors processing.
# TODO: Add Bench::Report and extract all metrics to it.
module Bench
module Matching
class Direct
include Helpers
def initialize(config)
@config = config
@injector = Injectors.initialize_injector(@config[:orders])
@currencies = Currency.where(id: @config[:currencies].split(',').map(&:squish).reject(&:blank?))
@matching = Workers::AMQP::Matching.new
# TODO: Print errors in the end of benchmark and include them into report.
@errors = []
end
def run!
Kernel.puts "Creating members ..."
@members = Factories.create_list(:member, @config[:traders])
Kernel.puts "Depositing funds ..."
@members.map(&method(:become_billionaire))
Kernel.puts "Generating orders by injector and saving them in db..."
# TODO: Add orders generation progress bar.
@injector.generate!(@members)
@orders_number = @injector.size
@matching_started_at = Time.now
process_messages
@matching_finished_at = Time.now
end
def process_messages
loop do
order = @injector.pop
break unless order
@matching.process({action: 'submit', order: order.to_matching_attributes}, 'metadata', 'delivery_info')
rescue StandardError => e
Kernel.puts e
@errors << e
end
end
# TODO: Add more useful metrics to result.
def result
@result ||=
begin
matching_ops = @orders_number / (@matching_finished_at - @matching_started_at)
# TODO: Deal with calling iso8601(6) everywhere.
{ config: @config,
matching: {
started_at: @matching_started_at.iso8601(6),
finished_at: @matching_finished_at.iso8601(6),
operations: @orders_number,
ops: matching_ops
}
}
end
end
def save_report
report_path = Rails.root.join(@config[:report_path])
FileUtils.mkpath(report_path)
report_name = "#{self.class.parent.name.demodulize.downcase}-"\
"#{self.class.name.humanize.demodulize}-#{@config[:orders][:injector]}-"\
"#{@config[:orders][:number]}-#{@matching_started_at.iso8601}.yml"
File.open(report_path.join(report_name), 'w') do |f|
f.puts YAML.dump(result.deep_stringify_keys)
end
end
end
end
end

View File

@@ -0,0 +1,95 @@
# frozen_string_literal: true
module Bench
module OrderProcessing
class AMQP < TradeExecution::AMQP
def run!
# TODO: Check if OrderProcessing daemon is running before start (use queue_info[:consumers]).
super
Kernel.puts "Init wait orders queue..."
@orders_for_cancel_number = init_wait_orders_queue!.size # TODO: If zero? raise Error.
Kernel.puts "Start wait orders publish..."
@cancel_publish_started_at = @order_processing_started_at = Time.now
publish_cancel_messages
@cancel_publish_finished_at = Time.now
Kernel.puts "Messages are published to RabbitMQ."
Kernel.puts "Waiting for order processing by order processor..."
wait_for_order_processing
@order_processing_finished_at = Time.now
end
def publish_cancel_messages
Array.new(@config[:threads]) do
Thread.new do
loop do
break if @wait_orders_queue.blank?
order = @wait_orders_queue.pop
AMQP::Queue.enqueue(:matching, action: 'cancel', order: order.to_matching_attributes)
rescue StandardError => e
Kernel.puts e
@errors << e
end
end
end.map(&:join)
end
def wait_for_order_processing
last_log_time = Time.at(0)
queue_status_file = File.open(queue_status_file_path('order-processing'), 'a')
loop do
queue_status = order_processing_queue_status
# NOTE: If no orders where cancelled idle_since would not change.
break if queue_status[:messages].zero? &&
queue_status[:idle_since].present? &&
Time.parse("#{queue_status[:idle_since]} UTC") >= @order_processing_started_at
if last_log_time + 5 < Time.now
queue_status_file.puts(YAML.dump([queue_status.merge(timestamp: Time.now.iso8601).deep_stringify_keys]))
last_log_time = Time.now
end
sleep 0.5
end
end
def result
@result ||=
begin
cancel_publish_ops = @orders_for_cancel_number / (@cancel_publish_finished_at - @cancel_publish_started_at)
order_processing_ops = @orders_for_cancel_number / (@order_processing_finished_at - @order_processing_started_at)
super.merge(
cancel_publish: {
started_at: @cancel_publish_started_at.iso8601(6),
finished_at: @cancel_publish_finished_at.iso8601(6),
operations: @orders_for_cancel_number,
ops: cancel_publish_ops
},
order_processing: {
started_at: @order_processing_started_at.iso8601(6),
finished_at: @order_processing_finished_at.iso8601(6),
operations: @orders_for_cancel_number,
ops: order_processing_ops
}
)
end
end
private
def init_wait_orders_queue!
orders = Order.where(state: Order::WAIT).shuffle
@wait_orders_queue =
orders.each_with_object(Queue.new) do |o, queue|
queue << o
end
end
def order_processing_queue_status
@rmq_http_client.list_queues.find { |q| q[:name] == AMQP::Config.binding_queue(:order_processor).first }
end
end
end
end

View File

@@ -0,0 +1,82 @@
# frozen_string_literal: true
# TODO: Add Bench::Error and better errors processing.
# TODO: Add Bench::Report and extract all metrics to it.
module Bench
module OrderProcessing
class Direct
include Helpers
def initialize(config)
@config = config
@injector = Injectors.initialize_injector(@config[:orders])
@currencies = Currency.where(id: @config[:currencies].split(',').map(&:squish).reject(&:blank?))
@order_processor = Workers::AMQP::OrderProcessor.new
# TODO: Print errors in the end of benchmark and include them into report.
@errors = []
end
def run!
Kernel.puts "Creating members ..."
@members = Factories.create_list(:member, @config[:traders])
Kernel.puts "Depositing funds ..."
@members.map(&method(:become_billionaire))
Kernel.puts "Generating orders by injector and saving them in db..."
# TODO: Add orders generation progress bar.
@injector.generate!(@members)
@orders_number = @injector.size
@processing_started_at = Time.now
process_orders
@processing_finished_at = Time.now
end
def process_orders
loop do
order = @injector.pop
break unless order
@order_processor.process({action: 'cancel', order: order.to_matching_attributes}.deep_stringify_keys!)
rescue StandardError => e
Kernel.puts e
@errors << e
end
end
# TODO: Add more useful metrics to result.
def result
@result ||=
begin
processing_ops = @orders_number / (@processing_finished_at - @processing_started_at)
# TODO: Deal with calling iso8601(6) everywhere.
{ config: @config,
order_processing: {
started_at: @processing_started_at.iso8601(6),
finished_at: @processing_finished_at.iso8601(6),
operations: @orders_number,
ops: processing_ops
}
}
end
end
def save_report
report_path = Rails.root.join(@config[:report_path])
FileUtils.mkpath(report_path)
report_name = "#{self.class.parent.name.demodulize.downcase}-"\
"#{self.class.name.humanize.demodulize}-#{@config[:orders][:injector]}-"\
"#{@config[:orders][:number]}-#{@processing_started_at.iso8601}.yml"
File.open(report_path.join(report_name), 'w') do |f|
f.puts YAML.dump(result.deep_stringify_keys)
end
end
end
end
end

View File

@@ -0,0 +1,67 @@
# frozen_string_literal: true
module Bench
module TradeExecution
class AMQP < Matching::AMQP
def run!
# TODO: Check if TradeExecutor daemon is running before start (use queue_info[:consumers]).
super
Kernel.puts 'Waiting for trades processing by trade execution daemon...'
@execution_started_at = @publish_started_at
wait_for_execution
@execution_finished_at = Time.now
end
def run_fee!
# TODO: Check if TradeExecutor daemon is running before start (use queue_info[:consumers]).
super
Kernel.puts 'Waiting for trades processing by trade execution daemon...'
@execution_started_at = @publish_started_at
wait_for_execution
@execution_finished_at = Time.now
end
def wait_for_execution
last_log_time = Time.at(0)
queue_status_file = File.open(queue_status_file_path('trade-execution'), 'a')
loop do
queue_status = trade_execution_queue_status
# NOTE: If no orders where matched idle_since would not change.
break if queue_status[:messages].zero? &&
queue_status[:idle_since].present? &&
Time.parse("#{queue_status[:idle_since]} UTC") >= @execution_started_at
if last_log_time + 5 < Time.now
queue_status_file.puts(YAML.dump([queue_status.merge(timestamp: Time.now.iso8601).deep_stringify_keys]))
last_log_time = Time.now
end
sleep 0.5
end
end
def result
@result ||=
begin
trades_number = Trade.where('created_at >= ?', @publish_started_at).length
trades_ops = trades_number / (@execution_finished_at - @execution_started_at)
super.merge(
trade_execution: {
started_at: @execution_started_at.iso8601(6),
finished_at: @execution_finished_at.iso8601(6),
operations: trades_number,
ops: trades_ops
}
)
end
end
private
def trade_execution_queue_status
@rmq_http_client.list_queues.find { |q| q[:name] == ::AMQP::Config.binding_queue(:trade_executor).first }
end
end
end
end

View File

@@ -0,0 +1,85 @@
# frozen_string_literal: true
module Bench
module TradeExecution
class Direct
include Helpers
def initialize(config)
@config = config
raise "This benchmark doesn't support Bitfinex injector" if config[:orders][:injector] == 'bitfinex'
@bid_injector = Injectors.initialize_injector(@config[:orders].merge(price: 1, side: 'OrderBid'))
@ask_injector = Injectors.initialize_injector(@config[:orders].merge(price: 0.9, side: 'OrderAsk'))
@currencies = Currency.where(id: @config[:currencies].split(',').map(&:squish).reject(&:blank?))
@executor = Workers::AMQP::TradeExecutor.new
# TODO: Print errors in the end of benchmark and include them into report.
@errors = []
end
def run!
Kernel.puts "Creating members ..."
@members = Factories.create_list(:member, @config[:traders])
Kernel.puts "Depositing funds ..."
@members.map(&method(:become_billionaire))
Kernel.puts "Generating orders by injector and saving them in db..."
Kernel.puts 'Waiting for trades processing by trade execution daemon...'
@bid_injector.generate!(@members)
@ask_injector.generate!(@members)
@execution_started_at = Time.now
process_messages
@execution_finished_at = Time.now
end
def process_messages
loop do
ask = @ask_injector.pop
bid = @bid_injector.pop
break unless ask && bid
volume = ask.volume > bid.volume ? bid.volume : ask.volume
@executor.process({ market_id: ask.market_id,
ask_id: ask.id,
bid_id: bid.id,
strike_price: ask.price,
volume: volume,
funds: volume * ask.price })
rescue StandardError => e
Kernel.puts e
@errors << e
end
end
def result
@result ||=
begin
trades_number = Trade.where('created_at >= ?', @execution_started_at).length
trades_ops = trades_number / (@execution_finished_at - @execution_started_at)
{ config: @config,
trade_execution: {
started_at: @execution_started_at.iso8601(6),
finished_at: @execution_finished_at.iso8601(6),
operations: trades_number,
ops: trades_ops
}
}
end
end
def save_report
report_path = Rails.root.join(@config[:report_path])
FileUtils.mkpath(report_path)
report_name = "#{self.class.parent.name.demodulize.downcase}-"\
"#{self.class.name.humanize.demodulize}-#{@config[:orders][:injector]}-"\
"#{@config[:orders][:number]}-#{@execution_started_at.iso8601}.yml"
File.open(report_path.join(report_name), 'w') do |f|
f.puts YAML.dump(result.deep_stringify_keys)
end
end
end
end
end