line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package App::cryp::arbit::Strategy::merge_order_book; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
our $DATE = '2018-11-29'; # DATE |
4
|
|
|
|
|
|
|
our $VERSION = '0.008'; # VERSION |
5
|
|
|
|
|
|
|
|
6
|
1
|
|
|
1
|
|
105269
|
use 5.010001; |
|
1
|
|
|
|
|
15
|
|
7
|
1
|
|
|
1
|
|
6
|
use strict; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
33
|
|
8
|
1
|
|
|
1
|
|
6
|
use warnings; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
27
|
|
9
|
1
|
|
|
1
|
|
2899
|
use Log::ger; |
|
1
|
|
|
|
|
55
|
|
|
1
|
|
|
|
|
6
|
|
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
require App::cryp::arbit; |
12
|
1
|
|
|
1
|
|
808
|
use Finance::Currency::FiatX; |
|
1
|
|
|
|
|
4177
|
|
|
1
|
|
|
|
|
39
|
|
13
|
1
|
|
|
1
|
|
7
|
use List::Util qw(min max shuffle); |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
106
|
|
14
|
1
|
|
|
1
|
|
608
|
use Storable qw(dclone); |
|
1
|
|
|
|
|
3794
|
|
|
1
|
|
|
|
|
67
|
|
15
|
1
|
|
|
1
|
|
575
|
use Time::HiRes qw(time); |
|
1
|
|
|
|
|
1431
|
|
|
1
|
|
|
|
|
5
|
|
16
|
|
|
|
|
|
|
|
17
|
1
|
|
|
1
|
|
628
|
use Role::Tiny::With; |
|
1
|
|
|
|
|
5242
|
|
|
1
|
|
|
|
|
3892
|
|
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
with 'App::cryp::Role::ArbitStrategy'; |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
sub _calculate_order_pairs_for_base_currency { |
22
|
17
|
|
|
17
|
|
84611
|
my %args = @_; |
23
|
|
|
|
|
|
|
|
24
|
17
|
|
|
|
|
41
|
my $base_currency = $args{base_currency}; |
25
|
17
|
|
|
|
|
33
|
my $all_buy_orders = $args{all_buy_orders}; |
26
|
17
|
|
|
|
|
28
|
my $all_sell_orders = $args{all_sell_orders}; |
27
|
17
|
|
50
|
|
|
50
|
my $min_net_profit_margin = $args{min_net_profit_margin} // 0; |
28
|
17
|
|
|
|
|
30
|
my $max_order_quote_size = $args{max_order_quote_size}; |
29
|
17
|
|
|
|
|
28
|
my $max_order_pairs = $args{max_order_pairs}; |
30
|
17
|
|
100
|
|
|
77
|
my $max_order_size_as_book_item_size_pct = $args{max_order_size_as_book_item_size_pct} // 100; |
31
|
17
|
|
|
|
|
30
|
my $account_balances = $args{account_balances}; |
32
|
17
|
|
|
|
|
26
|
my $min_account_balances = $args{min_account_balances}; |
33
|
17
|
|
|
|
|
26
|
my $exchange_pairs = $args{exchange_pairs}; |
34
|
17
|
|
|
|
|
29
|
my $forex_spreads = $args{forex_spreads}; |
35
|
|
|
|
|
|
|
|
36
|
17
|
|
|
|
|
33
|
my @order_pairs; |
37
|
|
|
|
|
|
|
my $opportunity; |
38
|
|
|
|
|
|
|
|
39
|
17
|
|
|
|
|
23
|
for (@{ $all_buy_orders }, @{ $all_sell_orders }) { |
|
17
|
|
|
|
|
31
|
|
|
17
|
|
|
|
|
37
|
|
40
|
51
|
|
|
|
|
114
|
$_->{base_size} *= $max_order_size_as_book_item_size_pct/100; |
41
|
|
|
|
|
|
|
} |
42
|
|
|
|
|
|
|
|
43
|
17
|
100
|
100
|
|
|
59
|
if ($account_balances && $min_account_balances) { |
44
|
1
|
|
|
|
|
5
|
for my $e (keys %$account_balances) { |
45
|
2
|
|
|
|
|
4
|
my $balances = $account_balances->{$e}; |
46
|
2
|
|
|
|
|
6
|
for my $cur (keys %$balances) { |
47
|
2
|
|
|
|
|
4
|
my $curbalances = $balances->{$cur}; |
48
|
2
|
|
|
|
|
4
|
for my $rec (@$curbalances) { |
49
|
3
|
|
|
|
|
9
|
my $eacc = "$e/$rec->{account}"; |
50
|
3
|
100
|
66
|
|
|
16
|
if (defined $min_account_balances->{$eacc} && |
51
|
|
|
|
|
|
|
defined $min_account_balances->{$eacc}{$cur}) { |
52
|
2
|
|
|
|
|
7
|
$rec->{available} -= $min_account_balances->{$eacc}{$cur}; |
53
|
|
|
|
|
|
|
} |
54
|
|
|
|
|
|
|
} |
55
|
|
|
|
|
|
|
} |
56
|
|
|
|
|
|
|
} |
57
|
1
|
|
|
|
|
6
|
App::cryp::arbit::_sort_account_balances($account_balances); |
58
|
|
|
|
|
|
|
} |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
CREATE: |
61
|
17
|
|
|
|
|
28
|
while (1) { |
62
|
52
|
100
|
100
|
|
|
134
|
last CREATE if defined $max_order_pairs && |
63
|
|
|
|
|
|
|
@order_pairs >= $max_order_pairs; |
64
|
|
|
|
|
|
|
|
65
|
50
|
|
|
|
|
78
|
my ($sell, $sell_index); |
66
|
|
|
|
|
|
|
FIND_BUYER: |
67
|
|
|
|
|
|
|
{ |
68
|
50
|
|
|
|
|
72
|
$sell_index = 0; |
|
50
|
|
|
|
|
73
|
|
69
|
50
|
|
|
|
|
119
|
while ($sell_index < @$all_buy_orders) { |
70
|
45
|
|
|
|
|
73
|
$sell = $all_buy_orders->[$sell_index]; |
71
|
45
|
100
|
|
|
|
88
|
if ($account_balances) { |
72
|
|
|
|
|
|
|
# we don't have any inventory left to sell on this selling |
73
|
|
|
|
|
|
|
# exchange |
74
|
15
|
100
|
50
|
|
|
22
|
unless (@{ $account_balances->{ $sell->{exchange} }{$base_currency} // [] }) { |
|
15
|
|
|
|
|
53
|
|
75
|
6
|
|
|
|
|
10
|
$sell_index++; next; |
|
6
|
|
|
|
|
15
|
|
76
|
|
|
|
|
|
|
} |
77
|
|
|
|
|
|
|
} |
78
|
39
|
|
|
|
|
56
|
last; |
79
|
|
|
|
|
|
|
} |
80
|
|
|
|
|
|
|
# there are no more buyers left we can sell to |
81
|
50
|
100
|
|
|
|
114
|
last CREATE unless $sell_index < @$all_buy_orders; |
82
|
|
|
|
|
|
|
} |
83
|
|
|
|
|
|
|
|
84
|
39
|
|
|
|
|
57
|
my ($buy, $buy_index); |
85
|
|
|
|
|
|
|
FIND_SELLER: |
86
|
|
|
|
|
|
|
{ |
87
|
39
|
|
|
|
|
59
|
$buy_index = 0; |
|
39
|
|
|
|
|
59
|
|
88
|
39
|
|
|
|
|
82
|
while ($buy_index < @$all_sell_orders) { |
89
|
37
|
|
|
|
|
55
|
$buy = $all_sell_orders->[$buy_index]; |
90
|
|
|
|
|
|
|
# shouldn't happen though |
91
|
37
|
50
|
|
|
|
84
|
if ($buy->{exchange} eq $sell->{exchange}) { |
92
|
0
|
|
|
|
|
0
|
$buy_index++; next; |
|
0
|
|
|
|
|
0
|
|
93
|
|
|
|
|
|
|
} |
94
|
37
|
100
|
|
|
|
77
|
if ($account_balances) { |
95
|
|
|
|
|
|
|
# we don't have any inventory left to buy from this exchange |
96
|
9
|
100
|
50
|
|
|
18
|
unless (@{ $account_balances->{ $buy->{exchange} }{$buy->{quote_currency}} // [] }) { |
|
9
|
|
|
|
|
32
|
|
97
|
1
|
|
|
|
|
2
|
$buy_index++; next; |
|
1
|
|
|
|
|
4
|
|
98
|
|
|
|
|
|
|
} |
99
|
|
|
|
|
|
|
} |
100
|
36
|
|
|
|
|
51
|
last; |
101
|
|
|
|
|
|
|
} |
102
|
|
|
|
|
|
|
# there are no more sellers left we can buy from |
103
|
39
|
100
|
|
|
|
84
|
last CREATE unless $buy_index < @$all_sell_orders; |
104
|
|
|
|
|
|
|
} |
105
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
my $gross_profit_margin = ($sell->{gross_price} - $buy->{gross_price}) / |
107
|
36
|
|
|
|
|
138
|
min($sell->{gross_price}, $buy->{gross_price}) * 100; |
108
|
|
|
|
|
|
|
my $trading_profit_margin = ($sell->{net_price} - $buy->{net_price}) / |
109
|
36
|
|
|
|
|
98
|
min($sell->{net_price}, $buy->{net_price}) * 100; |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
# record opportunity, the currently highest trading profit margin |
112
|
36
|
100
|
|
|
|
73
|
unless ($opportunity) { |
113
|
17
|
|
|
|
|
28
|
my $quote_currency = $sell->{quote_currency}; |
114
|
17
|
50
|
|
|
|
52
|
if (App::cryp::arbit::_is_fiat($quote_currency)) { |
115
|
17
|
|
|
|
|
33
|
$quote_currency = "USD"; |
116
|
|
|
|
|
|
|
} |
117
|
|
|
|
|
|
|
$opportunity = { |
118
|
|
|
|
|
|
|
time => time(), |
119
|
|
|
|
|
|
|
base_currency => $base_currency, |
120
|
|
|
|
|
|
|
quote_currency => $quote_currency, |
121
|
|
|
|
|
|
|
buy_exchange => $buy->{exchange}, |
122
|
|
|
|
|
|
|
buy_price => $buy->{gross_price}, |
123
|
|
|
|
|
|
|
sell_exchange => $sell->{exchange}, |
124
|
|
|
|
|
|
|
sell_price => $sell->{gross_price}, |
125
|
17
|
|
|
|
|
152
|
gross_profit_margin => $gross_profit_margin, |
126
|
|
|
|
|
|
|
trading_profit_margin => $trading_profit_margin, |
127
|
|
|
|
|
|
|
}; |
128
|
|
|
|
|
|
|
} |
129
|
|
|
|
|
|
|
|
130
|
36
|
100
|
|
|
|
94
|
if ($trading_profit_margin < $min_net_profit_margin) { |
131
|
1
|
|
|
|
|
53
|
log_trace "Ending matching buy->sell because trading profit margin is too low (%.4f%%, wants >= %.4f%%%)", |
132
|
|
|
|
|
|
|
$trading_profit_margin, $min_net_profit_margin; |
133
|
1
|
|
|
|
|
9
|
last CREATE; |
134
|
|
|
|
|
|
|
} |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
my $order_pair = { |
137
|
|
|
|
|
|
|
sell => { |
138
|
|
|
|
|
|
|
exchange => $sell->{exchange}, |
139
|
|
|
|
|
|
|
pair => "$base_currency/$sell->{quote_currency}", |
140
|
|
|
|
|
|
|
gross_price_orig => $sell->{gross_price_orig}, |
141
|
|
|
|
|
|
|
gross_price => $sell->{gross_price}, |
142
|
|
|
|
|
|
|
net_price_orig => $sell->{net_price_orig}, |
143
|
|
|
|
|
|
|
net_price => $sell->{net_price}, |
144
|
|
|
|
|
|
|
}, |
145
|
|
|
|
|
|
|
buy => { |
146
|
|
|
|
|
|
|
exchange => $buy->{exchange}, |
147
|
|
|
|
|
|
|
pair => "$base_currency/$buy->{quote_currency}", |
148
|
|
|
|
|
|
|
gross_price_orig => $buy->{gross_price_orig}, |
149
|
|
|
|
|
|
|
gross_price => $buy->{gross_price}, |
150
|
|
|
|
|
|
|
net_price_orig => $buy->{net_price_orig}, |
151
|
|
|
|
|
|
|
net_price => $buy->{net_price}, |
152
|
|
|
|
|
|
|
}, |
153
|
35
|
|
|
|
|
307
|
gross_profit_margin => $gross_profit_margin, |
154
|
|
|
|
|
|
|
trading_profit_margin => $trading_profit_margin, |
155
|
|
|
|
|
|
|
}; |
156
|
|
|
|
|
|
|
|
157
|
35
|
100
|
|
|
|
90
|
if ($account_balances) { |
158
|
8
|
|
|
|
|
24
|
$order_pair->{sell}{account} = $account_balances->{ $sell->{exchange} }{$base_currency}[0]{account}; |
159
|
8
|
|
|
|
|
23
|
$order_pair->{buy}{account} = $account_balances->{ $buy ->{exchange} }{$buy->{quote_currency}}[0]{account}; |
160
|
|
|
|
|
|
|
} |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
# limit maximum size of order |
163
|
|
|
|
|
|
|
my @sizes = ( |
164
|
|
|
|
|
|
|
{which => 'buy order' , size => $sell->{base_size}}, |
165
|
|
|
|
|
|
|
{which => 'sell order', size => $buy ->{base_size}}, |
166
|
35
|
|
|
|
|
149
|
); |
167
|
35
|
100
|
|
|
|
78
|
if (defined $max_order_quote_size) { |
168
|
|
|
|
|
|
|
push @sizes, ( |
169
|
7
|
|
|
|
|
29
|
{which => 'max_order_quote_size', size => $max_order_quote_size / max($sell->{gross_price}, $buy->{gross_price})}, |
170
|
|
|
|
|
|
|
); |
171
|
|
|
|
|
|
|
} |
172
|
35
|
100
|
|
|
|
67
|
if ($account_balances) { |
173
|
|
|
|
|
|
|
push @sizes, ( |
174
|
|
|
|
|
|
|
{ |
175
|
|
|
|
|
|
|
which => 'sell exchange balance', |
176
|
|
|
|
|
|
|
size => $account_balances->{ $sell->{exchange} }{$base_currency}[0]{available}, |
177
|
|
|
|
|
|
|
}, |
178
|
|
|
|
|
|
|
{ |
179
|
|
|
|
|
|
|
which => 'buy exchange balance', |
180
|
|
|
|
|
|
|
size => $account_balances->{ $buy ->{exchange} }{$buy->{quote_currency}}[0]{available} |
181
|
|
|
|
|
|
|
/ $buy->{gross_price_orig}, |
182
|
|
|
|
|
|
|
}, |
183
|
8
|
|
|
|
|
51
|
); |
184
|
|
|
|
|
|
|
} |
185
|
35
|
|
|
|
|
110
|
@sizes = sort { $a->{size} <=> $b->{size} } @sizes; |
|
76
|
|
|
|
|
173
|
|
186
|
35
|
|
|
|
|
68
|
my $order_size = $sizes[0]{size}; |
187
|
|
|
|
|
|
|
|
188
|
35
|
|
|
|
|
59
|
$order_pair->{base_size} = $order_size; |
189
|
|
|
|
|
|
|
$order_pair->{gross_profit} = $order_size * |
190
|
35
|
|
|
|
|
82
|
($order_pair->{sell}{gross_price} - $order_pair->{buy}{gross_price}); |
191
|
|
|
|
|
|
|
$order_pair->{trading_profit} = $order_size * |
192
|
35
|
|
|
|
|
69
|
($order_pair->{sell}{net_price} - $order_pair->{buy}{net_price}); |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
UPDATE_INVENTORY_BALANCES: |
195
|
35
|
|
|
|
|
93
|
for my $i (0..$#sizes) { |
196
|
93
|
|
|
|
|
155
|
my $size = $sizes[$i]{size}; |
197
|
93
|
|
|
|
|
150
|
my $which = $sizes[$i]{which}; |
198
|
93
|
|
|
|
|
147
|
my $used_up = $size - $order_size <= 1e-8; |
199
|
93
|
100
|
|
|
|
265
|
if ($which eq 'buy order') { |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
200
|
35
|
100
|
|
|
|
67
|
if ($used_up) { |
201
|
11
|
|
|
|
|
29
|
splice @$all_buy_orders, $sell_index, 1; |
202
|
|
|
|
|
|
|
} else { |
203
|
24
|
|
|
|
|
56
|
$all_buy_orders->[$sell_index]{base_size} -= $order_size; |
204
|
|
|
|
|
|
|
} |
205
|
|
|
|
|
|
|
} elsif ($which eq 'sell order') { |
206
|
35
|
100
|
|
|
|
58
|
if ($used_up) { |
207
|
13
|
|
|
|
|
33
|
splice @$all_sell_orders, $buy_index, 1; |
208
|
|
|
|
|
|
|
} else { |
209
|
22
|
|
|
|
|
52
|
$all_sell_orders->[$buy_index]{base_size} -= $order_size; |
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
} elsif ($which eq 'sell exchange balance') { |
212
|
8
|
100
|
|
|
|
17
|
if ($used_up) { |
213
|
5
|
|
|
|
|
7
|
shift @{ $account_balances->{ $sell->{exchange} }{$base_currency} }; |
|
5
|
|
|
|
|
20
|
|
214
|
|
|
|
|
|
|
} else { |
215
|
|
|
|
|
|
|
$account_balances->{ $sell->{exchange} }{$base_currency}[0]{available} -= |
216
|
3
|
|
|
|
|
8
|
$order_size; |
217
|
|
|
|
|
|
|
} |
218
|
|
|
|
|
|
|
} elsif ($which eq 'buy exchange balance') { |
219
|
8
|
|
|
|
|
17
|
my $c = $buy->{quote_currency}; |
220
|
8
|
100
|
|
|
|
17
|
if ($used_up) { |
221
|
2
|
|
|
|
|
3
|
shift @{ $account_balances->{ $buy->{exchange} }{$c} }; |
|
2
|
|
|
|
|
9
|
|
222
|
|
|
|
|
|
|
} else { |
223
|
|
|
|
|
|
|
$account_balances->{ $buy->{exchange} }{$c}[0]{available} -= |
224
|
6
|
|
|
|
|
20
|
$order_size * $buy->{gross_price_orig}; |
225
|
|
|
|
|
|
|
} |
226
|
|
|
|
|
|
|
} |
227
|
|
|
|
|
|
|
} # UPDATE_INVENTORY_BALANCES |
228
|
|
|
|
|
|
|
|
229
|
35
|
100
|
|
|
|
77
|
if ($account_balances) { |
230
|
8
|
|
|
|
|
24
|
App::cryp::arbit::_sort_account_balances($account_balances); |
231
|
|
|
|
|
|
|
} |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
CHECK_MINIMUM_BUY_SIZE: |
234
|
|
|
|
|
|
|
{ |
235
|
35
|
100
|
|
|
|
53
|
last unless $exchange_pairs; |
|
35
|
|
|
|
|
74
|
|
236
|
9
|
|
|
|
|
19
|
my $pair_recs = $exchange_pairs->{ $buy->{exchange} }; |
237
|
9
|
100
|
|
|
|
22
|
last unless $pair_recs; |
238
|
4
|
|
|
|
|
6
|
my $pair_rec; |
239
|
4
|
|
|
|
|
8
|
for (@$pair_recs) { |
240
|
4
|
50
|
|
|
|
12
|
if ($_->{base_currency} eq $base_currency) { |
241
|
4
|
|
|
|
|
8
|
$pair_rec = $_; last; |
|
4
|
|
|
|
|
9
|
|
242
|
|
|
|
|
|
|
} |
243
|
|
|
|
|
|
|
} |
244
|
4
|
50
|
|
|
|
33
|
last unless $pair_rec; |
245
|
4
|
100
|
100
|
|
|
21
|
if (defined($pair_rec->{min_base_size}) && $order_pair->{base_size} < $pair_rec->{min_base_size}) { |
246
|
|
|
|
|
|
|
log_trace "buy order base size is too small (%.4f < %.4f), skipping this order pair: %s", |
247
|
1
|
|
|
|
|
7
|
$order_pair->{base_size}, $pair_rec->{min_base_size}, $order_pair; |
248
|
1
|
|
|
|
|
10
|
next CREATE; |
249
|
|
|
|
|
|
|
} |
250
|
3
|
|
|
|
|
9
|
my $quote_size = $order_pair->{base_size}*$buy->{gross_price_orig}; |
251
|
3
|
100
|
100
|
|
|
16
|
if (defined($pair_rec->{min_quote_size}) && $quote_size < $pair_rec->{min_quote_size}) { |
252
|
|
|
|
|
|
|
log_trace "buy order quote size is too small (%.4f < %.4f), skipping this order pair: %s", |
253
|
1
|
|
|
|
|
7
|
$quote_size, $pair_rec->{min_quote_size}, $order_pair; |
254
|
1
|
|
|
|
|
9
|
next CREATE; |
255
|
|
|
|
|
|
|
} |
256
|
|
|
|
|
|
|
} # CHECK_MINIMUM_BUY_SIZE |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
CHECK_MINIMUM_SELL_SIZE: |
259
|
|
|
|
|
|
|
{ |
260
|
33
|
100
|
|
|
|
45
|
last unless $exchange_pairs; |
|
33
|
|
|
|
|
64
|
|
261
|
7
|
|
|
|
|
16
|
my $pair_recs = $exchange_pairs->{ $sell->{exchange} }; |
262
|
7
|
100
|
|
|
|
15
|
last unless $pair_recs; |
263
|
5
|
|
|
|
|
8
|
my $pair_rec; |
264
|
5
|
|
|
|
|
12
|
for (@$pair_recs) { |
265
|
5
|
50
|
|
|
|
13
|
if ($_->{base_currency} eq $base_currency) { |
266
|
5
|
|
|
|
|
7
|
$pair_rec = $_; last; |
|
5
|
|
|
|
|
11
|
|
267
|
|
|
|
|
|
|
} |
268
|
|
|
|
|
|
|
} |
269
|
5
|
50
|
|
|
|
10
|
last unless $pair_rec; |
270
|
5
|
100
|
100
|
|
|
20
|
if (defined $pair_rec->{min_base_size} && $order_pair->{base_size} < $pair_rec->{min_base_size}) { |
271
|
|
|
|
|
|
|
log_trace "sell order base size is too small (%.4f < %.4f), skipping this order pair: %s", |
272
|
1
|
|
|
|
|
8
|
$order_pair->{base_size}, $pair_rec->{min_base_size}, $order_pair; |
273
|
1
|
|
|
|
|
8
|
next CREATE; |
274
|
|
|
|
|
|
|
} |
275
|
4
|
|
|
|
|
11
|
my $quote_size = $order_pair->{base_size}*$sell->{gross_price_orig}; |
276
|
4
|
100
|
100
|
|
|
21
|
if (defined $pair_rec->{min_quote_size} && $quote_size < $pair_rec->{min_quote_size}) { |
277
|
|
|
|
|
|
|
log_trace "sell order quote size is too small (%.4f < %.4f), skipping this order pair: %s", |
278
|
2
|
|
|
|
|
11
|
$quote_size, $pair_rec->{min_quote_size}, $order_pair; |
279
|
2
|
|
|
|
|
14
|
next CREATE; |
280
|
|
|
|
|
|
|
} |
281
|
|
|
|
|
|
|
} # CHECK_MINIMUM_SELL_SIZE |
282
|
|
|
|
|
|
|
|
283
|
30
|
|
|
|
|
108
|
push @order_pairs, $order_pair; |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
} # CREATE |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
ADJUST_FOREX_SPREAD: |
288
|
|
|
|
|
|
|
{ |
289
|
17
|
|
|
|
|
24
|
my @tmp = @order_pairs; |
|
17
|
|
|
|
|
35
|
|
290
|
17
|
|
|
|
|
33
|
@order_pairs = (); |
291
|
17
|
|
|
|
|
27
|
my $i = 0; |
292
|
|
|
|
|
|
|
ORDER_PAIR: |
293
|
17
|
|
|
|
|
34
|
for my $op (@tmp) { |
294
|
30
|
|
|
|
|
47
|
$i++; |
295
|
30
|
|
|
|
|
161
|
my ($bcur) = $op->{buy}{pair} =~ m!/(.+)!; |
296
|
30
|
|
|
|
|
104
|
my ($scur) = $op->{sell}{pair} =~ m!/(.+)!; |
297
|
|
|
|
|
|
|
|
298
|
30
|
50
|
|
|
|
74
|
if ($bcur eq $scur) { |
299
|
|
|
|
|
|
|
# there is no forex spread |
300
|
0
|
|
|
|
|
0
|
$op->{net_profit_margin} = $op->{trading_profit_margin}; |
301
|
0
|
|
|
|
|
0
|
$op->{net_profit} = $op->{trading_profit}; |
302
|
0
|
|
|
|
|
0
|
goto ADD; |
303
|
|
|
|
|
|
|
} |
304
|
|
|
|
|
|
|
|
305
|
30
|
|
|
|
|
50
|
my $spread; |
306
|
30
|
50
|
|
|
|
79
|
$spread = $forex_spreads->{"$bcur/$scur"} if $forex_spreads; |
307
|
|
|
|
|
|
|
|
308
|
30
|
50
|
|
|
|
71
|
unless (defined $spread) { |
309
|
|
|
|
|
|
|
log_warn "Order pair #%d (buy %s - sell %s): didn't find ". |
310
|
|
|
|
|
|
|
"forex spread for %s/%s, not adjusting for forex spread", |
311
|
0
|
|
|
|
|
0
|
$i, $op->{buy}{pair}, $op->{sell}{pair}, $bcur, $scur; |
312
|
0
|
|
|
|
|
0
|
next ORDER_PAIR; |
313
|
|
|
|
|
|
|
} |
314
|
|
|
|
|
|
|
log_trace "Order pair #%d (buy %s - sell %s, trading profit margin %.4f%%): adjusting ". |
315
|
|
|
|
|
|
|
"with %s/%s forex spread %.4f%%", |
316
|
30
|
|
|
|
|
124
|
$i, $op->{buy}{pair}, $op->{sell}{pair}, $op->{trading_profit_margin}, $bcur, $scur, $spread; |
317
|
30
|
|
|
|
|
122
|
$op->{forex_spread} = $spread; |
318
|
30
|
|
|
|
|
68
|
$op->{net_profit_margin} = $op->{trading_profit_margin} - $spread; |
319
|
30
|
|
|
|
|
56
|
$op->{net_profit} = $op->{trading_profit} * $op->{net_profit_margin} / $op->{trading_profit_margin}; |
320
|
30
|
100
|
|
|
|
75
|
if ($op->{net_profit_margin} < $min_net_profit_margin) { |
321
|
|
|
|
|
|
|
log_trace "Order pair #%d: After forex spread adjustment, net profit margin is too small (%.4f%%, wants >= %.4f%%), skipping this order pair", |
322
|
1
|
|
|
|
|
4
|
$i, $op->{net_profit_margin}, $min_net_profit_margin; |
323
|
1
|
|
|
|
|
6
|
next ORDER_PAIR; |
324
|
|
|
|
|
|
|
} |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
ADD: |
327
|
29
|
|
|
|
|
60
|
push @order_pairs, $op; |
328
|
|
|
|
|
|
|
} |
329
|
|
|
|
|
|
|
} # ADJUST_FOREX_SPREAD |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
# re-sort |
332
|
17
|
|
|
|
|
41
|
@order_pairs = sort { $b->{net_profit_margin} <=> $a->{net_profit_margin} } @order_pairs; |
|
17
|
|
|
|
|
36
|
|
333
|
|
|
|
|
|
|
|
334
|
17
|
|
|
|
|
123
|
(\@order_pairs, $opportunity); |
335
|
|
|
|
|
|
|
} |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
sub calculate_order_pairs { |
338
|
0
|
|
|
0
|
0
|
|
my ($pkg, %args) = @_; |
339
|
|
|
|
|
|
|
|
340
|
0
|
|
|
|
|
|
my $r = $args{r}; |
341
|
0
|
|
|
|
|
|
my $dbh = $r->{_stash}{dbh}; |
342
|
|
|
|
|
|
|
|
343
|
0
|
|
|
|
|
|
my @order_pairs; |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
GET_ACCOUNT_BALANCES: |
346
|
|
|
|
|
|
|
{ |
347
|
0
|
0
|
|
|
|
|
last if $r->{args}{ignore_balance}; |
|
0
|
|
|
|
|
|
|
348
|
0
|
|
|
|
|
|
App::cryp::arbit::_get_account_balances($r, 'no-cache'); |
349
|
|
|
|
|
|
|
} # GET_ACCOUNT_BALANCES |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
GET_FOREX_RATES: |
352
|
|
|
|
|
|
|
{ |
353
|
|
|
|
|
|
|
# get foreign fiat currency vs USD exchange rate. we'll use the average |
354
|
|
|
|
|
|
|
# rate for this first. but we'll adjust the price difference percentage |
355
|
|
|
|
|
|
|
# with the buy-sell spread later. |
356
|
|
|
|
|
|
|
|
357
|
0
|
|
|
|
|
|
my %seen; |
|
0
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
|
359
|
0
|
|
|
|
|
|
$r->{_stash}{forex_rates} = {}; |
360
|
|
|
|
|
|
|
|
361
|
0
|
|
|
|
|
|
for my $cur (@{ $r->{_stash}{quote_currencies} }) { |
|
0
|
|
|
|
|
|
|
362
|
0
|
0
|
|
|
|
|
next unless App::cryp::arbit::_is_fiat($cur); |
363
|
0
|
0
|
|
|
|
|
next if $cur eq 'USD'; |
364
|
0
|
0
|
|
|
|
|
next if $seen{$cur}++; |
365
|
|
|
|
|
|
|
|
366
|
0
|
|
|
|
|
|
require Finance::Currency::FiatX; |
367
|
|
|
|
|
|
|
|
368
|
0
|
|
|
|
|
|
my $fxres_low = Finance::Currency::FiatX::get_spot_rate( |
369
|
|
|
|
|
|
|
dbh => $dbh, from => $cur, to => 'USD', type => 'buy', source => ':lowest'); |
370
|
0
|
0
|
|
|
|
|
if ($fxres_low->[0] != 200) { |
371
|
0
|
|
|
|
|
|
return [412, "Couldn't get conversion rate (lowest buy) from ". |
372
|
|
|
|
|
|
|
"$cur to USD: $fxres_low->[0] - $fxres_low->[1]"]; |
373
|
|
|
|
|
|
|
} |
374
|
|
|
|
|
|
|
|
375
|
0
|
|
|
|
|
|
my $fxres_high = Finance::Currency::FiatX::get_spot_rate( |
376
|
|
|
|
|
|
|
dbh => $dbh, from => $cur, to => 'USD', type => 'sell', source => ':highest'); |
377
|
0
|
0
|
|
|
|
|
if ($fxres_high->[0] != 200) { |
378
|
0
|
|
|
|
|
|
return [412, "Couldn't get conversion rate (highest sell) ". |
379
|
|
|
|
|
|
|
"from $cur to USD: $fxres_high->[0] - ". |
380
|
|
|
|
|
|
|
"$fxres_high->[1]"]; |
381
|
|
|
|
|
|
|
} |
382
|
|
|
|
|
|
|
|
383
|
0
|
|
|
|
|
|
my $fxrate_avg = ($fxres_low->[2]{rate} + $fxres_high->[2]{rate})/2; |
384
|
0
|
|
|
|
|
|
$r->{_stash}{forex_rates}{"$cur/USD"} = $fxrate_avg; |
385
|
|
|
|
|
|
|
} |
386
|
|
|
|
|
|
|
} # GET_FOREX_RATES |
387
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
GET_FOREX_SPREADS: |
389
|
|
|
|
|
|
|
{ |
390
|
|
|
|
|
|
|
# when we arbitrage using two different fiat currencies, e.g. BTC/USD |
391
|
|
|
|
|
|
|
# and BTC/IDR, we want to take into account the USD/IDR buy-sell spread |
392
|
|
|
|
|
|
|
# (the "forex spread") and subtract this from the price difference |
393
|
|
|
|
|
|
|
# percentage to be safer. |
394
|
|
|
|
|
|
|
|
395
|
0
|
|
|
|
|
|
$r->{_stash}{forex_spreads} = {}; |
|
0
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
|
397
|
0
|
|
|
|
|
|
my @curs; |
398
|
0
|
|
|
|
|
|
for my $cur (@{ $r->{_stash}{quote_currencies} }) { |
|
0
|
|
|
|
|
|
|
399
|
0
|
0
|
|
|
|
|
next unless App::cryp::arbit::_is_fiat($cur); |
400
|
0
|
0
|
|
|
|
|
push @curs, $cur unless grep { $cur eq $_ } @curs; |
|
0
|
|
|
|
|
|
|
401
|
|
|
|
|
|
|
} |
402
|
0
|
0
|
|
|
|
|
last unless @curs; |
403
|
|
|
|
|
|
|
|
404
|
0
|
|
|
|
|
|
require Finance::Currency::FiatX; |
405
|
|
|
|
|
|
|
|
406
|
0
|
|
|
|
|
|
for my $cur1 (@curs) { |
407
|
0
|
|
|
|
|
|
for my $cur2 (@curs) { |
408
|
0
|
0
|
|
|
|
|
next if $cur1 eq $cur2; |
409
|
|
|
|
|
|
|
|
410
|
0
|
|
|
|
|
|
my $fxres_low = Finance::Currency::FiatX::get_spot_rate( |
411
|
|
|
|
|
|
|
dbh => $dbh, from => $cur1, to => $cur2, type => 'buy', source => ':lowest'); |
412
|
0
|
0
|
|
|
|
|
if ($fxres_low->[0] != 200) { |
413
|
0
|
|
|
|
|
|
return [412, "Couldn't get conversion rate (lowest buy) for ". |
414
|
|
|
|
|
|
|
"$cur1/$cur2: $fxres_low->[0] - $fxres_low->[1]"]; |
415
|
|
|
|
|
|
|
} |
416
|
|
|
|
|
|
|
|
417
|
0
|
|
|
|
|
|
my $fxres_high = Finance::Currency::FiatX::get_spot_rate( |
418
|
|
|
|
|
|
|
dbh => $dbh, from => $cur1, to => $cur2, type => 'sell', source => ':highest'); |
419
|
0
|
0
|
|
|
|
|
if ($fxres_high->[0] != 200) { |
420
|
0
|
|
|
|
|
|
return [412, "Couldn't get conversion rate (highest sell) ". |
421
|
|
|
|
|
|
|
"for $cur1/$cur2: $fxres_high->[0] - ". |
422
|
|
|
|
|
|
|
"$fxres_high->[1]"]; |
423
|
|
|
|
|
|
|
} |
424
|
|
|
|
|
|
|
|
425
|
0
|
|
|
|
|
|
my $r1 = $fxres_low->[2]{rate}; |
426
|
0
|
|
|
|
|
|
my $r2 = $fxres_high->[2]{rate}; |
427
|
0
|
0
|
|
|
|
|
my $spread = $r1 > $r2 ? ($r1-$r2)/$r2*100 : ($r2-$r1)/$r1*100; |
428
|
0
|
|
|
|
|
|
$r->{_stash}{forex_spreads}{"$cur1/$cur2"} = abs $spread; |
429
|
|
|
|
|
|
|
} |
430
|
|
|
|
|
|
|
} |
431
|
|
|
|
|
|
|
} # GET_FOREX_SPREADS |
432
|
|
|
|
|
|
|
|
433
|
0
|
|
|
|
|
|
my %exchanges_for; # key="base currency"/"quote cryptocurrency or ':fiat'", value => [exchange, ...] |
434
|
|
|
|
|
|
|
my %fiat_for; # key=exchange safename, val=[fiat currency, ...] |
435
|
0
|
|
|
|
|
|
my %pairs_for; # key=exchange safename, val=[pair, ...] |
436
|
|
|
|
|
|
|
DETERMINE_SETS: |
437
|
0
|
|
|
|
|
|
for my $exchange (sort keys %{ $r->{_stash}{exchange_clients} }) { |
|
0
|
|
|
|
|
|
|
438
|
0
|
|
|
|
|
|
my $pair_recs = $r->{_stash}{exchange_pairs}{$exchange}; |
439
|
0
|
|
|
|
|
|
for my $pair_rec (@$pair_recs) { |
440
|
0
|
|
|
|
|
|
my $pair = $pair_rec->{name}; |
441
|
0
|
|
|
|
|
|
my ($basecur, $quotecur) = $pair =~ m!(.+)/(.+)!; |
442
|
0
|
0
|
|
|
|
|
next unless grep { $_ eq $basecur } @{ $r->{_stash}{base_currencies} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
443
|
0
|
0
|
|
|
|
|
next unless grep { $_ eq $quotecur } @{ $r->{_stash}{quote_currencies} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
|
445
|
0
|
|
|
|
|
|
my $key; |
446
|
0
|
0
|
|
|
|
|
if (App::cryp::arbit::_is_fiat($quotecur)) { |
447
|
0
|
|
|
|
|
|
$key = "$basecur/:fiat"; |
448
|
0
|
|
0
|
|
|
|
$fiat_for{$exchange} //= []; |
449
|
0
|
|
|
|
|
|
push @{ $fiat_for{$exchange} }, $quotecur |
450
|
0
|
0
|
|
|
|
|
unless grep { $_ eq $quotecur } @{ $fiat_for{$exchange} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
} else { |
452
|
0
|
|
|
|
|
|
$key = "$basecur/$quotecur"; |
453
|
|
|
|
|
|
|
} |
454
|
0
|
|
0
|
|
|
|
$exchanges_for{$key} //= []; |
455
|
0
|
|
|
|
|
|
push @{ $exchanges_for{$key} }, $exchange; |
|
0
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
|
457
|
0
|
|
0
|
|
|
|
$pairs_for{$exchange} //= []; |
458
|
0
|
|
|
|
|
|
push @{ $pairs_for{$exchange} }, $pair |
459
|
0
|
0
|
|
|
|
|
unless grep { $_ eq $pair } @{ $pairs_for{$exchange} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
} |
461
|
|
|
|
|
|
|
} # DETERMINE_SETS |
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
SET: |
464
|
0
|
|
|
|
|
|
for my $set (shuffle keys %exchanges_for) { |
465
|
0
|
|
|
|
|
|
my ($base_currency, $quote_currency0) = $set =~ m!(.+)/(.+)!; |
466
|
|
|
|
|
|
|
|
467
|
0
|
|
|
|
|
|
my %sell_orders; # key = exchange safename |
468
|
|
|
|
|
|
|
my %buy_orders ; # key = exchange safename |
469
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
# the final merged order book. each entry will be a hashref containing |
471
|
|
|
|
|
|
|
# these keys: |
472
|
|
|
|
|
|
|
# |
473
|
|
|
|
|
|
|
# - currency (the base/target currency to arbitrage) |
474
|
|
|
|
|
|
|
# |
475
|
|
|
|
|
|
|
# - gross_price_orig (ask/bid price in exchange's original quote |
476
|
|
|
|
|
|
|
# currency) |
477
|
|
|
|
|
|
|
# |
478
|
|
|
|
|
|
|
# - gross_price (like gross_price_orig, but price will be converted to |
479
|
|
|
|
|
|
|
# USD if quote currency is fiat) |
480
|
|
|
|
|
|
|
# |
481
|
|
|
|
|
|
|
# - net_price_orig (net price after adding [if sell order, because we'll |
482
|
|
|
|
|
|
|
# be buying these] or subtracting [if buy order, because we'll be |
483
|
|
|
|
|
|
|
# selling these] trading fee from the original ask/bid price. in |
484
|
|
|
|
|
|
|
# exchange's original quote currency) |
485
|
|
|
|
|
|
|
# |
486
|
|
|
|
|
|
|
# - net_price (like net_price_orig, but price will be converted to USD |
487
|
|
|
|
|
|
|
# if quote currency is fiat) |
488
|
|
|
|
|
|
|
# |
489
|
|
|
|
|
|
|
# - exchange (exchange safename) |
490
|
|
|
|
|
|
|
|
491
|
0
|
|
|
|
|
|
my @all_buy_orders; |
492
|
0
|
|
|
|
|
|
my @all_sell_orders; |
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
# produce final merged order book. |
495
|
|
|
|
|
|
|
EXCHANGE: |
496
|
0
|
|
|
|
|
|
for my $exchange (sort keys %{ $r->{_stash}{exchange_clients} }) { |
|
0
|
|
|
|
|
|
|
497
|
0
|
|
|
|
|
|
my $eid = App::cryp::arbit::_get_exchange_id($r, $exchange); |
498
|
0
|
|
|
|
|
|
my $clients = $r->{_stash}{exchange_clients}{$exchange}; |
499
|
0
|
|
|
|
|
|
my $client = $clients->{ (sort keys %$clients)[0] }; |
500
|
|
|
|
|
|
|
|
501
|
0
|
|
|
|
|
|
my @pairs; |
502
|
0
|
0
|
|
|
|
|
if ($quote_currency0 eq ':fiat') { |
503
|
0
|
|
|
|
|
|
push @pairs, map { "$base_currency/$_" } @{ $fiat_for{$exchange} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
} else { |
505
|
0
|
|
|
|
|
|
push @pairs, $set; |
506
|
|
|
|
|
|
|
} |
507
|
|
|
|
|
|
|
|
508
|
|
|
|
|
|
|
PAIR: |
509
|
0
|
|
|
|
|
|
for my $pair (@pairs) { |
510
|
0
|
|
|
|
|
|
my ($basecur, $quotecur) = split m!/!, $pair; |
511
|
0
|
0
|
|
|
|
|
next unless grep { $_ eq $pair } @{ $pairs_for{$exchange} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
|
513
|
0
|
|
|
|
|
|
my $time = time(); |
514
|
0
|
|
|
|
|
|
log_debug "Getting orderbook %s on %s ...", $pair, $exchange; |
515
|
0
|
|
|
|
|
|
my $res = $client->get_order_book(pair => $pair); |
516
|
0
|
0
|
|
|
|
|
unless ($res->[0] == 200) { |
517
|
0
|
|
|
|
|
|
log_error "Couldn't get orderbook %s on %s: %s, skipping this pair", |
518
|
|
|
|
|
|
|
$pair, $exchange, $res; |
519
|
0
|
|
|
|
|
|
next PAIR; |
520
|
|
|
|
|
|
|
} |
521
|
|
|
|
|
|
|
#log_trace "orderbook %s on %s: %s", $pair, $exchange, $res->[2]; # too much info to log |
522
|
|
|
|
|
|
|
|
523
|
|
|
|
|
|
|
# sanity checks |
524
|
0
|
0
|
|
|
|
|
unless (@{ $res->[2]{sell} }) { |
|
0
|
|
|
|
|
|
|
525
|
0
|
|
|
|
|
|
log_warn "No sell orders for %s on %s, skipping this pair", |
526
|
|
|
|
|
|
|
$pair, $exchange; |
527
|
0
|
|
|
|
|
|
next PAIR; |
528
|
|
|
|
|
|
|
} |
529
|
0
|
0
|
|
|
|
|
unless (@{ $res->[2]{buy} }) { |
|
0
|
|
|
|
|
|
|
530
|
0
|
|
|
|
|
|
log_warn "No buy orders for %s on %s, skipping this pair", |
531
|
|
|
|
|
|
|
$pair, $exchange; |
532
|
0
|
|
|
|
|
|
last PAIR; |
533
|
|
|
|
|
|
|
} |
534
|
|
|
|
|
|
|
|
535
|
0
|
|
|
|
|
|
my $buy_fee_pct = App::cryp::arbit::_get_trading_fee( |
536
|
|
|
|
|
|
|
$r, $exchange, $base_currency); |
537
|
0
|
|
|
|
|
|
for (@{ $res->[2]{buy} }) { |
|
0
|
|
|
|
|
|
|
538
|
0
|
|
|
|
|
|
push @{ $buy_orders{$exchange} }, { |
|
0
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
quote_currency => $quotecur, |
540
|
|
|
|
|
|
|
gross_price_orig => $_->[0], |
541
|
|
|
|
|
|
|
net_price_orig => $_->[0]*(1-$buy_fee_pct/100), |
542
|
|
|
|
|
|
|
base_size => $_->[1], |
543
|
|
|
|
|
|
|
}; |
544
|
|
|
|
|
|
|
} |
545
|
|
|
|
|
|
|
|
546
|
0
|
|
|
|
|
|
my $sell_fee_pct = App::cryp::arbit::_get_trading_fee( |
547
|
|
|
|
|
|
|
$r, $exchange, $base_currency); |
548
|
0
|
|
|
|
|
|
for (@{ $res->[2]{sell} }) { |
|
0
|
|
|
|
|
|
|
549
|
0
|
|
|
|
|
|
push @{ $sell_orders{$exchange} }, { |
|
0
|
|
|
|
|
|
|
550
|
|
|
|
|
|
|
quote_currency => $quotecur, |
551
|
|
|
|
|
|
|
gross_price_orig => $_->[0], |
552
|
|
|
|
|
|
|
net_price_orig => $_->[0]*(1+$sell_fee_pct/100), |
553
|
|
|
|
|
|
|
base_size => $_->[1], |
554
|
|
|
|
|
|
|
}; |
555
|
|
|
|
|
|
|
} |
556
|
|
|
|
|
|
|
|
557
|
0
|
0
|
0
|
|
|
|
if (!App::cryp::arbit::_is_fiat($quotecur) || $quotecur eq 'USD') { |
558
|
0
|
|
|
|
|
|
for (@{ $buy_orders{$exchange} }, @{ $sell_orders{$exchange} }) { |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
559
|
0
|
|
|
|
|
|
$_->{gross_price} = $_->{gross_price_orig}; |
560
|
0
|
|
|
|
|
|
$_->{net_price} = $_->{net_price_orig}; |
561
|
|
|
|
|
|
|
} |
562
|
|
|
|
|
|
|
$dbh->do("INSERT INTO price (time,base_currency,quote_currency,price,exchange_id,type) VALUES (?,?,?,?,?,?)", {}, |
563
|
0
|
|
|
|
|
|
$time, $base_currency, $quotecur, $buy_orders{$exchange}[0]{gross_price_orig}, $eid, "buy"); |
564
|
|
|
|
|
|
|
$dbh->do("INSERT INTO price (time,base_currency,quote_currency,price,exchange_id,type) VALUES (?,?,?,?,?,?)", {}, |
565
|
0
|
|
|
|
|
|
$time, $base_currency, $quotecur, $sell_orders{$exchange}[0]{gross_price_orig}, $eid, "sell"); |
566
|
|
|
|
|
|
|
} else { |
567
|
|
|
|
|
|
|
# convert fiat to USD |
568
|
0
|
0
|
|
|
|
|
my $fxrate = $r->{_stash}{forex_rates}{"$quotecur/USD"} |
569
|
|
|
|
|
|
|
or die "BUG: Didn't get forex rate for $quotecur/USD?"; |
570
|
|
|
|
|
|
|
|
571
|
0
|
|
|
|
|
|
for (@{ $buy_orders{$exchange} }) { |
|
0
|
|
|
|
|
|
|
572
|
0
|
|
|
|
|
|
$_->{gross_price} = $_->{gross_price_orig} * $fxrate; |
573
|
0
|
|
|
|
|
|
$_->{net_price} = $_->{net_price_orig} * $fxrate;; |
574
|
|
|
|
|
|
|
} |
575
|
|
|
|
|
|
|
|
576
|
0
|
|
|
|
|
|
my $fxrate_note = join( |
577
|
|
|
|
|
|
|
" ", |
578
|
|
|
|
|
|
|
sprintf("$quotecur/USD forex rate: %.8f", $fxrate), |
579
|
|
|
|
|
|
|
); |
580
|
|
|
|
|
|
|
|
581
|
0
|
|
|
|
|
|
for (@{ $sell_orders{$exchange} }) { |
|
0
|
|
|
|
|
|
|
582
|
0
|
|
|
|
|
|
$_->{gross_price} = $_->{gross_price_orig} * $fxrate; |
583
|
0
|
|
|
|
|
|
$_->{net_price} = $_->{net_price_orig} * $fxrate; |
584
|
|
|
|
|
|
|
} |
585
|
|
|
|
|
|
|
|
586
|
|
|
|
|
|
|
$dbh->do("INSERT INTO price (time,base_currency,quote_currency,price,exchange_id,type) VALUES (?,?,?,?,?,?)", {}, |
587
|
0
|
|
|
|
|
|
$time, $base_currency, $quotecur, $buy_orders{$exchange}[0]{gross_price_orig}, $eid, "buy"); |
588
|
|
|
|
|
|
|
$dbh->do("INSERT INTO price (time,base_currency,quote_currency,price,exchange_id,type) VALUES (?,?,?,?,?,?)", {}, |
589
|
0
|
|
|
|
|
|
$time, $base_currency, $quotecur, $sell_orders{$exchange}[0]{gross_price_orig}, $eid, "sell"); |
590
|
|
|
|
|
|
|
$dbh->do("INSERT INTO price (time,base_currency,quote_currency,price,exchange_id,type, note) VALUES (?,?,?,?,?,?, ?)", {}, |
591
|
0
|
|
|
|
|
|
$time, $base_currency, "USD", $buy_orders{$exchange}[0]{gross_price}, $eid, "buy", |
592
|
|
|
|
|
|
|
$fxrate_note); |
593
|
|
|
|
|
|
|
$dbh->do("INSERT INTO price (time,base_currency,quote_currency,price,exchange_id,type, note) VALUES (?,?,?,?,?,?, ?)", {}, |
594
|
0
|
|
|
|
|
|
$time, $base_currency, "USD", $sell_orders{$exchange}[0]{gross_price}, $eid, "sell", |
595
|
|
|
|
|
|
|
$fxrate_note); |
596
|
|
|
|
|
|
|
} # convert fiat currency to USD |
597
|
|
|
|
|
|
|
} # for pair |
598
|
|
|
|
|
|
|
} # for exchange |
599
|
|
|
|
|
|
|
|
600
|
|
|
|
|
|
|
# sanity checks |
601
|
0
|
0
|
|
|
|
|
if (keys(%buy_orders) < 2) { |
602
|
0
|
|
|
|
|
|
log_info "There are less than two exchanges that buy %s, ". |
603
|
|
|
|
|
|
|
"skipping this base currency"; |
604
|
0
|
|
|
|
|
|
next SET; |
605
|
|
|
|
|
|
|
} |
606
|
0
|
0
|
|
|
|
|
if (keys(%sell_orders) < 2) { |
607
|
0
|
|
|
|
|
|
log_debug "There are less than two exchanges that sell %s, skipping this base currency", |
608
|
|
|
|
|
|
|
$base_currency; |
609
|
0
|
|
|
|
|
|
next SET; |
610
|
|
|
|
|
|
|
} |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
# merge all buys from all exchanges, sort from highest net price |
613
|
0
|
|
|
|
|
|
for my $exchange (keys %buy_orders) { |
614
|
0
|
|
|
|
|
|
for (@{ $buy_orders{$exchange} }) { |
|
0
|
|
|
|
|
|
|
615
|
0
|
|
|
|
|
|
$_->{exchange} = $exchange; |
616
|
0
|
|
|
|
|
|
push @all_buy_orders, $_; |
617
|
|
|
|
|
|
|
} |
618
|
|
|
|
|
|
|
} |
619
|
0
|
|
|
|
|
|
@all_buy_orders = sort { $b->{net_price} <=> $a->{net_price} } |
|
0
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
@all_buy_orders; |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
# merge all sells from all exchanges, sort from lowest price |
623
|
0
|
|
|
|
|
|
for my $exchange (keys %sell_orders) { |
624
|
0
|
|
|
|
|
|
for (@{ $sell_orders{$exchange} }) { |
|
0
|
|
|
|
|
|
|
625
|
0
|
|
|
|
|
|
$_->{exchange} = $exchange; |
626
|
0
|
|
|
|
|
|
push @all_sell_orders, $_; |
627
|
|
|
|
|
|
|
} |
628
|
|
|
|
|
|
|
} |
629
|
0
|
|
|
|
|
|
@all_sell_orders = sort { $a->{net_price} <=> $b->{net_price} } |
|
0
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
@all_sell_orders; |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
#log_trace "all_buy_orders for %s: %s", $base_currency, \@all_buy_orders; |
633
|
|
|
|
|
|
|
#log_trace "all_sell_orders for %s: %s", $base_currency, \@all_sell_orders; |
634
|
|
|
|
|
|
|
|
635
|
0
|
|
|
|
|
|
my $account_balances = $r->{_stash}{account_balances}; |
636
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
my ($coin_order_pairs, $opportunity) = _calculate_order_pairs_for_base_currency( |
638
|
|
|
|
|
|
|
base_currency => $base_currency, |
639
|
|
|
|
|
|
|
all_buy_orders => \@all_buy_orders, |
640
|
|
|
|
|
|
|
all_sell_orders => \@all_sell_orders, |
641
|
|
|
|
|
|
|
min_net_profit_margin => $r->{args}{min_net_profit_margin}, |
642
|
|
|
|
|
|
|
max_order_quote_size => $r->{args}{max_order_quote_size}, |
643
|
|
|
|
|
|
|
max_order_size_as_book_item_size_pct => $r->{_cryp}{arbit_strategies}{merge_order_book}{max_order_size_as_book_item_size_pct}, |
644
|
|
|
|
|
|
|
max_order_pairs => $r->{args}{max_order_pairs_per_round}, |
645
|
|
|
|
|
|
|
(account_balances => $account_balances) x !$r->{args}{ignore_balance}, |
646
|
|
|
|
|
|
|
min_account_balances => $r->{args}{min_account_balances}, |
647
|
|
|
|
|
|
|
(exchange_pairs => $r->{_stash}{exchange_pairs}) x !$r->{args}{ignore_min_order_size}, |
648
|
|
|
|
|
|
|
forex_spreads => $r->{_stash}{forex_spreads}, |
649
|
0
|
|
|
|
|
|
); |
650
|
0
|
|
|
|
|
|
for (@$coin_order_pairs) { |
651
|
0
|
|
|
|
|
|
$_->{base_currency} = $base_currency; |
652
|
|
|
|
|
|
|
} |
653
|
0
|
|
|
|
|
|
push @order_pairs, @$coin_order_pairs; |
654
|
0
|
0
|
|
|
|
|
if ($opportunity) { |
655
|
|
|
|
|
|
|
$dbh->do("INSERT INTO arbit_opportunity |
656
|
|
|
|
|
|
|
(time,base_currency,quote_currency,buy_exchange_id,buy_price,sell_exchange_id,sell_price,gross_profit_margin,trading_profit_margin) VALUES |
657
|
|
|
|
|
|
|
(? ,? ,? ,? ,? ,? ,? ,? ,? )", {}, |
658
|
|
|
|
|
|
|
$opportunity->{time}, |
659
|
|
|
|
|
|
|
$opportunity->{base_currency}, |
660
|
|
|
|
|
|
|
$opportunity->{quote_currency}, |
661
|
|
|
|
|
|
|
App::cryp::arbit::_get_exchange_id($r, $opportunity->{buy_exchange}), |
662
|
|
|
|
|
|
|
$opportunity->{buy_price}, |
663
|
|
|
|
|
|
|
App::cryp::arbit::_get_exchange_id($r, $opportunity->{sell_exchange}), |
664
|
|
|
|
|
|
|
$opportunity->{sell_price}, |
665
|
|
|
|
|
|
|
$opportunity->{gross_profit_margin}, |
666
|
|
|
|
|
|
|
$opportunity->{trading_profit_margin}, |
667
|
0
|
|
|
|
|
|
); |
668
|
|
|
|
|
|
|
} |
669
|
|
|
|
|
|
|
} # for set (base currency) |
670
|
|
|
|
|
|
|
|
671
|
0
|
|
|
|
|
|
[200, "OK", \@order_pairs]; |
672
|
|
|
|
|
|
|
} |
673
|
|
|
|
|
|
|
|
674
|
|
|
|
|
|
|
1; |
675
|
|
|
|
|
|
|
# ABSTRACT: Using merged order books for arbitration |
676
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
__END__ |
678
|
|
|
|
|
|
|
|
679
|
|
|
|
|
|
|
=pod |
680
|
|
|
|
|
|
|
|
681
|
|
|
|
|
|
|
=encoding UTF-8 |
682
|
|
|
|
|
|
|
|
683
|
|
|
|
|
|
|
=head1 NAME |
684
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
App::cryp::arbit::Strategy::merge_order_book - Using merged order books for arbitration |
686
|
|
|
|
|
|
|
|
687
|
|
|
|
|
|
|
=head1 VERSION |
688
|
|
|
|
|
|
|
|
689
|
|
|
|
|
|
|
This document describes version 0.008 of App::cryp::arbit::Strategy::merge_order_book (from Perl distribution App-cryp-arbit), released on 2018-11-29. |
690
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
=head1 SYNOPSIS |
692
|
|
|
|
|
|
|
|
693
|
|
|
|
|
|
|
=head2 Using this strategy |
694
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
In your F<cryp.conf>: |
696
|
|
|
|
|
|
|
|
697
|
|
|
|
|
|
|
[program=cryp-arbit arbit] |
698
|
|
|
|
|
|
|
strategy=merge-order-book |
699
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
or in your F<cryp-arbit.conf>: |
701
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
[arbit] |
703
|
|
|
|
|
|
|
strategy=merge-order-book |
704
|
|
|
|
|
|
|
|
705
|
|
|
|
|
|
|
This is actually the default strategy, so you don't have to explicitly set |
706
|
|
|
|
|
|
|
C<strategy> to this strategy. |
707
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
=head2 Configuration |
709
|
|
|
|
|
|
|
|
710
|
|
|
|
|
|
|
In your F<cryp.conf>: |
711
|
|
|
|
|
|
|
|
712
|
|
|
|
|
|
|
[arbit-strategy/merge-order-book] |
713
|
|
|
|
|
|
|
... |
714
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
=head1 DESCRIPTION |
716
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
This arbitration strategy uses information from merged order books. Below is the |
718
|
|
|
|
|
|
|
description of the algorithm. Suppose we are arbitraging the pair BTC/USD. |
719
|
|
|
|
|
|
|
I<E1>, I<E2>, ... I<En> are exchanges. I<P*> are prices. I<S*> are sizes. I<i> |
720
|
|
|
|
|
|
|
denotes exchange index. |
721
|
|
|
|
|
|
|
|
722
|
|
|
|
|
|
|
B<First step:> get order books from all of the involved exchanges, for example: |
723
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
# buy orders on E1 # sell orders on E1 |
725
|
|
|
|
|
|
|
price size price size |
726
|
|
|
|
|
|
|
----- ---- ----- ---- |
727
|
|
|
|
|
|
|
P1b1 S1b1 P1s1 S1s1 |
728
|
|
|
|
|
|
|
P1b2 S1b2 P1s2 S1s2 |
729
|
|
|
|
|
|
|
P1b3 S1b3 P1s3 S1s3 |
730
|
|
|
|
|
|
|
... ... |
731
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
# buy orders on E2 # sell orders on E2 |
733
|
|
|
|
|
|
|
price size price size |
734
|
|
|
|
|
|
|
----- ---- ----- ---- |
735
|
|
|
|
|
|
|
P2b1 S2b1 P2s1 S2s1 |
736
|
|
|
|
|
|
|
P2b2 S2b2 P2s2 S2s2 |
737
|
|
|
|
|
|
|
P2b3 S2b3 P2s3 S2s3 |
738
|
|
|
|
|
|
|
... ... |
739
|
|
|
|
|
|
|
|
740
|
|
|
|
|
|
|
... |
741
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
Note that buy orders are sorted from highest to lowest price (I<Pib1> > I<Pib2> |
743
|
|
|
|
|
|
|
> I<Pib3> > ...) while sell orders are sorted from lowest to highest price |
744
|
|
|
|
|
|
|
(I<Pis1> < I<Pis2> < I<Pis3> < ...). Also note that I<P1b*> < I<P1s*>, unless |
745
|
|
|
|
|
|
|
something weird is going on. |
746
|
|
|
|
|
|
|
|
747
|
|
|
|
|
|
|
B<Second step:> merge all the orders from exchanges into just two lists: buy and |
748
|
|
|
|
|
|
|
sell orders. Sort buy orders, as usual, from highest to lowest price. Sort sell |
749
|
|
|
|
|
|
|
orders, as usual, from lowest to highest. For example: |
750
|
|
|
|
|
|
|
|
751
|
|
|
|
|
|
|
# buy orders # sell orders |
752
|
|
|
|
|
|
|
price size price size |
753
|
|
|
|
|
|
|
----- ---- ----- ---- |
754
|
|
|
|
|
|
|
P1b1 S1b1 P2s1 S2s1 |
755
|
|
|
|
|
|
|
P2b1 S2b1 P3s1 S3s1 |
756
|
|
|
|
|
|
|
P2b2 S2b2 P3s2 S3s2 |
757
|
|
|
|
|
|
|
P1b2 S1b2 P1s1 S1s1 |
758
|
|
|
|
|
|
|
... |
759
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
Arbitrage can happen if we can buy cheap bitcoin and sell our expensive bitcoin. |
761
|
|
|
|
|
|
|
This means I<P1b1> must be I<above> I<P2s1>, because we want to buy bitcoins on |
762
|
|
|
|
|
|
|
I<E1> from trader that is willing to sell at I<P2s1> then sell it on I<E1> to |
763
|
|
|
|
|
|
|
the trader that is willing to buy the bitcoins at I<P2b1>. Pocketing the |
764
|
|
|
|
|
|
|
difference (minus trading fees) as profit. |
765
|
|
|
|
|
|
|
|
766
|
|
|
|
|
|
|
No actual bitcoins will be transferred from I<E2> to I<E1> as that would take a |
767
|
|
|
|
|
|
|
long time and incurs relatively high network fees. Instead, we maintain bitcoin |
768
|
|
|
|
|
|
|
and USD balances on each exchange to be able to buy/sell quickly. The balances |
769
|
|
|
|
|
|
|
serve as "working capital" or "inventory". |
770
|
|
|
|
|
|
|
|
771
|
|
|
|
|
|
|
The minimum net profit margin is I<min_net_profit_margin>. We create buy/sell |
772
|
|
|
|
|
|
|
order pairs starting from the topmost of the merged order book, until we can't |
773
|
|
|
|
|
|
|
get I<min_net_profit_margin> anymore. |
774
|
|
|
|
|
|
|
|
775
|
|
|
|
|
|
|
Then we monitor our order pairs and cancel them if they remain unfilled for a |
776
|
|
|
|
|
|
|
while. |
777
|
|
|
|
|
|
|
|
778
|
|
|
|
|
|
|
Then we retrieve order books from the exchanges and start the process again. |
779
|
|
|
|
|
|
|
|
780
|
|
|
|
|
|
|
=head2 Strengths |
781
|
|
|
|
|
|
|
|
782
|
|
|
|
|
|
|
Order books contain information about prices and volumes at each price level. |
783
|
|
|
|
|
|
|
This serves as a guide on what size our orders should be, so we do not have to |
784
|
|
|
|
|
|
|
explicitly set order size. This is especially useful if we are not familiar with |
785
|
|
|
|
|
|
|
the typical volume of the pair on an exchange. |
786
|
|
|
|
|
|
|
|
787
|
|
|
|
|
|
|
By sorting the buy and sell orders, we get maximum price difference. |
788
|
|
|
|
|
|
|
|
789
|
|
|
|
|
|
|
=for Pod::Coverage ^(.+)$ |
790
|
|
|
|
|
|
|
|
791
|
|
|
|
|
|
|
=head1 Weaknesses |
792
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
Order books are changing rapidly. By the time we get the order book from the |
794
|
|
|
|
|
|
|
exchange API, that information is already stale. In the course of milliseconds, |
795
|
|
|
|
|
|
|
the order book can change, sometimes significantly. So when we submit an order |
796
|
|
|
|
|
|
|
to buy X BTC at price P, it might not get fulfilled completely or at all because |
797
|
|
|
|
|
|
|
the market price has moved above P, for example. |
798
|
|
|
|
|
|
|
|
799
|
|
|
|
|
|
|
=head1 CONFIGURATION |
800
|
|
|
|
|
|
|
|
801
|
|
|
|
|
|
|
=over |
802
|
|
|
|
|
|
|
|
803
|
|
|
|
|
|
|
=item * max_order_size_as_book_item_size_pct |
804
|
|
|
|
|
|
|
|
805
|
|
|
|
|
|
|
Number 0-100. Default is 100. This setting is used for more safety since order |
806
|
|
|
|
|
|
|
books are rapidly changing. For example, there is an item in the merged order |
807
|
|
|
|
|
|
|
book as follows: |
808
|
|
|
|
|
|
|
|
809
|
|
|
|
|
|
|
type exchange price size item# |
810
|
|
|
|
|
|
|
---- -------- ----- ---- ----- |
811
|
|
|
|
|
|
|
buy exchange1 800.1 12 B1 |
812
|
|
|
|
|
|
|
buy exchange1 798.1 24 B2 |
813
|
|
|
|
|
|
|
... |
814
|
|
|
|
|
|
|
sell exchange2 780.1 5 S1 |
815
|
|
|
|
|
|
|
sell exchange2 782.9 8 S2 |
816
|
|
|
|
|
|
|
... |
817
|
|
|
|
|
|
|
|
818
|
|
|
|
|
|
|
If `max_order_size_as_book_item_size_pct` is set to 100, then this will create |
819
|
|
|
|
|
|
|
order pairs as follows: |
820
|
|
|
|
|
|
|
|
821
|
|
|
|
|
|
|
size buy from buy price sell to sell price item# |
822
|
|
|
|
|
|
|
---- -------- --------- ------- ---------- ----- |
823
|
|
|
|
|
|
|
5 exchange2 780.1 exchange1 800.1 OP1 |
824
|
|
|
|
|
|
|
7 exchange2 782.9 exchange1 800.1 OP2 |
825
|
|
|
|
|
|
|
... |
826
|
|
|
|
|
|
|
|
827
|
|
|
|
|
|
|
The OP1 will use up (100%) of item #S1 from the order book, then OP2 will use up |
828
|
|
|
|
|
|
|
(100%) item #B1 from the order book. |
829
|
|
|
|
|
|
|
|
830
|
|
|
|
|
|
|
However, if `max_order_size_as_book_item_size_pct` is set to 75, then this will |
831
|
|
|
|
|
|
|
create order pairs as follows: |
832
|
|
|
|
|
|
|
|
833
|
|
|
|
|
|
|
size buy from buy price sell to sell price item# |
834
|
|
|
|
|
|
|
---- -------- --------- ------- ---------- ----- |
835
|
|
|
|
|
|
|
3.75 exchange2 780.1 exchange1 800.1 OP1 |
836
|
|
|
|
|
|
|
5.25 exchange2 782.9 exchange1 800.1 OP2 |
837
|
|
|
|
|
|
|
|
838
|
|
|
|
|
|
|
OP1 will use 75% item S1 from the order book, then the strategy will move on to |
839
|
|
|
|
|
|
|
the next sell order (S2). OP2 will also use only 75% of item B1 (3.75 + 5.25 = |
840
|
|
|
|
|
|
|
9, which is 75% of 12) before moving on to the next buy order. |
841
|
|
|
|
|
|
|
|
842
|
|
|
|
|
|
|
=back |
843
|
|
|
|
|
|
|
|
844
|
|
|
|
|
|
|
=head1 BUGS |
845
|
|
|
|
|
|
|
|
846
|
|
|
|
|
|
|
Please report all bug reports or feature requests to L<mailto:stevenharyanto@gmail.com>. |
847
|
|
|
|
|
|
|
|
848
|
|
|
|
|
|
|
=head1 SEE ALSO |
849
|
|
|
|
|
|
|
|
850
|
|
|
|
|
|
|
L<App::cryp::arbit> |
851
|
|
|
|
|
|
|
|
852
|
|
|
|
|
|
|
Other C<App::cryp::arbit::Strategy::*> modules. |
853
|
|
|
|
|
|
|
|
854
|
|
|
|
|
|
|
=head1 AUTHOR |
855
|
|
|
|
|
|
|
|
856
|
|
|
|
|
|
|
perlancar <perlancar@cpan.org> |
857
|
|
|
|
|
|
|
|
858
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
859
|
|
|
|
|
|
|
|
860
|
|
|
|
|
|
|
This software is copyright (c) 2018 by perlancar@cpan.org. |
861
|
|
|
|
|
|
|
|
862
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
863
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
864
|
|
|
|
|
|
|
|
865
|
|
|
|
|
|
|
=cut |