moneymarket 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/moneymarket +5 -0
- data/lib/moneymarket.rb +20 -0
- data/lib/moneymarket/commands/check_order_solvent.rb +12 -0
- data/lib/moneymarket/commands/execute_match.rb +109 -0
- data/lib/moneymarket/commands/limit_matches.rb +28 -0
- data/lib/moneymarket/commands/match_order_to_slope.rb +32 -0
- data/lib/moneymarket/core/account.rb +41 -0
- data/lib/moneymarket/core/account_provider.rb +34 -0
- data/lib/moneymarket/core/ask.rb +27 -0
- data/lib/moneymarket/core/bid.rb +31 -0
- data/lib/moneymarket/core/book.rb +76 -0
- data/lib/moneymarket/core/market.rb +39 -0
- data/lib/moneymarket/core/match.rb +18 -0
- data/lib/moneymarket/core/order.rb +60 -0
- data/lib/moneymarket/core/slope.rb +44 -0
- data/lib/moneymarket/utils/calculator.rb +27 -0
- data/lib/moneymarket/utils/command.rb +27 -0
- data/lib/moneymarket/version.rb +3 -0
- metadata +238 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 431f681b6306e4f8f9bac8017b05eb444b8fdf2e
|
4
|
+
data.tar.gz: f27dfd43db2c3aa4eadb7e4f9e82f44760b35c1a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c889833e9447fb731530f530aa588668e2d0d399d5730c8febaa935e7f5d8374dc88c3af6b40fa537238ef34b51527833c61235d982b768957d39bd95b98cac1
|
7
|
+
data.tar.gz: 9b9ecb7169ef94c1bb361e3a3c86f0bb85c5e633029c2bb716176594ac1646066bab861326227dacd89b26b02d3c823ddbac4cde512737ff47c8ccb52417934d
|
data/bin/moneymarket
ADDED
data/lib/moneymarket.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'moneymarket/version'
|
2
|
+
|
3
|
+
require 'money'
|
4
|
+
require 'moneymarket/utils/calculator'
|
5
|
+
require 'moneymarket/utils/command'
|
6
|
+
|
7
|
+
require 'moneymarket/core/order'
|
8
|
+
require 'moneymarket/core/bid'
|
9
|
+
require 'moneymarket/core/ask'
|
10
|
+
require 'moneymarket/core/market'
|
11
|
+
require 'moneymarket/core/match'
|
12
|
+
require 'moneymarket/core/account'
|
13
|
+
require 'moneymarket/core/account_provider'
|
14
|
+
require 'moneymarket/core/slope'
|
15
|
+
require 'moneymarket/core/book'
|
16
|
+
|
17
|
+
module Moneymarket
|
18
|
+
def self.book()
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CheckOrderSolvent < Command.new(:market, :order)
|
2
|
+
def perform
|
3
|
+
return true if order.market_order?
|
4
|
+
source_available_amount >= order.required_amount
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def source_available_amount
|
10
|
+
market.source_account_for(order).available
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
class ApplyMatch < Command.new(:market, :match)
|
2
|
+
def perform
|
3
|
+
cache_balances :before
|
4
|
+
execute_exchange
|
5
|
+
execute_fees unless same_user?
|
6
|
+
cache_balances :after
|
7
|
+
event
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def execute_exchange
|
13
|
+
bid_quote_account.unfreeze quote unless bid.new?
|
14
|
+
ask_base_account.unfreeze volume unless ask.new?
|
15
|
+
|
16
|
+
bid.consume volume
|
17
|
+
ask.consume volume
|
18
|
+
|
19
|
+
transfer quote, from: bid_quote_account, to: ask_quote_account
|
20
|
+
transfer volume, from: ask_base_account, to: bid_base_account
|
21
|
+
|
22
|
+
event.quote = quote
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute_fees
|
26
|
+
transfer bid_fee, from: bid_base_account, to: exchange_base_account
|
27
|
+
transfer ask_fee, from: ask_quote_account, to: exchange_quote_account
|
28
|
+
|
29
|
+
event.bid_fee = bid_fee
|
30
|
+
event.ask_fee = ask_fee
|
31
|
+
end
|
32
|
+
|
33
|
+
def cache_balances(_which)
|
34
|
+
event.public_send("bid_base_account_#{_which}=", bid_base_account.clone)
|
35
|
+
event.public_send("bid_quote_account_#{_which}=", bid_quote_account.clone)
|
36
|
+
event.public_send("ask_base_account_#{_which}=", ask_base_account.clone)
|
37
|
+
event.public_send("ask_quote_account_#{_which}=", ask_quote_account.clone)
|
38
|
+
event.public_send("ex_base_account_#{_which}=", exchange_base_account.clone)
|
39
|
+
event.public_send("ex_quote_account_#{_which}=", exchange_quote_account.clone)
|
40
|
+
end
|
41
|
+
|
42
|
+
def event
|
43
|
+
@event ||= Events::TransactionExecuted.new.tap do |ev|
|
44
|
+
ev.bid = bid.ref
|
45
|
+
ev.ask = ask.ref
|
46
|
+
ev.volume = volume
|
47
|
+
ev.quote = quote
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def bid
|
52
|
+
match.bid
|
53
|
+
end
|
54
|
+
|
55
|
+
def ask
|
56
|
+
match.ask
|
57
|
+
end
|
58
|
+
|
59
|
+
def volume
|
60
|
+
match.volume
|
61
|
+
end
|
62
|
+
|
63
|
+
def quote
|
64
|
+
@quote ||= begin
|
65
|
+
CalculateQuote.new(volume: volume, unit_price: match.price_paid).perform
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def transfer(_amount, from: nil, to: nil)
|
70
|
+
from.withdraw _amount
|
71
|
+
to.deposit _amount
|
72
|
+
end
|
73
|
+
|
74
|
+
def bid_base_account
|
75
|
+
market.base_account_for match.bid
|
76
|
+
end
|
77
|
+
|
78
|
+
def ask_base_account
|
79
|
+
market.base_account_for match.ask
|
80
|
+
end
|
81
|
+
|
82
|
+
def bid_quote_account
|
83
|
+
market.quote_account_for match.bid
|
84
|
+
end
|
85
|
+
|
86
|
+
def ask_quote_account
|
87
|
+
market.quote_account_for match.ask
|
88
|
+
end
|
89
|
+
|
90
|
+
def same_user?
|
91
|
+
bid.user == ask.user
|
92
|
+
end
|
93
|
+
|
94
|
+
def bid_fee
|
95
|
+
@bid_fee ||= CalculateFee.new(order: bid, collected_amount: volume).perform
|
96
|
+
end
|
97
|
+
|
98
|
+
def ask_fee
|
99
|
+
@ask_fee ||= CalculateFee.new(order: ask, collected_amount: quote).perform
|
100
|
+
end
|
101
|
+
|
102
|
+
def exchange_base_account
|
103
|
+
market.exchange_base_account
|
104
|
+
end
|
105
|
+
|
106
|
+
def exchange_quote_account
|
107
|
+
market.exchange_quote_account
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class LimitMatches < Command.new(:matches, :destination_limit)
|
2
|
+
def perform
|
3
|
+
matches.select do |match|
|
4
|
+
next false if destination_limit <= 0
|
5
|
+
|
6
|
+
quote = calc_quote(match)
|
7
|
+
if quote > destination_limit
|
8
|
+
# IDEA: instead of lowering the match amount maybe this should be considered insolvency
|
9
|
+
match.volume = calc_volume(match, destination_limit)
|
10
|
+
destination_limit = 0
|
11
|
+
else
|
12
|
+
destination_limit -= quote
|
13
|
+
end
|
14
|
+
|
15
|
+
match.volume > 0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def calc_quote(_match)
|
22
|
+
_match.match.destination_collected_amount for_volume: _match.volume
|
23
|
+
end
|
24
|
+
|
25
|
+
def calc_volume(_match, _new_quote)
|
26
|
+
_match.match.volume_required_to_collect _new_quote
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class MatchOrderToSlope < Command.new(:slope, :order)
|
2
|
+
def perform
|
3
|
+
Enumerator.new do |y|
|
4
|
+
remaining = order.volume
|
5
|
+
each_order do |match|
|
6
|
+
next if match.timestamp > _order.timestamp # << this should trigger an alert, should never happen
|
7
|
+
|
8
|
+
if match.volume >= remaining
|
9
|
+
y << build_match(match, remaining)
|
10
|
+
break
|
11
|
+
else
|
12
|
+
y << build_match(match, match.volume)
|
13
|
+
remaining -= match.volume
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def each_order
|
22
|
+
if order.market_order?
|
23
|
+
slope.each
|
24
|
+
else
|
25
|
+
slope.each_until_limit(order.limit)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_match(_match, _volume)
|
30
|
+
Match.new(trigger: order, match: _match, volume: _volume)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
class Account
|
3
|
+
attr_reader :user, :currency, :total, :frozen, :ref
|
4
|
+
|
5
|
+
def initialize(_user, _currency, _total, _frozen, _ref = nil)
|
6
|
+
@user = _user
|
7
|
+
@currency = _currency
|
8
|
+
@total = _total
|
9
|
+
@frozen = _frozen
|
10
|
+
@ref = _ref
|
11
|
+
end
|
12
|
+
|
13
|
+
def available
|
14
|
+
total - frozen
|
15
|
+
end
|
16
|
+
|
17
|
+
def freeze(_amount)
|
18
|
+
# TODO: check currency?
|
19
|
+
raise ArgumentError, 'amount to freeze must be positive' if _amount < 0
|
20
|
+
raise ArgumentError, 'trying to freeze more than is available' if _amount > available
|
21
|
+
@frozen += _amount
|
22
|
+
end
|
23
|
+
|
24
|
+
def unfreeze(_amount)
|
25
|
+
raise ArgumentError, 'amount to unfreeze must be positive' if _amount < 0
|
26
|
+
raise ArgumentError, 'trying to unfreeze more than is frozen' if _amount > frozen
|
27
|
+
@frozen -= _amount
|
28
|
+
end
|
29
|
+
|
30
|
+
def deposit(_amount)
|
31
|
+
raise ArgumentError, 'amount to deposit must be positive' if _amount < 0
|
32
|
+
@total += _amount
|
33
|
+
end
|
34
|
+
|
35
|
+
def withdraw(_amount)
|
36
|
+
raise ArgumentError, 'amount to withdraw must be positive' if _amount < 0
|
37
|
+
raise ArgumentError, 'not enough funds to withdraw' if _amount > available
|
38
|
+
@total -= _amount
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
class AccountProvider
|
3
|
+
def initialize
|
4
|
+
@accounts = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def reset
|
8
|
+
@accounts = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def exchange_account_for(_currency)
|
12
|
+
assert_implemented :exchange_account_for
|
13
|
+
end
|
14
|
+
|
15
|
+
def account_for(_user_id, _currency)
|
16
|
+
assert_implemented :account_for
|
17
|
+
end
|
18
|
+
|
19
|
+
def preload_user_balances(_users, _currencies)
|
20
|
+
assert_implemented :preload_user_balances
|
21
|
+
end
|
22
|
+
|
23
|
+
def preload_balances(_order)
|
24
|
+
_order.base_balance = fetch_balance(_order.account_id, _order.base_currency)
|
25
|
+
_order.quote_balance = fetch_balance(_order.account_id, _order.quote_currency)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def assert_implemented(_name)
|
31
|
+
raise NotImplementedError, "#{_name} is not implemented"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
class Ask < Order
|
3
|
+
def bid?
|
4
|
+
false
|
5
|
+
end
|
6
|
+
|
7
|
+
def source_account
|
8
|
+
:base_account
|
9
|
+
end
|
10
|
+
|
11
|
+
def destination_account
|
12
|
+
:quote_account
|
13
|
+
end
|
14
|
+
|
15
|
+
def source_required_amount(for_volume: nil)
|
16
|
+
volume || amount
|
17
|
+
end
|
18
|
+
|
19
|
+
def destination_collected_amount(for_volume: nil)
|
20
|
+
Calculator.quote(volume: volume || amount, unit_price: limit)
|
21
|
+
end
|
22
|
+
|
23
|
+
def volume_required_to_collect(_source_amount)
|
24
|
+
_source_amount
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
class Bid < Order
|
3
|
+
def bid?
|
4
|
+
true
|
5
|
+
end
|
6
|
+
|
7
|
+
def source_account
|
8
|
+
:quote_account
|
9
|
+
end
|
10
|
+
|
11
|
+
def destination_account
|
12
|
+
:base_account
|
13
|
+
end
|
14
|
+
|
15
|
+
def source_required_amount(for_volume: nil)
|
16
|
+
Calculator.quote(volume: for_volume || volume, unit_price: limit)
|
17
|
+
end
|
18
|
+
|
19
|
+
def destination_collected_amount(for_volume: nil)
|
20
|
+
for_volume || volume
|
21
|
+
end
|
22
|
+
|
23
|
+
def volume_required_to_collect(_source_amount)
|
24
|
+
Calculator.volume(
|
25
|
+
quote: _source_amount,
|
26
|
+
unit_price: limit,
|
27
|
+
unit_currency: volume.currency
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
def Book
|
3
|
+
attr_reader :asks, :bids
|
4
|
+
|
5
|
+
def initialize(_market)
|
6
|
+
@market = _market
|
7
|
+
@bids = Slope.new { |a, b| a.limit_cents <=> b.limit_cents }
|
8
|
+
@asks = Slope.new { |a, b| b.limit_cents <=> a.limit_cents }
|
9
|
+
end
|
10
|
+
|
11
|
+
def flush_events
|
12
|
+
flushed = @events
|
13
|
+
@events = []
|
14
|
+
flushed
|
15
|
+
end
|
16
|
+
|
17
|
+
def push(_order)
|
18
|
+
# TODO: check market(currency)
|
19
|
+
|
20
|
+
if _order.new?
|
21
|
+
raise ArgumentError, 'insolvent order' unless check_solvent _order
|
22
|
+
|
23
|
+
matches = match_to_slope _order
|
24
|
+
matches = adjust_market_order matches, _order if _order.market_order?
|
25
|
+
preload_balances matches
|
26
|
+
matches.each { |tx| execute_match tx }
|
27
|
+
_order.flag_as_open
|
28
|
+
|
29
|
+
freeze_source_balance _order
|
30
|
+
# TODO: execute_triggers (stop-loss, take-profit, etc)
|
31
|
+
end
|
32
|
+
|
33
|
+
slope_for(_order).add _order if _order.open?
|
34
|
+
end
|
35
|
+
|
36
|
+
def cancel(_order)
|
37
|
+
if _order.open?
|
38
|
+
|
39
|
+
slope_for(_order).remove _order
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def slope_for(_order)
|
46
|
+
if order.bid? then bids else asks end
|
47
|
+
end
|
48
|
+
|
49
|
+
def oposite_slope(_order)
|
50
|
+
if order.bid? then asks else bids end
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_solvent(_order)
|
54
|
+
CheckOrderSolvent.for market: market, order: _order
|
55
|
+
end
|
56
|
+
|
57
|
+
def match_to_slope(_order)
|
58
|
+
slope = oposite_slope(_order)
|
59
|
+
MatchOrderToSlope.for slope: slope, order: _order
|
60
|
+
end
|
61
|
+
|
62
|
+
def adjust_market_order(_matches, _order)
|
63
|
+
limit = market.source_account(_order).available_amount
|
64
|
+
LimitMatches.for matches: _matches, limit: limit
|
65
|
+
end
|
66
|
+
|
67
|
+
def execute_match(_match)
|
68
|
+
@events << ExecuteMatch.for(market: market, match: _match)
|
69
|
+
end
|
70
|
+
|
71
|
+
def preload_balances(_matches)
|
72
|
+
users = _matches.map { |m| [m.bid.user, m.ask.user] }.flatten.uniq
|
73
|
+
market.preload_user_balances users
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
class Market
|
3
|
+
attr_reader :account_provider
|
4
|
+
|
5
|
+
def initialize(_base_currency, _quote_currency, account_provider = nil)
|
6
|
+
@base_currency = _base_currency
|
7
|
+
@quote_currency = _quote_currency
|
8
|
+
@account_provider = account_provider
|
9
|
+
end
|
10
|
+
|
11
|
+
def preload_user_balances(_users)
|
12
|
+
account_provider.preload_balances(_users, [_base_currency, _quote_currency])
|
13
|
+
end
|
14
|
+
|
15
|
+
def exchange_base_account
|
16
|
+
account_provider.exchange_account_for base_currency
|
17
|
+
end
|
18
|
+
|
19
|
+
def exchange_quote_account
|
20
|
+
account_provider.exchange_account_for quote_currency
|
21
|
+
end
|
22
|
+
|
23
|
+
def base_account_for(_order)
|
24
|
+
account_provider.account_for(_order.user_id, base_currency)
|
25
|
+
end
|
26
|
+
|
27
|
+
def quote_account_for(_order)
|
28
|
+
account_provider.account_for(_order.user_id, quote_currency)
|
29
|
+
end
|
30
|
+
|
31
|
+
def source_account_for(_order)
|
32
|
+
public_send(_order.source_account, _order)
|
33
|
+
end
|
34
|
+
|
35
|
+
def destination_account_for(_order)
|
36
|
+
public_send(_order.destination_account, _order)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
class Match
|
3
|
+
attr_reader :market, :bid, :ask
|
4
|
+
attr_accessor :volume
|
5
|
+
|
6
|
+
def initialize(_trigger, _match, _volume)
|
7
|
+
@trigger = _trigger
|
8
|
+
@bid = _trigger.bid? ? _trigger : _match
|
9
|
+
@ask = _trigger.bid? ? _match : _trigger
|
10
|
+
@match = _match
|
11
|
+
@volume = _volume
|
12
|
+
end
|
13
|
+
|
14
|
+
def price_paid
|
15
|
+
match.limit
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
class Order
|
3
|
+
NEW = :new
|
4
|
+
OPEN = :open
|
5
|
+
TRADED = :traded
|
6
|
+
CANCELED = :canceled
|
7
|
+
|
8
|
+
attr_reader :user, :volume, :limit, :fee, :ref
|
9
|
+
|
10
|
+
def initialize(user: nil, status: NEW, volume: nil, limit: nil, fee: 0.0, ref: nil)
|
11
|
+
@user = user
|
12
|
+
@status = status
|
13
|
+
@volume = volume
|
14
|
+
@limit = limit
|
15
|
+
@fee = fee
|
16
|
+
@ref = ref
|
17
|
+
end
|
18
|
+
|
19
|
+
def market_order?
|
20
|
+
limit.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def base_currency
|
24
|
+
volume.currency
|
25
|
+
end
|
26
|
+
|
27
|
+
def quote_currency
|
28
|
+
limit.currency
|
29
|
+
end
|
30
|
+
|
31
|
+
def consume(_volume)
|
32
|
+
raise ArgumentError, 'cannot consume, order closed' if closed?
|
33
|
+
raise ArgumentError, 'amount to consume must be positive' if _volume < 0
|
34
|
+
raise ArgumentError, 'trying to consume more than is available' if _volume > @volume
|
35
|
+
@volume -= _volume
|
36
|
+
|
37
|
+
@status = TRADED if @volume == 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def flag_as_open
|
41
|
+
if market_order?
|
42
|
+
flag_as_canceled
|
43
|
+
else
|
44
|
+
@status = OPEN if new?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def flag_as_canceled
|
49
|
+
@status = CANCELED unless closed?
|
50
|
+
end
|
51
|
+
|
52
|
+
[NEW, OPEN, TRADED, CANCELED].each do |name|
|
53
|
+
define_method("#{name}?") { @status == name }
|
54
|
+
end
|
55
|
+
|
56
|
+
def closed?
|
57
|
+
traded? || canceled?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
class Slope
|
3
|
+
def initialize(&_comp_func)
|
4
|
+
@orders = []
|
5
|
+
@cmp = _comp_func
|
6
|
+
end
|
7
|
+
|
8
|
+
def add(_order)
|
9
|
+
idx = orders.index { |o| compare(o, _order) > 0 }
|
10
|
+
if idx.nil?
|
11
|
+
orders << _order
|
12
|
+
else
|
13
|
+
orders.insert idx, _order
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def remove(_order)
|
18
|
+
orders.delete(_order)
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
orders.each
|
23
|
+
end
|
24
|
+
|
25
|
+
def each_until_limit(_limit)
|
26
|
+
Enumerator.new do |y|
|
27
|
+
orders.each do |order|
|
28
|
+
break if cmp.call(order, _limit) > 0
|
29
|
+
y << order
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :orders, :cmp
|
37
|
+
|
38
|
+
def compare(a, b)
|
39
|
+
r = cmp.call a, b
|
40
|
+
r = a.created_at <=> b.created_at if r == 0
|
41
|
+
r
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
module Calculator
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def quote(volume: nil, unit_price: nil)
|
6
|
+
base_currency = volume.currency
|
7
|
+
quote = ((unit_price.cents * volume.cents).to_d / base_currency.subunit_to_unit).ceil
|
8
|
+
Money.new quote, unit_price.currency
|
9
|
+
end
|
10
|
+
|
11
|
+
def volume(quote: nil, unit_price: nil, unit_currency: nil)
|
12
|
+
volume = ((quote.cents * unit_currency.subunit_to_unit).to_d / unit_price.cents).floor
|
13
|
+
Money.new volume, unit_currency
|
14
|
+
end
|
15
|
+
|
16
|
+
def fee(amount: nil, percent: nil)
|
17
|
+
result = (amount.cents * (percent.to_d / 100)).floor # fees always round to floor
|
18
|
+
Money.new result, amount.currency
|
19
|
+
end
|
20
|
+
|
21
|
+
def order_fee(order: nil, collected_amount: nil) # move this to command
|
22
|
+
collected_amount = order.collected_amount if collected_amount.nil?
|
23
|
+
fee_cents = (collected_amount.cents * (order.fee.to_d / 100)).floor
|
24
|
+
Money.new fee_cents, collected_amount.currency
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Moneymarket
|
2
|
+
module Command
|
3
|
+
def self.new(*_attributes)
|
4
|
+
attr_names = []
|
5
|
+
attr_defaults = {}
|
6
|
+
|
7
|
+
_attributes.each do |att|
|
8
|
+
if att.is_a? Hash
|
9
|
+
attr_defaults.merge att
|
10
|
+
attr_names += att.keys
|
11
|
+
else
|
12
|
+
attr_names << att
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Struct.new(*attr_names) do
|
17
|
+
define_method(:initialize) do |kwargs={}|
|
18
|
+
kwargs = attr_defaults.merge kwargs
|
19
|
+
attr_values = attr_names.map { |a| kwargs[a] }
|
20
|
+
super(*attr_values)
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method(:perform) {}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: moneymarket
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ignacio Baixas
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: money
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.4'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-nc
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: guard
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.11'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.11'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: guard-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '4.5'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: terminal-notifier-guard
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.6'
|
118
|
+
- - '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: 1.6.1
|
121
|
+
type: :development
|
122
|
+
prerelease: false
|
123
|
+
version_requirements: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ~>
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '1.6'
|
128
|
+
- - '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 1.6.1
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: pry
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ~>
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0.10'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ~>
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0.10'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: pry-remote
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ~>
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0.1'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ~>
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0.1'
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: pry-byebug
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ~>
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '3.2'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ~>
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '3.2'
|
173
|
+
- !ruby/object:Gem::Dependency
|
174
|
+
name: pry-nav
|
175
|
+
requirement: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ~>
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0.2'
|
180
|
+
type: :development
|
181
|
+
prerelease: false
|
182
|
+
version_requirements: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ~>
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0.2'
|
187
|
+
description: Yep
|
188
|
+
email:
|
189
|
+
- ignacio@platan.us
|
190
|
+
executables:
|
191
|
+
- moneymarket
|
192
|
+
extensions: []
|
193
|
+
extra_rdoc_files: []
|
194
|
+
files:
|
195
|
+
- bin/moneymarket
|
196
|
+
- lib/moneymarket.rb
|
197
|
+
- lib/moneymarket/commands/check_order_solvent.rb
|
198
|
+
- lib/moneymarket/commands/execute_match.rb
|
199
|
+
- lib/moneymarket/commands/limit_matches.rb
|
200
|
+
- lib/moneymarket/commands/match_order_to_slope.rb
|
201
|
+
- lib/moneymarket/core/account.rb
|
202
|
+
- lib/moneymarket/core/account_provider.rb
|
203
|
+
- lib/moneymarket/core/ask.rb
|
204
|
+
- lib/moneymarket/core/bid.rb
|
205
|
+
- lib/moneymarket/core/book.rb
|
206
|
+
- lib/moneymarket/core/market.rb
|
207
|
+
- lib/moneymarket/core/match.rb
|
208
|
+
- lib/moneymarket/core/order.rb
|
209
|
+
- lib/moneymarket/core/slope.rb
|
210
|
+
- lib/moneymarket/utils/calculator.rb
|
211
|
+
- lib/moneymarket/utils/command.rb
|
212
|
+
- lib/moneymarket/version.rb
|
213
|
+
homepage: https://github.com/surbtc/moneymarket
|
214
|
+
licenses:
|
215
|
+
- MIT
|
216
|
+
metadata: {}
|
217
|
+
post_install_message:
|
218
|
+
rdoc_options: []
|
219
|
+
require_paths:
|
220
|
+
- lib
|
221
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
222
|
+
requirements:
|
223
|
+
- - '>='
|
224
|
+
- !ruby/object:Gem::Version
|
225
|
+
version: '0'
|
226
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
227
|
+
requirements:
|
228
|
+
- - '>='
|
229
|
+
- !ruby/object:Gem::Version
|
230
|
+
version: '0'
|
231
|
+
requirements: []
|
232
|
+
rubyforge_project:
|
233
|
+
rubygems_version: 2.6.1
|
234
|
+
signing_key:
|
235
|
+
specification_version: 4
|
236
|
+
summary: Moneymarket is a lightweight in-memory foreign exchange market engine
|
237
|
+
test_files: []
|
238
|
+
has_rdoc:
|