Browse Source

[WIP] kuna markets fetch & arbitration chains finder

master
komarov 4 years ago
parent
commit
807056cb0f
12 changed files with 499 additions and 19 deletions
  1. +1
    -0
      project/composer.json
  2. +126
    -1
      project/composer.lock
  3. +7
    -2
      project/config/services.yaml
  4. +31
    -0
      project/src/Arbitration/ArbitrationChain.php
  5. +158
    -0
      project/src/Arbitration/Inner/Stuff/ArbitrationMarketChainsFinder.php
  6. +50
    -0
      project/src/Command/TestCommand.php
  7. +16
    -7
      project/src/Exchanges/Kuna/KunaApiExecutor.php
  8. +28
    -0
      project/src/Exchanges/Kuna/KunaMarket.php
  9. +18
    -0
      project/src/Utils/Money/Currency.php
  10. +19
    -9
      project/src/Utils/Order/Book/OrderBook.php
  11. +39
    -0
      project/src/Utils/Order/Order.php
  12. +6
    -0
      project/symfony.lock

+ 1
- 0
project/composer.json View File

@@ -12,6 +12,7 @@
"symfony/dotenv": "5.0.*",
"symfony/flex": "^1.3.1",
"symfony/framework-bundle": "5.0.*",
"symfony/http-client": "5.0.*",
"symfony/yaml": "5.0.*"
},
"require-dev": {


+ 126
- 1
project/composer.lock View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ed2d4bb6d2a2abe584b5f0867281d0ab",
"content-hash": "4e5589a47c5b0dc0e0cd3d924e0621aa",
"packages": [
{
"name": "myclabs/php-enum",
@@ -1413,6 +1413,131 @@
"homepage": "https://symfony.com",
"time": "2020-03-30T11:42:42+00:00"
},
{
"name": "symfony/http-client",
"version": "v5.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "14d386ae55b699ea9a0ddb872fa5f3e35219bba8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/14d386ae55b699ea9a0ddb872fa5f3e35219bba8",
"reference": "14d386ae55b699ea9a0ddb872fa5f3e35219bba8",
"shasum": ""
},
"require": {
"php": "^7.2.5",
"psr/log": "^1.0",
"symfony/http-client-contracts": "^1.1.8|^2",
"symfony/polyfill-php73": "^1.11",
"symfony/service-contracts": "^1.0|^2"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "1.1"
},
"require-dev": {
"guzzlehttp/promises": "^1.3.1",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/http-kernel": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony HttpClient component",
"homepage": "https://symfony.com",
"time": "2020-03-27T16:56:45+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "378868b61b85c5cac6822d4f84e26999c9f2e881"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/378868b61b85c5cac6822d4f84e26999c9f2e881",
"reference": "378868b61b85c5cac6822d4f84e26999c9f2e881",
"shasum": ""
},
"require": {
"php": "^7.2.5"
},
"suggest": {
"symfony/http-client-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"time": "2019-11-26T23:25:11+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v5.0.7",


+ 7
- 2
project/config/services.yaml View File

@@ -23,5 +23,10 @@ services:
resource: '../src/Controller'
tags: ['controller.service_arguments']

# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Exchanges\Kuna\KunaSigner:
bind:
$publicKey: 'test'
$privateKey: 'test'

Http\Client\HttpClient:
class: Nyholm\HttpClient\Client

+ 31
- 0
project/src/Arbitration/ArbitrationChain.php View File

@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);


namespace App\Arbitration\Inner;


use App\Exchanges\MarketInterface;

class ArbitrationChain
{

/**
* @var MarketInterface[]
*/
private array $markets;

public function __construct(MarketInterface ...$markets)
{
$this->markets = $markets;
}

/**
* @return MarketInterface[]
*/
public function getMarkets(): array
{
return $this->markets;
}
}

+ 158
- 0
project/src/Arbitration/Inner/Stuff/ArbitrationMarketChainsFinder.php View File

@@ -0,0 +1,158 @@
<?php

declare(strict_types=1);

namespace App\Arbitration\Inner\Stuff;

use App\Arbitration\Inner\ArbitrationChain;
use App\Exchanges\ExchangeInterface;
use App\Exchanges\MarketInterface;
use App\Utils\Money\CurrencyPair;

class ArbitrationMarketChainsFinder
{

/**
* @param ExchangeInterface $exchange
* @param int $maxChainDepth
* @return ArbitrationChain[]
*/
public function findChainsInExchange(ExchangeInterface $exchange, int $maxChainDepth = 3): array
{
/**
* @var MarketInterface[] $markets
*/
$markets = $exchange->getMarkets();

[$rows, $map] = $this->buildMap($markets);

$cycles = [];
foreach (array_keys($rows) as $row) {
$rowCycles = [];
$this->findCycle($map, [$row], $rowCycles, $maxChainDepth);
$cycles[] = $this->clearDuplicates($rowCycles);
}

$cycles = array_merge(...$cycles);
$cycles = $this->clearDuplicates($cycles);

$result = [];
foreach ($cycles as $cycle) {
$result[] = $this->cycleToChain(
$cycle,
$rows,
$markets
);
}

return $result;
}

protected function cycleToChain(array $cycle, array $rows, array $markets): ArbitrationChain
{
$cycleMarkets = [];
for($i = 0; $i < \count($cycle); $i++) {
$base = $rows[$cycle[$i]];
if ($i + 1 >= \count($cycle)) {
$quote = $rows[$cycle[0]];
} else {
$quote = $rows[$cycle[$i + 1]];
}

foreach ($markets as $market) {
/**
* @var CurrencyPair $pair
*/
$pair = $market->getPair();

if (
(
$pair->getBase()->isEqual($base)
&& $pair->getQuote()->isEqual($quote)
)
|| (
$pair->getBase()->isEqual($quote)
&& $pair->getQuote()->isEqual($base)
)
) {
$cycleMarkets[] = $market;
}
}
}

return new ArbitrationChain(...$cycleMarkets);
}

/**
* @param MarketInterface[] $markets
* @return array
*/
protected function buildMap(array $markets): array
{
$map = [];
$rows = [];
foreach ($markets as $market) {
$pair = $market->getPair();

$base = $pair->getBase();
if (!in_array((string)$base, $rows)) {
$rows[] = $base;
}
$baseId = array_search((string)$base, $rows);

$quote = $pair->getQuote();
if (!in_array((string)$quote, $rows)) {
$rows[] = $quote;
}
$quoteId = array_search((string)$quote, $rows);

$map[$quoteId][$baseId] = 1;
$map[$baseId][$quoteId] = 1;
}

return [$rows, $map];
}

protected function clearDuplicates(array $cycles): array
{
$orderedCycles = [];
foreach ($cycles as $i => $cycle) {
$orderedCycle = $cycle;
sort($orderedCycle);
$orderedCycle = implode(',', $orderedCycle);
if (in_array($orderedCycle, $orderedCycles)) {
unset($cycles[$i]);
continue;
}
$orderedCycles[] = $orderedCycle;
}

return $cycles;
}

protected function findCycle(array $map, array $path, array &$result, int $maxDepth)
{
if (\count($path) > $maxDepth) {
return;
}
$key = $path[\count($path) - 1];
$row = $map[$key];


unset($row[$key]);
foreach ($row as $next => $v) {
if (array_search($next, $path)) {
continue;
}
$nextPath = $path;
$nextPath[] = $next;

if (\count($path) > 2 && $path[0] === $next) {
$result[] = $path;
continue;
}

$this->findCycle($map, $nextPath, $result, $maxDepth);
}
}
}

+ 50
- 0
project/src/Command/TestCommand.php View File

@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);


namespace App\Command;


use App\Arbitration\Inner\Stuff\ArbitrationMarketChainsFinder;
use App\Exchanges\Kuna\KunaExchange;
use App\Exchanges\MarketInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class TestCommand extends Command
{

protected static $defaultName = 'test:test';
/**
* @var KunaExchange
*/
private KunaExchange $kunaExchange;

public function __construct(
KunaExchange $kunaExchange
)
{
parent::__construct(self::$defaultName);
$this->kunaExchange = $kunaExchange;
}

public function execute(InputInterface $input, OutputInterface $output)
{
$finder = new ArbitrationMarketChainsFinder();

$chains = $finder->findChainsInExchange($this->kunaExchange, 5);

foreach ($chains as $chain) {
echo implode(' -> ', array_map(
function (MarketInterface $market) {
return $market->getPair()->getBase() . '/' . $market->getPair()->getQuote();
},
$chain->getMarkets()
)) . PHP_EOL;
}

return 0;
}
}

+ 16
- 7
project/src/Exchanges/Kuna/KunaApiExecutor.php View File

@@ -13,13 +13,8 @@ class KunaApiExecutor
{

public const API_BASE_URL = 'https://api.kuna.io';
/**
* @var ClientInterface
*/

private ClientInterface $client;
/**
* @var KunaSigner
*/
private KunaSigner $signer;

public function __construct(
@@ -42,6 +37,20 @@ class KunaApiExecutor
)
);

return json_decode($response->getBody(), true);
return json_decode((string)$response->getBody(), true);
}

