Преглед на файлове

[WIP] naive calculator

master
komarov преди 4 години
родител
ревизия
f7cf9e1a83
променени са 14 файла, в които са добавени 475 реда и са изтрити 27 реда
  1. +24
    -1
      project/src/Arbitration/Inner/ArbitrationChain.php
  2. +2
    -2
      project/src/Arbitration/Inner/ChainProviderInterface.php
  3. +43
    -0
      project/src/Arbitration/Inner/Receipt/Deal.php
  4. +28
    -0
      project/src/Arbitration/Inner/Receipt/Receipt.php
  5. +223
    -0
      project/src/Arbitration/Inner/ReceiptCalculus/Calculator.php
  6. +7
    -2
      project/src/Arbitration/Inner/Stuff/ArbitrationMarketChainsFinder.php
  7. +14
    -0
      project/src/Exchanges/ExchangeProvider.php
  8. +17
    -4
      project/src/Exchanges/Kuna/KunaMarket.php
  9. +4
    -1
      project/src/Exchanges/MarketInterface.php
  10. +33
    -17
      project/src/Infrastucture/Arbitration/Inner/ChainProvider.php
  11. +5
    -0
      project/src/Utils/Money/Currency.php
  12. +27
    -0
      project/src/Utils/Money/Money.php
  13. +32
    -0
      project/src/Utils/Wallet/Wallet.php
  14. +16
    -0
      project/src/Utils/Wallet/WalletInterface.php

+ 24
- 1
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;
}
}

+ 2
- 2
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;
}

+ 43
- 0
project/src/Arbitration/Inner/Receipt/Deal.php Целия файл

@@ -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;
}
}

+ 28
- 0
project/src/Arbitration/Inner/Receipt/Receipt.php Целия файл

@@ -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;
}
}

+ 223
- 0
project/src/Arbitration/Inner/ReceiptCalculus/Calculator.php Целия файл

@@ -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']
);
}
}

+ 7
- 2
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);
}

/**


+ 14
- 0
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;
}
}

+ 17
- 4
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;
}
}

+ 4
- 1
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;
}

+ 33
- 17
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)
]);
}
}

+ 5
- 0
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;


+ 27
- 0
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);
}
}

+ 32
- 0
project/src/Utils/Wallet/Wallet.php Целия файл

@@ -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);
}
}

+ 16
- 0
project/src/Utils/Wallet/WalletInterface.php Целия файл

@@ -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;
}

Loading…
Отказ
Запис