@@ -6,7 +6,9 @@ declare(strict_types=1); | |||||
namespace App\Arbitration\Inner; | namespace App\Arbitration\Inner; | ||||
use App\Exchanges\ExchangeInterface; | |||||
use App\Exchanges\MarketInterface; | use App\Exchanges\MarketInterface; | ||||
use App\Utils\Money\Currency; | |||||
class ArbitrationChain | class ArbitrationChain | ||||
{ | { | ||||
@@ -15,10 +17,16 @@ class ArbitrationChain | |||||
* @var MarketInterface[] | * @var MarketInterface[] | ||||
*/ | */ | ||||
private array $markets; | 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->markets = $markets; | ||||
$this->exchange = $exchange; | |||||
} | } | ||||
/** | /** | ||||
@@ -28,4 +36,19 @@ class ArbitrationChain | |||||
{ | { | ||||
return $this->markets; | 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 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; | 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 = []; | $cycleMarkets = []; | ||||
for($i = 0; $i < \count($cycle); $i++) { | 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()); | 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 = []; | $bids = []; | ||||
foreach ($this->apiExecutor->getBook($this->id) as $orderData) { | 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]; | $ordersCount = $orderData[2]; | ||||
while ($ordersCount) { | while ($ordersCount) { | ||||
@@ -76,4 +84,9 @@ class KunaMarket implements MarketInterface | |||||
return new OrderBook($bids, $asks); | 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\Money\CurrencyPair; | ||||
use App\Utils\Order\Book\OrderBook; | |||||
interface MarketInterface | 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\ArbitrationChain; | ||||
use App\Arbitration\Inner\ChainProviderInterface; | use App\Arbitration\Inner\ChainProviderInterface; | ||||
use App\Exchanges\ExchangeInterface; | use App\Exchanges\ExchangeInterface; | ||||
use App\Exchanges\ExchangeProvider; | |||||
use App\Exchanges\MarketInterface; | use App\Exchanges\MarketInterface; | ||||
use App\Utils\Money\Currency; | |||||
use MongoDB\Client; | use MongoDB\Client; | ||||
use MongoDB\Collection; | 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)) { | if (!empty($chainMarkets)) { | ||||
$chains[] = new ArbitrationChain(...$chainMarkets); | |||||
$chain = new ArbitrationChain($exchange, ...$chainMarkets); | |||||
if ($base) { | |||||
$chain->setBase($base); | |||||
} | |||||
$chains[] = $chain; | |||||
} | } | ||||
} | } | ||||
return $chains; | return $chains; | ||||
} | } | ||||
protected function calculateChainHash(ExchangeInterface $exchange, ArbitrationChain $chain): string | |||||
protected function calculateChainHash(ArbitrationChain $chain): string | |||||
{ | { | ||||
return md5( | return md5( | ||||
$exchange->getName() | |||||
$chain->getExchange()->getName() | |||||
. ';' | . ';' | ||||
. implode( | . 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 | $this | ||||
->collection | ->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 | $this | ||||
->collection | ->collection | ||||
->deleteOne([ | ->deleteOne([ | ||||
'_id' => $this->calculateChainHash($exchange, $chain) | |||||
'_id' => $this->calculateChainHash($chain) | |||||
]); | ]); | ||||
} | } | ||||
} | } |
@@ -33,6 +33,11 @@ class Currency | |||||
return $this->code; | return $this->code; | ||||
} | } | ||||
public function getPrecision(): int | |||||
{ | |||||
return $this->precision; | |||||
} | |||||
public function __toString() | public function __toString() | ||||
{ | { | ||||
return $this->code; | return $this->code; | ||||
@@ -36,4 +36,31 @@ class Money | |||||
{ | { | ||||
return $this->amount; | 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; | |||||
} |