From f7cf9e1a83e18e4198e8c33df3d889e71da6cb26 Mon Sep 17 00:00:00 2001 From: komarov Date: Sun, 26 Apr 2020 03:14:16 +0300 Subject: [PATCH] [WIP] naive calculator --- .../Arbitration/Inner/ArbitrationChain.php | 25 +- .../Inner/ChainProviderInterface.php | 4 +- .../src/Arbitration/Inner/Receipt/Deal.php | 43 ++++ .../src/Arbitration/Inner/Receipt/Receipt.php | 28 +++ .../Inner/ReceiptCalculus/Calculator.php | 223 ++++++++++++++++++ .../Stuff/ArbitrationMarketChainsFinder.php | 9 +- project/src/Exchanges/ExchangeProvider.php | 14 ++ project/src/Exchanges/Kuna/KunaMarket.php | 21 +- project/src/Exchanges/MarketInterface.php | 5 +- .../Arbitration/Inner/ChainProvider.php | 50 ++-- project/src/Utils/Money/Currency.php | 5 + project/src/Utils/Money/Money.php | 27 +++ project/src/Utils/Wallet/Wallet.php | 32 +++ project/src/Utils/Wallet/WalletInterface.php | 16 ++ 14 files changed, 475 insertions(+), 27 deletions(-) create mode 100644 project/src/Arbitration/Inner/Receipt/Deal.php create mode 100644 project/src/Arbitration/Inner/Receipt/Receipt.php create mode 100644 project/src/Arbitration/Inner/ReceiptCalculus/Calculator.php create mode 100644 project/src/Utils/Wallet/Wallet.php create mode 100644 project/src/Utils/Wallet/WalletInterface.php diff --git a/project/src/Arbitration/Inner/ArbitrationChain.php b/project/src/Arbitration/Inner/ArbitrationChain.php index f50b174..ffc2fcc 100644 --- a/project/src/Arbitration/Inner/ArbitrationChain.php +++ b/project/src/Arbitration/Inner/ArbitrationChain.php @@ -6,7 +6,9 @@ declare(strict_types=1); namespace App\Arbitration\Inner; +use App\Exchanges\ExchangeInterface; use App\Exchanges\MarketInterface; +use App\Utils\Money\Currency; class ArbitrationChain { @@ -15,10 +17,16 @@ class ArbitrationChain * @var MarketInterface[] */ private array $markets; + private ExchangeInterface $exchange; + private ?Currency $base = null; - public function __construct(MarketInterface ...$markets) + public function __construct( + ExchangeInterface $exchange, + MarketInterface ...$markets + ) { $this->markets = $markets; + $this->exchange = $exchange; } /** @@ -28,4 +36,19 @@ class ArbitrationChain { return $this->markets; } + + public function getExchange(): ExchangeInterface + { + return $this->exchange; + } + + public function getBase(): ?Currency + { + return $this->base; + } + + public function setBase(?Currency $base): void + { + $this->base = $base; + } } \ No newline at end of file diff --git a/project/src/Arbitration/Inner/ChainProviderInterface.php b/project/src/Arbitration/Inner/ChainProviderInterface.php index 646392e..f903d25 100644 --- a/project/src/Arbitration/Inner/ChainProviderInterface.php +++ b/project/src/Arbitration/Inner/ChainProviderInterface.php @@ -16,7 +16,7 @@ interface ChainProviderInterface */ public function getChainsForExchange(ExchangeInterface $exchange): array; - public function addChain(ExchangeInterface $exchange, ArbitrationChain $chain): void; + public function addChain(ArbitrationChain $chain): void; - public function removeChain(ExchangeInterface $exchange, ArbitrationChain $chain): void; + public function removeChain(ArbitrationChain $chain): void; } \ No newline at end of file diff --git a/project/src/Arbitration/Inner/Receipt/Deal.php b/project/src/Arbitration/Inner/Receipt/Deal.php new file mode 100644 index 0000000..1fd77a1 --- /dev/null +++ b/project/src/Arbitration/Inner/Receipt/Deal.php @@ -0,0 +1,43 @@ +market = $market; + $this->bid = $bid; + $this->ask = $ask; + } + + public function getBid(): Money + { + return $this->bid; + } + + public function getAsk(): Money + { + return $this->ask; + } + + public function getMarket(): MarketInterface + { + return $this->market; + } +} \ No newline at end of file diff --git a/project/src/Arbitration/Inner/Receipt/Receipt.php b/project/src/Arbitration/Inner/Receipt/Receipt.php new file mode 100644 index 0000000..4d79ada --- /dev/null +++ b/project/src/Arbitration/Inner/Receipt/Receipt.php @@ -0,0 +1,28 @@ +deals = $deals; + } + + /** + * @return Deal[] + */ + public function getDeals() + { + return $this->deals; + } +} \ No newline at end of file diff --git a/project/src/Arbitration/Inner/ReceiptCalculus/Calculator.php b/project/src/Arbitration/Inner/ReceiptCalculus/Calculator.php new file mode 100644 index 0000000..f6d6056 --- /dev/null +++ b/project/src/Arbitration/Inner/ReceiptCalculus/Calculator.php @@ -0,0 +1,223 @@ +wallet = $wallet; + } + + public function calculateArbitrationReceipt(ArbitrationChain $chain): ?Receipt + { + $base = $chain->getBase(); + if (!$base) { + return null; + } + + $markets = $chain->getMarkets(); + $baseMarkets = array_filter( + $markets, + function (MarketInterface $market) use($base): bool + { + $pair = $market->getPair(); + return $pair->getBase()->isEqual($base) || $pair->getQuote()->isEqual($base); + } + ); + + foreach ($baseMarkets as $baseMarket) { + usort( + $markets, + function (MarketInterface $a, MarketInterface $b) use($baseMarket) { + if ($a === $baseMarket) { + return 1; + } + + if ($b === $baseMarket) { + return -1; + } + + return 0; + } + ); + $orders = $this->getOrdersAgainstBase($markets, $base); + $receipt = $this->build($orders, $base); + + if ($receipt) { + return $receipt; + } + } + + return null; + } + + /** + * @param MarketInterface[] $markets + * @param Currency $base + * @return array + */ + public function getOrdersAgainstBase(array $markets, Currency $base): array + { + $current = $base; + $result = []; + while (\count($result) < \count($markets)) { + foreach ($markets as $market) { + $pair = $market->getPair(); + if ($pair->getBase()->isEqual($current)) { + $orders = $market->getOrderBook()->getBids(); + $current = $pair->getQuote(); + } elseif ($pair->getQuote()->isEqual($current)) { + $orders = $market->getOrderBook()->getBids(); + $current = $pair->getBase(); + } else { + continue; + } + + $result[] = [ + 'market' => $market, + 'orders' => $orders, + 'buy_currency' => $current + ]; + + break; + } + } + + return $result; + } + + + protected function build(array $ordersData, Currency $base): ?Receipt + { + $available = $this->wallet->getMoneyAmount($base); + + $receiptDeals = []; + foreach ($ordersData as $orderData) { + $buyAmount = $this->calculateAmount( + $ordersData['orders'], + $ordersData['buy_currency'], + $available + ); + + if (null === $available) { + return null; + } + + /** + * @var MarketInterface $market + */ + $market = $ordersData['market']; + $receiptDeals[] = new Deal( + $market, + $available, + $buyAmount + ); + + $available = new Money( + bcmul( + $buyAmount->getAmount(), + (string)(1 - $market->getFee()), + $buyAmount->getCurrency()->getPrecision() + ), + $buyAmount->getCurrency() + ); + } + /** + * @var Deal $last + */ + $last = end($receiptDeals); + + if ($this->wallet->getMoneyAmount($base)->compare($last->getAsk()) <= 0) { + return null; + } + + return new Receipt(...$receiptDeals); + } + + protected function calculateAmount(array $orders, Currency $buy, Money $maxAmount): ?Money + { + usort( + $orders, + function (Order $a, Order $b): int + { + return bccomp( + bcdiv( + $a->getFrom()->getAmount(), + $a->getTo()->getAmount(), + max($a->getFrom()->getCurrency()->getPrecision(), $a->getTo()->getCurrency()->getPrecision()) + ), + bcdiv( + $b->getFrom()->getAmount(), + $b->getTo()->getAmount(), + max($b->getFrom()->getCurrency()->getPrecision(), $b->getTo()->getCurrency()->getPrecision()) + ), + max( + $a->getFrom()->getCurrency()->getPrecision(), + $a->getTo()->getCurrency()->getPrecision(), + $b->getFrom()->getCurrency()->getPrecision(), + $b->getTo()->getCurrency()->getPrecision(), + ) + ); + } + ); + + $currency = new Money("0", $buy); + $precision = max($maxAmount->getCurrency()->getPrecision(), $buy->getPrecision()); + $highestPrice = null; + foreach ($orders as $order) { + /** + * @var Order $order + */ + $currency->add($order->getTo()); + $highestPrice = new Money( + bcdiv( + $order->getFrom()->getAmount(), + $order->getTo()->getAmount(), + $precision + ), + $precision + ); + $total = new Money( + bcmul( + $currency->getAmount(), + $highestPrice->getAmount(), + $precision + ), + $order->getFrom()->getCurrency() + ); + if ($total->compare($maxAmount) <= 0) { + break; + } + } + + if (null === $highestPrice) { + return null; + } + + return new Money( + bcdiv( + $maxAmount->getAmount(), + $highestPrice->getAmount(), + $precision + ), + $orders[0]['buy_currency'] + ); + } +} \ No newline at end of file diff --git a/project/src/Arbitration/Inner/Stuff/ArbitrationMarketChainsFinder.php b/project/src/Arbitration/Inner/Stuff/ArbitrationMarketChainsFinder.php index 35e81bf..2eb811a 100644 --- a/project/src/Arbitration/Inner/Stuff/ArbitrationMarketChainsFinder.php +++ b/project/src/Arbitration/Inner/Stuff/ArbitrationMarketChainsFinder.php @@ -48,7 +48,12 @@ class ArbitrationMarketChainsFinder return $result; } - protected function cycleToChain(array $cycle, array $rows, array $markets): ArbitrationChain + protected function cycleToChain( + ExchangeInterface $exchange, + array $cycle, + array $rows, + array $markets + ): ArbitrationChain { $cycleMarkets = []; for($i = 0; $i < \count($cycle); $i++) { @@ -80,7 +85,7 @@ class ArbitrationMarketChainsFinder } } - return new ArbitrationChain(...$cycleMarkets); + return new ArbitrationChain($exchange, ...$cycleMarkets); } /** diff --git a/project/src/Exchanges/ExchangeProvider.php b/project/src/Exchanges/ExchangeProvider.php index 9035ad5..5d33923 100644 --- a/project/src/Exchanges/ExchangeProvider.php +++ b/project/src/Exchanges/ExchangeProvider.php @@ -30,4 +30,18 @@ class ExchangeProvider { return iterator_to_array($this->exchanges->getIterator()); } + + public function getExchangeByName(string $name): ?ExchangeInterface + { + foreach ($this->exchanges as $exchange) { + /** + * @var ExchangeInterface $exchange + */ + if ($exchange->getName() === $name) { + return $exchange; + } + } + + return null; + } } \ No newline at end of file diff --git a/project/src/Exchanges/Kuna/KunaMarket.php b/project/src/Exchanges/Kuna/KunaMarket.php index 4401223..f0b5ea0 100644 --- a/project/src/Exchanges/Kuna/KunaMarket.php +++ b/project/src/Exchanges/Kuna/KunaMarket.php @@ -58,10 +58,18 @@ class KunaMarket implements MarketInterface $bids = []; foreach ($this->apiExecutor->getBook($this->id) as $orderData) { - $order = new Order( - new Money((string)$orderData[0], $this->currencyPair->getQuote()), - new Money((string)abs($orderData[1]), $this->currencyPair->getBase()), - ); + if ($orderData[1] > 0) { + $order = new Order( + new Money((string)$orderData[0], $this->currencyPair->getQuote()), + new Money((string)abs($orderData[1]), $this->currencyPair->getBase()), + ); + } else { + $order = new Order( + new Money((string)abs($orderData[1]), $this->currencyPair->getBase()), + new Money((string)$orderData[0], $this->currencyPair->getQuote()), + ); + } + $ordersCount = $orderData[2]; while ($ordersCount) { @@ -76,4 +84,9 @@ class KunaMarket implements MarketInterface return new OrderBook($bids, $asks); } + + public function getFee(): float + { + return 0.0025; + } } \ No newline at end of file diff --git a/project/src/Exchanges/MarketInterface.php b/project/src/Exchanges/MarketInterface.php index 3912b1c..e74c7e6 100644 --- a/project/src/Exchanges/MarketInterface.php +++ b/project/src/Exchanges/MarketInterface.php @@ -7,10 +7,13 @@ namespace App\Exchanges; use App\Utils\Money\CurrencyPair; +use App\Utils\Order\Book\OrderBook; interface MarketInterface { + public function getPair(): CurrencyPair; + public function getOrderBook(): OrderBook; - public function getPair(): CurrencyPair; + public function getFee(): float; } \ No newline at end of file diff --git a/project/src/Infrastucture/Arbitration/Inner/ChainProvider.php b/project/src/Infrastucture/Arbitration/Inner/ChainProvider.php index d147d97..1afa80a 100644 --- a/project/src/Infrastucture/Arbitration/Inner/ChainProvider.php +++ b/project/src/Infrastucture/Arbitration/Inner/ChainProvider.php @@ -9,7 +9,9 @@ namespace App\Infrastructure\Arbitration\Inner; use App\Arbitration\Inner\ArbitrationChain; use App\Arbitration\Inner\ChainProviderInterface; use App\Exchanges\ExchangeInterface; +use App\Exchanges\ExchangeProvider; use App\Exchanges\MarketInterface; +use App\Utils\Money\Currency; use MongoDB\Client; use MongoDB\Collection; @@ -45,18 +47,27 @@ class ChainProvider implements ChainProviderInterface } } + $base = null; + if (!empty($row['base'])) { + $base = new Currency($row['base']['code'], $row['base']['precision']); + } + if (!empty($chainMarkets)) { - $chains[] = new ArbitrationChain(...$chainMarkets); + $chain = new ArbitrationChain($exchange, ...$chainMarkets); + if ($base) { + $chain->setBase($base); + } + $chains[] = $chain; } } return $chains; } - protected function calculateChainHash(ExchangeInterface $exchange, ArbitrationChain $chain): string + protected function calculateChainHash(ArbitrationChain $chain): string { return md5( - $exchange->getName() + $chain->getExchange()->getName() . ';' . implode( ';', @@ -71,29 +82,34 @@ class ChainProvider implements ChainProviderInterface ); } - public function addChain(ExchangeInterface $exchange, ArbitrationChain $chain): void + public function addChain(ArbitrationChain $chain): void { + $base = $chain->getBase(); $this ->collection - ->insertOne([ - '_id' => $this->calculateChainHash($exchange, $chain), - 'exchange' => $exchange->getName(), - 'chain' => array_map( - static function (MarketInterface $market): string - { - return (string)$market->getPair(); - }, - $chain->getMarkets() - ) - ]); + ->updateOne( + ['_id' => $this->calculateChainHash($chain)], + [ + 'exchange' => $chain->getExchange()->getName(), + 'chain' => array_map( + static function (MarketInterface $market): string + { + return (string)$market->getPair(); + }, + $chain->getMarkets() + ), + 'base' => $base ? ['code' => $base->getCode(), 'precision' => $base->getPrecision()] : null + ], + ['upsert' => true] + ); } - public function removeChain(ExchangeInterface $exchange, ArbitrationChain $chain): void + public function removeChain(ArbitrationChain $chain): void { $this ->collection ->deleteOne([ - '_id' => $this->calculateChainHash($exchange, $chain) + '_id' => $this->calculateChainHash($chain) ]); } } \ No newline at end of file diff --git a/project/src/Utils/Money/Currency.php b/project/src/Utils/Money/Currency.php index 4da8551..6f754f4 100644 --- a/project/src/Utils/Money/Currency.php +++ b/project/src/Utils/Money/Currency.php @@ -33,6 +33,11 @@ class Currency return $this->code; } + public function getPrecision(): int + { + return $this->precision; + } + public function __toString() { return $this->code; diff --git a/project/src/Utils/Money/Money.php b/project/src/Utils/Money/Money.php index 9d86da2..5127588 100644 --- a/project/src/Utils/Money/Money.php +++ b/project/src/Utils/Money/Money.php @@ -36,4 +36,31 @@ class Money { return $this->amount; } + + public function compare(Money $money): int + { + if (!$this->currency->isEqual($money->getCurrency())) { + throw new \InvalidArgumentException('Different currencies cannot be compared'); + } + + return bccomp($this->amount, $money->getAmount(), $this->currency->getPrecision()); + } + + public function add(Money $money): Money + { + if (!$this->currency->isEqual($money->getCurrency())) { + throw new \InvalidArgumentException('Different currencies cannot added'); + } + + return new Money(bcadd($this->amount, $money->amount, $this->currency->getPrecision()), $this->currency); + } + + public function sub(Money $money): Money + { + if (!$this->currency->isEqual($money->getCurrency())) { + throw new \InvalidArgumentException('Different currencies cannot added'); + } + + return new Money(bcsub($this->amount, $money->amount, $this->currency->getPrecision()), $this->currency); + } } \ No newline at end of file diff --git a/project/src/Utils/Wallet/Wallet.php b/project/src/Utils/Wallet/Wallet.php new file mode 100644 index 0000000..ea6ea2e --- /dev/null +++ b/project/src/Utils/Wallet/Wallet.php @@ -0,0 +1,32 @@ +monies = $monies; + } + + public function getMoneyAmount(Currency $currency): Money + { + foreach ($this->monies as $money) { + if ($money->getCurrency()->isEqual($currency)) { + return $money; + } + } + + return new Money("0", $currency); + } +} \ No newline at end of file diff --git a/project/src/Utils/Wallet/WalletInterface.php b/project/src/Utils/Wallet/WalletInterface.php new file mode 100644 index 0000000..c7cd00d --- /dev/null +++ b/project/src/Utils/Wallet/WalletInterface.php @@ -0,0 +1,16 @@ +