File Coverage

blib/lib/App/cryp/arbit.pm
Criterion Covered Total %
statement 31 629 4.9
branch 1 230 0.4
condition 0 60 0.0
subroutine 9 33 27.2
pod 7 7 100.0
total 48 959 5.0


line stmt bran cond sub pod time code
1             package App::cryp::arbit;
2              
3             our $DATE = '2021-05-26'; # DATE
4             our $VERSION = '0.010'; # VERSION
5              
6 1     1   20 use 5.010001;
  1         3  
7 1     1   6 use strict;
  1         1  
  1         33  
8 1     1   6 use warnings;
  1         1  
  1         41  
9 1     1   579 use Devel::Confess;
  1         9945  
  1         6  
10 1     1   76 use Log::ger;
  1         2  
  1         9  
11              
12 1     1   253 use Time::HiRes qw(time);
  1         4  
  1         10  
13              
14             our %SPEC;
15              
16             $SPEC{':package'} = {
17             summary => 'Cryptocurrency arbitrage utility',
18             v => 1.1,
19             };
20              
21             our %args_db = (
22             db_name => {
23             schema => 'str*',
24             req => 1,
25             tags => ['category:database-connection'],
26             },
27             # XXX db_host
28             # XXX db_port
29             db_username => {
30             schema => 'str*',
31             tags => ['category:database-connection'],
32             },
33             db_password => {
34             schema => 'str*',
35             tags => ['category:database-connection'],
36             },
37             );
38              
39             # shared between these subcommands: opportunities, arbit, collect-orderbooks
40             our %args_accounts_and_currencies = (
41             accounts => {
42             summary => 'Cryptoexchange accounts',
43             schema => ['array*', of=>'cryptoexchange::account', min_len=>2],
44             description => <<'_',
45              
46             There should at least be two accounts, on at least two different
47             cryptoexchanges. If not specified, all accounts listed on the configuration file
48             will be included. Note that it's possible to include two or more accounts on the
49             same cryptoexchange.
50              
51             _
52             },
53             base_currencies => {
54             'x.name.is_plural' => 1,
55             'x.name.singular' => 'base_currency',
56             summary => 'Target (crypto)currencies to arbitrate',
57             schema => ['array*', of=>'cryptocurrency*', min_len=>1],
58             description => <<'_',
59              
60             If not specified, will list all supported pairs on all the exchanges and include
61             the base cryptocurrencies that are listed on at least 2 different exchanges (for
62             arbitrage possibility).
63              
64             _
65             },
66             quote_currencies => {
67             'x.name.is_plural' => 1,
68             'x.name.singular' => 'quote_currency',
69             summary => 'The currencies to exchange (buy/sell) the target currencies',
70             schema => ['array*', of=>'fiat_or_cryptocurrency*', min_len=>1],
71             description => <<'_',
72              
73             You can have fiat currencies as the quote currencies, to buy/sell the target
74             (base) currencies during arbitrage. For example, to arbitrage LTC against USD
75             and IDR, `base_currencies` is ['BTC'] and `quote_currencies` is ['USD', 'IDR'].
76              
77             You can also arbitrage cryptocurrencies against other cryptocurrency (usually
78             BTC, "the USD of cryptocurrencies"). For example, to arbitrage XMR and LTC
79             against BTC, `base_currencies` is ['XMR', 'LTC'] and `quote_currencies` is
80             ['BTC'].
81              
82             _
83             },
84             );
85              
86             # shared between these subcommands: opportunities, arbit
87             our %args_arbit_common = (
88             strategy => {
89             summary => 'Which strategy to use for arbitration',
90             schema => ['str*', match=>qr/\A\w+\z/],
91             default => 'merge_order_book',
92             description => <<'_',
93              
94             Strategy is implemented in a `App::cryp::arbit::Strategy::*` perl module.
95              
96             _
97             },
98             %args_accounts_and_currencies,
99             min_net_profit_margin => {
100             summary => 'Minimum net profit margin that will trigger an arbitrage '.
101             'trading, in percentage',
102             schema => 'float*',
103             default => 0,
104             description => <<'_',
105              
106             Below this percentage number, no order pairs will be sent to the exchanges to do
107             the arbitrage. Note that the net profit margin already takes into account
108             trading fees and forex spread (see Glossary section for more details and
109             illustration).
110              
111             Suggestion: If you set this option too high, there might not be any order pairs
112             possible. If you set this option too low, you will be getting too thin profits.
113             Run `cryp-arbit opportunities` or `cryp-arbit arbit --dry-run` for a while to
114             see what the average percentage is and then decide at which point you want to
115             perform arbitrage.
116              
117             _
118             },
119             max_order_quote_size => {
120             summary => 'What is the maximum amount of a single order',
121             schema => 'float*',
122             default => 100,
123             description => <<'_',
124              
125             A single order will be limited to not be above this value (in quote currency,
126             which if fiat will be converted to USD). This is the amount for the buying
127             (because an arbitrage transaction is comprised of a pair of orders, where one
128             order is a selling order at a higher quote currency size than the buying order).
129              
130             For example if you are arbitraging BTC against USD and IDR, and set this option
131             to 75, then orders will not be above 75 USD. If you are arbitraging LTC against
132             BTC and set this to 0.03 then orders will not be above 0.03 BTC.
133              
134             Suggestion: If you set this option too high, a few orders can use up your
135             inventory (and you might not be getting optimal profit percentage). Also, large
136             orders can take a while (or too long) to fill. If you set this option too low,
137             you will hit the exchanges' minimum order size and no orders can be created.
138             Since we want smaller risk of orders not getting filled quickly, we want small
139             order sizes. The optimum number range a little above the exchanges' minimum
140             order size.
141              
142             _
143             },
144             max_order_pairs_per_round => {
145             summary => 'Maximum number of order pairs to create per round',
146             schema => 'posint*',
147             },
148             min_account_balances => {
149             summary => 'What are the minimum account balances',
150             schema => ['hash*', {
151             each_key => 'cryptoexchange::account*',
152             each_value => ['hash*', {
153             each_key => 'fiat_or_cryptocurrency*',
154             each_value => 'float',
155             }],
156             }],
157             },
158             );
159              
160             our %arg_max_order_age = (
161             max_order_age => {
162             summary => 'How long should we wait for orders to be completed '.
163             'before cancelling them (in seconds)',
164             schema => 'posint*',
165             default => 86400,
166             description => <<'_',
167              
168             Sometimes because of rapid trading and price movement, our order might not be
169             filled immediately. This setting sets a limit on how long should an order be
170             left open. After this limit is reached, we cancel the order. The imbalance of
171             the arbitrage transaction will be recorded.
172              
173             _
174             },
175             );
176              
177             our %arg_usd_rates = (
178             usd_rates => {
179             summary => 'Set USD rates',
180             'x.name.is_plural' => 1,
181             'x.name.singular' => 'usd_rate',
182             schema => ['hash*', each_key=>["str*", match=>qr/\A[A-Z]{3}\z/], each_value=>'float*'],
183             description => <<'_',
184              
185             Example:
186              
187             --usd-rate IDR=14500 --usd-rate THB=33.25
188              
189             _
190             },
191             );
192              
193             our $db_schema_spec = {
194             component_name => 'cryp_arbit',
195             latest_v => 3,
196             provides => [qw/exchange account balance tx price order_pair/],
197             install => [
198             # XXX later move to cryp-folio?
199             'CREATE TABLE exchange (
200             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
201             safename VARCHAR(100) NOT NULL, UNIQUE(safename)
202             )',
203              
204             # XXX later move to cryp-folio?
205             'CREATE TABLE account (
206             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
207             exchange_id INT NOT NULL,
208             nickname VARCHAR(64) NOT NULL,
209             UNIQUE(exchange_id,nickname),
210             note VARCHAR(255)
211             )',
212              
213             # XXX later move to cryp-folio?
214             'CREATE TABLE latest_balance (
215             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
216             time DOUBLE NOT NULL,
217             account_id INT NOT NULL,
218             currency VARCHAR(10) NOT NULL,
219             UNIQUE(account_id, currency),
220             available DECIMAL(21,8) NOT NULL
221             )',
222              
223             # XXX later move to cryp-folio?
224             'CREATE TABLE balance_history (
225             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
226             time DOUBLE NOT NULL,
227             account_id INT NOT NULL,
228             currency VARCHAR(10) NOT NULL,
229             UNIQUE(time, account_id, currency),
230             available DECIMAL(21,8) NOT NULL
231             )',
232              
233             # XXX later move to cryp-folio
234             'CREATE TABLE price (
235             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
236             time DOUBLE NOT NULL, INDEX(time),
237             base_currency VARCHAR(10) NOT NULL,
238             quote_currency VARCHAR(10) NOT NULL,
239             type VARCHAR(4) NOT NULL, -- "buy" or "sell"
240             price DECIMAL(21,8) NOT NULL, -- price to buy (or sell) base_currency in quote_currency, e.g. if base_currency = BTC, quote_currency = USD, price = 11150 means 1 BTC is $11150
241             exchange_id INT NOT NULL,
242             note VARCHAR(255)
243             )',
244              
245             'CREATE TABLE arbit_opportunity (
246             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
247             time DOUBLE NOT NULL, INDEX(time),
248             base_currency VARCHAR(10) NOT NULL,
249             quote_currency VARCHAR(10) NOT NULL,
250             -- base_size DECIMAL(21,8),
251             buy_exchange_id INT NOT NULL,
252             buy_price DECIMAL(21,8) NOT NULL,
253             sell_exchange_id INT NOT NULL,
254             sell_price DECIMAL(21,8) NOT NULL,
255             gross_profit_margin DOUBLE NOT NULL,
256             trading_profit_margin DOUBLE NOT NULL,
257             net_profit_margin DOUBLE
258             )',
259              
260             # to collect historical orderbook data
261             'CREATE TABLE orderbook (
262             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
263             time DOUBLE NOT NULL, INDEX(time),
264             exchange_id INT NOT NULL,
265             base_currency VARCHAR(10) NOT NULL,
266             quote_currency VARCHAR(10) NOT NULL,
267             type TEXT NOT NULL -- "buy" or "sell"
268             )',
269              
270             'CREATE TABLE orderbook_item (
271             id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
272             orderbook_id INT NOT NULL, INDEX(orderbook_id),
273             amount DECIMAL(21,8) NOT NULL,
274             price DECIMAL(21,8) NOT NULL
275             ) ENGINE=MyISAM',
276              
277             'CREATE TABLE order_pair (
278             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
279             ctime DOUBLE NOT NULL, INDEX(ctime), -- create time in our database
280              
281             base_currency VARCHAR(10) NOT NULL, -- the currency we are arbitraging, e.g. LTC
282             base_size DECIMAL(21,8) NOT NULL, -- amount of "currency" that we are arbitraging (sell on "sell exchange" and buy on "buy exchange")
283              
284             expected_profit_margin DOUBLE NOT NULL, -- expected profit percentage (after trading fees & forex spread)
285             expected_net_profit DOUBLE NOT NULL, -- expected net profit (after trading fees & forex spread) in quote currency (converted to USD if fiat) if fully executed
286              
287             -- we buy "base_size" of "base_currency" on "buy exchange" at
288             -- "buy_gross_price_orig" (in "buy_quote_currency") a.k.a
289             -- "buy_gross_price" (in "buy_quote_currency" converted to USD if
290             -- fiat)
291              
292             -- possible statuses/lifecyle: creating (submitting to exchange),
293             -- open (created and open), cancelling, cancelled, done
294              
295             buy_exchange_id INT NOT NULL,
296             buy_account_id INT NOT NULL,
297             buy_quote_currency VARCHAR(10) NOT NULL,
298             buy_gross_price_orig DECIMAL(21,8) NOT NULL,
299             buy_gross_price DECIMAL(21,8) NOT NULL,
300             buy_status VARCHAR(16) NOT NULL,
301              
302             buy_ctime DOUBLE, -- order create time in "buy_exchange"
303             buy_order_id VARCHAR(80),
304             buy_actual_price DECIMAL(21,8), -- actual price after we create on exchange
305             buy_actual_base_size DECIMAL(21,8), -- actual size after we create on exchange
306             buy_filled_base_size DECIMAL(21,8),
307              
308             -- then sell the same "base_size" of "base_currency"" on "sell
309             -- exchange" (the "base_currency"/"sell_exchange_quote_currency"
310             -- market pair) at "sell_gross_price_orig" (in
311             -- "sell_exchange_quote_currency") a.k.a "sell_gross_price" (in
312             -- "sell_exchange_quote_currency" converted to USD if fiat)
313              
314             sell_exchange_id INT NOT NULL,
315             sell_account_id INT NOT NULL,
316             sell_quote_currency VARCHAR(10) NOT NULL,
317             sell_gross_price_orig DECIMAL(21,8) NOT NULL,
318             sell_gross_price DECIMAL(21,8) NOT NULL,
319             sell_status VARCHAR(16) NOT NULL,
320              
321             sell_ctime DOUBLE, -- create time in "sell exchange"
322             sell_order_id VARCHAR(80),
323             sell_actual_price DECIMAL(21,8), -- actual price after we create on exchange
324             sell_actual_base_size DECIMAL(21,8), -- actual size after we create on exchange
325             sell_filled_base_size DECIMAL(21,8)
326             )',
327              
328             'CREATE TABLE arbit_order_log (
329             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
330             order_pair_id INT NOT NULL,
331             type VARCHAR(4) NOT NULL, -- "buy" or "sell"
332             summary TEXT NOT NULL
333             )',
334             ],
335             upgrade_to_v3 => [
336             'ALTER TABLE orderbook_item ENGINE=MyISAM, CHANGE COLUMN id id BIGINT NOT NULL AUTO_INCREMENT',
337             ],
338             upgrade_to_v2 => [
339             # to collect historical orderbook data
340             'CREATE TABLE orderbook (
341             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
342             time DOUBLE NOT NULL, INDEX(time),
343             exchange_id INT NOT NULL,
344             base_currency VARCHAR(10) NOT NULL,
345             quote_currency VARCHAR(10) NOT NULL,
346             type TEXT NOT NULL -- "sell" or "buy"
347             )',
348              
349             'CREATE TABLE orderbook_item (
350             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
351             orderbook_id INT NOT NULL, INDEX(orderbook_id),
352             amount DECIMAL(21,8) NOT NULL,
353             price DECIMAL(21,8) NOT NULL
354             )',
355             ],
356             install_v1 => [
357             # XXX later move to cryp-folio?
358             'CREATE TABLE exchange (
359             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
360             safename VARCHAR(100) NOT NULL, UNIQUE(safename)
361             )',
362              
363             # XXX later move to cryp-folio?
364             'CREATE TABLE account (
365             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
366             exchange_id INT NOT NULL,
367             nickname VARCHAR(64) NOT NULL,
368             UNIQUE(exchange_id,nickname),
369             note VARCHAR(255)
370             )',
371              
372             # XXX later move to cryp-folio?
373             'CREATE TABLE latest_balance (
374             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
375             time DOUBLE NOT NULL,
376             account_id INT NOT NULL,
377             currency VARCHAR(10) NOT NULL,
378             UNIQUE(account_id, currency),
379             available DECIMAL(21,8) NOT NULL
380             )',
381              
382             # XXX later move to cryp-folio?
383             'CREATE TABLE balance_history (
384             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
385             time DOUBLE NOT NULL,
386             account_id INT NOT NULL,
387             currency VARCHAR(10) NOT NULL,
388             UNIQUE(time, account_id, currency),
389             available DECIMAL(21,8) NOT NULL
390             )',
391              
392             # XXX later move to cryp-folio
393             'CREATE TABLE price (
394             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
395             time DOUBLE NOT NULL, INDEX(time),
396             base_currency VARCHAR(10) NOT NULL,
397             quote_currency VARCHAR(10) NOT NULL,
398             type VARCHAR(4) NOT NULL, -- "buy" or "sell"
399             price DECIMAL(21,8) NOT NULL, -- price to buy (or sell) base_currency in quote_currency, e.g. if base_currency = BTC, quote_currency = USD, price = 11150 means 1 BTC is $11150
400             exchange_id INT NOT NULL,
401             note VARCHAR(255)
402             )',
403              
404             'CREATE TABLE arbit_opportunity (
405             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
406             time DOUBLE NOT NULL, INDEX(time),
407             base_currency VARCHAR(10) NOT NULL,
408             quote_currency VARCHAR(10) NOT NULL,
409             -- base_size DECIMAL(21,8),
410             buy_exchange_id INT NOT NULL,
411             buy_price DECIMAL(21,8) NOT NULL,
412             sell_exchange_id INT NOT NULL,
413             sell_price DECIMAL(21,8) NOT NULL,
414             gross_profit_margin DOUBLE NOT NULL,
415             trading_profit_margin DOUBLE NOT NULL,
416             net_profit_margin DOUBLE
417             )',
418              
419             'CREATE TABLE order_pair (
420             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
421             ctime DOUBLE NOT NULL, INDEX(ctime), -- create time in our database
422              
423             base_currency VARCHAR(10) NOT NULL, -- the currency we are arbitraging, e.g. LTC
424             base_size DECIMAL(21,8) NOT NULL, -- amount of "currency" that we are arbitraging (sell on "sell exchange" and buy on "buy exchange")
425              
426             expected_profit_margin DOUBLE NOT NULL, -- expected profit percentage (after trading fees & forex spread)
427             expected_net_profit DOUBLE NOT NULL, -- expected net profit (after trading fees & forex spread) in quote currency (converted to USD if fiat) if fully executed
428              
429             -- we buy "base_size" of "base_currency" on "buy exchange" at
430             -- "buy_gross_price_orig" (in "buy_quote_currency") a.k.a
431             -- "buy_gross_price" (in "buy_quote_currency" converted to USD if
432             -- fiat)
433              
434             -- possible statuses/lifecyle: creating (submitting to exchange),
435             -- open (created and open), cancelling, cancelled, done
436              
437             buy_exchange_id INT NOT NULL,
438             buy_account_id INT NOT NULL,
439             buy_quote_currency VARCHAR(10) NOT NULL,
440             buy_gross_price_orig DECIMAL(21,8) NOT NULL,
441             buy_gross_price DECIMAL(21,8) NOT NULL,
442             buy_status VARCHAR(16) NOT NULL,
443              
444             buy_ctime DOUBLE, -- order create time in "buy_exchange"
445             buy_order_id VARCHAR(80),
446             buy_actual_price DECIMAL(21,8), -- actual price after we create on exchange
447             buy_actual_base_size DECIMAL(21,8), -- actual size after we create on exchange
448             buy_filled_base_size DECIMAL(21,8),
449              
450             -- then sell the same "base_size" of "base_currency"" on "sell
451             -- exchange" (the "base_currency"/"sell_exchange_quote_currency"
452             -- market pair) at "sell_gross_price_orig" (in
453             -- "sell_exchange_quote_currency") a.k.a "sell_gross_price" (in
454             -- "sell_exchange_quote_currency" converted to USD if fiat)
455              
456             sell_exchange_id INT NOT NULL,
457             sell_account_id INT NOT NULL,
458             sell_quote_currency VARCHAR(10) NOT NULL,
459             sell_gross_price_orig DECIMAL(21,8) NOT NULL,
460             sell_gross_price DECIMAL(21,8) NOT NULL,
461             sell_status VARCHAR(16) NOT NULL,
462              
463             sell_ctime DOUBLE, -- create time in "sell exchange"
464             sell_order_id VARCHAR(80),
465             sell_actual_price DECIMAL(21,8), -- actual price after we create on exchange
466             sell_actual_base_size DECIMAL(21,8), -- actual size after we create on exchange
467             sell_filled_base_size DECIMAL(21,8)
468             )',
469              
470             'CREATE TABLE arbit_order_log (
471             id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
472             order_pair_id INT NOT NULL,
473             type VARCHAR(4) NOT NULL, -- "buy" or "sell"
474             summary TEXT NOT NULL
475             )',
476             ],
477             };
478              
479             my $fnum2 = [number => {precision=>2}];
480             my $fnum4 = [number => {precision=>4}];
481             my $fnum8 = [number => {precision=>8}];
482              
483             sub _exchange_catalog {
484 0     0   0 state $xcat = do {
485 0         0 require CryptoExchange::Catalog;
486 0         0 CryptoExchange::Catalog->new;
487             };
488 0         0 $xcat;
489             }
490              
491             # to only show currency rate in log when they are different from last log
492             my %rate_mem;
493             sub _convert_to_usd {
494 0     0   0 require Finance::Currency::FiatX;
495              
496 0         0 my ($r, $amount, $cur) = @_;
497              
498 0         0 my $dbh = $r->{_stash}{dbh};
499              
500 0         0 my $fxres;
501 0 0 0     0 if ($r->{args}{usd_rates} && $r->{args}{usd_rates}{$cur}) {
502 0         0 $fxres = [200, "OK (user-set)", {rate=>1 / $r->{args}{usd_rates}{$cur}}];
503             } else {
504 0         0 $fxres = Finance::Currency::FiatX::get_spot_rate(
505             dbh => $dbh, from => $cur, to => 'USD', type => 'sell');
506 0 0 0     0 die "Couldn't get conversion rate from $cur to USD: $fxres->[0] - $fxres->[1]"
507             unless $fxres->[0] == 200 || $fxres->[0] == 304;
508             }
509 0 0 0     0 if (!$rate_mem{$cur} || $rate_mem{$cur} != $fxres->[2]{rate}) {
510 0         0 log_info "Using currency conversion rate for $cur/USD: %s", $fxres;
511 0         0 $rate_mem{$cur} = $fxres->[2]{rate};
512             }
513              
514 0         0 $r->{_stash}{fx}{$cur} = $fxres;
515              
516 0         0 $amount * $fxres->[2]{rate};
517             }
518              
519             # XXX move to App::cryp::Util or folio? given a safename, get or assign exchange
520             # numeric ID from the database
521             sub _get_exchange_id {
522 0     0   0 my ($r, $exchange) = @_;
523              
524             return $r->{_stash}{exchange_ids}{$exchange} if
525 0 0       0 $r->{_stash}{exchange_ids}{$exchange};
526              
527 0         0 my $xcat = _exchange_catalog();
528 0         0 my $rec = $xcat->by_safename($exchange);
529 0 0       0 $rec or die "BUG: Unknown exchange '$exchange'";
530              
531 0         0 my $dbh = $r->{_stash}{dbh};
532              
533 0         0 my ($eid) = $dbh->selectrow_array("SELECT id FROM exchange WHERE safename=?", {}, $exchange);
534 0 0       0 unless ($eid) {
535 0         0 $dbh->do("INSERT INTO exchange (safename) VALUES (?)", {},
536             $exchange);
537 0         0 $eid = $dbh->last_insert_id("","","","");
538             }
539              
540 0         0 $r->{_stash}{exchange_ids}{$exchange} = $eid;
541 0         0 $eid;
542             }
543              
544             sub _get_account_id {
545 0     0   0 my ($r, $exchange, $account) = @_;
546              
547             return $r->{_stash}{account_ids}{$exchange}{$account} if
548 0 0       0 $r->{_stash}{account_ids}{$exchange}{$account};
549              
550 0         0 my $dbh = $r->{_stash}{dbh};
551              
552 0         0 my $eid = _get_exchange_id($r, $exchange);
553              
554 0         0 my ($aid) = $dbh->selectrow_array("SELECT id FROM account WHERE exchange_id=? AND nickname=?", {}, $eid, $account);
555 0 0       0 unless ($aid) {
556 0         0 $dbh->do("INSERT INTO account (exchange_id,nickname) VALUES (?,?)", {}, $eid, $account);
557 0         0 $aid = $dbh->last_insert_id("","","","");
558             }
559              
560 0         0 $r->{_stash}{account_ids}{$exchange}{$account} = $aid;
561 0         0 $aid;
562             }
563              
564             sub _sort_account_balances {
565 9     9   13 my $account_balances = shift;
566              
567 9         25 for my $e (keys %$account_balances) {
568 18         28 my $balances = $account_balances->{$e};
569 18         29 for my $cur (keys %$balances) {
570             $balances->{$cur} = [
571 16         55 grep { $_->{available} >= 1e-8 }
572 2         8 sort { $b->{available} <=> $a->{available} }
573 18         21 @{ $balances->{$cur} }
  18         42  
574             ];
575             }
576             }
577             }
578              
579             sub _get_exchange_client {
580 0     0   0 my ($r, $exchange, $account) = @_;
581              
582             # if account is unspecified (caller doesn't care which account, e.g. he just
583             # wants to get som exchange-related information), then we pick an account
584             # from the configuration
585 0 0       0 unless (defined $account) {
586 0         0 my $h = $r->{_cryp}{exchanges}{$exchange};
587 0 0       0 die "No configuration found for exchange $exchange. ".
588             "Please specify [exchange/$exchange] section in configuration"
589             unless keys %$h;
590 0         0 $account = (keys %$h)[0];
591             }
592              
593             return $r->{_stash}{exchange_clients}{$exchange}{$account} if
594 0 0       0 $r->{_stash}{exchange_clients}{$exchange}{$account};
595              
596 0         0 my $mod = "App::cryp::Exchange::$exchange";
597 0         0 $mod =~ s/-/_/g;
598 0         0 (my $modpm = "$mod.pm") =~ s!::!/!g;
599 0         0 require $modpm;
600              
601 0 0       0 unless ($r->{_cryp}{exchanges}{$exchange}{$account}) {
602 0         0 die "No configuration found for exchange $exchange (account $account). ".
603             "Please specify [exchange/$exchange/$account] section in configuration";
604             }
605              
606 0   0     0 my %client_args = %{ $r->{_cryp}{exchanges}{$exchange}{$account} // {} };
  0         0  
607 0 0       0 unless ($client_args{api_key}) {
608 0         0 $client_args{public_only} = 1;
609             }
610              
611 0         0 my $client = $mod->new(%client_args);
612              
613 0         0 $r->{_stash}{exchange_clients}{$exchange}{$account} = $client;
614             }
615              
616             sub _get_account_balances {
617 0     0   0 my ($r, $no_cache) = @_;
618              
619 0         0 my $dbh = $r->{_stash}{dbh};
620              
621 0         0 $r->{_stash}{account_balances} = {};
622 0         0 for my $e (sort keys %{ $r->{_stash}{account_exchanges} }) {
  0         0  
623 0         0 my $accounts = $r->{_stash}{account_exchanges}{$e};
624             ACC:
625 0         0 for my $acc (sort keys %$accounts) {
626 0         0 my $client = _get_exchange_client($r, $e, $acc);
627 0         0 my $time = time();
628 0         0 my $res = $client->list_balances;
629 0 0       0 unless ($res->[0] == 200) {
630 0         0 log_error "Couldn't list balances for account %s/%s: %s, skipping account",
631             $e, $acc, $res;
632 0         0 next ACC;
633             }
634 0         0 for my $rec (@{ $res->[2] }) {
  0         0  
635 0         0 $rec->{account} = $acc;
636 0         0 $rec->{account_id} = _get_account_id($r, $e, $acc);
637 0         0 push @{ $r->{_stash}{account_balances}{$e}{$rec->{currency}} }, $rec;
  0         0  
638             $dbh->do(
639             "REPLACE INTO latest_balance (time, account_id, currency, available) VALUES (?,?,?,?)",
640             {},
641             $time, $rec->{account_id}, $rec->{currency}, $rec->{available},
642 0         0 );
643             $dbh->do(
644             "INSERT INTO balance_history (time, account_id, currency, available) VALUES (?,?,?,?)",
645             {},
646             $time, $rec->{account_id}, $rec->{currency}, $rec->{available},
647 0         0 );
648             } # for rec
649             } # for account
650             } # for exchange
651              
652             # sort by largest available balance first
653 0         0 _sort_account_balances($r->{_stash}{account_balances});
654              
655             #log_trace "account_balances: %s", $r->{_stash}{account_balances};
656 0         0 $r->{_stash}{account_balances};
657             }
658              
659             sub _get_exchange_pairs {
660 0     0   0 my ($r, $exchange) = @_;
661              
662             return $r->{_stash}{exchange_pairs}{$exchange} if
663 0 0       0 $r->{_stash}{exchange_pairs}{$exchange};
664              
665 0         0 my $client = _get_exchange_client($r, $exchange);
666              
667 0         0 my $res = $client->list_pairs(detail=>1);
668 0 0       0 if ($res->[0] == 200) {
669 0         0 $r->{_stash}{exchange_pairs}{$exchange} = $res->[2];
670             } else {
671 0         0 log_error "Couldn't list pairs on %s: %s, ".
672             "skipping this exchange", $exchange, $res;
673 0         0 $r->{_stash}{exchange_pairs}{$exchange} = [];
674             }
675              
676 0         0 $r->{_stash}{exchange_pairs}{$exchange};
677             }
678              
679             sub _get_trading_fee {
680 0     0   0 my ($r, $exchange, $currency) = @_;
681              
682 0         0 my $fees = $r->{_stash}{trading_fees};
683 0   0     0 my $fees_exchange = $fees->{$exchange} // $fees->{':default'};
684 0   0     0 my $fee = $fees_exchange->{$currency} // $fees_exchange->{':default'};
685             }
686              
687             sub _is_fiat {
688 17     17   2003 require Locale::Codes::Currency_Codes;
689 1     1   2009 no warnings 'once';
  1         3  
  1         5795  
690 17         5738 my $code = shift;
691 17 50       176 $Locale::Codes::Data{'currency'}{'code2id'}{alpha}{uc $code} ? 1:0;
692             }
693              
694             # should be used by all subcommands
695             sub _init {
696 0     0     my ($r, $opts) = @_;
697              
698 0   0       $opts //= {};
699              
700 0           my %account_exchanges; # key = exchange safename, value = {account1=>1, account2=>1, ...)
701              
702 0           my $xcat = _exchange_catalog();
703              
704             CHECK_ARGUMENTS:
705             {
706 0           CHECK_ACCOUNTS:
707             {
708 0 0         last CHECK_ACCOUNTS unless exists $r->{args}{accounts};
  0            
709              
710             # accounts: there must be at least two accounts on two different
711             # exchanges
712             return [400, "Please specify at least two accounts"]
713 0 0 0       unless $r->{args}{accounts} && @{ $r->{args}{accounts} } >= 2;
  0            
714 0           for (@{ $r->{args}{accounts} }) {
  0            
715 0 0         m!(.+)/(.+)! or return [400, "Invalid account '$_', please use EXCHANGE/ACCOUNT syntax"];
716 0           my ($xchg, $acc) = ($1, $2);
717 0 0         unless (exists $account_exchanges{$xchg}) {
718 0 0         return [400, "Unknown exchange '$xchg'"]
719             unless $xcat->by_safename($xchg);
720             }
721 0           $account_exchanges{$xchg}{$acc} = 1;
722             }
723 0 0         return [400, "Please specify accounts on at least two ".
724             "cryptoexchanges, you only specify account(s) on " .
725             join(", ", keys %account_exchanges)]
726             unless keys(%account_exchanges) >= 2;
727 0           $r->{_stash}{account_exchanges} = \%account_exchanges;
728             }
729             }
730              
731 0           my $dbh;
732             CONNECT:
733             {
734 0 0         last if $opts->{skip_connect};
  0            
735              
736 0           require DBIx::Connect::MySQL;
737 0           log_trace "Connecting to database ...";
738             $r->{_stash}{dbh} = DBIx::Connect::MySQL->connect(
739             "dbi:mysql:database=$r->{args}{db_name}",
740             $r->{args}{db_username},
741             $r->{args}{db_password},
742 0           {RaiseError => 1},
743             );
744 0           $dbh = $r->{_stash}{dbh};
745             }
746              
747             SETUP_SCHEMA:
748             {
749 0 0         last if $opts->{skip_connect};
  0            
750              
751 0           require SQL::Schema::Versioned;
752             my $res = SQL::Schema::Versioned::create_or_update_db_schema(
753 0           dbh => $r->{_stash}{dbh}, spec => $db_schema_spec,
754             );
755 0 0         die "Cannot run the application: cannot create/upgrade database schema: $res->[1]"
756             unless $res->[0] == 200;
757             }
758              
759 0           [200];
760             }
761              
762             sub _init_arbit {
763 0     0     my $r = shift;
764              
765             DETERMINE_QUOTE_CURRENCIES:
766             {
767 0           my @quotecurs;
  0            
768             my %fiatquotecurs; # key=fiat, value=1
769 0   0       my @quotecurs_arg = @{ $r->{args}{quote_currencies} // [] };
  0            
770 0           my %quotecur_exchanges; # key=(cryptocurrency code or ':fiat'), value={exchange1=>1, ...}
771              
772             # list pairs on all exchanges
773 0           for my $e (sort keys %{ $r->{_stash}{account_exchanges} }) {
  0            
774 0           my $pair_recs = _get_exchange_pairs($r, $e);
775 0           for my $pair_rec (@$pair_recs) {
776 0           my $pair = $pair_rec->{name};
777 0           my ($basecur, $quotecur) = split m!/!, $pair;
778             # consider all fiat currencies as a single ":fiat" because we
779             # assume fiat currencies can be converted from one to the aother
780             # at a stable rate.
781 0           my $key;
782 0 0         if (_is_fiat($quotecur)) {
783 0           $key = ':fiat';
784 0           $fiatquotecurs{$quotecur} = 1;
785             } else {
786 0           $key = $quotecur;
787             }
788 0           $quotecur_exchanges{$key}{$e} = 1;
789             }
790             }
791              
792             # only consider quote currencies that are traded in >1 exchanges, for
793             # arbitrage possibility.
794 0           my @possible_quotecurs = grep { keys(%{$quotecur_exchanges{$_}}) > 1 }
  0            
  0            
795             sort keys %quotecur_exchanges;
796             # convert back fiat currencies back to their original
797 0 0         if (grep {':fiat'} @possible_quotecurs) {
  0            
798 0           @possible_quotecurs = grep {$_ ne ':fiat'} @possible_quotecurs;
  0            
799 0           push @possible_quotecurs, sort keys %fiatquotecurs;
800             }
801              
802 0 0         if (@quotecurs_arg) {
803 0           my @impossible_quotecurs;
804 0           for my $c (@quotecurs_arg) {
805 0 0         if (grep { $c eq $_ } @possible_quotecurs) {
  0            
806 0           push @quotecurs, $c;
807             } else {
808 0           push @impossible_quotecurs, $c;
809             }
810             }
811 0 0         if (@impossible_quotecurs) {
812 0           log_warn "The following quote currencies are not traded on at least two exchanges: %s, excluding these quote currencies",
813             \@impossible_quotecurs;
814             }
815             } else {
816 0           log_warn "Will be arbitraging using these quote currencies: %s",
817             \@possible_quotecurs;
818 0           @quotecurs = @possible_quotecurs;
819             }
820              
821 0           $r->{_stash}{quote_currencies} = \@quotecurs;
822             } # DETERMINE_QUOTE_CURRENCIES
823              
824             # determine possible base currencies to arbitrage against
825             DETERMINE_BASE_CURRENCIES:
826             {
827              
828 0           my @basecurs;
  0            
829 0   0       my @basecurs_arg = @{ $r->{args}{base_currencies} // [] };
  0            
830 0           my %basecur_exchanges; # key=currency code, value={exchange1=>1, ...}
831              
832             # list pairs on all exchanges
833 0           for my $e (sort keys %{ $r->{_stash}{account_exchanges} }) {
  0            
834 0           my $pair_recs = _get_exchange_pairs($r, $e);
835 0           for my $pair_rec (@$pair_recs) {
836 0           my $pair = $pair_rec->{name};
837 0           my ($basecur, $quotecur) = split m!/!, $pair;
838 0 0         next unless grep { $_ eq $quotecur } @{ $r->{_stash}{quote_currencies} };
  0            
  0            
839 0           $basecur_exchanges{$basecur}{$e} = 1;
840             }
841             }
842              
843             # only consider base currencies that are traded in >1 exchanges, for
844             # arbitrage possibility
845 0           my @possible_basecurs = grep { keys(%{$basecur_exchanges{$_}}) > 1 }
  0            
  0            
846             keys %basecur_exchanges;
847              
848 0 0         if (@basecurs_arg) {
849 0           my @impossible_basecurs;
850 0           for my $c (@basecurs_arg) {
851 0 0         if (grep { $c eq $_ } @possible_basecurs) {
  0            
852 0           push @basecurs, $c;
853             } else {
854 0           push @impossible_basecurs, $c;
855             }
856             }
857 0 0         if (@impossible_basecurs) {
858 0           log_warn "The following base currencies are not traded on at least two exchanges: %s, excluding these base currencies",
859             \@impossible_basecurs;
860             }
861             } else {
862 0           log_warn "Will be arbitraging these base currencies that are traded on at least two exchanges: %s",
863             \@possible_basecurs;
864 0           @basecurs = @possible_basecurs;
865             }
866              
867 0 0         return [412, "No base currencies possible for arbitraging"] unless @basecurs;
868 0           $r->{_stash}{base_currencies} = \@basecurs;
869             } # DETERMINE_BASE_CURRENCIES
870              
871             DETERMINE_TRADING_FEES:
872             {
873             # XXX hardcoded for now
874 0           $r->{_stash}{trading_fees} = {
875 0           ':default' => {':default'=>0.3},
876             'indodax' => {':default'=>0.3},
877             'coinbase-pro' => {BTC=>0.25, ':default'=>0.3},
878             };
879             }
880              
881 0           [200];
882             }
883              
884             sub _format_order_pairs_response {
885 0     0     my $order_pairs = shift;
886              
887             # format for table display
888 0           my @res;
889 0           for my $op (@$order_pairs) {
890 0           my $size = $op->{base_size};
891 0           my ($base_currency, $buy_currency) = $op->{buy}{pair} =~ m!(.+)/(.+)!;
892 0           my ($sell_currency) = $op->{sell}{pair} =~ m!/(.+)!;
893 0 0         my $profit_currency = _is_fiat($buy_currency) ? 'USD' : $buy_currency;
894              
895             my $rec = {
896             size => $size,
897             currency => $base_currency,
898             buy_from => $op->{buy}{exchange},
899             buy_currency => $buy_currency,
900             buy_gross_price => $op->{buy}{gross_price_orig},
901             sell_to => $op->{sell}{exchange},
902             sell_currency => $sell_currency,
903             sell_gross_price => $op->{sell}{gross_price_orig},
904             (gross_profit_margin => $op->{gross_profit_margin}) x !!exists($op->{gross_profit_margin}),
905             (trading_profit_margin => $op->{trading_profit_margin}) x !!exists($op->{trading_profit_margin}),
906             (forex_spread => $op->{forex_spread}) x !!exists($op->{forex_spread}),
907             (net_profit_margin => $op->{net_profit_margin}) x !!exists($op->{net_profit_margin}),
908             profit_currency => $profit_currency,
909             profit => $op->{profit},
910 0           };
911 0 0 0       if (_is_fiat($buy_currency) && $buy_currency ne 'USD') {
912 0           $rec->{buy_gross_price_usd} = $op->{buy}{gross_price};
913             #$rec->{buy_net_price_usd} = $op->{buy}{net_price};
914             }
915 0 0 0       if (_is_fiat($sell_currency) && $sell_currency ne 'USD') {
916 0           $rec->{sell_gross_price_usd} = $op->{sell}{gross_price};
917             #$rec->{sell_net_price_usd} = $op->{sell}{net_price};
918             }
919 0           push @res, $rec;
920             }
921              
922 0           my $resmeta = {};
923 0           $resmeta->{'table.fields'} = ['size', 'currency', 'buy_from', 'buy_currency', 'buy_gross_price', 'buy_gross_price_usd', 'sell_to', 'sell_currency', 'sell_gross_price', 'sell_gross_price_usd', 'gross_profit_margin', 'trading_profit_margin', 'forex_spread', 'net_profit_margin', 'profit_currency', 'profit'];
924 0           $resmeta->{'table.field_labels'} = [undef, 'c', 'buyFrom', 'buyC', 'buyGrossP', 'buyGrossP.USD', 'sellTo', 'sellC', 'sellGrossP', 'sellGrossP.USD', 'grossProfitM', 'trdProfitM', 'fxSpread', 'netProfitM', 'profitC', undef];
925 0           $resmeta->{'table.field_formats'} = [$fnum8, undef, undef, undef, $fnum8, $fnum8, undef, undef, $fnum8, $fnum8, $fnum4, $fnum4, $fnum4, $fnum4, undef, $fnum8];
926 0           $resmeta->{'table.field_aligns'} = ['right', 'left', 'left', 'left', 'right', 'right', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'left', 'right'];
927              
928 0           [200, "OK", \@res, $resmeta];
929             }
930              
931             sub _create_orders {
932 0     0     my $r = shift;
933              
934 0           my $dbh = $r->{_stash}{dbh};
935              
936 0           local $dbh->{RaiseError};
937              
938             ORDER_PAIR:
939 0           for my $i (0..$#{ $r->{_stash}{order_pairs} }) {
  0            
940 0           my $op = $r->{_stash}{order_pairs}[$i];
941 0           my $is_err;
942             my $do_cancel_buy_order_on_err;
943 0           my $do_cancel_sell_order_on_err;
944              
945             log_debug "[%d/%d] Creating order pair on the exchanges: %s ...",
946 0           $i+1, scalar(@{ $r->{_stash}{order_pairs} }), $op;
  0            
947 0           my $buy = $op->{buy};
948 0           my $sell = $op->{sell};
949 0           my $buy_eid = _get_exchange_id($r, $buy ->{exchange});
950 0           my $buy_aid = _get_account_id ($r, $buy ->{exchange}, $buy ->{account});
951 0           my ($buy_quotecur) = $buy->{pair} =~ m!/(.+)!;
952 0           my $sell_eid = _get_exchange_id($r, $sell->{exchange});
953 0           my $sell_aid = _get_account_id ($r, $sell->{exchange}, $sell->{account});
954 0           my ($sell_quotecur) = $sell->{pair} =~ m!/(.+)!;
955              
956 0           my $time = time();
957             # first, insert to database with status 'creating'
958             $dbh->do(
959             "INSERT INTO order_pair (
960             ctime,
961             base_currency, base_size,
962             expected_net_profit_margin, expected_net_profit,
963              
964             buy_exchange_id , buy_account_id , buy_quote_currency , buy_gross_price_orig , buy_gross_price , buy_status,
965             sell_exchange_id, sell_account_id, sell_quote_currency, sell_gross_price_orig, sell_gross_price, sell_status
966              
967             ) VALUES (
968             ?,
969             ?, ?,
970             ?, ?,
971              
972             ?, ?, ?, ?, ?, ?,
973             ?, ?, ?, ?, ?, ?
974             )",
975              
976             {},
977              
978             $time,
979             $op->{base_currency}, $op->{base_size},
980             $op->{net_profit_margin}, $op->{net_profit},
981              
982             $buy_eid , $buy_aid , $buy_quotecur , $buy ->{gross_price_orig}, $buy ->{gross_price}, 'creating',
983             $sell_eid, $sell_aid, $sell_quotecur, $sell->{gross_price_orig}, $sell->{gross_price}, 'creating',
984 0 0         ) or do {
985 0           log_error "Couldn't record order_pair in db: %s, skipping this order pair", $dbh->errstr;
986 0           next ORDER_PAIR;
987             };
988 0           my $pair_id = $dbh->last_insert_id("", "", "", "");
989              
990 0           my $buy_client = _get_exchange_client($r, $buy->{exchange}, $buy->{account});
991 0           my $buy_order_id;
992             CREATE_BUY_ORDER:
993             {
994 0           my $res = $buy_client->create_limit_order(
995             pair => $buy->{pair},
996             type => 'buy',
997             price => $buy->{gross_price_orig},
998             base_size => $op->{base_size},
999 0           );
1000 0 0         unless ($res->[0] == 200) {
1001 0           log_error "Couldn't create buy order: %s", $res;
1002 0           $is_err++;
1003 0           goto CLEANUP;
1004             }
1005 0           $buy_order_id = $res->[2]{order_id};
1006 0           $do_cancel_buy_order_on_err++;
1007             my $res2 = $buy_client->get_order(
1008             pair => $buy->{pair},
1009 0           type => 'buy',
1010             order_id => $buy_order_id,
1011             );
1012 0 0         unless ($res2->[0] == 200) {
1013 0           log_error "Couldn't get buy order: %s", $res2;
1014 0           $is_err++;
1015 0           goto CLEANUP;
1016             }
1017             $dbh->do(
1018             "UPDATE order_pair SET buy_ctime=?, buy_order_id=?, buy_actual_price=?, buy_actual_base_size=?, buy_status=? WHERE id=?",
1019             {},
1020             $res2->[2]{create_time}, $buy_order_id, $res->[2]{price}, $res->[2]{base_size}, 'open',
1021             $pair_id,
1022 0 0         ) or do {
1023 0           log_error "Couldn't update order status in db for buy order: %s", $res2;
1024             };
1025             }
1026              
1027 0           my $sell_client = _get_exchange_client($r, $sell->{exchange}, $sell->{account});
1028 0           my $sell_order_id;
1029             CREATE_SELL_ORDER:
1030             {
1031 0           my $res = $sell_client->create_limit_order(
1032             pair => $sell->{pair},
1033             type => 'sell',
1034             price => $sell->{gross_price_orig},
1035             base_size => $op->{base_size},
1036 0           );
1037 0 0         unless ($res->[0] == 200) {
1038 0           log_error "Couldn't create sell order: %s", $res;
1039 0           $is_err++;
1040 0           goto CLEANUP;
1041             }
1042 0           $sell_order_id = $res->[2]{order_id};
1043 0           $do_cancel_sell_order_on_err++;
1044             my $res2 = $sell_client->get_order(
1045             pair => $sell->{pair},
1046 0           type => 'sell',
1047             order_id => $sell_order_id,
1048             );
1049 0 0         unless ($res2->[0] == 200) {
1050 0           log_error "Couldn't get sell order: %s", $res2;
1051 0           $is_err++;
1052 0           goto CLEANUP;
1053             }
1054             $dbh->do(
1055             "UPDATE order_pair SET sell_ctime=?, sell_order_id=?, sell_actual_price=?, sell_actual_base_size=?, sell_status=? WHERE id=?",
1056             {},
1057             $res2->[2]{create_time}, $sell_order_id, $res->[2]{price}, $res->[2]{base_size}, 'open',
1058             $pair_id,
1059 0 0         ) or do {
1060 0           log_error "Couldn't update order status in db for sell order: %s", $res2;
1061             };
1062             }
1063              
1064             CLEANUP:
1065             {
1066 0 0         last unless $is_err;
  0            
1067 0 0         if ($do_cancel_buy_order_on_err) {
1068 0           $dbh->do("UPDATE order_pair SET buy_status='cancelling' WHERE id=?", {}, $pair_id);
1069             my $res = $buy_client->cancel_order(
1070             type => 'buy',
1071             pair => $buy->{pair},
1072 0           order_id => $buy_order_id,
1073             );
1074 0 0         if ($res->[0] != 200) {
1075 0           log_error "Couldn't cancel buy order #%s (order pair ID %d): %s",
1076             $buy_order_id, $pair_id, $res;
1077             } else {
1078 0           $dbh->do("UPDATE order_pair SET buy_status='cancelled' WHERE id=?", {}, $pair_id)
1079             }
1080             }
1081 0 0         if ($do_cancel_sell_order_on_err) {
1082 0           $dbh->do("UPDATE order_pair SET sell_status='cancelling' WHERE id=?", {}, $pair_id);
1083             my $res = $sell_client->cancel_order(
1084             type => 'sell',
1085             pair => $sell->{pair},
1086 0           order_id => $sell_order_id,
1087             );
1088 0 0         if ($res->[0] != 200) {
1089 0           log_error "Couldn't cancel sell order #%s (order pair ID %d): %s",
1090             $sell_order_id, $pair_id, $res;
1091             } else {
1092 0           $dbh->do("UPDATE order_pair SET sell_status='cancelled' WHERE id=?", {}, $pair_id)
1093             }
1094             }
1095             }
1096             } # ORDER_PAIR
1097             }
1098              
1099             $SPEC{dump_cryp_config} = {
1100             v => 1.1,
1101             args => {
1102             },
1103             };
1104             sub dump_cryp_config {
1105 0     0 1   my %args = @_;
1106              
1107 0           my $r = $args{-cmdline_r};
1108 0           my $res;
1109              
1110 0 0         $res = _init($r, {skip_connect=>1}); return $res unless $res->[0] == 200;
  0            
1111              
1112 0           [200, "OK", $r->{_cryp}];
1113             }
1114              
1115             $SPEC{show_opportunities} = {
1116             v => 1.1,
1117             summary => 'Show arbitrage opportunities',
1118             description => <<'_',
1119              
1120             This subcommand, like the `arbit` subcommand, checks prices of cryptocurrencies
1121             on several exchanges for arbitrage possibility; but does not actually perform
1122             the arbitraging.
1123              
1124             _
1125             args => {
1126             %args_db,
1127             %args_arbit_common,
1128             ignore_balance => {
1129             summary => 'Ignore account balances',
1130             schema => 'bool*',
1131             default => 0,
1132             },
1133             ignore_min_order_size => {
1134             summary => 'Ignore minimum order size limitation from exchanges',
1135             schema => 'bool*',
1136             default => 0,
1137             },
1138             },
1139             };
1140             sub show_opportunities {
1141 0     0 1   my %args = @_;
1142              
1143 0           my $r = $args{-cmdline_r};
1144             # XXX schema
1145 0   0       my $strategy = $args{strategy} // 'merge_order_book';
1146              
1147 0           my $res;
1148              
1149 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1150 0 0         $res = _init_arbit($r); return $res unless $res->[0] == 200;
  0            
1151              
1152 0           my $strategy_mod = "App::cryp::arbit::Strategy::$strategy";
1153 0           (my $strategy_modpm = "$strategy_mod.pm") =~ s!::!/!g;
1154 0           require $strategy_modpm;
1155              
1156 0           $res = $strategy_mod->calculate_order_pairs(r => $r);
1157 0 0         return $res unless $res->[0] == 200;
1158              
1159             #log_trace "order pairs: %s", $res->[2];
1160              
1161 0           _format_order_pairs_response($res->[2]);
1162             }
1163              
1164             $SPEC{arbit} = {
1165             v => 1.1,
1166             summary => 'Perform arbitrage',
1167             description => <<'_',
1168              
1169             This utility monitors prices of several cryptocurrencies ("base currencies",
1170             e.g. LTC) in several cryptoexchanges. The "quote currency" can be fiat (e.g.
1171             USD, all other fiat currencies will be converted to USD) or another
1172             cryptocurrency (usually BTC).
1173              
1174             When it detects a net price difference for a base currency that is large enough
1175             (see `min_net_profit_margin` option), it will perform a buy order on the
1176             exchange that has the lower price and sell the exact same amount of base
1177             currency on the exchange that has the higher price. For example, if on XCHG1 the
1178             buy price of LTC 100.01 USD and on XCHG2 the sell price of LTC is 98.80 USD,
1179             then this utility will buy LTC on XCHG2 for 98.80 USD and sell the same amount
1180             of LTD on XCHG1 for 100.01 USD. The profit is (100.01 - 98.80 - trading fees)
1181             per LTC arbitraged. You have to maintain enough LTC balance on XCHG1 and enough
1182             USD balance on XCHG2.
1183              
1184             The balances are called inventories or your working capital. You fill and
1185             transfer inventories manually to refill balances and/or to collect profits.
1186              
1187             _
1188             args => {
1189             %args_db,
1190             %args_arbit_common,
1191             rounds => {
1192             summary => 'How many rounds',
1193             schema => 'int*',
1194             default => 1,
1195             cmdline_aliases => {
1196             loop => {is_flag=>1, code=>sub { $_[0]{rounds} = -1 }, summary => 'Shortcut for --rounds -1'},
1197             },
1198             description => <<'_',
1199              
1200             -1 means unlimited.
1201              
1202             _
1203             },
1204             frequency => {
1205             summary => 'How many seconds to wait between rounds (in seconds)',
1206             schema => 'posint*',
1207             default => 30,
1208             description => <<'_',
1209              
1210             A round consists of checking prices and then creating arbitraging order pairs.
1211              
1212             _
1213             },
1214             %arg_max_order_age,
1215             },
1216             features => {
1217             dry_run => 1,
1218             },
1219             };
1220             sub arbit {
1221 0     0 1   my %args = @_;
1222              
1223 0           my $r = $args{-cmdline_r};
1224             # XXX schema
1225 0   0       my $strategy = $args{strategy} // 'merge_order_book';
1226 0 0         $args{min_net_profit_margin} > 0 or return [412, "Refusing to do arbitrage with no positive min_net_profit_margin"];
1227              
1228 0           my $res;
1229              
1230 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1231 0 0         $res = _init_arbit($r); return $res unless $res->[0] == 200;
  0            
1232              
1233 0           log_info "Starting arbitration with '%s' strategy ...", $strategy;
1234              
1235 0           my $strategy_mod = "App::cryp::arbit::Strategy::$strategy";
1236 0           (my $strategy_modpm = "$strategy_mod.pm") =~ s!::!/!g;
1237 0           require $strategy_modpm;
1238              
1239 0           my $round = 0;
1240             ROUND:
1241 0           while (1) {
1242 0           $round++;
1243 0           log_info "Round #%d", $round;
1244              
1245 0           $res = $strategy_mod->calculate_order_pairs(r => $r);
1246              
1247 0 0         if ($res->[0] == 200) {
1248 0           log_debug "Got these order pairs from arbit strategy module: %s",
1249             $res->[2];
1250             } else {
1251 0           log_error "Got error response from arbit strategy module: %s, ".
1252             "skipping this round", $res;
1253 0           goto SLEEP;
1254             }
1255 0           $r->{_stash}{order_pairs} = $res->[2];
1256              
1257 0 0         if ($args{-dry_run}) {
1258 0 0         if ($args{rounds} == 1) {
1259 0           log_info "[DRY-RUN] Will not actually be creating order pairs on the exchanges, showing possible order pairs ...";
1260 0           return _format_order_pairs_response($r->{_stash}{order_pairs});
1261             } else {
1262 0           log_info "[DRY-RUN] Will not actually be creating order pairs on the exchanges, waiting for next round ...";
1263 0           goto SLEEP;
1264             }
1265             }
1266              
1267 0           _create_orders($r);
1268              
1269 0           _check_orders($r);
1270              
1271 0 0 0       last if $args{rounds} > 0 && $round >= $args{rounds};
1272              
1273             SLEEP:
1274             log_trace "Sleeping for %d second(s) before next round ...",
1275 0           $args{frequency};
1276 0           sleep $args{frequency};
1277             }
1278              
1279 0           [200];
1280             }
1281              
1282             $SPEC{collect_orderbooks} = {
1283             v => 1.1,
1284             summary => 'Collect orderbooks into the database',
1285             description => <<'_',
1286              
1287             This utility collect orderbooks from exchanges and put it into the database. The
1288             data can be used later e.g. for backtesting.
1289              
1290             _
1291             args => {
1292             %args_db,
1293             %args_accounts_and_currencies,
1294             frequency => {
1295             summary => 'How many seconds to wait between rounds (in seconds)',
1296             schema => 'posint*',
1297             default => 30,
1298             },
1299             },
1300             };
1301             sub collect_orderbooks {
1302 0     0 1   my %args = @_;
1303              
1304 0           my $r = $args{-cmdline_r};
1305 0           my $res;
1306 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1307 0 0         $res = _init_arbit($r); return $res unless $res->[0] == 200;
  0            
1308              
1309 0           my $dbh = $r->{_stash}{dbh};
1310              
1311             # this section is borrowed from App::cryp::arbit::Strategy::merge_order_book
1312              
1313 0           my %exchanges_for; # key="base currency"/"quote cryptocurrency or ':fiat'", value => [exchange, ...]
1314             my %fiat_for; # key=exchange safename, val=[fiat currency, ...]
1315 0           my %pairs_for; # key=exchange safename, val=[pair, ...]
1316             DETERMINE_SETS:
1317 0           for my $exchange (sort keys %{ $r->{_stash}{exchange_clients} }) {
  0            
1318 0           my $pair_recs = $r->{_stash}{exchange_pairs}{$exchange};
1319 0           for my $pair_rec (@$pair_recs) {
1320 0           my $pair = $pair_rec->{name};
1321 0           my ($basecur, $quotecur) = $pair =~ m!(.+)/(.+)!;
1322 0 0         next unless grep { $_ eq $basecur } @{ $r->{_stash}{base_currencies} };
  0            
  0            
1323 0 0         next unless grep { $_ eq $quotecur } @{ $r->{_stash}{quote_currencies} };
  0            
  0            
1324              
1325 0           my $key;
1326 0 0         if (App::cryp::arbit::_is_fiat($quotecur)) {
1327 0           $key = "$basecur/:fiat";
1328 0   0       $fiat_for{$exchange} //= [];
1329 0           push @{ $fiat_for{$exchange} }, $quotecur
1330 0 0         unless grep { $_ eq $quotecur } @{ $fiat_for{$exchange} };
  0            
  0            
1331             } else {
1332 0           $key = "$basecur/$quotecur";
1333             }
1334 0   0       $exchanges_for{$key} //= [];
1335 0           push @{ $exchanges_for{$key} }, $exchange;
  0            
1336              
1337 0   0       $pairs_for{$exchange} //= [];
1338 0           push @{ $pairs_for{$exchange} }, $pair
1339 0 0         unless grep { $_ eq $pair } @{ $pairs_for{$exchange} };
  0            
  0            
1340             }
1341             } # DETERMINE_SETS
1342              
1343             ROUND:
1344 0           while (1) {
1345             SET:
1346 0           for my $set (keys %exchanges_for) {
1347 0           my ($base_currency, $quote_currency0) = $set =~ m!(.+)/(.+)!;
1348              
1349             EXCHANGE:
1350 0           for my $exchange (sort keys %{ $r->{_stash}{exchange_clients} }) {
  0            
1351 0           my $eid = App::cryp::arbit::_get_exchange_id($r, $exchange);
1352 0           my $clients = $r->{_stash}{exchange_clients}{$exchange};
1353 0           my $client = $clients->{ (sort keys %$clients)[0] };
1354              
1355 0           my @pairs;
1356 0 0         if ($quote_currency0 eq ':fiat') {
1357 0           push @pairs, map { "$base_currency/$_" } @{ $fiat_for{$exchange} };
  0            
  0            
1358             } else {
1359 0           push @pairs, $set;
1360             }
1361              
1362             PAIR:
1363 0           for my $pair (@pairs) {
1364 0           my ($basecur, $quotecur) = split m!/!, $pair;
1365 0 0         next unless grep { $_ eq $pair } @{ $pairs_for{$exchange} };
  0            
  0            
1366              
1367 0           my $time = time();
1368 0           log_debug "Getting orderbook %s on %s ...", $pair, $exchange;
1369 0           my $res = $client->get_order_book(pair => $pair);
1370 0 0         unless ($res->[0] == 200) {
1371 0           log_error "Couldn't get orderbook %s on %s: %s, skipping this pair",
1372             $pair, $exchange, $res;
1373 0           next PAIR;
1374             }
1375              
1376             # save orderbook to database
1377             TYPE:
1378 0           for my $type ("buy", "sell") {
1379             # sanity checks
1380 0 0 0       unless ($res->[2]{$type} && @{ $res->[2]{$type} }) {
  0            
1381 0           log_warn "No $type orders for %s on %s, skipping",
1382             $pair, $exchange;
1383 0           next;
1384             }
1385 0           $dbh->do("INSERT INTO orderbook (time,exchange_id,base_currency,quote_currency,type) VALUES (?,?,?,?,?)", {}, $time, $eid, $basecur, $quotecur, $type);
1386 0           my $orderbook_id = $dbh->last_insert_id("","","","");
1387 0           my $sth = $dbh->prepare("INSERT INTO orderbook_item (orderbook_id, price, amount) VALUES (?,?,?)");
1388 0           for my $item (@{ $res->[2]{$type} }) {
  0            
1389             #log_trace "item: %s", $item;
1390 0           $sth->execute($orderbook_id, $item->[0], $item->[1]);
1391             }
1392             } # TYPE
1393             } # PAIR
1394             } # EXCHANGE
1395             } # SET
1396              
1397             SLEEP:
1398             log_trace "Sleeping for %d second(s) before next round ...",
1399 0           $args{frequency};
1400 0           sleep $args{frequency};
1401             } # ROUND
1402              
1403 0           [200];
1404             }
1405              
1406             sub _check_orders {
1407 0     0     my $r = shift;
1408              
1409 0           my $dbh = $r->{_stash}{dbh};
1410              
1411             my $code_update_buy_status = sub {
1412 0     0     my ($id, $status, $summary) = @_;
1413 0           local $dbh->{RaiseError};
1414             $dbh->do(
1415             "UPDATE order_pair SET buy_status=? WHERE id=?",
1416             {},
1417             $status,
1418             $id,
1419 0 0         ) or do {
1420 0           log_warn "Couldn't update buy status for order pair #%d: %s",
1421             $id, $dbh->errstr;
1422 0           return;
1423             };
1424 0 0         $dbh->do(
1425             "INSERT INTO arbit_order_log (order_pair_id, type, summary) VALUES (?,?,?)",
1426             {},
1427             $id, 'buy', "status changed to $status" . ($summary ? ": $summary" : ""),
1428             );
1429 0           };
1430              
1431             my $code_update_sell_status = sub {
1432 0     0     my ($id, $status, $summary) = @_;
1433 0           local $dbh->{RaiseError};
1434             $dbh->do(
1435             "UPDATE order_pair SET sell_status=? WHERE id=?",
1436             {},
1437             $status,
1438             $id,
1439 0 0         ) or do {
1440 0           log_warn "Couldn't update sell status for order pair #%d: %s",
1441             $id, $dbh->errstr;
1442 0           return;
1443             };
1444 0 0         $dbh->do(
1445             "INSERT INTO arbit_order_log (order_pair_id, type, summary) VALUES (?,?,?)",
1446             {},
1447             $id, 'sell', "status changed to $status" . ($summary ? ": $summary" : ""),
1448             );
1449 0           };
1450              
1451             my $code_update_buy_filled_base_size = sub {
1452 0     0     my ($id, $size, $summary) = @_;
1453 0           local $dbh->{RaiseError};
1454             $dbh->do(
1455             "UPDATE order_pair SET buy_filled_base_size=? WHERE id=?",
1456             {},
1457             $size,
1458             $id,
1459 0 0         ) or do {
1460 0           log_warn "Couldn't update buy filled base size for order pair #%d: %s",
1461             $id, $dbh->errstr;
1462 0           return;
1463             };
1464 0 0         $dbh->do(
1465             "INSERT INTO arbit_order_log (order_pair_id, type, summary) VALUES (?,?,?)",
1466             {},
1467             $id, 'buy', "filled_base_size changed to $size" . ($summary ? ": $summary" : ""),
1468             );
1469 0           };
1470              
1471             my $code_update_sell_filled_base_size = sub {
1472 0     0     my ($id, $size, $summary) = @_;
1473 0           local $dbh->{RaiseError};
1474             $dbh->do(
1475             "UPDATE order_pair SET sell_filled_base_size=? WHERE id=?",
1476             {},
1477             $size,
1478             $id,
1479 0 0         ) or do {
1480 0           log_warn "Couldn't update sell filled base size for order pair #%d: %s",
1481             $id, $dbh->errstr;
1482 0           return;
1483             };
1484 0 0         $dbh->do(
1485             "INSERT INTO arbit_order_log (order_pair_id, type, summary) VALUES (?,?,?)",
1486             {},
1487             $id, 'sell', "filled_base_size changed to $size" . ($summary ? ": $summary" : ""),
1488             );
1489 0           };
1490              
1491 0           my @open_order_pairs;
1492 0           my $sth = $dbh->prepare(
1493             "SELECT
1494             op.id id,
1495             op.ctime ctime,
1496             CONCAT(op.base_currency, '/', op.buy_quote_currency) buy_pair,
1497             op.buy_status buy_status,
1498             (SELECT safename FROM exchange WHERE id=op.buy_exchange_id) buy_exchange,
1499             (SELECT nickname FROM account WHERE id=op.buy_account_id) buy_account,
1500             op.buy_order_id buy_order_id,
1501              
1502             op.sell_status sell_status,
1503             CONCAT(op.base_currency, '/', op.sell_quote_currency) sell_pair,
1504             (SELECT safename FROM exchange WHERE id=op.sell_exchange_id) sell_exchange,
1505             (SELECT nickname FROM account WHERE id=op.sell_account_id) sell_account,
1506             op.sell_order_id sell_order_id
1507             FROM order_pair op
1508             WHERE
1509             (op.buy_order_id IS NOT NULL AND
1510             op.buy_status NOT IN ('done','filled','cancelled')) OR
1511             (op.sell_order_id IS NOT NULL AND
1512             op.sell_status NOT IN ('done','filled','cancelled'))
1513             ORDER BY op.ctime");
1514 0           $sth->execute;
1515 0           while (my $row = $sth->fetchrow_hashref) {
1516 0           push @open_order_pairs, $row;
1517             }
1518              
1519 0           my $time = time();
1520 0           for my $op (@open_order_pairs) {
1521             log_debug "Checking order pair #%d (buy status=%s, sell status=%s) ...",
1522 0           $op->{id}, $op->{buy_status}, $op->{sell_status};
1523              
1524             CHECK_BUY_ORDER: {
1525 0 0         last if $op->{buy_status} =~ /\A(done|cancelled)\z/;
  0            
1526 0           my $client = _get_exchange_client($r, $op->{buy_exchange}, $op->{buy_account});
1527 0           my $res = $client->get_order(pair=>$op->{buy_pair}, type=>'buy', order_id=>$op->{buy_order_id});
1528 0 0         if ($res->[0] == 404) {
    0          
1529             # assume 404 as order which was never filled and got cancelled.
1530             # some exchanges, e.g. coinbase-pro returns 404 for such orders
1531 0           $code_update_buy_status->($op->{id}, 'cancelled', 'not found via get_order(), assume cancelled without being filled');
1532 0           last;
1533             } elsif ($res->[0] != 200) {
1534             log_error "Couldn't get buy order %s (pair %s): %s",
1535 0           $op->{buy_order_id}, $op->{buy_pair}, $res;
1536 0           last;
1537             } else {
1538 0           my $status = $res->[2]{status};
1539 0           $code_update_buy_filled_base_size->($op->{id}, $res->[2]{filled_base_size});
1540 0           $code_update_buy_status->($op->{id}, $status);
1541              
1542 0 0 0       if ($status eq 'open' && $time - $op->{ctime} > $r->{args}{max_order_age}) {
1543 0           log_info "Order %s (buy) has been open for too long (>%d secs), cancelling ...";
1544 0           my $cancelres = $client->cancel_order(pair=>$op->{buy_pair}, type=>'buy', order_id=>$op->{buy_order_id});
1545 0 0         if ($cancelres->[0] != 200) {
1546 0           log_error "Couldn't cancel order %s (buy): %s", $op->{buy_order_id}, $cancelres;
1547             } else {
1548 0           $code_update_buy_status->($op->{id}, "cancelled");
1549             }
1550             }
1551             }
1552             } # CHECK_BUY_ORDER
1553              
1554             CHECK_SELL_ORDER: {
1555 0 0         last if $op->{sell_status} =~ /\A(done|cancelled)\z/;
  0            
1556 0           my $client = _get_exchange_client($r, $op->{sell_exchange}, $op->{sell_account});
1557 0           my $res = $client->get_order(pair=>$op->{sell_pair}, type=>'sell', order_id=>$op->{sell_order_id});
1558 0 0         if ($res->[0] == 404) {
    0          
1559             # assume 404 as order which was never filled and got cancelled.
1560             # some exchanges, e.g. coinbase-pro returns 404 for such orders
1561 0           $code_update_sell_status->($op->{id}, 'cancelled', 'not found via get_order(), assume cancelled without being filled');
1562 0           last;
1563             } elsif ($res->[0] != 200) {
1564             log_error "Couldn't get sell order %s (pair %s): %s",
1565 0           $op->{sell_order_id}, $op->{sell_pair}, $res;
1566 0           last;
1567             } else {
1568 0           my $status = $res->[2]{status};
1569 0           $code_update_sell_filled_base_size->($op->{id}, $res->[2]{filled_base_size});
1570 0           $code_update_sell_status->($op->{id}, $status);
1571              
1572 0 0 0       if ($status eq 'open' && $time - $op->{ctime} > $r->{args}{max_order_age}) {
1573 0           log_info "Order %s (sell) has been open for too long (>%d secs), cancelling ...";
1574 0           my $cancelres = $client->cancel_order(pair=>$op->{sell_pair}, type=>'sell', order_id=>$op->{sell_order_id});
1575 0 0         if ($cancelres->[0] != 200) {
1576 0           log_error "Couldn't cancel order %s (sell): %s", $op->{sell_order_id}, $cancelres;
1577             } else {
1578 0           $code_update_sell_status->($op->{id}, "cancelled");
1579             }
1580             }
1581             }
1582             } # CHECK_SELL_ORDER
1583             }
1584             }
1585              
1586             $SPEC{check_orders} = {
1587             v => 1.1,
1588             summary => 'Check the orders that have been created',
1589             description => <<'_',
1590              
1591             This subcommand will check the orders that have been created previously by
1592             `arbit` subcommand. It will update the order status and filled size (if still
1593             open). It will cancel (give up) the orders if deemed too old.
1594              
1595             _
1596             args => {
1597             %args_db,
1598             %arg_max_order_age,
1599             },
1600             };
1601             sub check_orders {
1602 0     0 1   my %args = @_;
1603              
1604 0           my $r = $args{-cmdline_r};
1605              
1606 0           my $res;
1607              
1608             # [ux] remove extraneous arguments supplied by config
1609 0           delete $r->{args}{accounts};
1610              
1611 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1612              
1613 0           _check_orders($r);
1614 0           [200];
1615             }
1616              
1617             $SPEC{list_order_pairs} = {
1618             v => 1.1,
1619             summary => 'List created order pairs',
1620             args => {
1621             %args_db,
1622             time_start => {
1623             schema => 'date*',
1624             tags => ['category:filtering'],
1625             },
1626             time_end => {
1627             schema => 'date*',
1628             tags => ['category:filtering'],
1629             },
1630             open => {
1631             schema => 'bool*',
1632             tags => ['category:filtering'],
1633             },
1634             },
1635             };
1636             sub list_order_pairs {
1637 0     0 1   my %args = @_;
1638              
1639 0           my $r = $args{-cmdline_r};
1640              
1641             # [ux] remove extraneous arguments supplied by config
1642 0           delete $r->{args}{accounts};
1643              
1644 0           my $res;
1645              
1646 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1647              
1648 0           my $dbh = $r->{_stash}{dbh};
1649              
1650 0           my @wheres;
1651             my @binds;
1652 0 0         if (defined $args{open}) {
1653 0 0         if ($args{open}) {
1654 0           push @wheres, "buy_status NOT IN ('done', 'cancelled', 'filled')";
1655             } else {
1656 0           push @wheres, "buy_status IN ('done', 'cancelled', 'filled')";
1657             }
1658             }
1659 0 0         if ($args{time_start}) {
1660 0           push @wheres, "ctime >= ?";
1661 0           push @binds, $args{time_start};
1662             }
1663 0 0         if ($args{time_end}) {
1664 0           push @wheres, "ctime <= ?";
1665 0           push @binds, $args{time_end};
1666             }
1667 0 0         my $sth = $dbh->prepare(
1668             "SELECT *, eb.safename buy_exchange, es.safename sell_exchange
1669             FROM order_pair op
1670             LEFT JOIN exchange eb ON op.buy_exchange_id=eb.id
1671             LEFT JOIN exchange es ON op.sell_exchange_id=es.id
1672             ".
1673             (@wheres ? "WHERE ".join(" AND ", @wheres)." " : "").
1674             "ORDER BY ctime");
1675 0           $sth->execute(@binds);
1676              
1677 0           my @recs;
1678 0           while (my $op = $sth->fetchrow_hashref) {
1679             my $rec = {
1680             ctime => int $op->{ctime},
1681             base_size => $op->{base_size},
1682             base_currency => $op->{base_currency},
1683              
1684             buy_exchange => $op->{buy_exchange},
1685             buy_quote_currency => $op->{buy_quote_currency},
1686             buy_actual_base_size => $op->{buy_actual_base_size},
1687             buy_actual_price => $op->{buy_actual_price},
1688             buy_filled_pct => defined($op->{buy_filled_base_size}) ? $op->{buy_filled_base_size} / $op->{buy_actual_base_size}*100 : undef,
1689             buy_status => $op->{buy_status},
1690              
1691             sell_exchange => $op->{sell_exchange},
1692             sell_quote_currency => $op->{sell_quote_currency},
1693             sell_actual_base_size => $op->{sell_actual_base_size},
1694             sell_actual_price => $op->{sell_actual_price},
1695             sell_filled_pct => defined($op->{sell_filled_base_size}) ? $op->{sell_filled_base_size} / $op->{sell_actual_base_size}*100 : undef,
1696             sell_status => $op->{sell_status},
1697              
1698 0 0         };
    0          
1699 0           push @recs, $rec;
1700             }
1701              
1702 0           my $resmeta = {};
1703 0           $resmeta->{'table.fields'} = ['ctime' , 'base_size', 'base_currency', 'buy_exchange', 'buy_actual_base_size', 'buy_actual_price', 'buy_quote_currency', 'buy_filled_pct', 'buy_status', 'sell_exchange', 'sell_actual_base_size', 'sell_actual_price', 'sell_quote_currency', 'sell_filled_pct', 'sell_status',];
1704 0           $resmeta->{'table.field_labels'} = [undef , 'amount' , 'c' , 'buyFrom' , 'buyAmount' , 'buyPrice' , 'buyC' , 'buy%' , 'buySt' , 'sellTo' , 'sellAmount' , 'sellPrice' , 'sellC' , 'sell%' , 'sellSt' ,];
1705 0           $resmeta->{'table.field_formats'} = ['iso8601_datetime' , $fnum8 , undef , undef , $fnum8 , $fnum8 , undef , $fnum2 , undef , undef , $fnum8 , $fnum8 , undef , $fnum2 , undef ,];
1706 0           $resmeta->{'table.field_aligns'} = ['left' , 'right' , 'left' , 'left' , 'right' , 'right' , 'left' , 'right' , 'left' , 'left' , 'right' , 'right' , 'left' , 'right' , 'left' ,];
1707              
1708 0           [200, "OK", \@recs, $resmeta];
1709             }
1710              
1711             $SPEC{get_profit_report} = {
1712             v => 1.1,
1713             summary => 'Get profit report',
1714             args => {
1715             %args_db,
1716             time_start => {
1717             schema => 'date*',
1718             tags => ['category:filtering'],
1719             },
1720             time_end => {
1721             schema => 'date*',
1722             tags => ['category:filtering'],
1723             },
1724             detail => {
1725             schema => 'bool*',
1726             cmdline_aliases => {l=>{}},
1727             },
1728             %arg_usd_rates,
1729             },
1730             };
1731             sub get_profit_report {
1732 0     0 1   my %args = @_;
1733              
1734 0           my $r = $args{-cmdline_r};
1735              
1736             # [ux] remove extraneous arguments supplied by config
1737 0           delete $r->{args}{accounts};
1738              
1739 0           my $res;
1740              
1741 0 0         $res = _init($r); return $res unless $res->[0] == 200;
  0            
1742              
1743 0           my $dbh = $r->{_stash}{dbh};
1744              
1745 0           my @wheres;
1746             my @binds;
1747 0 0         if ($args{time_start}) {
1748 0           push @wheres, "ctime >= ?";
1749 0           push @binds, $args{time_start};
1750             }
1751 0 0         if ($args{time_end}) {
1752 0           push @wheres, "ctime <= ?";
1753 0           push @binds, $args{time_end};
1754             }
1755 0 0         my $sth = $dbh->prepare(
1756             "SELECT
1757             *,
1758             eb.safename buy_exchange, es.safename sell_exchange,
1759             acb.nickname buy_account, acs.nickname sell_account
1760             FROM order_pair op
1761             LEFT JOIN exchange eb ON op.buy_exchange_id=eb.id
1762             LEFT JOIN exchange es ON op.sell_exchange_id=es.id
1763             LEFT JOIN account acb ON op.buy_account_id=acb.id
1764             LEFT JOIN account acs ON op.sell_account_id=acs.id
1765             ".
1766             (@wheres ? "WHERE ".join(" AND ", @wheres)." " : "").
1767             "ORDER BY ctime");
1768 0           $sth->execute(@binds);
1769              
1770 0           my @recs;
1771             my %per_currency_sums; # key = currency
1772 0           my %per_currency_sums_usd; # key = currency
1773 0           my %per_account_per_currency_sums; # key = "exchange/account", val = { currency1 => total, ... }
1774 0           while (my $op = $sth->fetchrow_hashref) {
1775             RECORD_BUY: {
1776 0 0 0       last unless defined $op->{buy_filled_base_size} && $op->{buy_filled_base_size};
  0            
1777 0           my $frac_b = $op->{buy_filled_base_size} / $op->{buy_actual_base_size};
1778             my $rec_b = {
1779             time => int $op->{ctime},
1780             currency => $op->{base_currency},
1781             amount => $frac_b * $op->{buy_actual_base_size},
1782 0           summary => "bought on $op->{buy_exchange} \@$op->{buy_actual_price}",
1783             };
1784             my $rec_q = {
1785             time => int $op->{ctime},
1786             currency => $op->{buy_quote_currency},
1787             amount => -$frac_b * $op->{buy_actual_base_size} * $op->{buy_actual_price},
1788 0           summary => "spent on $op->{buy_exchange} for buying $op->{base_currency} \@$op->{buy_actual_price}",
1789             };
1790 0           $per_currency_sums{ $op->{base_currency} } += $rec_b->{amount};
1791 0           $per_currency_sums{ $op->{buy_quote_currency} } += $rec_q->{amount};
1792              
1793 0           $per_currency_sums_usd{ $op->{buy_quote_currency} } += _convert_to_usd($r, $rec_q->{amount}, $op->{buy_quote_currency});
1794              
1795 0 0         my $acckey = $op->{buy_exchange} . ($op->{buy_account} eq 'default' ? '' : "/$op->{buy_account}");
1796 0           $per_account_per_currency_sums{ $acckey }{ $op->{base_currency} } += $rec_b->{amount};
1797 0           $per_account_per_currency_sums{ $acckey }{ $op->{buy_quote_currency} } += $rec_q->{amount};
1798 0 0         push @recs, $rec_b, $rec_q if $args{detail};
1799             }
1800             RECORD_SELL: {
1801 0 0 0       last unless defined $op->{sell_filled_base_size} && $op->{sell_filled_base_size};
  0            
1802 0           my $frac_s = $op->{sell_filled_base_size} / $op->{sell_actual_base_size};
1803             my $rec_b = {
1804             time => int $op->{ctime},
1805             summary => "sold on $op->{sell_exchange} \@$op->{sell_actual_price}",
1806             currency => $op->{base_currency},
1807             amount => -$frac_s * $op->{sell_actual_base_size},
1808 0           };
1809             my $rec_q = {
1810             time => int $op->{ctime},
1811             summary => "received on $op->{sell_exchange} for selling $op->{base_currency} \@$op->{sell_actual_price}",
1812             currency => $op->{sell_quote_currency},
1813             amount => $frac_s * $op->{sell_actual_base_size} * $op->{sell_actual_price},
1814 0           };
1815 0           $per_currency_sums{ $op->{base_currency} } += $rec_b->{amount};
1816 0           $per_currency_sums{ $op->{sell_quote_currency} } += $rec_q->{amount};
1817              
1818 0           $per_currency_sums_usd{ $op->{sell_quote_currency} } += _convert_to_usd($r, $rec_q->{amount}, $op->{sell_quote_currency});
1819              
1820 0 0         my $acckey = $op->{sell_exchange} . ($op->{sell_account} eq 'default' ? '' : "/$op->{sell_account}");
1821 0           $per_account_per_currency_sums{ $acckey }{ $op->{base_currency} } += $rec_b->{amount};
1822 0           $per_account_per_currency_sums{ $acckey }{ $op->{sell_quote_currency} } += $rec_q->{amount};
1823 0 0         push @recs, $rec_b, $rec_q if $args{detail};
1824             }
1825             }
1826              
1827             PER_CURRENCY_PER_ACCOUNT_SUBTOTAL: {
1828 0           for my $acckey (sort keys %per_account_per_currency_sums) {
  0            
1829 0           my $per_currency_sums = $per_account_per_currency_sums{$acckey};
1830 0           my $i = 0;
1831 0           for my $cur (sort keys %$per_currency_sums) {
1832             push @recs, {
1833             summary => $i++ ? '' : "Account $acckey subtotal",
1834             currency => $cur,
1835 0 0         amount => $per_currency_sums->{$cur},
1836             };
1837             }
1838             }
1839             }
1840              
1841             PER_CURRENCY_SUBTOTAL: {
1842 0           my $i = 0;
  0            
1843 0           for my $cur (sort keys %per_currency_sums) {
1844             my $rec = {
1845             summary => $i++ ? '' : "Per-currency subtotal",
1846             currency => $cur,
1847 0 0         amount => $per_currency_sums{$cur},
1848             };
1849             $rec->{amount_usd} = $per_currency_sums_usd{$cur}
1850 0 0         if exists $per_currency_sums_usd{$cur};
1851 0           push @recs, $rec;
1852             }
1853             }
1854              
1855             FIAT_PROFIT: {
1856 0           my $profit = 0;
  0            
1857 0           for my $cur (sort keys %per_currency_sums_usd) {
1858 0           $profit += $per_currency_sums_usd{$cur};
1859             }
1860 0           push @recs, {
1861             summary => 'Profit',
1862             currency => 'USD',
1863             amount => $profit,
1864             amount_usd => $profit,
1865             };
1866 0           for my $cur (sort keys %per_currency_sums) {
1867 0 0         next if _is_fiat($cur);
1868 0 0         next if $per_currency_sums{$cur} == 0;
1869             push @recs, {
1870             currency => $cur,
1871 0           amount => $per_currency_sums{$cur},
1872             };
1873             }
1874             }
1875              
1876 0           my $resmeta = {
1877             'table.fields' => ['time' , 'summary', 'currency', 'amount', 'amount_usd'],
1878             'table.field_labels' => [undef , undef , 'c' , undef , 'amountUSD'],
1879             'table.field_formats' => ['iso8601_datetime', undef , undef , $fnum8 , $fnum8],
1880             'table.field_aligns' => ['left' , 'left' , 'left' , 'right' , 'right'],
1881             };
1882              
1883 0           [200, "OK", \@recs, $resmeta];
1884             }
1885              
1886             1;
1887             # ABSTRACT: Cryptocurrency arbitrage utility
1888              
1889             __END__
1890              
1891             =pod
1892              
1893             =encoding UTF-8
1894              
1895             =head1 NAME
1896              
1897             App::cryp::arbit - Cryptocurrency arbitrage utility
1898              
1899             =head1 VERSION
1900              
1901             This document describes version 0.010 of App::cryp::arbit (from Perl distribution App-cryp-arbit), released on 2021-05-26.
1902              
1903             =head1 SYNOPSIS
1904              
1905             Please see included script L<cryp-arbit>.
1906              
1907             =head1 DESCRIPTION
1908              
1909             =head2 Glossary
1910              
1911             =over
1912              
1913             =item * inventory
1914              
1915             =item * order pair
1916              
1917             =item * gross profit margin
1918              
1919             Price difference percentage of a cryptocurrency between two exchanges, without
1920             taking into account trading fees and foreign exchange spread.
1921              
1922             For example, suppose BTC is being offered (ask price, sell price) at 7010 USD on
1923             exchange1 and is being bidden (bid price, buy price) at 7150 USD on exchange2.
1924             This means there is a (7150-7010)/7010 = 1.997% gross profit margin. We can buy
1925             BTC on exchange1 for 7010 USD then sell the same amout of BTC on exchange2 for
1926             7150 USD and gain (7150-7010) = 140 USD per BTC, before fees.
1927              
1928             =item * trading profit margin
1929              
1930             Price difference percentage of a cryptocurrency between two exchanges, after
1931             taking into account trading fees.
1932              
1933             For example, suppose BTC is being offered (ask price, sell price) at 7010 USD on
1934             exchange1 and is being bidden (bid price, buy price) at 7150 USD on exchange2.
1935             Trading (market maker) fee on exchange1 is 0.3% and on exchange2 is 0.25%. After
1936             trading fees, the ask price becomes 7010 * (1+0.3%) = 7031.03 USD and the bid
1937             price becomes 7150 * (1-0.25%) = 7132.125. The trading profit margin is
1938             (7132.125-7031.03)/7031.03 = 1.438%. We can buy BTC on exchange1 for 7010 USD
1939             then sell the same amout of BTC on exchange2 for 7150 USD and still gain
1940             (7132.125-7031.03) = 101.095 USD per BTC, after trading fees.
1941              
1942             =item * net profit margin
1943              
1944             Price difference percentage of a cryptocurrency between two exchanges, after
1945             taking into account trading fees and foreign exchange spread. If the price on
1946             both exchanges are quoted in the same currency (e.g. USD) then there is no forex
1947             spread and net profit margin is the same as trading profit margin.
1948              
1949             If the quoting currencies are different, e.g. USD on exchange1 and IDR on
1950             exchange2, then first we calculate gross and trading profit margin using prices
1951             converted to USD using average forex rate (highest forex dealer's sell price +
1952             lowest buy price, divided by two). Then we subtract trading profit margin with
1953             forex spread for safety.
1954              
1955             For example, suppose BTC is being offered (ask price, sell price) at 7010 USD on
1956             exchange1 and is being bidden (bid price, buy price) at 99,500,000 IDR on
1957             exchange2. The forex rate for USD/IDR is: buy 13,895, sell 13,925, average
1958             (13,925+13,895)/2 = 13,910, spread (13,925-13,895)/13,895 = 0.216%. The price on
1959             exchange2 in USD is 99,500,000 / 13,910 = 7153.127 USD. Trading (market maker)
1960             fee on exchange1 is 0.3% and on exchange2 is 0.25%. After trading fees, the ask
1961             price becomes 7010 * (1+0.3%) = 7031.03 USD and the bid price becomes 7153.127 *
1962             (1-0.25%) = 7135.244. The trading profit margin is (7135.244-7031.03)/7031.03 =
1963             1.482%. We can buy BTC on exchange1 for 7010 USD then sell the same amout of BTC
1964             on exchange2 for 7150 USD and still gain (7132.125-7031.03) = 101.095 USD per
1965             BTC, after trading fees. The net profit margin is 1.482% - 0.216% = 1.266%.
1966              
1967             =back
1968              
1969             =head1 INTERNAL NOTES
1970              
1971             The cryp app family uses L<Perinci::CmdLine::cryp> which puts cryp-specific
1972             information from the configuration into the $r->{_cryp} hash:
1973              
1974             $r->{_cryp}
1975             {arbit_strategies} # from [arbit-strategy/XXX] config sections
1976             {exchanges} # from [exchange/XXX(/YYY)?] config sections
1977             {masternodes} # from [masternode/XXX(/YYY)?] config sections
1978             {wallet} # from [wallet/COIN]
1979              
1980             Routines inside this module communicate with one another either using the
1981             database (obviously), or by putting stuffs in C<$r> (the request hash/stash) and
1982             passing C<$r> around. The keys that are used by routines in this module:
1983              
1984             $r->{_stash}
1985             {dbh}
1986             {account_balances} # key=exchange safename, value={currency1 => [{account=>account1, account_id=>aid, available=>..., ...}, {...}]}. value->{currency} sorted by largest available balance first
1987             {account_exchanges} # key=exchange safename, value={account1 => 1, ...}
1988             {account_ids} # key=exchange safename, value={account1 => numeric ID from db, ...}
1989             {base_currencies} # target (crypto)currencies to arbitrage
1990             {exchange_clients} # key=exchange safename, value={account1 => $client1, ...}
1991             {exchange_ids} # key=exchange safename, value=exchange (numeric) ID from db
1992             {exchange_recs} # key=exchange safename, value=hash (from CryptoExchange::Catalog)
1993             {exchange_coins} # key=exchange safename, value=[COIN1, COIN2, ...]
1994             {exchange_pairs} # key=exchange safename, value=[{name=>PAIR1, min_base_size=>..., min_quote_size=>...}, ...]
1995             {forex_rates} # key=currency pair (e.g. IDR/USD), val=exchange rate (avg rate)
1996             {forex_spreads} # key=fiat currency pair, e.g. USD/IDR, value=percentage
1997             {fx} # key=currency value=result from get_spot_rate()
1998             {order_pairs} # result from calculate_order_pairs()
1999             {quote_currencies} # what currencies we use to buy/sell the base currencies
2000             {quote_currencies_for} # key=base currency, value={quotecurrency1 => 1, quotecurrency2=>1, ...}
2001             {trading_fees} # key=exchange safename, value={coin1=>num (in percent) market taker fees, ...}, ':default' for all other coins, ':default' for all other exchanges
2002              
2003             =head1 FUNCTIONS
2004              
2005              
2006             =head2 arbit
2007              
2008             Usage:
2009              
2010             arbit(%args) -> [$status_code, $reason, $payload, \%result_meta]
2011              
2012             Perform arbitrage.
2013              
2014             This utility monitors prices of several cryptocurrencies ("base currencies",
2015             e.g. LTC) in several cryptoexchanges. The "quote currency" can be fiat (e.g.
2016             USD, all other fiat currencies will be converted to USD) or another
2017             cryptocurrency (usually BTC).
2018              
2019             When it detects a net price difference for a base currency that is large enough
2020             (see C<min_net_profit_margin> option), it will perform a buy order on the
2021             exchange that has the lower price and sell the exact same amount of base
2022             currency on the exchange that has the higher price. For example, if on XCHG1 the
2023             buy price of LTC 100.01 USD and on XCHG2 the sell price of LTC is 98.80 USD,
2024             then this utility will buy LTC on XCHG2 for 98.80 USD and sell the same amount
2025             of LTD on XCHG1 for 100.01 USD. The profit is (100.01 - 98.80 - trading fees)
2026             per LTC arbitraged. You have to maintain enough LTC balance on XCHG1 and enough
2027             USD balance on XCHG2.
2028              
2029             The balances are called inventories or your working capital. You fill and
2030             transfer inventories manually to refill balances and/or to collect profits.
2031              
2032             This function is not exported.
2033              
2034             This function supports dry-run operation.
2035              
2036              
2037             Arguments ('*' denotes required arguments):
2038              
2039             =over 4
2040              
2041             =item * B<accounts> => I<array[cryptoexchange::account]>
2042              
2043             Cryptoexchange accounts.
2044              
2045             There should at least be two accounts, on at least two different
2046             cryptoexchanges. If not specified, all accounts listed on the configuration file
2047             will be included. Note that it's possible to include two or more accounts on the
2048             same cryptoexchange.
2049              
2050             =item * B<base_currencies> => I<array[cryptocurrency]>
2051              
2052             Target (crypto)currencies to arbitrate.
2053              
2054             If not specified, will list all supported pairs on all the exchanges and include
2055             the base cryptocurrencies that are listed on at least 2 different exchanges (for
2056             arbitrage possibility).
2057              
2058             =item * B<db_name>* => I<str>
2059              
2060             =item * B<db_password> => I<str>
2061              
2062             =item * B<db_username> => I<str>
2063              
2064             =item * B<frequency> => I<posint> (default: 30)
2065              
2066             How many seconds to wait between rounds (in seconds).
2067              
2068             A round consists of checking prices and then creating arbitraging order pairs.
2069              
2070             =item * B<max_order_age> => I<posint> (default: 86400)
2071              
2072             How long should we wait for orders to be completed before cancelling them (in seconds).
2073              
2074             Sometimes because of rapid trading and price movement, our order might not be
2075             filled immediately. This setting sets a limit on how long should an order be
2076             left open. After this limit is reached, we cancel the order. The imbalance of
2077             the arbitrage transaction will be recorded.
2078              
2079             =item * B<max_order_pairs_per_round> => I<posint>
2080              
2081             Maximum number of order pairs to create per round.
2082              
2083             =item * B<max_order_quote_size> => I<float> (default: 100)
2084              
2085             What is the maximum amount of a single order.
2086              
2087             A single order will be limited to not be above this value (in quote currency,
2088             which if fiat will be converted to USD). This is the amount for the buying
2089             (because an arbitrage transaction is comprised of a pair of orders, where one
2090             order is a selling order at a higher quote currency size than the buying order).
2091              
2092             For example if you are arbitraging BTC against USD and IDR, and set this option
2093             to 75, then orders will not be above 75 USD. If you are arbitraging LTC against
2094             BTC and set this to 0.03 then orders will not be above 0.03 BTC.
2095              
2096             Suggestion: If you set this option too high, a few orders can use up your
2097             inventory (and you might not be getting optimal profit percentage). Also, large
2098             orders can take a while (or too long) to fill. If you set this option too low,
2099             you will hit the exchanges' minimum order size and no orders can be created.
2100             Since we want smaller risk of orders not getting filled quickly, we want small
2101             order sizes. The optimum number range a little above the exchanges' minimum
2102             order size.
2103              
2104             =item * B<min_account_balances> => I<hash>
2105              
2106             What are the minimum account balances.
2107              
2108             =item * B<min_net_profit_margin> => I<float> (default: 0)
2109              
2110             Minimum net profit margin that will trigger an arbitrage trading, in percentage.
2111              
2112             Below this percentage number, no order pairs will be sent to the exchanges to do
2113             the arbitrage. Note that the net profit margin already takes into account
2114             trading fees and forex spread (see Glossary section for more details and
2115             illustration).
2116              
2117             Suggestion: If you set this option too high, there might not be any order pairs
2118             possible. If you set this option too low, you will be getting too thin profits.
2119             Run C<cryp-arbit opportunities> or C<cryp-arbit arbit --dry-run> for a while to
2120             see what the average percentage is and then decide at which point you want to
2121             perform arbitrage.
2122              
2123             =item * B<quote_currencies> => I<array[fiat_or_cryptocurrency]>
2124              
2125             The currencies to exchange (buyE<sol>sell) the target currencies.
2126              
2127             You can have fiat currencies as the quote currencies, to buy/sell the target
2128             (base) currencies during arbitrage. For example, to arbitrage LTC against USD
2129             and IDR, C<base_currencies> is ['BTC'] and C<quote_currencies> is ['USD', 'IDR'].
2130              
2131             You can also arbitrage cryptocurrencies against other cryptocurrency (usually
2132             BTC, "the USD of cryptocurrencies"). For example, to arbitrage XMR and LTC
2133             against BTC, C<base_currencies> is ['XMR', 'LTC'] and C<quote_currencies> is
2134             ['BTC'].
2135              
2136             =item * B<rounds> => I<int> (default: 1)
2137              
2138             How many rounds.
2139              
2140             -1 means unlimited.
2141              
2142             =item * B<strategy> => I<str> (default: "merge_order_book")
2143              
2144             Which strategy to use for arbitration.
2145              
2146             Strategy is implemented in a C<App::cryp::arbit::Strategy::*> perl module.
2147              
2148              
2149             =back
2150              
2151             Special arguments:
2152              
2153             =over 4
2154              
2155             =item * B<-dry_run> => I<bool>
2156              
2157             Pass -dry_run=E<gt>1 to enable simulation mode.
2158              
2159             =back
2160              
2161             Returns an enveloped result (an array).
2162              
2163             First element ($status_code) is an integer containing HTTP-like status code
2164             (200 means OK, 4xx caller error, 5xx function error). Second element
2165             ($reason) is a string containing error message, or something like "OK" if status is
2166             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
2167             element (%result_meta) is called result metadata and is optional, a hash
2168             that contains extra information, much like how HTTP response headers provide additional metadata.
2169              
2170             Return value: (any)
2171              
2172              
2173              
2174             =head2 check_orders
2175              
2176             Usage:
2177              
2178             check_orders(%args) -> [$status_code, $reason, $payload, \%result_meta]
2179              
2180             Check the orders that have been created.
2181              
2182             This subcommand will check the orders that have been created previously by
2183             C<arbit> subcommand. It will update the order status and filled size (if still
2184             open). It will cancel (give up) the orders if deemed too old.
2185              
2186             This function is not exported.
2187              
2188             Arguments ('*' denotes required arguments):
2189              
2190             =over 4
2191              
2192             =item * B<db_name>* => I<str>
2193              
2194             =item * B<db_password> => I<str>
2195              
2196             =item * B<db_username> => I<str>
2197              
2198             =item * B<max_order_age> => I<posint> (default: 86400)
2199              
2200             How long should we wait for orders to be completed before cancelling them (in seconds).
2201              
2202             Sometimes because of rapid trading and price movement, our order might not be
2203             filled immediately. This setting sets a limit on how long should an order be
2204             left open. After this limit is reached, we cancel the order. The imbalance of
2205             the arbitrage transaction will be recorded.
2206              
2207              
2208             =back
2209              
2210             Returns an enveloped result (an array).
2211              
2212             First element ($status_code) is an integer containing HTTP-like status code
2213             (200 means OK, 4xx caller error, 5xx function error). Second element
2214             ($reason) is a string containing error message, or something like "OK" if status is
2215             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
2216             element (%result_meta) is called result metadata and is optional, a hash
2217             that contains extra information, much like how HTTP response headers provide additional metadata.
2218              
2219             Return value: (any)
2220              
2221              
2222              
2223             =head2 collect_orderbooks
2224              
2225             Usage:
2226              
2227             collect_orderbooks(%args) -> [$status_code, $reason, $payload, \%result_meta]
2228              
2229             Collect orderbooks into the database.
2230              
2231             This utility collect orderbooks from exchanges and put it into the database. The
2232             data can be used later e.g. for backtesting.
2233              
2234             This function is not exported.
2235              
2236             Arguments ('*' denotes required arguments):
2237              
2238             =over 4
2239              
2240             =item * B<accounts> => I<array[cryptoexchange::account]>
2241              
2242             Cryptoexchange accounts.
2243              
2244             There should at least be two accounts, on at least two different
2245             cryptoexchanges. If not specified, all accounts listed on the configuration file
2246             will be included. Note that it's possible to include two or more accounts on the
2247             same cryptoexchange.
2248              
2249             =item * B<base_currencies> => I<array[cryptocurrency]>
2250              
2251             Target (crypto)currencies to arbitrate.
2252              
2253             If not specified, will list all supported pairs on all the exchanges and include
2254             the base cryptocurrencies that are listed on at least 2 different exchanges (for
2255             arbitrage possibility).
2256              
2257             =item * B<db_name>* => I<str>
2258              
2259             =item * B<db_password> => I<str>
2260              
2261             =item * B<db_username> => I<str>
2262              
2263             =item * B<frequency> => I<posint> (default: 30)
2264              
2265             How many seconds to wait between rounds (in seconds).
2266              
2267             =item * B<quote_currencies> => I<array[fiat_or_cryptocurrency]>
2268              
2269             The currencies to exchange (buyE<sol>sell) the target currencies.
2270              
2271             You can have fiat currencies as the quote currencies, to buy/sell the target
2272             (base) currencies during arbitrage. For example, to arbitrage LTC against USD
2273             and IDR, C<base_currencies> is ['BTC'] and C<quote_currencies> is ['USD', 'IDR'].
2274              
2275             You can also arbitrage cryptocurrencies against other cryptocurrency (usually
2276             BTC, "the USD of cryptocurrencies"). For example, to arbitrage XMR and LTC
2277             against BTC, C<base_currencies> is ['XMR', 'LTC'] and C<quote_currencies> is
2278             ['BTC'].
2279              
2280              
2281             =back
2282              
2283             Returns an enveloped result (an array).
2284              
2285             First element ($status_code) is an integer containing HTTP-like status code
2286             (200 means OK, 4xx caller error, 5xx function error). Second element
2287             ($reason) is a string containing error message, or something like "OK" if status is
2288             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
2289             element (%result_meta) is called result metadata and is optional, a hash
2290             that contains extra information, much like how HTTP response headers provide additional metadata.
2291              
2292             Return value: (any)
2293              
2294              
2295              
2296             =head2 dump_cryp_config
2297              
2298             Usage:
2299              
2300             dump_cryp_config() -> [$status_code, $reason, $payload, \%result_meta]
2301              
2302             This function is not exported.
2303              
2304             No arguments.
2305              
2306             Returns an enveloped result (an array).
2307              
2308             First element ($status_code) is an integer containing HTTP-like status code
2309             (200 means OK, 4xx caller error, 5xx function error). Second element
2310             ($reason) is a string containing error message, or something like "OK" if status is
2311             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
2312             element (%result_meta) is called result metadata and is optional, a hash
2313             that contains extra information, much like how HTTP response headers provide additional metadata.
2314              
2315             Return value: (any)
2316              
2317              
2318              
2319             =head2 get_profit_report
2320              
2321             Usage:
2322              
2323             get_profit_report(%args) -> [$status_code, $reason, $payload, \%result_meta]
2324              
2325             Get profit report.
2326              
2327             This function is not exported.
2328              
2329             Arguments ('*' denotes required arguments):
2330              
2331             =over 4
2332              
2333             =item * B<db_name>* => I<str>
2334              
2335             =item * B<db_password> => I<str>
2336              
2337             =item * B<db_username> => I<str>
2338              
2339             =item * B<detail> => I<bool>
2340              
2341             =item * B<time_end> => I<date>
2342              
2343             =item * B<time_start> => I<date>
2344              
2345             =item * B<usd_rates> => I<hash>
2346              
2347             Set USD rates.
2348              
2349             Example:
2350              
2351             --usd-rate IDR=14500 --usd-rate THB=33.25
2352              
2353              
2354             =back
2355              
2356             Returns an enveloped result (an array).
2357              
2358             First element ($status_code) is an integer containing HTTP-like status code
2359             (200 means OK, 4xx caller error, 5xx function error). Second element
2360             ($reason) is a string containing error message, or something like "OK" if status is
2361             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
2362             element (%result_meta) is called result metadata and is optional, a hash
2363             that contains extra information, much like how HTTP response headers provide additional metadata.
2364              
2365             Return value: (any)
2366              
2367              
2368              
2369             =head2 list_order_pairs
2370              
2371             Usage:
2372              
2373             list_order_pairs(%args) -> [$status_code, $reason, $payload, \%result_meta]
2374              
2375             List created order pairs.
2376              
2377             This function is not exported.
2378              
2379             Arguments ('*' denotes required arguments):
2380              
2381             =over 4
2382              
2383             =item * B<db_name>* => I<str>
2384              
2385             =item * B<db_password> => I<str>
2386              
2387             =item * B<db_username> => I<str>
2388              
2389             =item * B<open> => I<bool>
2390              
2391             =item * B<time_end> => I<date>
2392              
2393             =item * B<time_start> => I<date>
2394              
2395              
2396             =back
2397              
2398             Returns an enveloped result (an array).
2399              
2400             First element ($status_code) is an integer containing HTTP-like status code
2401             (200 means OK, 4xx caller error, 5xx function error). Second element
2402             ($reason) is a string containing error message, or something like "OK" if status is
2403             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
2404             element (%result_meta) is called result metadata and is optional, a hash
2405             that contains extra information, much like how HTTP response headers provide additional metadata.
2406              
2407             Return value: (any)
2408              
2409              
2410              
2411             =head2 show_opportunities
2412              
2413             Usage:
2414              
2415             show_opportunities(%args) -> [$status_code, $reason, $payload, \%result_meta]
2416              
2417             Show arbitrage opportunities.
2418              
2419             This subcommand, like the C<arbit> subcommand, checks prices of cryptocurrencies
2420             on several exchanges for arbitrage possibility; but does not actually perform
2421             the arbitraging.
2422              
2423             This function is not exported.
2424              
2425             Arguments ('*' denotes required arguments):
2426              
2427             =over 4
2428              
2429             =item * B<accounts> => I<array[cryptoexchange::account]>
2430              
2431             Cryptoexchange accounts.
2432              
2433             There should at least be two accounts, on at least two different
2434             cryptoexchanges. If not specified, all accounts listed on the configuration file
2435             will be included. Note that it's possible to include two or more accounts on the
2436             same cryptoexchange.
2437              
2438             =item * B<base_currencies> => I<array[cryptocurrency]>
2439              
2440             Target (crypto)currencies to arbitrate.
2441              
2442             If not specified, will list all supported pairs on all the exchanges and include
2443             the base cryptocurrencies that are listed on at least 2 different exchanges (for
2444             arbitrage possibility).
2445              
2446             =item * B<db_name>* => I<str>
2447              
2448             =item * B<db_password> => I<str>
2449              
2450             =item * B<db_username> => I<str>
2451              
2452             =item * B<ignore_balance> => I<bool> (default: 0)
2453              
2454             Ignore account balances.
2455              
2456             =item * B<ignore_min_order_size> => I<bool> (default: 0)
2457              
2458             Ignore minimum order size limitation from exchanges.
2459              
2460             =item * B<max_order_pairs_per_round> => I<posint>
2461              
2462             Maximum number of order pairs to create per round.
2463              
2464             =item * B<max_order_quote_size> => I<float> (default: 100)
2465              
2466             What is the maximum amount of a single order.
2467              
2468             A single order will be limited to not be above this value (in quote currency,
2469             which if fiat will be converted to USD). This is the amount for the buying
2470             (because an arbitrage transaction is comprised of a pair of orders, where one
2471             order is a selling order at a higher quote currency size than the buying order).
2472              
2473             For example if you are arbitraging BTC against USD and IDR, and set this option
2474             to 75, then orders will not be above 75 USD. If you are arbitraging LTC against
2475             BTC and set this to 0.03 then orders will not be above 0.03 BTC.
2476              
2477             Suggestion: If you set this option too high, a few orders can use up your
2478             inventory (and you might not be getting optimal profit percentage). Also, large
2479             orders can take a while (or too long) to fill. If you set this option too low,
2480             you will hit the exchanges' minimum order size and no orders can be created.
2481             Since we want smaller risk of orders not getting filled quickly, we want small
2482             order sizes. The optimum number range a little above the exchanges' minimum
2483             order size.
2484              
2485             =item * B<min_account_balances> => I<hash>
2486              
2487             What are the minimum account balances.
2488              
2489             =item * B<min_net_profit_margin> => I<float> (default: 0)
2490              
2491             Minimum net profit margin that will trigger an arbitrage trading, in percentage.
2492              
2493             Below this percentage number, no order pairs will be sent to the exchanges to do
2494             the arbitrage. Note that the net profit margin already takes into account
2495             trading fees and forex spread (see Glossary section for more details and
2496             illustration).
2497              
2498             Suggestion: If you set this option too high, there might not be any order pairs
2499             possible. If you set this option too low, you will be getting too thin profits.
2500             Run C<cryp-arbit opportunities> or C<cryp-arbit arbit --dry-run> for a while to
2501             see what the average percentage is and then decide at which point you want to
2502             perform arbitrage.
2503              
2504             =item * B<quote_currencies> => I<array[fiat_or_cryptocurrency]>
2505              
2506             The currencies to exchange (buyE<sol>sell) the target currencies.
2507              
2508             You can have fiat currencies as the quote currencies, to buy/sell the target
2509             (base) currencies during arbitrage. For example, to arbitrage LTC against USD
2510             and IDR, C<base_currencies> is ['BTC'] and C<quote_currencies> is ['USD', 'IDR'].
2511              
2512             You can also arbitrage cryptocurrencies against other cryptocurrency (usually
2513             BTC, "the USD of cryptocurrencies"). For example, to arbitrage XMR and LTC
2514             against BTC, C<base_currencies> is ['XMR', 'LTC'] and C<quote_currencies> is
2515             ['BTC'].
2516              
2517             =item * B<strategy> => I<str> (default: "merge_order_book")
2518              
2519             Which strategy to use for arbitration.
2520              
2521             Strategy is implemented in a C<App::cryp::arbit::Strategy::*> perl module.
2522              
2523              
2524             =back
2525              
2526             Returns an enveloped result (an array).
2527              
2528             First element ($status_code) is an integer containing HTTP-like status code
2529             (200 means OK, 4xx caller error, 5xx function error). Second element
2530             ($reason) is a string containing error message, or something like "OK" if status is
2531             200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
2532             element (%result_meta) is called result metadata and is optional, a hash
2533             that contains extra information, much like how HTTP response headers provide additional metadata.
2534              
2535             Return value: (any)
2536              
2537             =head1 HOMEPAGE
2538              
2539             Please visit the project's homepage at L<https://metacpan.org/release/App-cryp-arbit>.
2540              
2541             =head1 SOURCE
2542              
2543             Source repository is at L<https://github.com/perlancar/perl-App-cryp-arbit>.
2544              
2545             =head1 BUGS
2546              
2547             Please report any bugs or feature requests on the bugtracker website L<https://github.com/perlancar/perl-App-cryp-arbit/issues>
2548              
2549             When submitting a bug or request, please include a test-file or a
2550             patch to an existing test-file that illustrates the bug or desired
2551             feature.
2552              
2553             =head1 SEE ALSO
2554              
2555             =head1 AUTHOR
2556              
2557             perlancar <perlancar@cpan.org>
2558              
2559             =head1 COPYRIGHT AND LICENSE
2560              
2561             This software is copyright (c) 2021, 2018 by perlancar@cpan.org.
2562              
2563             This is free software; you can redistribute it and/or modify it under
2564             the same terms as the Perl 5 programming language system itself.
2565              
2566             =cut