@@ -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; | |||
} | |||
} |
@@ -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; | |||
} |
@@ -0,0 +1,43 @@ | |||
<?php | |||
declare(strict_types=1); | |||
namespace App\Arbitration\Inner\Receipt; | |||
use App\Exchanges\MarketInterface; | |||
use App\Utils\Money\Money; | |||
class Deal | |||
{ | |||
private MarketInterface $market; | |||
private Money $bid; | |||
private Money $ask; | |||
public function __construct( | |||
MarketInterface $market, | |||
Money $bid, | |||
Money $ask | |||
) | |||
{ | |||
$this->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; | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
<?php | |||
declare(strict_types=1); | |||
namespace App\Arbitration\Inner\Receipt; | |||
class Receipt | |||
{ | |||
/** | |||
* @var Deal[] | |||
*/ | |||
private array $deals; | |||
public function __construct(Deal ...$deals) | |||
{ | |||
$this->deals = $deals; | |||
} | |||
/** | |||
* @return Deal[] | |||
*/ | |||
public function getDeals() | |||
{ | |||
return $this->deals; | |||
} | |||
} |
@@ -0,0 +1,223 @@ | |||
<?php | |||
declare(strict_types=1); | |||
namespace App\Arbitration\Inner\ReceiptCalculus; | |||
use App\Arbitration\Inner\ArbitrationChain; | |||
use App\Arbitration\Inner\Receipt\Deal; | |||
use App\Arbitration\Inner\Receipt\Receipt; | |||
use App\Exchanges\MarketInterface; | |||
use App\Utils\Money\Currency; | |||
use App\Utils\Money\Money; | |||
use App\Utils\Order\Order; | |||
use App\Utils\Wallet\WalletInterface; | |||
class Calculator | |||
{ | |||
private WalletInterface $wallet; | |||
public function __construct(WalletInterface $wallet) | |||
{ | |||
$this->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'] | |||
); | |||
} | |||
} |
@@ -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); | |||
} | |||
/** | |||
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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) | |||
]); | |||
} | |||
} |
@@ -33,6 +33,11 @@ class Currency | |||
return $this->code; | |||
} | |||
public function getPrecision(): int | |||
{ | |||
return $this->precision; | |||
} | |||
public function __toString() | |||
{ | |||
return $this->code; | |||
@@ -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); | |||
} | |||
} |
@@ -0,0 +1,32 @@ | |||
<?php | |||
declare(strict_types=1); | |||
namespace App\Utils\Wallet; | |||
use App\Utils\Money\Currency; | |||
use App\Utils\Money\Money; | |||
class Wallet implements WalletInterface | |||
{ | |||
private array $monies; | |||
public function __construct(Money ...$monies) | |||
{ | |||
$this->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); | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
<?php | |||
declare(strict_types=1); | |||
namespace App\Utils\Wallet; | |||
use App\Utils\Money\Currency; | |||
use App\Utils\Money\Money; | |||
interface WalletInterface | |||
{ | |||
public function getMoneyAmount(Currency $currency): Money; | |||
} |