| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package App::cryp::arbit::Strategy::merge_order_book; | 
| 2 |  |  |  |  |  |  |  | 
| 3 |  |  |  |  |  |  | our $DATE = '2021-05-26'; # DATE | 
| 4 |  |  |  |  |  |  | our $VERSION = '0.010'; # VERSION | 
| 5 |  |  |  |  |  |  |  | 
| 6 | 1 |  |  | 1 |  | 96563 | use 5.010001; | 
|  | 1 |  |  |  |  | 12 |  | 
| 7 | 1 |  |  | 1 |  | 5 | use strict; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 20 |  | 
| 8 | 1 |  |  | 1 |  | 4 | use warnings; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 45 |  | 
| 9 | 1 |  |  | 1 |  | 2164 | use Log::ger; | 
|  | 1 |  |  |  |  | 49 |  | 
|  | 1 |  |  |  |  | 5 |  | 
| 10 |  |  |  |  |  |  |  | 
| 11 |  |  |  |  |  |  | require App::cryp::arbit; | 
| 12 | 1 |  |  | 1 |  | 806 | use Finance::Currency::FiatX; | 
|  | 1 |  |  |  |  | 3843 |  | 
|  | 1 |  |  |  |  | 67 |  | 
| 13 | 1 |  |  | 1 |  | 13 | use List::Util qw(min max shuffle); | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 74 |  | 
| 14 | 1 |  |  | 1 |  | 721 | use Storable qw(dclone); | 
|  | 1 |  |  |  |  | 3279 |  | 
|  | 1 |  |  |  |  | 83 |  | 
| 15 | 1 |  |  | 1 |  | 568 | use Time::HiRes qw(time); | 
|  | 1 |  |  |  |  | 1226 |  | 
|  | 1 |  |  |  |  | 4 |  | 
| 16 |  |  |  |  |  |  |  | 
| 17 | 1 |  |  | 1 |  | 648 | use Role::Tiny::With; | 
|  | 1 |  |  |  |  | 4438 |  | 
|  | 1 |  |  |  |  | 3359 |  | 
| 18 |  |  |  |  |  |  |  | 
| 19 |  |  |  |  |  |  | with 'App::cryp::Role::ArbitStrategy'; | 
| 20 |  |  |  |  |  |  |  | 
| 21 |  |  |  |  |  |  | sub _calculate_order_pairs_for_base_currency { | 
| 22 | 17 |  |  | 17 |  | 94827 | my %args = @_; | 
| 23 |  |  |  |  |  |  |  | 
| 24 | 17 |  |  |  |  | 49 | my $base_currency         = $args{base_currency}; | 
| 25 | 17 |  |  |  |  | 45 | my $all_buy_orders        = $args{all_buy_orders}; | 
| 26 | 17 |  |  |  |  | 29 | my $all_sell_orders       = $args{all_sell_orders}; | 
| 27 | 17 |  | 50 |  |  | 61 | my $min_net_profit_margin = $args{min_net_profit_margin} // 0; | 
| 28 | 17 |  |  |  |  | 39 | my $max_order_quote_size  = $args{max_order_quote_size}; | 
| 29 | 17 |  |  |  |  | 37 | my $max_order_pairs       = $args{max_order_pairs}; | 
| 30 | 17 |  | 100 |  |  | 94 | my $max_order_size_as_book_item_size_pct = $args{max_order_size_as_book_item_size_pct} // 100; | 
| 31 | 17 |  |  |  |  | 42 | my $account_balances      = $args{account_balances}; | 
| 32 | 17 |  |  |  |  | 34 | my $min_account_balances  = $args{min_account_balances}; | 
| 33 | 17 |  |  |  |  | 35 | my $exchange_pairs        = $args{exchange_pairs}; | 
| 34 | 17 |  |  |  |  | 34 | my $forex_spreads         = $args{forex_spreads}; | 
| 35 |  |  |  |  |  |  |  | 
| 36 | 17 |  |  |  |  | 31 | my @order_pairs; | 
| 37 |  |  |  |  |  |  | my $opportunity; | 
| 38 |  |  |  |  |  |  |  | 
| 39 | 17 |  |  |  |  | 29 | for (@{ $all_buy_orders }, @{ $all_sell_orders }) { | 
|  | 17 |  |  |  |  | 33 |  | 
|  | 17 |  |  |  |  | 48 |  | 
| 40 | 51 |  |  |  |  | 103 | $_->{base_size} *= $max_order_size_as_book_item_size_pct/100; | 
| 41 |  |  |  |  |  |  | } | 
| 42 |  |  |  |  |  |  |  | 
| 43 | 17 | 100 | 100 |  |  | 80 | if ($account_balances && $min_account_balances) { | 
| 44 | 1 |  |  |  |  | 4 | for my $e (keys %$account_balances) { | 
| 45 | 2 |  |  |  |  | 6 | my $balances = $account_balances->{$e}; | 
| 46 | 2 |  |  |  |  | 5 | for my $cur (keys %$balances) { | 
| 47 | 2 |  |  |  |  | 5 | my $curbalances = $balances->{$cur}; | 
| 48 | 2 |  |  |  |  | 4 | for my $rec (@$curbalances) { | 
| 49 | 3 |  |  |  |  | 7 | my $eacc = "$e/$rec->{account}"; | 
| 50 | 3 | 100 | 66 |  |  | 14 | if (defined $min_account_balances->{$eacc} && | 
| 51 |  |  |  |  |  |  | defined $min_account_balances->{$eacc}{$cur}) { | 
| 52 | 2 |  |  |  |  | 5 | $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 |  |  |  |  | 31 | while (1) { | 
| 62 | 52 | 100 | 100 |  |  | 127 | last CREATE if defined $max_order_pairs && | 
| 63 |  |  |  |  |  |  | @order_pairs >= $max_order_pairs; | 
| 64 |  |  |  |  |  |  |  | 
| 65 | 50 |  |  |  |  | 120 | my ($sell, $sell_index); | 
| 66 |  |  |  |  |  |  | FIND_BUYER: | 
| 67 |  |  |  |  |  |  | { | 
| 68 | 50 |  |  |  |  | 60 | $sell_index = 0; | 
|  | 50 |  |  |  |  | 76 |  | 
| 69 | 50 |  |  |  |  | 166 | while ($sell_index < @$all_buy_orders) { | 
| 70 | 45 |  |  |  |  | 78 | $sell = $all_buy_orders->[$sell_index]; | 
| 71 | 45 | 100 |  |  |  | 93 | if ($account_balances) { | 
| 72 |  |  |  |  |  |  | # we don't have any inventory left to sell on this selling | 
| 73 |  |  |  |  |  |  | # exchange | 
| 74 | 15 | 100 | 50 |  |  | 18 | unless (@{ $account_balances->{ $sell->{exchange} }{$base_currency} // [] }) { | 
|  | 15 |  |  |  |  | 129 |  | 
| 75 | 6 |  |  |  |  | 9 | $sell_index++; next; | 
|  | 6 |  |  |  |  | 11 |  | 
| 76 |  |  |  |  |  |  | } | 
| 77 |  |  |  |  |  |  | } | 
| 78 | 39 |  |  |  |  | 53 | last; | 
| 79 |  |  |  |  |  |  | } | 
| 80 |  |  |  |  |  |  | # there are no more buyers left we can sell to | 
| 81 | 50 | 100 |  |  |  | 108 | last CREATE unless $sell_index < @$all_buy_orders; | 
| 82 |  |  |  |  |  |  | } | 
| 83 |  |  |  |  |  |  |  | 
| 84 | 39 |  |  |  |  | 97 | my ($buy, $buy_index); | 
| 85 |  |  |  |  |  |  | FIND_SELLER: | 
| 86 |  |  |  |  |  |  | { | 
| 87 | 39 |  |  |  |  | 54 | $buy_index = 0; | 
|  | 39 |  |  |  |  | 63 |  | 
| 88 | 39 |  |  |  |  | 86 | while ($buy_index < @$all_sell_orders) { | 
| 89 | 37 |  |  |  |  | 49 | $buy = $all_sell_orders->[$buy_index]; | 
| 90 |  |  |  |  |  |  | # shouldn't happen though | 
| 91 | 37 | 50 |  |  |  | 93 | if ($buy->{exchange} eq $sell->{exchange}) { | 
| 92 | 0 |  |  |  |  | 0 | $buy_index++; next; | 
|  | 0 |  |  |  |  | 0 |  | 
| 93 |  |  |  |  |  |  | } | 
| 94 | 37 | 100 |  |  |  | 164 | 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 |  |  |  |  | 33 |  | 
| 97 | 1 |  |  |  |  | 3 | $buy_index++; next; | 
|  | 1 |  |  |  |  | 3 |  | 
| 98 |  |  |  |  |  |  | } | 
| 99 |  |  |  |  |  |  | } | 
| 100 | 36 |  |  |  |  | 54 | 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 |  |  |  |  | 205 | min($sell->{gross_price}, $buy->{gross_price}) * 100; | 
| 108 |  |  |  |  |  |  | my $trading_profit_margin = ($sell->{net_price} - $buy->{net_price}) / | 
| 109 | 36 |  |  |  |  | 113 | min($sell->{net_price}, $buy->{net_price}) * 100; | 
| 110 |  |  |  |  |  |  |  | 
| 111 |  |  |  |  |  |  | # record opportunity, the currently highest trading profit margin | 
| 112 | 36 | 100 |  |  |  | 70 | unless ($opportunity) { | 
| 113 | 17 |  |  |  |  | 31 | my $quote_currency = $sell->{quote_currency}; | 
| 114 | 17 | 50 |  |  |  | 83 | if (App::cryp::arbit::_is_fiat($quote_currency)) { | 
| 115 | 17 |  |  |  |  | 50 | $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 |  |  |  |  | 194 | gross_profit_margin   => $gross_profit_margin, | 
| 126 |  |  |  |  |  |  | trading_profit_margin => $trading_profit_margin, | 
| 127 |  |  |  |  |  |  | }; | 
| 128 |  |  |  |  |  |  | } | 
| 129 |  |  |  |  |  |  |  | 
| 130 | 36 | 100 |  |  |  | 88 | if ($trading_profit_margin < $min_net_profit_margin) { | 
| 131 | 1 |  |  |  |  | 44 | 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 |  |  |  |  | 311 | gross_profit_margin => $gross_profit_margin, | 
| 154 |  |  |  |  |  |  | trading_profit_margin => $trading_profit_margin, | 
| 155 |  |  |  |  |  |  | }; | 
| 156 |  |  |  |  |  |  |  | 
| 157 | 35 | 100 |  |  |  | 90 | if ($account_balances) { | 
| 158 | 8 |  |  |  |  | 26 | $order_pair->{sell}{account} = $account_balances->{ $sell->{exchange} }{$base_currency}[0]{account}; | 
| 159 | 8 |  |  |  |  | 25 | $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 |  |  |  |  | 231 | ); | 
| 167 | 35 | 100 |  |  |  | 76 | if (defined $max_order_quote_size) { | 
| 168 |  |  |  |  |  |  | push @sizes, ( | 
| 169 | 7 |  |  |  |  | 44 | {which => 'max_order_quote_size', size => $max_order_quote_size / max($sell->{gross_price}, $buy->{gross_price})}, | 
| 170 |  |  |  |  |  |  | ); | 
| 171 |  |  |  |  |  |  | } | 
| 172 | 35 | 100 |  |  |  | 83 | 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 |  |  |  |  | 52 | ); | 
| 184 |  |  |  |  |  |  | } | 
| 185 | 35 |  |  |  |  | 153 | @sizes = sort { $a->{size} <=> $b->{size} } @sizes; | 
|  | 76 |  |  |  |  | 168 |  | 
| 186 | 35 |  |  |  |  | 63 | my $order_size = $sizes[0]{size}; | 
| 187 |  |  |  |  |  |  |  | 
| 188 | 35 |  |  |  |  | 108 | $order_pair->{base_size} = $order_size; | 
| 189 |  |  |  |  |  |  | $order_pair->{gross_profit} = $order_size * | 
| 190 | 35 |  |  |  |  | 93 | ($order_pair->{sell}{gross_price} - $order_pair->{buy}{gross_price}); | 
| 191 |  |  |  |  |  |  | $order_pair->{trading_profit} = $order_size * | 
| 192 | 35 |  |  |  |  | 74 | ($order_pair->{sell}{net_price} - $order_pair->{buy}{net_price}); | 
| 193 |  |  |  |  |  |  |  | 
| 194 |  |  |  |  |  |  | UPDATE_INVENTORY_BALANCES: | 
| 195 | 35 |  |  |  |  | 90 | for my $i (0..$#sizes) { | 
| 196 | 93 |  |  |  |  | 136 | my $size  = $sizes[$i]{size}; | 
| 197 | 93 |  |  |  |  | 130 | my $which = $sizes[$i]{which}; | 
| 198 | 93 |  |  |  |  | 167 | my $used_up = $size - $order_size <= 1e-8; | 
| 199 | 93 | 100 |  |  |  | 238 | if ($which eq 'buy order') { | 
|  |  | 100 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
| 200 | 35 | 100 |  |  |  | 67 | if ($used_up) { | 
| 201 | 11 |  |  |  |  | 33 | splice @$all_buy_orders, $sell_index, 1; | 
| 202 |  |  |  |  |  |  | } else { | 
| 203 | 24 |  |  |  |  | 55 | $all_buy_orders->[$sell_index]{base_size} -= $order_size; | 
| 204 |  |  |  |  |  |  | } | 
| 205 |  |  |  |  |  |  | } elsif ($which eq 'sell order') { | 
| 206 | 35 | 100 |  |  |  | 61 | if ($used_up) { | 
| 207 | 13 |  |  |  |  | 44 | splice @$all_sell_orders, $buy_index, 1; | 
| 208 |  |  |  |  |  |  | } else { | 
| 209 | 22 |  |  |  |  | 47 | $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 |  |  |  |  | 7 | shift @{ $account_balances->{ $sell->{exchange} }{$base_currency} }; | 
|  | 5 |  |  |  |  | 18 |  | 
| 214 |  |  |  |  |  |  | } else { | 
| 215 |  |  |  |  |  |  | $account_balances->{ $sell->{exchange} }{$base_currency}[0]{available} -= | 
| 216 | 3 |  |  |  |  | 10 | $order_size; | 
| 217 |  |  |  |  |  |  | } | 
| 218 |  |  |  |  |  |  | } elsif ($which eq 'buy exchange balance') { | 
| 219 | 8 |  |  |  |  | 15 | my $c = $buy->{quote_currency}; | 
| 220 | 8 | 100 |  |  |  | 17 | 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 |  |  |  |  | 23 | $order_size * $buy->{gross_price_orig}; | 
| 225 |  |  |  |  |  |  | } | 
| 226 |  |  |  |  |  |  | } | 
| 227 |  |  |  |  |  |  | } # UPDATE_INVENTORY_BALANCES | 
| 228 |  |  |  |  |  |  |  | 
| 229 | 35 | 100 |  |  |  | 86 | if ($account_balances) { | 
| 230 | 8 |  |  |  |  | 28 | App::cryp::arbit::_sort_account_balances($account_balances); | 
| 231 |  |  |  |  |  |  | } | 
| 232 |  |  |  |  |  |  |  | 
| 233 |  |  |  |  |  |  | CHECK_MINIMUM_BUY_SIZE: | 
| 234 |  |  |  |  |  |  | { | 
| 235 | 35 | 100 |  |  |  | 50 | last unless $exchange_pairs; | 
|  | 35 |  |  |  |  | 87 |  | 
| 236 | 9 |  |  |  |  | 17 | my $pair_recs = $exchange_pairs->{ $buy->{exchange} }; | 
| 237 | 9 | 100 |  |  |  | 21 | last unless $pair_recs; | 
| 238 | 4 |  |  |  |  | 6 | my $pair_rec; | 
| 239 | 4 |  |  |  |  | 9 | for (@$pair_recs) { | 
| 240 | 4 | 50 |  |  |  | 12 | if ($_->{base_currency} eq $base_currency) { | 
| 241 | 4 |  |  |  |  | 6 | $pair_rec = $_; last; | 
|  | 4 |  |  |  |  | 6 |  | 
| 242 |  |  |  |  |  |  | } | 
| 243 |  |  |  |  |  |  | } | 
| 244 | 4 | 50 |  |  |  | 9 | last unless $pair_rec; | 
| 245 | 4 | 100 | 100 |  |  | 17 | 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 |  |  |  |  | 6 | $order_pair->{base_size}, $pair_rec->{min_base_size}, $order_pair; | 
| 248 | 1 |  |  |  |  | 7 | next CREATE; | 
| 249 |  |  |  |  |  |  | } | 
| 250 | 3 |  |  |  |  | 7 | my $quote_size = $order_pair->{base_size}*$buy->{gross_price_orig}; | 
| 251 | 3 | 100 | 100 |  |  | 15 | 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 |  |  |  |  | 8 | $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 |  |  |  | 47 | last unless $exchange_pairs; | 
|  | 33 |  |  |  |  | 64 |  | 
| 261 | 7 |  |  |  |  | 13 | my $pair_recs = $exchange_pairs->{ $sell->{exchange} }; | 
| 262 | 7 | 100 |  |  |  | 15 | last unless $pair_recs; | 
| 263 | 5 |  |  |  |  | 7 | my $pair_rec; | 
| 264 | 5 |  |  |  |  | 9 | for (@$pair_recs) { | 
| 265 | 5 | 50 |  |  |  | 14 | if ($_->{base_currency} eq $base_currency) { | 
| 266 | 5 |  |  |  |  | 7 | $pair_rec = $_; last; | 
|  | 5 |  |  |  |  | 7 |  | 
| 267 |  |  |  |  |  |  | } | 
| 268 |  |  |  |  |  |  | } | 
| 269 | 5 | 50 |  |  |  | 13 | last unless $pair_rec; | 
| 270 | 5 | 100 | 100 |  |  | 19 | 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 |  |  |  |  | 8 | my $quote_size = $order_pair->{base_size}*$sell->{gross_price_orig}; | 
| 276 | 4 | 100 | 100 |  |  | 20 | 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 |  |  |  |  | 9 | $quote_size, $pair_rec->{min_quote_size}, $order_pair; | 
| 279 | 2 |  |  |  |  | 12 | next CREATE; | 
| 280 |  |  |  |  |  |  | } | 
| 281 |  |  |  |  |  |  | } # CHECK_MINIMUM_SELL_SIZE | 
| 282 |  |  |  |  |  |  |  | 
| 283 | 30 |  |  |  |  | 96 | push @order_pairs, $order_pair; | 
| 284 |  |  |  |  |  |  |  | 
| 285 |  |  |  |  |  |  | } # CREATE | 
| 286 |  |  |  |  |  |  |  | 
| 287 |  |  |  |  |  |  | ADJUST_FOREX_SPREAD: | 
| 288 |  |  |  |  |  |  | { | 
| 289 | 17 |  |  |  |  | 31 | my @tmp = @order_pairs; | 
|  | 17 |  |  |  |  | 47 |  | 
| 290 | 17 |  |  |  |  | 38 | @order_pairs = (); | 
| 291 | 17 |  |  |  |  | 26 | my $i = 0; | 
| 292 |  |  |  |  |  |  | ORDER_PAIR: | 
| 293 | 17 |  |  |  |  | 39 | for my $op (@tmp) { | 
| 294 | 30 |  |  |  |  | 43 | $i++; | 
| 295 | 30 |  |  |  |  | 165 | my ($bcur) = $op->{buy}{pair}  =~ m!/(.+)!; | 
| 296 | 30 |  |  |  |  | 106 | my ($scur) = $op->{sell}{pair} =~ m!/(.+)!; | 
| 297 |  |  |  |  |  |  |  | 
| 298 | 30 | 50 |  |  |  | 91 | 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 |  |  |  |  | 45 | my $spread; | 
| 306 | 30 | 50 |  |  |  | 269 | $spread = $forex_spreads->{"$bcur/$scur"} if $forex_spreads; | 
| 307 |  |  |  |  |  |  |  | 
| 308 | 30 | 50 |  |  |  | 165 | 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 |  |  |  |  | 150 | $i, $op->{buy}{pair}, $op->{sell}{pair}, $op->{trading_profit_margin}, $bcur, $scur, $spread; | 
| 317 | 30 |  |  |  |  | 112 | $op->{forex_spread} = $spread; | 
| 318 | 30 |  |  |  |  | 73 | $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 |  |  |  | 71 | 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 |  |  |  |  | 5 | $i, $op->{net_profit_margin}, $min_net_profit_margin; | 
| 323 | 1 |  |  |  |  | 5 | next ORDER_PAIR; | 
| 324 |  |  |  |  |  |  | } | 
| 325 |  |  |  |  |  |  |  | 
| 326 |  |  |  |  |  |  | ADD: | 
| 327 | 29 |  |  |  |  | 64 | push @order_pairs, $op; | 
| 328 |  |  |  |  |  |  | } | 
| 329 |  |  |  |  |  |  | } # ADJUST_FOREX_SPREAD | 
| 330 |  |  |  |  |  |  |  | 
| 331 |  |  |  |  |  |  | # re-sort | 
| 332 | 17 |  |  |  |  | 43 | @order_pairs = sort { $b->{net_profit_margin} <=> $a->{net_profit_margin} } @order_pairs; | 
|  | 17 |  |  |  |  | 36 |  | 
| 333 |  |  |  |  |  |  |  | 
| 334 | 17 |  |  |  |  | 83 | (\@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.010 of App::cryp::arbit::Strategy::merge_order_book (from Perl distribution App-cryp-arbit), released on 2021-05-26. | 
| 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 HOMEPAGE | 
| 845 |  |  |  |  |  |  |  | 
| 846 |  |  |  |  |  |  | Please visit the project's homepage at L<https://metacpan.org/release/App-cryp-arbit>. | 
| 847 |  |  |  |  |  |  |  | 
| 848 |  |  |  |  |  |  | =head1 SOURCE | 
| 849 |  |  |  |  |  |  |  | 
| 850 |  |  |  |  |  |  | Source repository is at L<https://github.com/perlancar/perl-App-cryp-arbit>. | 
| 851 |  |  |  |  |  |  |  | 
| 852 |  |  |  |  |  |  | =head1 BUGS | 
| 853 |  |  |  |  |  |  |  | 
| 854 |  |  |  |  |  |  | Please report any bugs or feature requests on the bugtracker website L<https://github.com/perlancar/perl-App-cryp-arbit/issues> | 
| 855 |  |  |  |  |  |  |  | 
| 856 |  |  |  |  |  |  | When submitting a bug or request, please include a test-file or a | 
| 857 |  |  |  |  |  |  | patch to an existing test-file that illustrates the bug or desired | 
| 858 |  |  |  |  |  |  | feature. | 
| 859 |  |  |  |  |  |  |  | 
| 860 |  |  |  |  |  |  | =head1 SEE ALSO | 
| 861 |  |  |  |  |  |  |  | 
| 862 |  |  |  |  |  |  | L<App::cryp::arbit> | 
| 863 |  |  |  |  |  |  |  | 
| 864 |  |  |  |  |  |  | Other C<App::cryp::arbit::Strategy::*> modules. | 
| 865 |  |  |  |  |  |  |  | 
| 866 |  |  |  |  |  |  | =head1 AUTHOR | 
| 867 |  |  |  |  |  |  |  | 
| 868 |  |  |  |  |  |  | perlancar <perlancar@cpan.org> | 
| 869 |  |  |  |  |  |  |  | 
| 870 |  |  |  |  |  |  | =head1 COPYRIGHT AND LICENSE | 
| 871 |  |  |  |  |  |  |  | 
| 872 |  |  |  |  |  |  | This software is copyright (c) 2021, 2018 by perlancar@cpan.org. | 
| 873 |  |  |  |  |  |  |  | 
| 874 |  |  |  |  |  |  | This is free software; you can redistribute it and/or modify it under | 
| 875 |  |  |  |  |  |  | the same terms as the Perl 5 programming language system itself. | 
| 876 |  |  |  |  |  |  |  | 
| 877 |  |  |  |  |  |  | =cut |