public function getBook(string $symbol): array
{
$response = $this
->client
->sendRequest(
new Request(
'GET',
self::API_BASE_URL . '/v3/book/' . $symbol
)
);

return json_decode((string)$response->getBody(), true);
}
}

+ 28
- 0
project/src/Exchanges/Kuna/KunaMarket.php View File

@@ -9,6 +9,9 @@ namespace App\Exchanges\Kuna;
use App\Exchanges\MarketInterface;
use App\Utils\Money\Currency;
use App\Utils\Money\CurrencyPair;
use App\Utils\Money\Money;
use App\Utils\Order\Book\OrderBook;
use App\Utils\Order\Order;

class KunaMarket implements MarketInterface
{
@@ -48,4 +51,29 @@ class KunaMarket implements MarketInterface
{
return $this->currencyPair;
}

public function getOrderBook(): OrderBook
{
$asks = [];
$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()),
);

$ordersCount = $orderData[2];
while ($ordersCount) {
if ($orderData[1] > 0) {
$bids[] = clone $order;
} else {
$asks[] = clone $order;
}
$ordersCount--;
}
}

return new OrderBook($bids, $asks);
}
}

+ 18
- 0
project/src/Utils/Money/Currency.php View File

@@ -19,4 +19,22 @@ class Currency
$this->code = $code;
$this->precision = $precision;
}

