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-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