moneymarket 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|