public function isEqual(Currency $currency): bool
{
return $this->code === $currency->getCode();
}

/**
* @return string
*/
public function getCode(): string
{
return $this->code;
}

public function __toString()
{
return $this->code;
}
}

+ 19
- 9
project/src/Utils/Order/Book/OrderBook.php View File

@@ -6,24 +6,18 @@ declare(strict_types=1);
namespace App\Utils\Order\Book;


use App\Utils\Money\Money;
use App\Utils\Order\Order;

class OrderBook
{

/**
* @var array
*/
private array $bids;
/**
* @var array
*/
private array $asks;

/**
* OrderBook constructor.
* @param Money[] $bids
* @param Money[] $asks
* @param Order[] $bids
* @param Order[] $asks
*/
public function __construct(
array $bids,
@@ -33,4 +27,20 @@ class OrderBook
$this->bids = $bids;
$this->asks = $asks;
}

/**
* @return Order[]
*/
public function getAsks()
{
return $this->asks;
}

/**
* @return Order[]
*/
public function getBids()
{
return $this->bids;
}
}

+ 39
- 0
project/src/Utils/Order/Order.php View File

@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace App\Utils\Order;

use App\Utils\Money\Money;

class Order
{

private Money $from;
private Money $to;

public function __construct(
Money $from,
Money $to
)
{
$this->from = $from;
$this->to = $to;
}

/**
* @return Money
*/
public function getFrom(): Money
{
return $this->from;
}

/**
* @return Money
*/
public function getTo(): Money
{
return $this->to;
}
}

+ 6
- 0
project/symfony.lock View File

@@ -116,6 +116,12 @@
"src/Kernel.php"
]
},
"symfony/http-client": {
"version": "v5.0.7"
},
"symfony/http-client-contracts": {
"version": "v2.0.1"
},
"symfony/http-foundation": {
"version": "v5.0.7"
},


Loading…
Cancel
Save