File Coverage

blib/lib/App/cryp/arbit/Strategy/merge_order_book.pm
Criterion Covered Total %
statement 185 365 50.6
branch 73 138 52.9
condition 25 38 65.7
subroutine 10 11 90.9
pod 0 1 0.0
total 293 553 52.9


line stmt bran cond sub pod time code
1             package App::cryp::arbit::Strategy::merge_order_book;
2              
3             our $DATE = '2018-12-03'; # DATE
4             our $VERSION = '0.009'; # VERSION
5              
6 1     1   99088 use 5.010001;
  1         14  
7 1     1   5 use strict;
  1         3  
  1         31  
8 1     1   6 use warnings;
  1         2  
  1         24  
9 1     1   2157 use Log::ger;
  1         51  
  1         6  
10              
11             require App::cryp::arbit;
12 1     1   779 use Finance::Currency::FiatX;
  1         4006  
  1         77  
13 1     1   9 use List::Util qw(min max shuffle);
  1         2  
  1         70  
14 1     1   588 use Storable qw(dclone);
  1         3133  
  1         66  
15 1     1   560 use Time::HiRes qw(time);
  1         1333  
  1         4  
16              
17 1     1   591 use Role::Tiny::With;
  1         5233  
  1         3794  
18              
19             with 'App::cryp::Role::ArbitStrategy';
20              
21             sub _calculate_order_pairs_for_base_currency {
22 17     17   82023 my %args = @_;
23              
24 17         44 my $base_currency = $args{base_currency};
25 17         32 my $all_buy_orders = $args{all_buy_orders};
26 17         29 my $all_sell_orders = $args{all_sell_orders};
27 17   50     51 my $min_net_profit_margin = $args{min_net_profit_margin} // 0;
28 17         26 my $max_order_quote_size = $args{max_order_quote_size};
29 17         32 my $max_order_pairs = $args{max_order_pairs};
30 17   100     64 my $max_order_size_as_book_item_size_pct = $args{max_order_size_as_book_item_size_pct} // 100;
31 17         28 my $account_balances = $args{account_balances};
32 17         32 my $min_account_balances = $args{min_account_balances};
33 17         31 my $exchange_pairs = $args{exchange_pairs};
34 17         27 my $forex_spreads = $args{forex_spreads};
35              
36 17         31 my @order_pairs;
37             my $opportunity;
38              
39 17         26 for (@{ $all_buy_orders }, @{ $all_sell_orders }) {
  17         47  
  17         37  
40 51         111 $_->{base_size} *= $max_order_size_as_book_item_size_pct/100;
41             }
42              
43 17 100 100     73 if ($account_balances && $min_account_balances) {
44 1         6 for my $e (keys %$account_balances) {
45 2         4 my $balances = $account_balances->{$e};
46 2         7 for my $cur (keys %$balances) {
47 2         4 my $curbalances = $balances->{$cur};
48 2         5 for my $rec (@$curbalances) {
49 3         9 my $eacc = "$e/$rec->{account}";
50 3 100 66     17 if (defined $min_account_balances->{$eacc} &&
51             defined $min_account_balances->{$eacc}{$cur}) {
52 2         6 $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         30 while (1) {
62 52 100 100     133 last CREATE if defined $max_order_pairs &&
63             @order_pairs >= $max_order_pairs;
64              
65 50         84 my ($sell, $sell_index);
66             FIND_BUYER:
67             {
68 50         66 $sell_index = 0;
  50         81  
69 50         102 while ($sell_index < @$all_buy_orders) {
70 45         75 $sell = $all_buy_orders->[$sell_index];
71 45 100       87 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         54  
75 6         10 $sell_index++; next;
  6         15  
76             }
77             }
78 39         67 last;
79             }
80             # there are no more buyers left we can sell to
81 50 100       106 last CREATE unless $sell_index < @$all_buy_orders;
82             }
83              
84 39         62 my ($buy, $buy_index);
85             FIND_SELLER:
86             {
87 39         55 $buy_index = 0;
  39         55  
88 39         76 while ($buy_index < @$all_sell_orders) {
89 37         59 $buy = $all_sell_orders->[$buy_index];
90             # shouldn't happen though
91 37 50       80 if ($buy->{exchange} eq $sell->{exchange}) {
92 0         0 $buy_index++; next;
  0         0  
93             }
94 37 100       68 if ($account_balances) {
95             # we don't have any inventory left to buy from this exchange
96 9 100 50     15 unless (@{ $account_balances->{ $buy->{exchange} }{$buy->{quote_currency}} // [] }) {
  9         30  
97 1         2 $buy_index++; next;
  1         6  
98             }
99             }
100 36         72 last;
101             }
102             # there are no more sellers left we can buy from
103 39 100       82 last CREATE unless $buy_index < @$all_sell_orders;
104             }
105              
106             my $gross_profit_margin = ($sell->{gross_price} - $buy->{gross_price}) /
107 36         128 min($sell->{gross_price}, $buy->{gross_price}) * 100;
108             my $trading_profit_margin = ($sell->{net_price} - $buy->{net_price}) /
109 36         93 min($sell->{net_price}, $buy->{net_price}) * 100;
110              
111             # record opportunity, the currently highest trading profit margin
112 36 100       75 unless ($opportunity) {
113 17         30 my $quote_currency = $sell->{quote_currency};
114 17 50       54 if (App::cryp::arbit::_is_fiat($quote_currency)) {
115 17         35 $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         150 gross_profit_margin => $gross_profit_margin,
126             trading_profit_margin => $trading_profit_margin,
127             };
128             }
129              
130 36 100       90 if ($trading_profit_margin < $min_net_profit_margin) {
131 1         39 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         7 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         339 gross_profit_margin => $gross_profit_margin,
154             trading_profit_margin => $trading_profit_margin,
155             };
156              
157 35 100       83 if ($account_balances) {
158 8         27 $order_pair->{sell}{account} = $account_balances->{ $sell->{exchange} }{$base_currency}[0]{account};
159 8         38 $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         146 );
167 35 100       79 if (defined $max_order_quote_size) {
168             push @sizes, (
169 7         30 {which => 'max_order_quote_size', size => $max_order_quote_size / max($sell->{gross_price}, $buy->{gross_price})},
170             );
171             }
172 35 100       72 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         48 );
184             }
185 35         105 @sizes = sort { $a->{size} <=> $b->{size} } @sizes;
  76         176  
186 35         67 my $order_size = $sizes[0]{size};
187              
188 35         56 $order_pair->{base_size} = $order_size;
189             $order_pair->{gross_profit} = $order_size *
190 35         73 ($order_pair->{sell}{gross_price} - $order_pair->{buy}{gross_price});
191             $order_pair->{trading_profit} = $order_size *
192 35         73 ($order_pair->{sell}{net_price} - $order_pair->{buy}{net_price});
193              
194             UPDATE_INVENTORY_BALANCES:
195 35         83 for my $i (0..$#sizes) {
196 93         150 my $size = $sizes[$i]{size};
197 93         137 my $which = $sizes[$i]{which};
198 93         162 my $used_up = $size - $order_size <= 1e-8;
199 93 100       205 if ($which eq 'buy order') {
    100          
    100          
    100          
200 35 100       67 if ($used_up) {
201 11         26 splice @$all_buy_orders, $sell_index, 1;
202             } else {
203 24         52 $all_buy_orders->[$sell_index]{base_size} -= $order_size;
204             }
205             } elsif ($which eq 'sell order') {
206 35 100       56 if ($used_up) {
207 13         36 splice @$all_sell_orders, $buy_index, 1;
208             } else {
209 22         51 $all_sell_orders->[$buy_index]{base_size} -= $order_size;
210             }
211             } elsif ($which eq 'sell exchange balance') {
212 8 100       18 if ($used_up) {
213 5         8 shift @{ $account_balances->{ $sell->{exchange} }{$base_currency} };
  5         20  
214             } else {
215             $account_balances->{ $sell->{exchange} }{$base_currency}[0]{available} -=
216 3         9 $order_size;
217             }
218             } elsif ($which eq 'buy exchange balance') {
219 8         13 my $c = $buy->{quote_currency};
220 8 100       16 if ($used_up) {
221 2         4 shift @{ $account_balances->{ $buy->{exchange} }{$c} };
  2         8  
222             } else {
223             $account_balances->{ $buy->{exchange} }{$c}[0]{available} -=
224 6         19 $order_size * $buy->{gross_price_orig};
225             }
226             }
227             } # UPDATE_INVENTORY_BALANCES
228              
229 35 100       72 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       54 last unless $exchange_pairs;
  35         69  
236 9         18 my $pair_recs = $exchange_pairs->{ $buy->{exchange} };
237 9 100       22 last unless $pair_recs;
238 4         7 my $pair_rec;
239 4         9 for (@$pair_recs) {
240 4 50       13 if ($_->{base_currency} eq $base_currency) {
241 4         8 $pair_rec = $_; last;
  4         32  
242             }
243             }
244 4 50       12 last unless $pair_rec;
245 4 100 100     18 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         10 $order_pair->{base_size}, $pair_rec->{min_base_size}, $order_pair;
248 1         9 next CREATE;
249             }
250 3         9 my $quote_size = $order_pair->{base_size}*$buy->{gross_price_orig};
251 3 100 100     17 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         6 $quote_size, $pair_rec->{min_quote_size}, $order_pair;
254 1         7 next CREATE;
255             }
256             } # CHECK_MINIMUM_BUY_SIZE
257              
258             CHECK_MINIMUM_SELL_SIZE:
259             {
260 33 100       41 last unless $exchange_pairs;
  33         66  
261 7         14 my $pair_recs = $exchange_pairs->{ $sell->{exchange} };
262 7 100       20 last unless $pair_recs;
263 5         7 my $pair_rec;
264 5         11 for (@$pair_recs) {
265 5 50       17 if ($_->{base_currency} eq $base_currency) {
266 5         9 $pair_rec = $_; last;
  5         8  
267             }
268             }
269 5 50       12 last unless $pair_rec;
270 5 100 100     21 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         6 $order_pair->{base_size}, $pair_rec->{min_base_size}, $order_pair;
273 1         8 next CREATE;
274             }
275 4         10 my $quote_size = $order_pair->{base_size}*$sell->{gross_price_orig};
276 4 100 100     19 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         16 $quote_size, $pair_rec->{min_quote_size}, $order_pair;
279 2         15 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         30 my @tmp = @order_pairs;
  17         35  
290 17         31 @order_pairs = ();
291 17         26 my $i = 0;
292             ORDER_PAIR:
293 17         30 for my $op (@tmp) {
294 30         45 $i++;
295 30         157 my ($bcur) = $op->{buy}{pair} =~ m!/(.+)!;
296 30         106 my ($scur) = $op->{sell}{pair} =~ m!/(.+)!;
297              
298 30 50       103 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         42 my $spread;
306 30 50       86 $spread = $forex_spreads->{"$bcur/$scur"} if $forex_spreads;
307              
308 30 50       58 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         181 $i, $op->{buy}{pair}, $op->{sell}{pair}, $op->{trading_profit_margin}, $bcur, $scur, $spread;
317 30         127 $op->{forex_spread} = $spread;
318 30         63 $op->{net_profit_margin} = $op->{trading_profit_margin} - $spread;
319 30         58 $op->{net_profit} = $op->{trading_profit} * $op->{net_profit_margin} / $op->{trading_profit_margin};
320 30 100       65 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         67 push @order_pairs, $op;
328             }
329             } # ADJUST_FOREX_SPREAD
330              
331             # re-sort
332 17         36 @order_pairs = sort { $b->{net_profit_margin} <=> $a->{net_profit_margin} } @order_pairs;
  17         38  
333              
334 17         95 (\@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.009 of App::cryp::arbit::Strategy::merge_order_book (from Perl distribution App-cryp-arbit), released on 2018-12-03.
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