mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
move C++ SDK out of Go repo — luxcpp/dex is canonical
Deletes orphaned C++ duplicates from the dex Go repo: sdk/cpp/ (→ luxcpp/dex/sdk/cpp/) sdk/lx-trading-c/ (→ luxcpp/dex/sdk/trading-c/) sdk/lx-trading-cpp/(→ luxcpp/dex/sdk/trading-cpp/) client/cpp/ (→ luxcpp/dex/client/cpp/) Confirmed orphaned: the live CGO consumers (pkg/lx/amm_gpu.go, pkg/lx/signed_order_gpu.go, pkg/lx/signed_order_metal_anchor_darwin.go) already point their CFLAGS/LDFLAGS at ~/work/luxcpp/dex/build and ~/work/luxcpp/crypto/build. None reference the deleted trees. luxcpp/dex is 5 months newer (2026-05-05 vs 2025-12-11), so deletion preserves the canonical version. pkg/orderbook/cpp_orderbook.go's inline placeholder C++ left untouched — it's a stub, no external link.
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
# LX C++ CLI Trading Client
|
||||
# Copyright (c) 2025 Lux Partners Limited
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(lx-cli VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Release with debug info by default for profiling
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE RelWithDebInfo)
|
||||
endif()
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# nlohmann/json - JSON parsing
|
||||
FetchContent_Declare(
|
||||
json
|
||||
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||
GIT_TAG v3.11.3
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
# websocketpp - WebSocket client
|
||||
FetchContent_Declare(
|
||||
websocketpp
|
||||
GIT_REPOSITORY https://github.com/zaphoyd/websocketpp.git
|
||||
GIT_TAG 0.8.2
|
||||
)
|
||||
FetchContent_Populate(websocketpp)
|
||||
|
||||
# asio standalone (no Boost)
|
||||
FetchContent_Declare(
|
||||
asio
|
||||
GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git
|
||||
GIT_TAG asio-1-28-2
|
||||
)
|
||||
FetchContent_Populate(asio)
|
||||
|
||||
# CLI executable
|
||||
add_executable(lx-cli
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lx-cli
|
||||
PRIVATE
|
||||
${websocketpp_SOURCE_DIR}
|
||||
${asio_SOURCE_DIR}/asio/include
|
||||
)
|
||||
|
||||
target_link_libraries(lx-cli
|
||||
PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
target_compile_definitions(lx-cli
|
||||
PRIVATE
|
||||
ASIO_STANDALONE
|
||||
_WEBSOCKETPP_CPP11_STL_
|
||||
)
|
||||
|
||||
# Platform-specific
|
||||
if(APPLE)
|
||||
target_compile_definitions(lx-cli PRIVATE _DARWIN_C_SOURCE)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(lx-cli PRIVATE ws2_32)
|
||||
endif()
|
||||
|
||||
# Optimization flags for HFT
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(lx-cli PRIVATE
|
||||
-O3
|
||||
-march=native
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
)
|
||||
endif()
|
||||
|
||||
# Install
|
||||
install(TARGETS lx-cli
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
@@ -1,142 +0,0 @@
|
||||
# LX C++ CLI Trading Client
|
||||
|
||||
Command-line trading interface for LX WebSocket API.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Interactive mode (default)
|
||||
./lx-cli
|
||||
|
||||
# Interactive mode with authentication
|
||||
./lx-cli -k YOUR_API_KEY -s YOUR_API_SECRET
|
||||
|
||||
# Single command mode
|
||||
./lx-cli place_order BTC-USD buy limit 50000 0.1
|
||||
./lx-cli cancel_order 12345
|
||||
./lx-cli get_orderbook BTC-USD
|
||||
./lx-cli get_balances
|
||||
./lx-cli ping
|
||||
|
||||
# Custom server
|
||||
./lx-cli -u ws://trading.example.com:8081 get_positions
|
||||
|
||||
# Verbose output
|
||||
./lx-cli -v ping
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-u, --url <url>` | WebSocket server URL (default: ws://localhost:8081) |
|
||||
| `-k, --key <key>` | API key for authentication |
|
||||
| `-s, --secret <secret>` | API secret for authentication |
|
||||
| `-i, --interactive` | Interactive REPL mode |
|
||||
| `-v, --verbose` | Show raw JSON messages |
|
||||
| `-h, --help` | Show help |
|
||||
|
||||
## Commands
|
||||
|
||||
### Trading
|
||||
|
||||
```
|
||||
place_order <symbol> <side> <type> <price> <size>
|
||||
Place a new order
|
||||
Example: place_order BTC-USD buy limit 50000 0.1
|
||||
|
||||
cancel_order <order_id>
|
||||
Cancel an existing order
|
||||
Example: cancel_order 12345
|
||||
```
|
||||
|
||||
### Market Data
|
||||
|
||||
```
|
||||
get_orderbook <symbol>
|
||||
Get orderbook snapshot
|
||||
Example: get_orderbook BTC-USD
|
||||
|
||||
subscribe <symbol>
|
||||
Subscribe to orderbook updates
|
||||
Example: subscribe ETH-USD
|
||||
```
|
||||
|
||||
### Account
|
||||
|
||||
```
|
||||
get_positions
|
||||
Show all open positions
|
||||
|
||||
get_orders
|
||||
Show all open orders
|
||||
|
||||
get_balances
|
||||
Show account balances
|
||||
```
|
||||
|
||||
### Utility
|
||||
|
||||
```
|
||||
ping
|
||||
Test connection latency
|
||||
|
||||
help
|
||||
Show available commands
|
||||
|
||||
quit / exit
|
||||
Exit the CLI
|
||||
```
|
||||
|
||||
## Interactive Example
|
||||
|
||||
```
|
||||
$ ./lx-cli -k mykey -s mysecret
|
||||
LX CLI - Type 'help' for commands
|
||||
> ping
|
||||
Pong: 1234 us
|
||||
> place_order BTC-USD buy limit 50000 0.1
|
||||
{
|
||||
"type": "order_placed",
|
||||
"data": {
|
||||
"orderId": 12345,
|
||||
"status": "open"
|
||||
}
|
||||
}
|
||||
> get_orders
|
||||
{
|
||||
"type": "orders",
|
||||
"data": {
|
||||
"orders": [...]
|
||||
}
|
||||
}
|
||||
> quit
|
||||
Goodbye
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Fetched automatically via CMake FetchContent:
|
||||
- [nlohmann/json](https://github.com/nlohmann/json) - JSON parsing
|
||||
- [websocketpp](https://github.com/zaphoyd/websocketpp) - WebSocket client
|
||||
- [asio](https://github.com/chriskohlhoff/asio) - Async I/O (standalone, no Boost)
|
||||
|
||||
## Performance
|
||||
|
||||
Optimized for HFT:
|
||||
- Native compilation with `-march=native`
|
||||
- Lock-free message queue
|
||||
- Microsecond-precision latency measurement
|
||||
- Minimal allocations in hot path
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Copyright (c) 2025 Lux Partners Limited
|
||||
@@ -1,813 +0,0 @@
|
||||
// LX C++ CLI Trading Client
|
||||
// Copyright (c) 2025 Lux Partners Limited
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Command-line trading interface for LX WebSocket API.
|
||||
// Connect to ws://localhost:8081 for real-time trading.
|
||||
|
||||
#include <websocketpp/config/asio_no_tls_client.hpp>
|
||||
#include <websocketpp/client.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using WsClient = websocketpp::client<websocketpp::config::asio_client>;
|
||||
using ConnectionHdl = websocketpp::connection_hdl;
|
||||
using MessagePtr = websocketpp::config::asio_client::message_type::ptr;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
struct Config {
|
||||
std::string ws_url = "ws://localhost:8081";
|
||||
std::string api_key;
|
||||
std::string api_secret;
|
||||
bool verbose = false;
|
||||
bool interactive = true;
|
||||
std::vector<std::string> command_args;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// WebSocket Client
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class Client {
|
||||
public:
|
||||
explicit Client(const Config& config)
|
||||
: config_(config)
|
||||
, connected_(false)
|
||||
, authenticated_(false)
|
||||
, request_counter_(0)
|
||||
, running_(false)
|
||||
{
|
||||
ws_.clear_access_channels(websocketpp::log::alevel::all);
|
||||
ws_.clear_error_channels(websocketpp::log::elevel::all);
|
||||
ws_.init_asio();
|
||||
|
||||
ws_.set_open_handler([this](ConnectionHdl) {
|
||||
std::lock_guard<std::mutex> lock(connect_mutex_);
|
||||
connected_ = true;
|
||||
connect_cv_.notify_all();
|
||||
});
|
||||
|
||||
ws_.set_close_handler([this](ConnectionHdl) {
|
||||
std::lock_guard<std::mutex> lock(connect_mutex_);
|
||||
connected_ = false;
|
||||
connect_cv_.notify_all();
|
||||
});
|
||||
|
||||
ws_.set_fail_handler([this](ConnectionHdl) {
|
||||
std::lock_guard<std::mutex> lock(connect_mutex_);
|
||||
connected_ = false;
|
||||
connect_cv_.notify_all();
|
||||
});
|
||||
|
||||
ws_.set_message_handler([this](ConnectionHdl, MessagePtr msg) {
|
||||
on_message(msg);
|
||||
});
|
||||
}
|
||||
|
||||
~Client() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
bool connect() {
|
||||
websocketpp::lib::error_code ec;
|
||||
auto con = ws_.get_connection(config_.ws_url, ec);
|
||||
if (ec) {
|
||||
std::cerr << "Connection error: " << ec.message() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
connection_ = con->get_handle();
|
||||
ws_.connect(con);
|
||||
|
||||
running_ = true;
|
||||
io_thread_ = std::thread([this]() { ws_.run(); });
|
||||
|
||||
// Wait for connection
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(connect_mutex_);
|
||||
bool ok = connect_cv_.wait_for(lock, std::chrono::seconds(10), [this]() {
|
||||
return connected_.load();
|
||||
});
|
||||
if (!ok) {
|
||||
std::cerr << "Connection timeout\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for connected message from server
|
||||
auto resp = wait_response("", std::chrono::seconds(5));
|
||||
if (resp && resp->contains("type") && (*resp)["type"] == "connected") {
|
||||
if (config_.verbose) {
|
||||
std::cout << "Connected to LX\n";
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
if (!running_) return;
|
||||
running_ = false;
|
||||
|
||||
try {
|
||||
if (!connection_.expired()) {
|
||||
ws_.close(connection_, websocketpp::close::status::normal, "");
|
||||
}
|
||||
} catch (...) {}
|
||||
|
||||
ws_.stop();
|
||||
if (io_thread_.joinable()) {
|
||||
io_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
bool is_connected() const { return connected_.load(); }
|
||||
|
||||
bool authenticate() {
|
||||
if (config_.api_key.empty() || config_.api_secret.empty()) {
|
||||
return true; // No auth required
|
||||
}
|
||||
|
||||
json msg = {
|
||||
{"type", "auth"},
|
||||
{"apiKey", config_.api_key},
|
||||
{"apiSecret", config_.api_secret},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
auto resp = send_and_wait(msg);
|
||||
if (!resp) {
|
||||
std::cerr << "Auth timeout\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resp->contains("error") && !(*resp)["error"].is_null()) {
|
||||
std::cerr << "Auth failed: " << (*resp)["error"] << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
authenticated_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Place order
|
||||
std::optional<json> place_order(const std::string& symbol, const std::string& side,
|
||||
const std::string& type, double price, double size) {
|
||||
json order = {
|
||||
{"symbol", symbol},
|
||||
{"side", side},
|
||||
{"type", type},
|
||||
{"price", price},
|
||||
{"size", size}
|
||||
};
|
||||
|
||||
json msg = {
|
||||
{"type", "place_order"},
|
||||
{"order", order},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
return send_and_wait(msg);
|
||||
}
|
||||
|
||||
// Cancel order
|
||||
std::optional<json> cancel_order(uint64_t order_id) {
|
||||
json msg = {
|
||||
{"type", "cancel_order"},
|
||||
{"orderID", order_id},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
return send_and_wait(msg);
|
||||
}
|
||||
|
||||
// Get positions
|
||||
std::optional<json> get_positions() {
|
||||
json msg = {
|
||||
{"type", "get_positions"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
return send_and_wait(msg);
|
||||
}
|
||||
|
||||
// Get open orders
|
||||
std::optional<json> get_orders() {
|
||||
json msg = {
|
||||
{"type", "get_orders"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
return send_and_wait(msg);
|
||||
}
|
||||
|
||||
// Get balances
|
||||
std::optional<json> get_balances() {
|
||||
json msg = {
|
||||
{"type", "get_balances"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
return send_and_wait(msg);
|
||||
}
|
||||
|
||||
// Subscribe to orderbook
|
||||
bool subscribe(const std::string& symbol) {
|
||||
json msg = {
|
||||
{"type", "subscribe"},
|
||||
{"symbols", json::array({symbol})},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
return send(msg);
|
||||
}
|
||||
|
||||
// Get orderbook (via subscribe)
|
||||
std::optional<json> get_orderbook(const std::string& symbol) {
|
||||
subscribe(symbol);
|
||||
// Wait for orderbook message
|
||||
return wait_response("", std::chrono::seconds(5));
|
||||
}
|
||||
|
||||
// Ping
|
||||
std::optional<json> ping() {
|
||||
json msg = {
|
||||
{"type", "ping"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
return send_and_wait(msg);
|
||||
}
|
||||
|
||||
private:
|
||||
void on_message(MessagePtr msg) {
|
||||
try {
|
||||
json j = json::parse(msg->get_payload());
|
||||
|
||||
if (config_.verbose) {
|
||||
std::cout << "<< " << j.dump(2) << "\n";
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(response_mutex_);
|
||||
responses_.push(j);
|
||||
response_cv_.notify_all();
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "JSON parse error: " << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
bool send(const json& msg) {
|
||||
if (!is_connected()) return false;
|
||||
|
||||
try {
|
||||
std::string payload = msg.dump();
|
||||
if (config_.verbose) {
|
||||
std::cout << ">> " << msg.dump(2) << "\n";
|
||||
}
|
||||
ws_.send(connection_, payload, websocketpp::frame::opcode::text);
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Send error: " << e.what() << "\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<json> wait_response(const std::string& request_id,
|
||||
std::chrono::seconds timeout = std::chrono::seconds(5)) {
|
||||
std::unique_lock<std::mutex> lock(response_mutex_);
|
||||
|
||||
auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (!responses_.empty()) {
|
||||
json resp = responses_.front();
|
||||
responses_.pop();
|
||||
|
||||
// If request_id specified, match it
|
||||
if (!request_id.empty()) {
|
||||
if (resp.contains("request_id") && resp["request_id"] == request_id) {
|
||||
return resp;
|
||||
}
|
||||
// Put it back if not matching (simple approach)
|
||||
responses_.push(resp);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
response_cv_.wait_for(lock, std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<json> send_and_wait(const json& msg,
|
||||
std::chrono::seconds timeout = std::chrono::seconds(5)) {
|
||||
std::string req_id = msg["request_id"];
|
||||
if (!send(msg)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
std::unique_lock<std::mutex> lock(response_mutex_);
|
||||
if (!responses_.empty()) {
|
||||
json resp = responses_.front();
|
||||
responses_.pop();
|
||||
|
||||
if (resp.contains("request_id") && resp["request_id"] == req_id) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
// Handle non-matching responses (subscriptions, etc)
|
||||
if (resp.contains("type")) {
|
||||
std::string type = resp["type"];
|
||||
if (type != "connected" && type != "pong") {
|
||||
print_response(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
response_cv_.wait_for(lock, std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string next_request_id() {
|
||||
return "req-" + std::to_string(++request_counter_);
|
||||
}
|
||||
|
||||
void print_response(const json& resp) {
|
||||
if (resp.contains("error") && !resp["error"].is_null()) {
|
||||
std::cout << "Error: " << resp["error"] << "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resp.contains("type")) {
|
||||
std::cout << resp.dump(2) << "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string type = resp["type"];
|
||||
|
||||
if (type == "order_update" && resp.contains("data")) {
|
||||
std::cout << "Order Update: " << resp["data"].dump() << "\n";
|
||||
} else if (type == "position_update" && resp.contains("data")) {
|
||||
std::cout << "Position Update: " << resp["data"].dump() << "\n";
|
||||
} else if (type == "orderbook" && resp.contains("data")) {
|
||||
auto& data = resp["data"];
|
||||
if (data.contains("symbol")) {
|
||||
std::cout << "OrderBook [" << data["symbol"] << "]:\n";
|
||||
}
|
||||
if (data.contains("bids") && data["bids"].is_array()) {
|
||||
std::cout << " Bids: " << data["bids"].size() << " levels\n";
|
||||
int count = 0;
|
||||
for (const auto& bid : data["bids"]) {
|
||||
if (count++ >= 5) break;
|
||||
if (bid.contains("price") && bid.contains("size")) {
|
||||
std::cout << " " << std::fixed << std::setprecision(2)
|
||||
<< bid["price"].get<double>() << " @ "
|
||||
<< std::setprecision(4) << bid["size"].get<double>() << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.contains("asks") && data["asks"].is_array()) {
|
||||
std::cout << " Asks: " << data["asks"].size() << " levels\n";
|
||||
int count = 0;
|
||||
for (const auto& ask : data["asks"]) {
|
||||
if (count++ >= 5) break;
|
||||
if (ask.contains("price") && ask.contains("size")) {
|
||||
std::cout << " " << std::fixed << std::setprecision(2)
|
||||
<< ask["price"].get<double>() << " @ "
|
||||
<< std::setprecision(4) << ask["size"].get<double>() << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::cout << resp.dump(2) << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
Config config_;
|
||||
WsClient ws_;
|
||||
ConnectionHdl connection_;
|
||||
std::thread io_thread_;
|
||||
|
||||
std::atomic<bool> connected_;
|
||||
std::atomic<bool> authenticated_;
|
||||
std::atomic<uint64_t> request_counter_;
|
||||
std::atomic<bool> running_;
|
||||
|
||||
std::mutex connect_mutex_;
|
||||
std::condition_variable connect_cv_;
|
||||
|
||||
std::mutex response_mutex_;
|
||||
std::condition_variable response_cv_;
|
||||
std::queue<json> responses_;
|
||||
|
||||
friend void print_message(Client& client, const json& msg);
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// CLI Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void print_help() {
|
||||
std::cout << R"(
|
||||
LX CLI Commands:
|
||||
|
||||
place_order <symbol> <side> <type> <price> <size>
|
||||
Example: place_order BTC-USD buy limit 50000 0.1
|
||||
|
||||
cancel_order <order_id>
|
||||
Example: cancel_order 12345
|
||||
|
||||
get_orderbook <symbol>
|
||||
Example: get_orderbook BTC-USD
|
||||
|
||||
get_positions
|
||||
Show all open positions
|
||||
|
||||
get_orders
|
||||
Show all open orders
|
||||
|
||||
get_balances
|
||||
Show account balances
|
||||
|
||||
subscribe <symbol>
|
||||
Subscribe to orderbook updates
|
||||
|
||||
ping
|
||||
Test connection latency
|
||||
|
||||
help
|
||||
Show this help message
|
||||
|
||||
quit / exit
|
||||
Exit the CLI
|
||||
)";
|
||||
}
|
||||
|
||||
void print_message(const json& msg) {
|
||||
if (msg.contains("error") && !msg["error"].is_null()) {
|
||||
std::cout << "Error: " << msg["error"] << "\n";
|
||||
return;
|
||||
}
|
||||
std::cout << msg.dump(2) << "\n";
|
||||
}
|
||||
|
||||
std::vector<std::string> split(const std::string& s) {
|
||||
std::vector<std::string> tokens;
|
||||
std::istringstream iss(s);
|
||||
std::string token;
|
||||
while (iss >> token) {
|
||||
tokens.push_back(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t start = s.find_first_not_of(" \t\r\n");
|
||||
if (start == std::string::npos) return "";
|
||||
size_t end = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
void run_interactive(Client& client) {
|
||||
std::cout << "LX CLI - Type 'help' for commands\n> ";
|
||||
|
||||
std::string line;
|
||||
while (std::getline(std::cin, line)) {
|
||||
line = trim(line);
|
||||
if (line.empty()) {
|
||||
std::cout << "> ";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto parts = split(line);
|
||||
if (parts.empty()) {
|
||||
std::cout << "> ";
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string cmd = parts[0];
|
||||
// Convert to lowercase
|
||||
for (auto& c : cmd) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
|
||||
if (cmd == "help") {
|
||||
print_help();
|
||||
} else if (cmd == "quit" || cmd == "exit") {
|
||||
std::cout << "Goodbye\n";
|
||||
break;
|
||||
} else if (cmd == "place_order") {
|
||||
if (parts.size() < 6) {
|
||||
std::cout << "Usage: place_order <symbol> <side> <type> <price> <size>\n";
|
||||
} else {
|
||||
try {
|
||||
double price = std::stod(parts[4]);
|
||||
double size = std::stod(parts[5]);
|
||||
auto resp = client.place_order(parts[1], parts[2], parts[3], price, size);
|
||||
if (resp) {
|
||||
print_message(*resp);
|
||||
} else {
|
||||
std::cout << "Timeout waiting for response\n";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "Invalid price or size: " << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
} else if (cmd == "cancel_order") {
|
||||
if (parts.size() < 2) {
|
||||
std::cout << "Usage: cancel_order <order_id>\n";
|
||||
} else {
|
||||
try {
|
||||
uint64_t order_id = std::stoull(parts[1]);
|
||||
auto resp = client.cancel_order(order_id);
|
||||
if (resp) {
|
||||
print_message(*resp);
|
||||
} else {
|
||||
std::cout << "Timeout waiting for response\n";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "Invalid order ID: " << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
} else if (cmd == "get_orderbook") {
|
||||
if (parts.size() < 2) {
|
||||
std::cout << "Usage: get_orderbook <symbol>\n";
|
||||
} else {
|
||||
auto resp = client.get_orderbook(parts[1]);
|
||||
if (resp) {
|
||||
print_message(*resp);
|
||||
} else {
|
||||
std::cout << "Subscribed to " << parts[1] << " orderbook\n";
|
||||
}
|
||||
}
|
||||
} else if (cmd == "get_positions") {
|
||||
auto resp = client.get_positions();
|
||||
if (resp) {
|
||||
print_message(*resp);
|
||||
} else {
|
||||
std::cout << "Timeout waiting for response\n";
|
||||
}
|
||||
} else if (cmd == "get_orders") {
|
||||
auto resp = client.get_orders();
|
||||
if (resp) {
|
||||
print_message(*resp);
|
||||
} else {
|
||||
std::cout << "Timeout waiting for response\n";
|
||||
}
|
||||
} else if (cmd == "get_balances") {
|
||||
auto resp = client.get_balances();
|
||||
if (resp) {
|
||||
print_message(*resp);
|
||||
} else {
|
||||
std::cout << "Timeout waiting for response\n";
|
||||
}
|
||||
} else if (cmd == "subscribe") {
|
||||
if (parts.size() < 2) {
|
||||
std::cout << "Usage: subscribe <symbol>\n";
|
||||
} else {
|
||||
if (client.subscribe(parts[1])) {
|
||||
std::cout << "Subscribed to " << parts[1] << "\n";
|
||||
} else {
|
||||
std::cout << "Failed to subscribe\n";
|
||||
}
|
||||
}
|
||||
} else if (cmd == "ping") {
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
auto resp = client.ping();
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
if (resp) {
|
||||
auto latency = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
|
||||
std::cout << "Pong: " << latency.count() << " us\n";
|
||||
} else {
|
||||
std::cout << "Ping timeout\n";
|
||||
}
|
||||
} else {
|
||||
std::cout << "Unknown command: " << cmd << ". Type 'help' for commands.\n";
|
||||
}
|
||||
|
||||
std::cout << "> ";
|
||||
}
|
||||
}
|
||||
|
||||
void run_command(Client& client, const std::vector<std::string>& args) {
|
||||
if (args.empty()) {
|
||||
std::cerr << "No command specified. Use -h for help.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string cmd = args[0];
|
||||
// Convert to lowercase
|
||||
for (auto& c : cmd) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
|
||||
if (cmd == "place_order") {
|
||||
if (args.size() < 6) {
|
||||
std::cerr << "Usage: lx-cli place_order <symbol> <side> <type> <price> <size>\n";
|
||||
std::exit(1);
|
||||
}
|
||||
try {
|
||||
double price = std::stod(args[4]);
|
||||
double size = std::stod(args[5]);
|
||||
auto resp = client.place_order(args[1], args[2], args[3], price, size);
|
||||
if (resp) {
|
||||
std::cout << resp->dump(2) << "\n";
|
||||
} else {
|
||||
std::cerr << "Timeout\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Invalid price or size: " << e.what() << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} else if (cmd == "cancel_order") {
|
||||
if (args.size() < 2) {
|
||||
std::cerr << "Usage: lx-cli cancel_order <order_id>\n";
|
||||
std::exit(1);
|
||||
}
|
||||
try {
|
||||
uint64_t order_id = std::stoull(args[1]);
|
||||
auto resp = client.cancel_order(order_id);
|
||||
if (resp) {
|
||||
std::cout << resp->dump(2) << "\n";
|
||||
} else {
|
||||
std::cerr << "Timeout\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Invalid order ID: " << e.what() << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} else if (cmd == "get_orderbook") {
|
||||
if (args.size() < 2) {
|
||||
std::cerr << "Usage: lx-cli get_orderbook <symbol>\n";
|
||||
std::exit(1);
|
||||
}
|
||||
auto resp = client.get_orderbook(args[1]);
|
||||
if (resp) {
|
||||
std::cout << resp->dump(2) << "\n";
|
||||
} else {
|
||||
std::cerr << "Timeout\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} else if (cmd == "get_positions") {
|
||||
auto resp = client.get_positions();
|
||||
if (resp) {
|
||||
std::cout << resp->dump(2) << "\n";
|
||||
} else {
|
||||
std::cerr << "Timeout\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} else if (cmd == "get_orders") {
|
||||
auto resp = client.get_orders();
|
||||
if (resp) {
|
||||
std::cout << resp->dump(2) << "\n";
|
||||
} else {
|
||||
std::cerr << "Timeout\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} else if (cmd == "get_balances") {
|
||||
auto resp = client.get_balances();
|
||||
if (resp) {
|
||||
std::cout << resp->dump(2) << "\n";
|
||||
} else {
|
||||
std::cerr << "Timeout\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} else if (cmd == "ping") {
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
auto resp = client.ping();
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
if (resp) {
|
||||
auto latency = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
|
||||
std::cout << "{\"latency_us\": " << latency.count() << "}\n";
|
||||
} else {
|
||||
std::cerr << "Timeout\n";
|
||||
std::exit(1);
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Unknown command: " << cmd << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void print_usage(const char* prog) {
|
||||
std::cout << "LX C++ CLI Trading Client\n\n"
|
||||
<< "Usage: " << prog << " [options] [command] [args...]\n\n"
|
||||
<< "Options:\n"
|
||||
<< " -u, --url <url> WebSocket server URL (default: ws://localhost:8081)\n"
|
||||
<< " -k, --key <key> API key for authentication\n"
|
||||
<< " -s, --secret <secret> API secret for authentication\n"
|
||||
<< " -i, --interactive Interactive mode (default if no command)\n"
|
||||
<< " -v, --verbose Verbose output\n"
|
||||
<< " -h, --help Show this help message\n\n"
|
||||
<< "Commands:\n"
|
||||
<< " place_order <symbol> <side> <type> <price> <size>\n"
|
||||
<< " cancel_order <order_id>\n"
|
||||
<< " get_orderbook <symbol>\n"
|
||||
<< " get_positions\n"
|
||||
<< " get_orders\n"
|
||||
<< " get_balances\n"
|
||||
<< " ping\n\n"
|
||||
<< "Examples:\n"
|
||||
<< " " << prog << " -i # Interactive mode\n"
|
||||
<< " " << prog << " place_order BTC-USD buy limit 50000 0.1\n"
|
||||
<< " " << prog << " cancel_order 12345\n"
|
||||
<< " " << prog << " get_orderbook BTC-USD\n"
|
||||
<< " " << prog << " -v ping # Ping with verbose output\n";
|
||||
}
|
||||
|
||||
Config parse_args(int argc, char* argv[]) {
|
||||
Config config;
|
||||
|
||||
int i = 1;
|
||||
while (i < argc) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "-h" || arg == "--help") {
|
||||
print_usage(argv[0]);
|
||||
std::exit(0);
|
||||
} else if (arg == "-u" || arg == "--url") {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "Missing URL argument\n";
|
||||
std::exit(1);
|
||||
}
|
||||
config.ws_url = argv[++i];
|
||||
} else if (arg == "-k" || arg == "--key") {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "Missing API key argument\n";
|
||||
std::exit(1);
|
||||
}
|
||||
config.api_key = argv[++i];
|
||||
} else if (arg == "-s" || arg == "--secret") {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "Missing API secret argument\n";
|
||||
std::exit(1);
|
||||
}
|
||||
config.api_secret = argv[++i];
|
||||
} else if (arg == "-i" || arg == "--interactive") {
|
||||
config.interactive = true;
|
||||
} else if (arg == "-v" || arg == "--verbose") {
|
||||
config.verbose = true;
|
||||
} else if (arg[0] != '-') {
|
||||
// Command and its arguments
|
||||
config.interactive = false;
|
||||
while (i < argc) {
|
||||
config.command_args.push_back(argv[i++]);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
std::cerr << "Unknown option: " << arg << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
// Default to interactive if no command
|
||||
if (config.command_args.empty()) {
|
||||
config.interactive = true;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
Config config = parse_args(argc, argv);
|
||||
|
||||
// Create and connect client
|
||||
Client client(config);
|
||||
if (!client.connect()) {
|
||||
std::cerr << "Failed to connect to " << config.ws_url << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Authenticate if credentials provided
|
||||
if (!config.api_key.empty() && !config.api_secret.empty()) {
|
||||
if (!client.authenticate()) {
|
||||
std::cerr << "Authentication failed\n";
|
||||
return 1;
|
||||
}
|
||||
if (config.verbose) {
|
||||
std::cout << "Authenticated\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Run in interactive or command mode
|
||||
if (config.interactive) {
|
||||
run_interactive(client);
|
||||
} else {
|
||||
run_command(client, config.command_args);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Allow older cmake_minimum_required in fetched projects
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "Minimum policy version for compatibility")
|
||||
|
||||
project(lxdex VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Options
|
||||
option(LXDEX_BUILD_EXAMPLES "Build example applications" ON)
|
||||
option(LXDEX_BUILD_TESTS "Build tests" OFF)
|
||||
|
||||
# Find dependencies
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# Include FetchContent for external dependencies
|
||||
include(FetchContent)
|
||||
|
||||
# nlohmann/json
|
||||
FetchContent_Declare(
|
||||
json
|
||||
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||
GIT_TAG v3.11.3
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
|
||||
# websocketpp - header-only, just need source dir
|
||||
FetchContent_Declare(
|
||||
websocketpp
|
||||
GIT_REPOSITORY https://github.com/zaphoyd/websocketpp.git
|
||||
GIT_TAG 0.8.2
|
||||
)
|
||||
FetchContent_Populate(websocketpp)
|
||||
|
||||
# asio (standalone, no Boost)
|
||||
FetchContent_Declare(
|
||||
asio
|
||||
GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git
|
||||
GIT_TAG asio-1-28-2
|
||||
)
|
||||
FetchContent_Populate(asio)
|
||||
|
||||
# Library target
|
||||
add_library(lxdex
|
||||
src/client.cpp
|
||||
src/orderbook.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lxdex
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
PRIVATE
|
||||
${websocketpp_SOURCE_DIR}
|
||||
${asio_SOURCE_DIR}/asio/include
|
||||
)
|
||||
|
||||
target_link_libraries(lxdex
|
||||
PUBLIC
|
||||
nlohmann_json::nlohmann_json
|
||||
PRIVATE
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
target_compile_definitions(lxdex
|
||||
PRIVATE
|
||||
ASIO_STANDALONE
|
||||
_WEBSOCKETPP_CPP11_STL_
|
||||
)
|
||||
|
||||
# Platform-specific settings
|
||||
if(APPLE)
|
||||
target_compile_definitions(lxdex PRIVATE _DARWIN_C_SOURCE)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(lxdex PRIVATE ws2_32)
|
||||
endif()
|
||||
|
||||
# Examples
|
||||
if(LXDEX_BUILD_EXAMPLES)
|
||||
add_executable(lxdex_basic examples/basic.cpp)
|
||||
target_link_libraries(lxdex_basic PRIVATE lxdex)
|
||||
target_include_directories(lxdex_basic
|
||||
PRIVATE
|
||||
${websocketpp_SOURCE_DIR}
|
||||
${asio_SOURCE_DIR}/asio/include
|
||||
)
|
||||
target_compile_definitions(lxdex_basic
|
||||
PRIVATE
|
||||
ASIO_STANDALONE
|
||||
_WEBSOCKETPP_CPP11_STL_
|
||||
)
|
||||
endif()
|
||||
|
||||
# Install
|
||||
install(TARGETS lxdex
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
install(DIRECTORY include/
|
||||
DESTINATION include
|
||||
)
|
||||
@@ -1,364 +0,0 @@
|
||||
# LX C++ SDK
|
||||
|
||||
Production-ready C++17 SDK for the LX Decentralized Exchange.
|
||||
|
||||
## Features
|
||||
|
||||
- WebSocket client with automatic reconnection
|
||||
- Full trading API (orders, positions, balances)
|
||||
- Real-time market data subscriptions
|
||||
- Thread-safe local orderbook management
|
||||
- RAII-compliant resource management
|
||||
- Zero external runtime dependencies (header-only deps fetched at build)
|
||||
|
||||
## Requirements
|
||||
|
||||
- C++17 compiler (GCC 8+, Clang 7+, MSVC 2019+)
|
||||
- CMake 3.16+
|
||||
- Git (for dependency fetching)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
### Build Options
|
||||
|
||||
```bash
|
||||
cmake -DLXDEX_BUILD_EXAMPLES=ON .. # Build examples (default: ON)
|
||||
cmake -DLXDEX_BUILD_TESTS=ON .. # Build tests (default: OFF)
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```cpp
|
||||
#include <lxdex/client.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
// Configure
|
||||
lxdex::ClientConfig config;
|
||||
config.ws_url = "ws://localhost:8081";
|
||||
config.api_key = "your_key";
|
||||
config.api_secret = "your_secret";
|
||||
|
||||
// Create client
|
||||
auto client = lxdex::make_client(config);
|
||||
|
||||
// Connect
|
||||
if (auto err = client->connect(); err) {
|
||||
std::cerr << "Connect failed: " << err.message << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
if (auto err = client->authenticate(); err) {
|
||||
std::cerr << "Auth failed: " << err.message << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Place order
|
||||
lxdex::Order order;
|
||||
order.symbol = "BTC-USDT";
|
||||
order.side = lxdex::Side::Buy;
|
||||
order.type = lxdex::OrderType::Limit;
|
||||
order.price = 40000.0;
|
||||
order.size = 0.01;
|
||||
|
||||
auto result = client->place_order(order);
|
||||
if (result.ok()) {
|
||||
std::cout << "Order ID: " << result.value.order_id << "\n";
|
||||
}
|
||||
|
||||
// Subscribe to orderbook
|
||||
client->subscribe_orderbook({"BTC-USDT"}, [](const lxdex::OrderBook& ob) {
|
||||
std::cout << "Best bid: " << ob.best_bid()
|
||||
<< " Best ask: " << ob.best_ask() << "\n";
|
||||
});
|
||||
|
||||
// Keep running...
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Client Configuration
|
||||
|
||||
```cpp
|
||||
struct ClientConfig {
|
||||
std::string ws_url = "ws://localhost:8081";
|
||||
std::string api_key;
|
||||
std::string api_secret;
|
||||
|
||||
std::chrono::seconds connect_timeout{10};
|
||||
std::chrono::seconds ping_interval{30};
|
||||
std::chrono::seconds reconnect_delay{5};
|
||||
int max_reconnect_attempts = 5;
|
||||
bool auto_reconnect = true;
|
||||
};
|
||||
```
|
||||
|
||||
### Connection
|
||||
|
||||
```cpp
|
||||
// Connect to server
|
||||
Error connect();
|
||||
|
||||
// Disconnect
|
||||
void disconnect();
|
||||
|
||||
// Check connection state
|
||||
bool is_connected() const;
|
||||
ConnectionState state() const;
|
||||
|
||||
// Authenticate
|
||||
Error authenticate();
|
||||
bool is_authenticated() const;
|
||||
```
|
||||
|
||||
### Trading
|
||||
|
||||
```cpp
|
||||
// Place order
|
||||
Result<OrderResponse> place_order(const Order& order);
|
||||
std::future<Result<OrderResponse>> place_order_async(const Order& order);
|
||||
|
||||
// Cancel order
|
||||
Error cancel_order(uint64_t order_id);
|
||||
std::future<Error> cancel_order_async(uint64_t order_id);
|
||||
|
||||
// Modify order
|
||||
Error modify_order(uint64_t order_id, double new_price, double new_size);
|
||||
|
||||
// Cancel all
|
||||
Result<int> cancel_all_orders(const std::string& symbol = "");
|
||||
```
|
||||
|
||||
### Market Data
|
||||
|
||||
```cpp
|
||||
// Get orderbook snapshot
|
||||
Result<OrderBook> get_orderbook(const std::string& symbol, int32_t depth = 20);
|
||||
|
||||
// Get recent trades
|
||||
Result<std::vector<Trade>> get_trades(const std::string& symbol, int32_t limit = 100);
|
||||
|
||||
// Subscribe to updates
|
||||
Error subscribe_orderbook(const std::vector<std::string>& symbols, OrderBookCallback cb);
|
||||
Error subscribe_trades(const std::vector<std::string>& symbols, TradeCallback cb);
|
||||
|
||||
// Unsubscribe
|
||||
Error unsubscribe(const std::string& channel, const std::vector<std::string>& symbols);
|
||||
```
|
||||
|
||||
### Account
|
||||
|
||||
```cpp
|
||||
Result<std::vector<Balance>> get_balances();
|
||||
Result<std::vector<Position>> get_positions();
|
||||
Result<std::vector<Order>> get_orders();
|
||||
Result<NodeInfo> get_info();
|
||||
```
|
||||
|
||||
### Callbacks
|
||||
|
||||
```cpp
|
||||
void on_error(ErrorCallback callback);
|
||||
void on_order(OrderCallback callback);
|
||||
void on_trade(TradeCallback callback);
|
||||
void on_connection(ConnectionCallback callback);
|
||||
void on_message(MessageCallback callback); // Raw messages
|
||||
```
|
||||
|
||||
### Local Data
|
||||
|
||||
```cpp
|
||||
// Access local caches
|
||||
OrderBookManager& orderbooks();
|
||||
OrderTracker& orders();
|
||||
TradeTracker& trades();
|
||||
```
|
||||
|
||||
### Metrics
|
||||
|
||||
```cpp
|
||||
ClientMetrics metrics() const;
|
||||
void reset_metrics();
|
||||
Result<int64_t> ping(); // Returns latency in microseconds
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### Order
|
||||
|
||||
```cpp
|
||||
struct Order {
|
||||
uint64_t order_id;
|
||||
std::string symbol;
|
||||
OrderType type; // Limit, Market, Stop, StopLimit, Iceberg, Peg
|
||||
Side side; // Buy, Sell
|
||||
double price;
|
||||
double size;
|
||||
double filled;
|
||||
double remaining;
|
||||
OrderStatus status; // Open, Partial, Filled, Cancelled, Rejected
|
||||
std::string user_id;
|
||||
std::string client_id;
|
||||
int64_t timestamp;
|
||||
TimeInForce time_in_force; // GTC, IOC, FOK, DAY
|
||||
bool post_only;
|
||||
bool reduce_only;
|
||||
|
||||
bool is_open() const;
|
||||
bool is_closed() const;
|
||||
double fill_rate() const;
|
||||
};
|
||||
```
|
||||
|
||||
### Trade
|
||||
|
||||
```cpp
|
||||
struct Trade {
|
||||
uint64_t trade_id;
|
||||
std::string symbol;
|
||||
double price;
|
||||
double size;
|
||||
Side side;
|
||||
uint64_t buy_order_id;
|
||||
uint64_t sell_order_id;
|
||||
std::string buyer_id;
|
||||
std::string seller_id;
|
||||
int64_t timestamp;
|
||||
|
||||
double total_value() const;
|
||||
};
|
||||
```
|
||||
|
||||
### OrderBook
|
||||
|
||||
```cpp
|
||||
struct OrderBook {
|
||||
std::string symbol;
|
||||
std::vector<PriceLevel> bids;
|
||||
std::vector<PriceLevel> asks;
|
||||
int64_t timestamp;
|
||||
|
||||
double best_bid() const;
|
||||
double best_ask() const;
|
||||
double spread() const;
|
||||
double mid_price() const;
|
||||
double spread_percentage() const;
|
||||
};
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```cpp
|
||||
struct Error {
|
||||
int code;
|
||||
std::string message;
|
||||
std::string request_id;
|
||||
|
||||
explicit operator bool() const; // true if error
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct Result {
|
||||
T value;
|
||||
Error error;
|
||||
|
||||
bool ok() const;
|
||||
explicit operator bool() const; // true if ok
|
||||
};
|
||||
```
|
||||
|
||||
## Thread Safety
|
||||
|
||||
- All client methods are thread-safe
|
||||
- Callbacks are invoked from the IO thread; avoid blocking
|
||||
- Local orderbook/trade managers use internal locking
|
||||
|
||||
## Example: High-Frequency Trading
|
||||
|
||||
```cpp
|
||||
#include <lxdex/client.hpp>
|
||||
|
||||
int main() {
|
||||
lxdex::ClientConfig config;
|
||||
config.ws_url = "ws://localhost:8081";
|
||||
config.api_key = "key";
|
||||
config.api_secret = "secret";
|
||||
|
||||
auto client = lxdex::make_client(config);
|
||||
client->connect();
|
||||
client->authenticate();
|
||||
|
||||
// Track local orderbook
|
||||
client->subscribe_orderbook({"BTC-USDT"}, [&](const lxdex::OrderBook& ob) {
|
||||
double mid = ob.mid_price();
|
||||
double spread = ob.spread();
|
||||
|
||||
// Simple market making logic
|
||||
if (spread > 10.0) {
|
||||
// Place bid slightly below mid
|
||||
lxdex::Order bid;
|
||||
bid.symbol = "BTC-USDT";
|
||||
bid.side = lxdex::Side::Buy;
|
||||
bid.type = lxdex::OrderType::Limit;
|
||||
bid.price = mid - 2.0;
|
||||
bid.size = 0.001;
|
||||
bid.post_only = true;
|
||||
bid.client_id = lxdex::Client::generate_client_id();
|
||||
|
||||
client->place_order_async(bid);
|
||||
|
||||
// Place ask slightly above mid
|
||||
lxdex::Order ask = bid;
|
||||
ask.side = lxdex::Side::Sell;
|
||||
ask.price = mid + 2.0;
|
||||
ask.client_id = lxdex::Client::generate_client_id();
|
||||
|
||||
client->place_order_async(ask);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle fills
|
||||
client->on_order([](const lxdex::Order& order) {
|
||||
if (order.status == lxdex::OrderStatus::Filled) {
|
||||
std::cout << "Filled: " << order.order_id << "\n";
|
||||
}
|
||||
});
|
||||
|
||||
// Run until interrupted
|
||||
while (true) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with CMake Projects
|
||||
|
||||
```cmake
|
||||
find_package(lxdex REQUIRED)
|
||||
target_link_libraries(your_target PRIVATE lxdex::lxdex)
|
||||
```
|
||||
|
||||
Or use FetchContent:
|
||||
|
||||
```cmake
|
||||
FetchContent_Declare(
|
||||
lxdex
|
||||
GIT_REPOSITORY https://github.com/luxfi/dex.git
|
||||
SOURCE_SUBDIR sdk/cpp
|
||||
)
|
||||
FetchContent_MakeAvailable(lxdex)
|
||||
target_link_libraries(your_target PRIVATE lxdex)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Copyright (c) 2025 Lux Partners Limited
|
||||
@@ -1,250 +0,0 @@
|
||||
// LX C++ SDK - Basic Usage Example
|
||||
// Copyright (c) 2025 Lux Partners Limited
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include <lxdex/client.hpp>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <thread>
|
||||
#include <csignal>
|
||||
#include <atomic>
|
||||
|
||||
std::atomic<bool> g_running{true};
|
||||
|
||||
void signal_handler(int signal) {
|
||||
std::cout << "\nReceived signal " << signal << ", shutting down...\n";
|
||||
g_running = false;
|
||||
}
|
||||
|
||||
void print_orderbook(const lxdex::OrderBook& ob) {
|
||||
std::cout << "\n=== " << ob.symbol << " Orderbook ===\n";
|
||||
std::cout << std::fixed << std::setprecision(2);
|
||||
|
||||
std::cout << "Asks:\n";
|
||||
size_t ask_count = std::min(ob.asks.size(), size_t{5});
|
||||
for (size_t i = ask_count; i > 0; --i) {
|
||||
const auto& level = ob.asks[i - 1];
|
||||
std::cout << " " << std::setw(12) << level.price
|
||||
<< " | " << std::setw(10) << level.size << "\n";
|
||||
}
|
||||
|
||||
std::cout << "----------------------------\n";
|
||||
|
||||
std::cout << "Bids:\n";
|
||||
for (size_t i = 0; i < std::min(ob.bids.size(), size_t{5}); ++i) {
|
||||
const auto& level = ob.bids[i];
|
||||
std::cout << " " << std::setw(12) << level.price
|
||||
<< " | " << std::setw(10) << level.size << "\n";
|
||||
}
|
||||
|
||||
std::cout << "\nSpread: " << ob.spread() << " ("
|
||||
<< std::setprecision(4) << ob.spread_percentage() << "%)\n";
|
||||
std::cout << "Mid: " << std::setprecision(2) << ob.mid_price() << "\n";
|
||||
}
|
||||
|
||||
void print_trade(const lxdex::Trade& trade) {
|
||||
std::cout << "[Trade] " << trade.symbol
|
||||
<< " " << (trade.side == lxdex::Side::Buy ? "BUY" : "SELL")
|
||||
<< " " << trade.size << " @ " << trade.price
|
||||
<< " (id: " << trade.trade_id << ")\n";
|
||||
}
|
||||
|
||||
void print_order(const lxdex::Order& order) {
|
||||
std::cout << "[Order] " << order.symbol
|
||||
<< " " << (order.side == lxdex::Side::Buy ? "BUY" : "SELL")
|
||||
<< " " << order.size << " @ " << order.price
|
||||
<< " status: ";
|
||||
|
||||
switch (order.status) {
|
||||
case lxdex::OrderStatus::Open: std::cout << "OPEN"; break;
|
||||
case lxdex::OrderStatus::Partial: std::cout << "PARTIAL"; break;
|
||||
case lxdex::OrderStatus::Filled: std::cout << "FILLED"; break;
|
||||
case lxdex::OrderStatus::Cancelled: std::cout << "CANCELLED"; break;
|
||||
case lxdex::OrderStatus::Rejected: std::cout << "REJECTED"; break;
|
||||
}
|
||||
std::cout << " (id: " << order.order_id << ")\n";
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// Setup signal handler
|
||||
std::signal(SIGINT, signal_handler);
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
|
||||
std::cout << "LX C++ SDK Example\n";
|
||||
std::cout << "======================\n\n";
|
||||
|
||||
// Configure client
|
||||
lxdex::ClientConfig config;
|
||||
config.ws_url = "ws://localhost:8081";
|
||||
config.auto_reconnect = true;
|
||||
config.max_reconnect_attempts = 5;
|
||||
|
||||
// Parse command line args
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
if (arg == "--url" && i + 1 < argc) {
|
||||
config.ws_url = argv[++i];
|
||||
} else if (arg == "--key" && i + 1 < argc) {
|
||||
config.api_key = argv[++i];
|
||||
} else if (arg == "--secret" && i + 1 < argc) {
|
||||
config.api_secret = argv[++i];
|
||||
} else if (arg == "--help") {
|
||||
std::cout << "Usage: " << argv[0] << " [options]\n"
|
||||
<< " --url URL WebSocket URL (default: ws://localhost:8081)\n"
|
||||
<< " --key KEY API key for authentication\n"
|
||||
<< " --secret SEC API secret for authentication\n"
|
||||
<< " --help Show this help\n";
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Create client
|
||||
auto client = lxdex::make_client(config);
|
||||
|
||||
// Setup callbacks
|
||||
client->on_error([](const lxdex::Error& err) {
|
||||
std::cerr << "[Error] " << err.message;
|
||||
if (!err.request_id.empty()) {
|
||||
std::cerr << " (request: " << err.request_id << ")";
|
||||
}
|
||||
std::cerr << "\n";
|
||||
});
|
||||
|
||||
client->on_connection([](lxdex::ConnectionState state) {
|
||||
std::cout << "[Connection] State: ";
|
||||
switch (state) {
|
||||
case lxdex::ConnectionState::Disconnected:
|
||||
std::cout << "DISCONNECTED";
|
||||
break;
|
||||
case lxdex::ConnectionState::Connecting:
|
||||
std::cout << "CONNECTING";
|
||||
break;
|
||||
case lxdex::ConnectionState::Connected:
|
||||
std::cout << "CONNECTED";
|
||||
break;
|
||||
case lxdex::ConnectionState::Reconnecting:
|
||||
std::cout << "RECONNECTING";
|
||||
break;
|
||||
case lxdex::ConnectionState::Failed:
|
||||
std::cout << "FAILED";
|
||||
break;
|
||||
}
|
||||
std::cout << "\n";
|
||||
});
|
||||
|
||||
client->on_order([](const lxdex::Order& order) {
|
||||
print_order(order);
|
||||
});
|
||||
|
||||
// Connect
|
||||
std::cout << "Connecting to " << config.ws_url << "...\n";
|
||||
auto err = client->connect();
|
||||
if (err) {
|
||||
std::cerr << "Failed to connect: " << err.message << "\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Connected!\n\n";
|
||||
|
||||
// Ping test
|
||||
auto ping_result = client->ping();
|
||||
if (ping_result.ok()) {
|
||||
std::cout << "Ping: " << ping_result.value << " us\n\n";
|
||||
}
|
||||
|
||||
// Authenticate if credentials provided
|
||||
if (!config.api_key.empty()) {
|
||||
std::cout << "Authenticating...\n";
|
||||
err = client->authenticate();
|
||||
if (err) {
|
||||
std::cerr << "Authentication failed: " << err.message << "\n";
|
||||
} else {
|
||||
std::cout << "Authenticated!\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to orderbook updates
|
||||
std::vector<std::string> symbols = {"BTC-USDT", "ETH-USDT"};
|
||||
|
||||
std::cout << "Subscribing to orderbook updates...\n";
|
||||
err = client->subscribe_orderbook(symbols, [](const lxdex::OrderBook& ob) {
|
||||
print_orderbook(ob);
|
||||
});
|
||||
if (err) {
|
||||
std::cerr << "Failed to subscribe: " << err.message << "\n";
|
||||
}
|
||||
|
||||
// Subscribe to trades
|
||||
std::cout << "Subscribing to trade updates...\n";
|
||||
err = client->subscribe_trades(symbols, [](const lxdex::Trade& trade) {
|
||||
print_trade(trade);
|
||||
});
|
||||
if (err) {
|
||||
std::cerr << "Failed to subscribe: " << err.message << "\n";
|
||||
}
|
||||
|
||||
// Example: Place a test order (if authenticated)
|
||||
if (client->is_authenticated()) {
|
||||
std::cout << "\nPlacing test order...\n";
|
||||
|
||||
lxdex::Order order;
|
||||
order.symbol = "BTC-USDT";
|
||||
order.side = lxdex::Side::Buy;
|
||||
order.type = lxdex::OrderType::Limit;
|
||||
order.price = 40000.0;
|
||||
order.size = 0.001;
|
||||
order.time_in_force = lxdex::TimeInForce::GTC;
|
||||
order.client_id = lxdex::Client::generate_client_id();
|
||||
|
||||
auto result = client->place_order(order);
|
||||
if (result.ok()) {
|
||||
std::cout << "Order placed! ID: " << result.value.order_id
|
||||
<< " Status: " << result.value.status << "\n";
|
||||
|
||||
// Cancel the order after a brief delay
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
|
||||
std::cout << "Cancelling order...\n";
|
||||
err = client->cancel_order(result.value.order_id);
|
||||
if (err) {
|
||||
std::cerr << "Failed to cancel: " << err.message << "\n";
|
||||
} else {
|
||||
std::cout << "Order cancelled!\n";
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Failed to place order: " << result.error.message << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Main loop - wait for data
|
||||
std::cout << "\nWaiting for market data (Ctrl+C to exit)...\n\n";
|
||||
|
||||
int update_count = 0;
|
||||
while (g_running) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
|
||||
// Periodic metrics report
|
||||
if (++update_count % 30 == 0) {
|
||||
auto metrics = client->metrics();
|
||||
std::cout << "\n--- Metrics ---\n"
|
||||
<< "Messages sent: " << metrics.messages_sent << "\n"
|
||||
<< "Messages received: " << metrics.messages_received << "\n"
|
||||
<< "Orders sent: " << metrics.orders_sent << "\n"
|
||||
<< "Trades received: " << metrics.trades_received << "\n"
|
||||
<< "Last latency: " << metrics.last_latency_us << " us\n"
|
||||
<< "Reconnects: " << metrics.reconnect_count << "\n"
|
||||
<< "Errors: " << metrics.error_count << "\n\n";
|
||||
}
|
||||
|
||||
// Check connection
|
||||
if (!client->is_connected()) {
|
||||
std::cout << "Connection lost, waiting for reconnect...\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
std::cout << "\nDisconnecting...\n";
|
||||
client->disconnect();
|
||||
std::cout << "Done.\n";
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
// LX C++ SDK - Client
|
||||
// Copyright (c) 2025 Lux Partners Limited
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef LXDEX_CLIENT_HPP
|
||||
#define LXDEX_CLIENT_HPP
|
||||
|
||||
#include "types.hpp"
|
||||
#include "orderbook.hpp"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <future>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
|
||||
namespace lxdex {
|
||||
|
||||
/// Connection state
|
||||
enum class ConnectionState {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Reconnecting,
|
||||
Failed
|
||||
};
|
||||
|
||||
/// Client configuration
|
||||
struct ClientConfig {
|
||||
std::string ws_url = "ws://localhost:8081";
|
||||
std::string api_key;
|
||||
std::string api_secret;
|
||||
|
||||
// Connection settings
|
||||
std::chrono::seconds connect_timeout{10};
|
||||
std::chrono::seconds ping_interval{30};
|
||||
std::chrono::seconds reconnect_delay{5};
|
||||
int max_reconnect_attempts = 5;
|
||||
bool auto_reconnect = true;
|
||||
|
||||
// Performance settings
|
||||
size_t send_queue_size = 10000;
|
||||
size_t recv_queue_size = 10000;
|
||||
};
|
||||
|
||||
/// Client metrics
|
||||
struct ClientMetrics {
|
||||
uint64_t messages_sent = 0;
|
||||
uint64_t messages_received = 0;
|
||||
uint64_t orders_sent = 0;
|
||||
uint64_t trades_received = 0;
|
||||
int64_t last_latency_us = 0;
|
||||
int64_t avg_latency_us = 0;
|
||||
uint64_t reconnect_count = 0;
|
||||
uint64_t error_count = 0;
|
||||
};
|
||||
|
||||
/// Error information
|
||||
struct Error {
|
||||
int code = 0;
|
||||
std::string message;
|
||||
std::string request_id;
|
||||
|
||||
explicit operator bool() const noexcept { return code != 0; }
|
||||
};
|
||||
|
||||
/// Result type for async operations
|
||||
template<typename T>
|
||||
struct Result {
|
||||
T value;
|
||||
Error error;
|
||||
|
||||
[[nodiscard]] bool ok() const noexcept { return !error; }
|
||||
explicit operator bool() const noexcept { return ok(); }
|
||||
};
|
||||
|
||||
/// Callback types
|
||||
using ErrorCallback = std::function<void(const Error&)>;
|
||||
using OrderCallback = std::function<void(const Order&)>;
|
||||
using TradeCallback = std::function<void(const Trade&)>;
|
||||
using OrderBookCallback = std::function<void(const OrderBook&)>;
|
||||
using MessageCallback = std::function<void(const Message&)>;
|
||||
using ConnectionCallback = std::function<void(ConnectionState)>;
|
||||
|
||||
/// LX WebSocket Client
|
||||
/// Thread-safe, RAII-compliant client for trading operations
|
||||
class Client {
|
||||
public:
|
||||
/// Construct client with configuration
|
||||
explicit Client(ClientConfig config = {});
|
||||
|
||||
/// Destructor - ensures clean disconnection
|
||||
~Client();
|
||||
|
||||
// Non-copyable, non-movable (owns thread resources)
|
||||
Client(const Client&) = delete;
|
||||
Client& operator=(const Client&) = delete;
|
||||
Client(Client&&) = delete;
|
||||
Client& operator=(Client&&) = delete;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Connection
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/// Connect to server
|
||||
/// @return Error if connection fails
|
||||
Error connect();
|
||||
|
||||
/// Disconnect from server
|
||||
void disconnect();
|
||||
|
||||
/// Check if connected
|
||||
[[nodiscard]] bool is_connected() const noexcept;
|
||||
|
||||
/// Get connection state
|
||||
[[nodiscard]] ConnectionState state() const noexcept;
|
||||
|
||||
/// Authenticate with API credentials
|
||||
/// @return Error if authentication fails
|
||||
Error authenticate();
|
||||
|
||||
/// Check if authenticated
|
||||
[[nodiscard]] bool is_authenticated() const noexcept;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Trading Operations
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/// Place a new order
|
||||
/// @param order Order to place
|
||||
/// @return Result with order response
|
||||
Result<OrderResponse> place_order(const Order& order);
|
||||
|
||||
/// Place order asynchronously
|
||||
/// @param order Order to place
|
||||
/// @return Future with result
|
||||
std::future<Result<OrderResponse>> place_order_async(const Order& order);
|
||||
|
||||
/// Cancel an order
|
||||
/// @param order_id Order ID to cancel
|
||||
/// @return Error if cancellation fails
|
||||
Error cancel_order(uint64_t order_id);
|
||||
|
||||
/// Cancel order asynchronously
|
||||
std::future<Error> cancel_order_async(uint64_t order_id);
|
||||
|
||||
/// Modify an existing order
|
||||
/// @param order_id Order ID to modify
|
||||
/// @param new_price New price (0 to keep current)
|
||||
/// @param new_size New size (0 to keep current)
|
||||
/// @return Error if modification fails
|
||||
Error modify_order(uint64_t order_id, double new_price, double new_size);
|
||||
|
||||
/// Cancel all orders for a symbol
|
||||
/// @param symbol Symbol to cancel orders for (empty for all)
|
||||
/// @return Number of orders cancelled
|
||||
Result<int> cancel_all_orders(const std::string& symbol = "");
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Market Data
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/// Get current orderbook snapshot
|
||||
/// @param symbol Trading pair symbol
|
||||
/// @param depth Number of levels (0 for all)
|
||||
/// @return Result with orderbook
|
||||
Result<OrderBook> get_orderbook(const std::string& symbol, int32_t depth = 20);
|
||||
|
||||
/// Get recent trades
|
||||
/// @param symbol Trading pair symbol
|
||||
/// @param limit Maximum number of trades
|
||||
/// @return Result with trades
|
||||
Result<std::vector<Trade>> get_trades(const std::string& symbol, int32_t limit = 100);
|
||||
|
||||
/// Subscribe to orderbook updates
|
||||
/// @param symbols Symbols to subscribe to
|
||||
/// @param callback Callback for updates
|
||||
/// @return Error if subscription fails
|
||||
Error subscribe_orderbook(
|
||||
const std::vector<std::string>& symbols,
|
||||
OrderBookCallback callback
|
||||
);
|
||||
|
||||
/// Subscribe to trade updates
|
||||
/// @param symbols Symbols to subscribe to
|
||||
/// @param callback Callback for trades
|
||||
/// @return Error if subscription fails
|
||||
Error subscribe_trades(
|
||||
const std::vector<std::string>& symbols,
|
||||
TradeCallback callback
|
||||
);
|
||||
|
||||
/// Unsubscribe from channel
|
||||
/// @param channel Channel name (e.g., "orderbook", "trades")
|
||||
/// @param symbols Symbols to unsubscribe from
|
||||
/// @return Error if unsubscription fails
|
||||
Error unsubscribe(
|
||||
const std::string& channel,
|
||||
const std::vector<std::string>& symbols
|
||||
);
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Account
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/// Get account balances
|
||||
/// @return Result with balances
|
||||
Result<std::vector<Balance>> get_balances();
|
||||
|
||||
/// Get open positions
|
||||
/// @return Result with positions
|
||||
Result<std::vector<Position>> get_positions();
|
||||
|
||||
/// Get open orders
|
||||
/// @return Result with orders
|
||||
Result<std::vector<Order>> get_orders();
|
||||
|
||||
/// Get node information
|
||||
/// @return Result with node info
|
||||
Result<NodeInfo> get_info();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Callbacks
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/// Set error callback
|
||||
void on_error(ErrorCallback callback);
|
||||
|
||||
/// Set order update callback
|
||||
void on_order(OrderCallback callback);
|
||||
|
||||
/// Set trade callback
|
||||
void on_trade(TradeCallback callback);
|
||||
|
||||
/// Set connection state callback
|
||||
void on_connection(ConnectionCallback callback);
|
||||
|
||||
/// Set raw message callback (for debugging)
|
||||
void on_message(MessageCallback callback);
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Local Data
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/// Get local orderbook manager
|
||||
[[nodiscard]] OrderBookManager& orderbooks() noexcept;
|
||||
|
||||
/// Get order tracker
|
||||
[[nodiscard]] OrderTracker& orders() noexcept;
|
||||
|
||||
/// Get trade tracker
|
||||
[[nodiscard]] TradeTracker& trades() noexcept;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Metrics
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/// Get client metrics
|
||||
[[nodiscard]] ClientMetrics metrics() const noexcept;
|
||||
|
||||
/// Reset metrics
|
||||
void reset_metrics();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Utility
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/// Send ping and measure latency
|
||||
/// @return Round-trip time in microseconds
|
||||
Result<int64_t> ping();
|
||||
|
||||
/// Generate client order ID
|
||||
[[nodiscard]] static std::string generate_client_id();
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
/// Convenience factory function
|
||||
inline std::unique_ptr<Client> make_client(ClientConfig config = {}) {
|
||||
return std::make_unique<Client>(std::move(config));
|
||||
}
|
||||
|
||||
} // namespace lxdex
|
||||
|
||||
#endif // LXDEX_CLIENT_HPP
|
||||
@@ -1,188 +0,0 @@
|
||||
// LX C++ SDK - Orderbook Interface
|
||||
// Copyright (c) 2025 Lux Partners Limited
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef LXDEX_ORDERBOOK_HPP
|
||||
#define LXDEX_ORDERBOOK_HPP
|
||||
|
||||
#include "types.hpp"
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
|
||||
namespace lxdex {
|
||||
|
||||
/// Local orderbook for market data tracking
|
||||
/// Thread-safe implementation for real-time updates
|
||||
class LocalOrderBook {
|
||||
public:
|
||||
explicit LocalOrderBook(std::string symbol);
|
||||
~LocalOrderBook() = default;
|
||||
|
||||
// Non-copyable, non-movable (contains atomic and mutex)
|
||||
LocalOrderBook(const LocalOrderBook&) = delete;
|
||||
LocalOrderBook& operator=(const LocalOrderBook&) = delete;
|
||||
LocalOrderBook(LocalOrderBook&&) = delete;
|
||||
LocalOrderBook& operator=(LocalOrderBook&&) = delete;
|
||||
|
||||
/// Get symbol
|
||||
[[nodiscard]] const std::string& symbol() const noexcept { return symbol_; }
|
||||
|
||||
/// Apply full snapshot
|
||||
void apply_snapshot(const OrderBook& snapshot);
|
||||
|
||||
/// Update single price level
|
||||
void update_level(Side side, double price, double size);
|
||||
|
||||
/// Remove price level
|
||||
void remove_level(Side side, double price);
|
||||
|
||||
/// Get current snapshot
|
||||
[[nodiscard]] OrderBook get_snapshot(int32_t depth = 0) const;
|
||||
|
||||
/// Get best bid
|
||||
[[nodiscard]] std::optional<PriceLevel> best_bid() const;
|
||||
|
||||
/// Get best ask
|
||||
[[nodiscard]] std::optional<PriceLevel> best_ask() const;
|
||||
|
||||
/// Get mid price
|
||||
[[nodiscard]] double mid_price() const;
|
||||
|
||||
/// Get spread
|
||||
[[nodiscard]] double spread() const;
|
||||
|
||||
/// Get total bid depth
|
||||
[[nodiscard]] double bid_depth() const;
|
||||
|
||||
/// Get total ask depth
|
||||
[[nodiscard]] double ask_depth() const;
|
||||
|
||||
/// Clear all levels
|
||||
void clear();
|
||||
|
||||
/// Get last update timestamp
|
||||
[[nodiscard]] int64_t last_update() const noexcept { return last_update_.load(); }
|
||||
|
||||
/// Set callback for orderbook updates
|
||||
void on_update(std::function<void(const OrderBook&)> callback);
|
||||
|
||||
private:
|
||||
std::string symbol_;
|
||||
|
||||
// Bids: highest price first (descending)
|
||||
std::map<double, PriceLevel, std::greater<double>> bids_;
|
||||
|
||||
// Asks: lowest price first (ascending)
|
||||
std::map<double, PriceLevel, std::less<double>> asks_;
|
||||
|
||||
std::atomic<int64_t> last_update_{0};
|
||||
mutable std::mutex mutex_;
|
||||
std::function<void(const OrderBook&)> update_callback_;
|
||||
|
||||
void notify_update();
|
||||
};
|
||||
|
||||
/// Order tracker for managing local order state
|
||||
class OrderTracker {
|
||||
public:
|
||||
OrderTracker() = default;
|
||||
~OrderTracker() = default;
|
||||
|
||||
/// Add or update order
|
||||
void upsert(const Order& order);
|
||||
|
||||
/// Remove order by ID
|
||||
bool remove(uint64_t order_id);
|
||||
|
||||
/// Get order by ID
|
||||
[[nodiscard]] std::optional<Order> get(uint64_t order_id) const;
|
||||
|
||||
/// Get all orders for symbol
|
||||
[[nodiscard]] std::vector<Order> get_by_symbol(const std::string& symbol) const;
|
||||
|
||||
/// Get all open orders
|
||||
[[nodiscard]] std::vector<Order> get_open() const;
|
||||
|
||||
/// Get all orders
|
||||
[[nodiscard]] std::vector<Order> get_all() const;
|
||||
|
||||
/// Clear all orders
|
||||
void clear();
|
||||
|
||||
/// Get order count
|
||||
[[nodiscard]] size_t count() const;
|
||||
|
||||
private:
|
||||
std::unordered_map<uint64_t, Order> orders_;
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
/// Trade tracker for recent trades
|
||||
class TradeTracker {
|
||||
public:
|
||||
explicit TradeTracker(size_t max_trades = 1000);
|
||||
~TradeTracker() = default;
|
||||
|
||||
/// Add trade
|
||||
void add(const Trade& trade);
|
||||
|
||||
/// Get recent trades for symbol
|
||||
[[nodiscard]] std::vector<Trade> get_by_symbol(
|
||||
const std::string& symbol,
|
||||
size_t limit = 100
|
||||
) const;
|
||||
|
||||
/// Get all recent trades
|
||||
[[nodiscard]] std::vector<Trade> get_recent(size_t limit = 100) const;
|
||||
|
||||
/// Clear all trades
|
||||
void clear();
|
||||
|
||||
/// Get trade count
|
||||
[[nodiscard]] size_t count() const;
|
||||
|
||||
/// Set callback for new trades
|
||||
void on_trade(std::function<void(const Trade&)> callback);
|
||||
|
||||
private:
|
||||
std::vector<Trade> trades_;
|
||||
size_t max_trades_;
|
||||
mutable std::mutex mutex_;
|
||||
std::function<void(const Trade&)> trade_callback_;
|
||||
};
|
||||
|
||||
/// Multi-symbol orderbook manager
|
||||
class OrderBookManager {
|
||||
public:
|
||||
OrderBookManager() = default;
|
||||
~OrderBookManager() = default;
|
||||
|
||||
/// Get or create orderbook for symbol
|
||||
LocalOrderBook& get_or_create(const std::string& symbol);
|
||||
|
||||
/// Get orderbook for symbol (returns nullptr if not exists)
|
||||
LocalOrderBook* get(const std::string& symbol);
|
||||
|
||||
/// Check if orderbook exists
|
||||
[[nodiscard]] bool has(const std::string& symbol) const;
|
||||
|
||||
/// Remove orderbook
|
||||
bool remove(const std::string& symbol);
|
||||
|
||||
/// Get all symbols
|
||||
[[nodiscard]] std::vector<std::string> symbols() const;
|
||||
|
||||
/// Clear all orderbooks
|
||||
void clear();
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::unique_ptr<LocalOrderBook>> books_;
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
} // namespace lxdex
|
||||
|
||||
#endif // LXDEX_ORDERBOOK_HPP
|
||||
@@ -1,376 +0,0 @@
|
||||
// LX C++ SDK - Type Definitions
|
||||
// Copyright (c) 2025 Lux Partners Limited
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef LXDEX_TYPES_HPP
|
||||
#define LXDEX_TYPES_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace lxdex {
|
||||
|
||||
/// Order side
|
||||
enum class Side : int32_t {
|
||||
Buy = 0,
|
||||
Sell = 1
|
||||
};
|
||||
|
||||
/// Order type
|
||||
enum class OrderType : int32_t {
|
||||
Limit = 0,
|
||||
Market = 1,
|
||||
Stop = 2,
|
||||
StopLimit = 3,
|
||||
Iceberg = 4,
|
||||
Peg = 5
|
||||
};
|
||||
|
||||
/// Order status
|
||||
enum class OrderStatus {
|
||||
Open,
|
||||
Partial,
|
||||
Filled,
|
||||
Cancelled,
|
||||
Rejected
|
||||
};
|
||||
|
||||
/// Time in force
|
||||
enum class TimeInForce {
|
||||
GTC, // Good Till Cancelled
|
||||
IOC, // Immediate Or Cancel
|
||||
FOK, // Fill Or Kill
|
||||
DAY // Day Order
|
||||
};
|
||||
|
||||
/// Trading order
|
||||
struct Order {
|
||||
uint64_t order_id = 0;
|
||||
std::string symbol;
|
||||
OrderType type = OrderType::Limit;
|
||||
Side side = Side::Buy;
|
||||
double price = 0.0;
|
||||
double size = 0.0;
|
||||
double filled = 0.0;
|
||||
double remaining = 0.0;
|
||||
OrderStatus status = OrderStatus::Open;
|
||||
std::string user_id;
|
||||
std::string client_id;
|
||||
int64_t timestamp = 0;
|
||||
TimeInForce time_in_force = TimeInForce::GTC;
|
||||
bool post_only = false;
|
||||
bool reduce_only = false;
|
||||
|
||||
/// Check if order is open
|
||||
[[nodiscard]] bool is_open() const noexcept {
|
||||
return status == OrderStatus::Open || status == OrderStatus::Partial;
|
||||
}
|
||||
|
||||
/// Check if order is closed
|
||||
[[nodiscard]] bool is_closed() const noexcept {
|
||||
return status == OrderStatus::Filled ||
|
||||
status == OrderStatus::Cancelled ||
|
||||
status == OrderStatus::Rejected;
|
||||
}
|
||||
|
||||
/// Get fill rate [0.0, 1.0]
|
||||
[[nodiscard]] double fill_rate() const noexcept {
|
||||
return (size > 0.0) ? filled / size : 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Order response from server
|
||||
struct OrderResponse {
|
||||
uint64_t order_id = 0;
|
||||
std::string status;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
/// Executed trade
|
||||
struct Trade {
|
||||
uint64_t trade_id = 0;
|
||||
std::string symbol;
|
||||
double price = 0.0;
|
||||
double size = 0.0;
|
||||
Side side = Side::Buy;
|
||||
uint64_t buy_order_id = 0;
|
||||
uint64_t sell_order_id = 0;
|
||||
std::string buyer_id;
|
||||
std::string seller_id;
|
||||
int64_t timestamp = 0;
|
||||
|
||||
/// Get total trade value
|
||||
[[nodiscard]] double total_value() const noexcept {
|
||||
return price * size;
|
||||
}
|
||||
|
||||
/// Get timestamp as time_point
|
||||
[[nodiscard]] std::chrono::system_clock::time_point timestamp_time() const noexcept {
|
||||
return std::chrono::system_clock::from_time_t(timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
/// Price level in orderbook
|
||||
struct PriceLevel {
|
||||
double price = 0.0;
|
||||
double size = 0.0;
|
||||
int32_t count = 0;
|
||||
};
|
||||
|
||||
/// Orderbook snapshot
|
||||
struct OrderBook {
|
||||
std::string symbol;
|
||||
std::vector<PriceLevel> bids;
|
||||
std::vector<PriceLevel> asks;
|
||||
int64_t timestamp = 0;
|
||||
|
||||
/// Get best bid price (0 if empty)
|
||||
[[nodiscard]] double best_bid() const noexcept {
|
||||
return bids.empty() ? 0.0 : bids.front().price;
|
||||
}
|
||||
|
||||
/// Get best ask price (0 if empty)
|
||||
[[nodiscard]] double best_ask() const noexcept {
|
||||
return asks.empty() ? 0.0 : asks.front().price;
|
||||
}
|
||||
|
||||
/// Get bid-ask spread
|
||||
[[nodiscard]] double spread() const noexcept {
|
||||
double bid = best_bid();
|
||||
double ask = best_ask();
|
||||
return (bid > 0.0 && ask > 0.0) ? (ask - bid) : 0.0;
|
||||
}
|
||||
|
||||
/// Get mid price
|
||||
[[nodiscard]] double mid_price() const noexcept {
|
||||
double bid = best_bid();
|
||||
double ask = best_ask();
|
||||
return (bid > 0.0 && ask > 0.0) ? (bid + ask) / 2.0 : 0.0;
|
||||
}
|
||||
|
||||
/// Get spread as percentage of mid price
|
||||
[[nodiscard]] double spread_percentage() const noexcept {
|
||||
double mid = mid_price();
|
||||
return (mid > 0.0) ? (spread() / mid) * 100.0 : 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Node/server information
|
||||
struct NodeInfo {
|
||||
std::string version;
|
||||
std::string network;
|
||||
int64_t order_count = 0;
|
||||
int64_t trade_count = 0;
|
||||
int64_t timestamp = 0;
|
||||
int64_t block_height = 0;
|
||||
bool syncing = false;
|
||||
int64_t uptime = 0;
|
||||
};
|
||||
|
||||
/// Account balance
|
||||
struct Balance {
|
||||
std::string asset;
|
||||
double available = 0.0;
|
||||
double locked = 0.0;
|
||||
double total = 0.0;
|
||||
|
||||
/// Get utilization ratio [0.0, 1.0]
|
||||
[[nodiscard]] double utilization() const noexcept {
|
||||
return (total > 0.0) ? locked / total : 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Trading position
|
||||
struct Position {
|
||||
std::string symbol;
|
||||
double size = 0.0;
|
||||
double entry_price = 0.0;
|
||||
double mark_price = 0.0;
|
||||
double pnl = 0.0;
|
||||
double margin = 0.0;
|
||||
|
||||
/// Calculate unrealized PnL
|
||||
[[nodiscard]] double unrealized_pnl() const noexcept {
|
||||
return (mark_price - entry_price) * size;
|
||||
}
|
||||
|
||||
/// Calculate PnL percentage
|
||||
[[nodiscard]] double pnl_percentage() const noexcept {
|
||||
return (entry_price > 0.0)
|
||||
? ((mark_price - entry_price) / entry_price) * 100.0
|
||||
: 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
/// WebSocket message
|
||||
struct Message {
|
||||
std::string type;
|
||||
nlohmann::json data;
|
||||
std::string error;
|
||||
std::string request_id;
|
||||
int64_t timestamp = 0;
|
||||
};
|
||||
|
||||
// JSON serialization
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(Side, {
|
||||
{Side::Buy, "buy"},
|
||||
{Side::Sell, "sell"}
|
||||
})
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(OrderType, {
|
||||
{OrderType::Limit, "limit"},
|
||||
{OrderType::Market, "market"},
|
||||
{OrderType::Stop, "stop"},
|
||||
{OrderType::StopLimit, "stop_limit"},
|
||||
{OrderType::Iceberg, "iceberg"},
|
||||
{OrderType::Peg, "peg"}
|
||||
})
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(OrderStatus, {
|
||||
{OrderStatus::Open, "open"},
|
||||
{OrderStatus::Partial, "partial"},
|
||||
{OrderStatus::Filled, "filled"},
|
||||
{OrderStatus::Cancelled, "cancelled"},
|
||||
{OrderStatus::Rejected, "rejected"}
|
||||
})
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(TimeInForce, {
|
||||
{TimeInForce::GTC, "GTC"},
|
||||
{TimeInForce::IOC, "IOC"},
|
||||
{TimeInForce::FOK, "FOK"},
|
||||
{TimeInForce::DAY, "DAY"}
|
||||
})
|
||||
|
||||
inline void to_json(nlohmann::json& j, const Order& o) {
|
||||
j = nlohmann::json{
|
||||
{"orderId", o.order_id},
|
||||
{"symbol", o.symbol},
|
||||
{"type", o.type},
|
||||
{"side", o.side},
|
||||
{"price", o.price},
|
||||
{"size", o.size},
|
||||
{"filled", o.filled},
|
||||
{"remaining", o.remaining},
|
||||
{"status", o.status},
|
||||
{"userId", o.user_id},
|
||||
{"clientId", o.client_id},
|
||||
{"timestamp", o.timestamp},
|
||||
{"timeInForce", o.time_in_force},
|
||||
{"postOnly", o.post_only},
|
||||
{"reduceOnly", o.reduce_only}
|
||||
};
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json& j, Order& o) {
|
||||
if (j.contains("orderId")) j.at("orderId").get_to(o.order_id);
|
||||
if (j.contains("symbol")) j.at("symbol").get_to(o.symbol);
|
||||
if (j.contains("type")) j.at("type").get_to(o.type);
|
||||
if (j.contains("side")) j.at("side").get_to(o.side);
|
||||
if (j.contains("price")) j.at("price").get_to(o.price);
|
||||
if (j.contains("size")) j.at("size").get_to(o.size);
|
||||
if (j.contains("filled")) j.at("filled").get_to(o.filled);
|
||||
if (j.contains("remaining")) j.at("remaining").get_to(o.remaining);
|
||||
if (j.contains("status")) j.at("status").get_to(o.status);
|
||||
if (j.contains("userId")) j.at("userId").get_to(o.user_id);
|
||||
if (j.contains("clientId")) j.at("clientId").get_to(o.client_id);
|
||||
if (j.contains("timestamp")) j.at("timestamp").get_to(o.timestamp);
|
||||
if (j.contains("timeInForce")) j.at("timeInForce").get_to(o.time_in_force);
|
||||
if (j.contains("postOnly")) j.at("postOnly").get_to(o.post_only);
|
||||
if (j.contains("reduceOnly")) j.at("reduceOnly").get_to(o.reduce_only);
|
||||
}
|
||||
|
||||
inline void to_json(nlohmann::json& j, const Trade& t) {
|
||||
j = nlohmann::json{
|
||||
{"tradeId", t.trade_id},
|
||||
{"symbol", t.symbol},
|
||||
{"price", t.price},
|
||||
{"size", t.size},
|
||||
{"side", t.side},
|
||||
{"buyOrderId", t.buy_order_id},
|
||||
{"sellOrderId", t.sell_order_id},
|
||||
{"buyerId", t.buyer_id},
|
||||
{"sellerId", t.seller_id},
|
||||
{"timestamp", t.timestamp}
|
||||
};
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json& j, Trade& t) {
|
||||
if (j.contains("tradeId")) j.at("tradeId").get_to(t.trade_id);
|
||||
if (j.contains("symbol")) j.at("symbol").get_to(t.symbol);
|
||||
if (j.contains("price")) j.at("price").get_to(t.price);
|
||||
if (j.contains("size")) j.at("size").get_to(t.size);
|
||||
if (j.contains("side")) j.at("side").get_to(t.side);
|
||||
if (j.contains("buyOrderId")) j.at("buyOrderId").get_to(t.buy_order_id);
|
||||
if (j.contains("sellOrderId")) j.at("sellOrderId").get_to(t.sell_order_id);
|
||||
if (j.contains("buyerId")) j.at("buyerId").get_to(t.buyer_id);
|
||||
if (j.contains("sellerId")) j.at("sellerId").get_to(t.seller_id);
|
||||
if (j.contains("timestamp")) j.at("timestamp").get_to(t.timestamp);
|
||||
}
|
||||
|
||||
inline void to_json(nlohmann::json& j, const PriceLevel& p) {
|
||||
j = nlohmann::json{{"price", p.price}, {"size", p.size}, {"count", p.count}};
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json& j, PriceLevel& p) {
|
||||
if (j.contains("price")) j.at("price").get_to(p.price);
|
||||
if (j.contains("size")) j.at("size").get_to(p.size);
|
||||
if (j.contains("count")) j.at("count").get_to(p.count);
|
||||
}
|
||||
|
||||
inline void to_json(nlohmann::json& j, const OrderBook& ob) {
|
||||
j = nlohmann::json{
|
||||
{"symbol", ob.symbol},
|
||||
{"bids", ob.bids},
|
||||
{"asks", ob.asks},
|
||||
{"timestamp", ob.timestamp}
|
||||
};
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json& j, OrderBook& ob) {
|
||||
if (j.contains("symbol")) j.at("symbol").get_to(ob.symbol);
|
||||
if (j.contains("bids")) j.at("bids").get_to(ob.bids);
|
||||
if (j.contains("asks")) j.at("asks").get_to(ob.asks);
|
||||
if (j.contains("timestamp")) j.at("timestamp").get_to(ob.timestamp);
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json& j, OrderResponse& r) {
|
||||
if (j.contains("orderId")) j.at("orderId").get_to(r.order_id);
|
||||
if (j.contains("status")) j.at("status").get_to(r.status);
|
||||
if (j.contains("message")) j.at("message").get_to(r.message);
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json& j, Balance& b) {
|
||||
if (j.contains("asset")) j.at("asset").get_to(b.asset);
|
||||
if (j.contains("available")) j.at("available").get_to(b.available);
|
||||
if (j.contains("locked")) j.at("locked").get_to(b.locked);
|
||||
if (j.contains("total")) j.at("total").get_to(b.total);
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json& j, Position& p) {
|
||||
if (j.contains("symbol")) j.at("symbol").get_to(p.symbol);
|
||||
if (j.contains("size")) j.at("size").get_to(p.size);
|
||||
if (j.contains("entryPrice")) j.at("entryPrice").get_to(p.entry_price);
|
||||
if (j.contains("markPrice")) j.at("markPrice").get_to(p.mark_price);
|
||||
if (j.contains("pnl")) j.at("pnl").get_to(p.pnl);
|
||||
if (j.contains("margin")) j.at("margin").get_to(p.margin);
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json& j, NodeInfo& n) {
|
||||
if (j.contains("version")) j.at("version").get_to(n.version);
|
||||
if (j.contains("network")) j.at("network").get_to(n.network);
|
||||
if (j.contains("orderCount")) j.at("orderCount").get_to(n.order_count);
|
||||
if (j.contains("tradeCount")) j.at("tradeCount").get_to(n.trade_count);
|
||||
if (j.contains("timestamp")) j.at("timestamp").get_to(n.timestamp);
|
||||
if (j.contains("blockHeight")) j.at("blockHeight").get_to(n.block_height);
|
||||
if (j.contains("syncing")) j.at("syncing").get_to(n.syncing);
|
||||
if (j.contains("uptime")) j.at("uptime").get_to(n.uptime);
|
||||
}
|
||||
|
||||
} // namespace lxdex
|
||||
|
||||
#endif // LXDEX_TYPES_HPP
|
||||
@@ -1,994 +0,0 @@
|
||||
// LX C++ SDK - Client Implementation
|
||||
// Copyright (c) 2025 Lux Partners Limited
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "lxdex/client.hpp"
|
||||
|
||||
#include <websocketpp/config/asio_no_tls_client.hpp>
|
||||
#include <websocketpp/client.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <queue>
|
||||
#include <condition_variable>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
namespace lxdex {
|
||||
|
||||
using WsClient = websocketpp::client<websocketpp::config::asio_client>;
|
||||
using ConnectionHdl = websocketpp::connection_hdl;
|
||||
using MessagePtr = websocketpp::config::asio_client::message_type::ptr;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Client::Impl
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class Client::Impl {
|
||||
public:
|
||||
explicit Impl(ClientConfig config)
|
||||
: config_(std::move(config))
|
||||
, state_(ConnectionState::Disconnected)
|
||||
, authenticated_(false)
|
||||
, request_id_(0)
|
||||
, running_(false)
|
||||
{
|
||||
ws_client_.clear_access_channels(websocketpp::log::alevel::all);
|
||||
ws_client_.clear_error_channels(websocketpp::log::elevel::all);
|
||||
|
||||
ws_client_.init_asio();
|
||||
|
||||
ws_client_.set_open_handler([this](ConnectionHdl hdl) {
|
||||
on_open(hdl);
|
||||
});
|
||||
|
||||
ws_client_.set_close_handler([this](ConnectionHdl hdl) {
|
||||
on_close(hdl);
|
||||
});
|
||||
|
||||
ws_client_.set_fail_handler([this](ConnectionHdl hdl) {
|
||||
on_fail(hdl);
|
||||
});
|
||||
|
||||
ws_client_.set_message_handler([this](ConnectionHdl hdl, MessagePtr msg) {
|
||||
on_message(hdl, msg);
|
||||
});
|
||||
}
|
||||
|
||||
~Impl() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
Error connect() {
|
||||
if (state_ == ConnectionState::Connected) {
|
||||
return {};
|
||||
}
|
||||
|
||||
state_ = ConnectionState::Connecting;
|
||||
|
||||
websocketpp::lib::error_code ec;
|
||||
auto con = ws_client_.get_connection(config_.ws_url, ec);
|
||||
|
||||
if (ec) {
|
||||
state_ = ConnectionState::Failed;
|
||||
return Error{-1, "Failed to create connection: " + ec.message()};
|
||||
}
|
||||
|
||||
connection_ = con->get_handle();
|
||||
ws_client_.connect(con);
|
||||
|
||||
// Start IO thread
|
||||
running_ = true;
|
||||
io_thread_ = std::thread([this]() {
|
||||
ws_client_.run();
|
||||
});
|
||||
|
||||
// Wait for connection with timeout
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(connect_mutex_);
|
||||
auto result = connect_cv_.wait_for(
|
||||
lock,
|
||||
config_.connect_timeout,
|
||||
[this]() {
|
||||
return state_ == ConnectionState::Connected ||
|
||||
state_ == ConnectionState::Failed;
|
||||
}
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
state_ = ConnectionState::Failed;
|
||||
return Error{-2, "Connection timeout"};
|
||||
}
|
||||
}
|
||||
|
||||
if (state_ != ConnectionState::Connected) {
|
||||
return Error{-3, "Connection failed"};
|
||||
}
|
||||
|
||||
// Notify callback
|
||||
if (connection_callback_) {
|
||||
connection_callback_(state_);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
if (state_ == ConnectionState::Disconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
running_ = false;
|
||||
state_ = ConnectionState::Disconnected;
|
||||
authenticated_ = false;
|
||||
|
||||
try {
|
||||
if (!connection_.expired()) {
|
||||
ws_client_.close(connection_, websocketpp::close::status::normal, "");
|
||||
}
|
||||
} catch (...) {
|
||||
// Ignore close errors
|
||||
}
|
||||
|
||||
ws_client_.stop();
|
||||
|
||||
if (io_thread_.joinable()) {
|
||||
io_thread_.join();
|
||||
}
|
||||
|
||||
// Notify callback
|
||||
if (connection_callback_) {
|
||||
connection_callback_(state_);
|
||||
}
|
||||
}
|
||||
|
||||
bool is_connected() const noexcept {
|
||||
return state_ == ConnectionState::Connected;
|
||||
}
|
||||
|
||||
ConnectionState state() const noexcept {
|
||||
return state_;
|
||||
}
|
||||
|
||||
Error authenticate() {
|
||||
if (!is_connected()) {
|
||||
return Error{-1, "Not connected"};
|
||||
}
|
||||
|
||||
if (config_.api_key.empty() || config_.api_secret.empty()) {
|
||||
return Error{-2, "API credentials not configured"};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "auth"},
|
||||
{"apiKey", config_.api_key},
|
||||
{"apiSecret", config_.api_secret},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(10));
|
||||
if (!result.ok()) {
|
||||
return result.error;
|
||||
}
|
||||
|
||||
if (result.value.contains("type") && result.value["type"] == "auth_success") {
|
||||
authenticated_ = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
return Error{-3, "Authentication failed"};
|
||||
}
|
||||
|
||||
bool is_authenticated() const noexcept {
|
||||
return authenticated_;
|
||||
}
|
||||
|
||||
Result<OrderResponse> place_order(const Order& order) {
|
||||
if (!is_connected()) {
|
||||
return {{}, Error{-1, "Not connected"}};
|
||||
}
|
||||
|
||||
if (!is_authenticated()) {
|
||||
return {{}, Error{-2, "Not authenticated"}};
|
||||
}
|
||||
|
||||
nlohmann::json order_data = {
|
||||
{"symbol", order.symbol},
|
||||
{"type", order.type},
|
||||
{"side", order.side},
|
||||
{"price", order.price},
|
||||
{"size", order.size}
|
||||
};
|
||||
|
||||
if (!order.client_id.empty()) {
|
||||
order_data["clientId"] = order.client_id;
|
||||
}
|
||||
if (order.time_in_force != TimeInForce::GTC) {
|
||||
order_data["timeInForce"] = order.time_in_force;
|
||||
}
|
||||
if (order.post_only) {
|
||||
order_data["postOnly"] = true;
|
||||
}
|
||||
if (order.reduce_only) {
|
||||
order_data["reduceOnly"] = true;
|
||||
}
|
||||
|
||||
std::string req_id = next_request_id();
|
||||
nlohmann::json msg = {
|
||||
{"type", "place_order"},
|
||||
{"order", order_data},
|
||||
{"request_id", req_id}
|
||||
};
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(10));
|
||||
if (!result.ok()) {
|
||||
return {{}, result.error};
|
||||
}
|
||||
|
||||
metrics_.orders_sent++;
|
||||
|
||||
OrderResponse resp;
|
||||
if (result.value.contains("data")) {
|
||||
auto& data = result.value["data"];
|
||||
if (data.contains("order") && data["order"].contains("ID")) {
|
||||
resp.order_id = data["order"]["ID"].get<uint64_t>();
|
||||
}
|
||||
if (data.contains("status")) {
|
||||
resp.status = data["status"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
return {resp, {}};
|
||||
}
|
||||
|
||||
std::future<Result<OrderResponse>> place_order_async(const Order& order) {
|
||||
return std::async(std::launch::async, [this, order]() {
|
||||
return place_order(order);
|
||||
});
|
||||
}
|
||||
|
||||
Error cancel_order(uint64_t order_id) {
|
||||
if (!is_connected()) {
|
||||
return Error{-1, "Not connected"};
|
||||
}
|
||||
|
||||
if (!is_authenticated()) {
|
||||
return Error{-2, "Not authenticated"};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "cancel_order"},
|
||||
{"orderID", order_id},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(10));
|
||||
if (!result.ok()) {
|
||||
return result.error;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::future<Error> cancel_order_async(uint64_t order_id) {
|
||||
return std::async(std::launch::async, [this, order_id]() {
|
||||
return cancel_order(order_id);
|
||||
});
|
||||
}
|
||||
|
||||
Error modify_order(uint64_t order_id, double new_price, double new_size) {
|
||||
if (!is_connected()) {
|
||||
return Error{-1, "Not connected"};
|
||||
}
|
||||
|
||||
if (!is_authenticated()) {
|
||||
return Error{-2, "Not authenticated"};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "modify_order"},
|
||||
{"orderID", order_id},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
if (new_price > 0) {
|
||||
msg["newPrice"] = new_price;
|
||||
}
|
||||
if (new_size > 0) {
|
||||
msg["newSize"] = new_size;
|
||||
}
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(10));
|
||||
return result.error;
|
||||
}
|
||||
|
||||
Result<int> cancel_all_orders(const std::string& symbol) {
|
||||
// Cancel all would need to get orders first, then cancel each
|
||||
auto orders_result = get_orders();
|
||||
if (!orders_result.ok()) {
|
||||
return {0, orders_result.error};
|
||||
}
|
||||
|
||||
int cancelled = 0;
|
||||
for (const auto& order : orders_result.value) {
|
||||
if (symbol.empty() || order.symbol == symbol) {
|
||||
auto err = cancel_order(order.order_id);
|
||||
if (!err) {
|
||||
cancelled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {cancelled, {}};
|
||||
}
|
||||
|
||||
Error subscribe_orderbook(
|
||||
const std::vector<std::string>& symbols,
|
||||
OrderBookCallback callback
|
||||
) {
|
||||
orderbook_callback_ = std::move(callback);
|
||||
return subscribe("orderbook", symbols);
|
||||
}
|
||||
|
||||
Error subscribe_trades(
|
||||
const std::vector<std::string>& symbols,
|
||||
TradeCallback callback
|
||||
) {
|
||||
trade_callback_ = std::move(callback);
|
||||
return subscribe("trades", symbols);
|
||||
}
|
||||
|
||||
Error subscribe(
|
||||
const std::string& channel,
|
||||
const std::vector<std::string>& symbols
|
||||
) {
|
||||
if (!is_connected()) {
|
||||
return Error{-1, "Not connected"};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "subscribe"},
|
||||
{"channel", channel},
|
||||
{"symbols", symbols},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
return send(msg);
|
||||
}
|
||||
|
||||
Error unsubscribe(
|
||||
const std::string& channel,
|
||||
const std::vector<std::string>& symbols
|
||||
) {
|
||||
if (!is_connected()) {
|
||||
return Error{-1, "Not connected"};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "unsubscribe"},
|
||||
{"channel", channel},
|
||||
{"symbols", symbols},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
return send(msg);
|
||||
}
|
||||
|
||||
Result<std::vector<Balance>> get_balances() {
|
||||
if (!is_connected()) {
|
||||
return {{}, Error{-1, "Not connected"}};
|
||||
}
|
||||
|
||||
if (!is_authenticated()) {
|
||||
return {{}, Error{-2, "Not authenticated"}};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "get_balances"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(10));
|
||||
if (!result.ok()) {
|
||||
return {{}, result.error};
|
||||
}
|
||||
|
||||
std::vector<Balance> balances;
|
||||
if (result.value.contains("data") && result.value["data"].contains("balances")) {
|
||||
auto& bal_data = result.value["data"]["balances"];
|
||||
for (auto& [asset, amount] : bal_data.items()) {
|
||||
Balance b;
|
||||
b.asset = asset;
|
||||
if (amount.is_string()) {
|
||||
b.available = std::stod(amount.get<std::string>());
|
||||
} else {
|
||||
b.available = amount.get<double>();
|
||||
}
|
||||
b.total = b.available;
|
||||
balances.push_back(b);
|
||||
}
|
||||
}
|
||||
|
||||
return {balances, {}};
|
||||
}
|
||||
|
||||
Result<std::vector<Position>> get_positions() {
|
||||
if (!is_connected()) {
|
||||
return {{}, Error{-1, "Not connected"}};
|
||||
}
|
||||
|
||||
if (!is_authenticated()) {
|
||||
return {{}, Error{-2, "Not authenticated"}};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "get_positions"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(10));
|
||||
if (!result.ok()) {
|
||||
return {{}, result.error};
|
||||
}
|
||||
|
||||
std::vector<Position> positions;
|
||||
if (result.value.contains("data") && result.value["data"].contains("positions")) {
|
||||
for (auto& pos_data : result.value["data"]["positions"]) {
|
||||
Position p;
|
||||
if (pos_data.contains("symbol")) p.symbol = pos_data["symbol"];
|
||||
if (pos_data.contains("size")) p.size = pos_data["size"];
|
||||
if (pos_data.contains("entryPrice")) p.entry_price = pos_data["entryPrice"];
|
||||
if (pos_data.contains("markPrice")) p.mark_price = pos_data["markPrice"];
|
||||
if (pos_data.contains("pnl")) p.pnl = pos_data["pnl"];
|
||||
if (pos_data.contains("margin")) p.margin = pos_data["margin"];
|
||||
positions.push_back(p);
|
||||
}
|
||||
}
|
||||
|
||||
return {positions, {}};
|
||||
}
|
||||
|
||||
Result<std::vector<Order>> get_orders() {
|
||||
if (!is_connected()) {
|
||||
return {{}, Error{-1, "Not connected"}};
|
||||
}
|
||||
|
||||
if (!is_authenticated()) {
|
||||
return {{}, Error{-2, "Not authenticated"}};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "get_orders"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(10));
|
||||
if (!result.ok()) {
|
||||
return {{}, result.error};
|
||||
}
|
||||
|
||||
std::vector<Order> orders;
|
||||
if (result.value.contains("data") && result.value["data"].contains("orders")) {
|
||||
for (auto& order_data : result.value["data"]["orders"]) {
|
||||
Order o = order_data.get<Order>();
|
||||
orders.push_back(o);
|
||||
}
|
||||
}
|
||||
|
||||
return {orders, {}};
|
||||
}
|
||||
|
||||
Result<OrderBook> get_orderbook(const std::string& symbol, int32_t depth) {
|
||||
// Return from local cache if available
|
||||
auto* book = orderbook_manager_.get(symbol);
|
||||
if (book) {
|
||||
return {book->get_snapshot(depth), {}};
|
||||
}
|
||||
|
||||
// Otherwise subscribe and wait for first snapshot
|
||||
Error err = subscribe("orderbook", {symbol});
|
||||
if (err) {
|
||||
return {{}, err};
|
||||
}
|
||||
|
||||
// Wait briefly for snapshot
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
book = orderbook_manager_.get(symbol);
|
||||
if (book) {
|
||||
return {book->get_snapshot(depth), {}};
|
||||
}
|
||||
|
||||
return {{}, Error{-1, "Failed to get orderbook"}};
|
||||
}
|
||||
|
||||
Result<std::vector<Trade>> get_trades(const std::string& symbol, int32_t limit) {
|
||||
return {trade_tracker_.get_by_symbol(symbol, limit), {}};
|
||||
}
|
||||
|
||||
Result<NodeInfo> get_info() {
|
||||
if (!is_connected()) {
|
||||
return {{}, Error{-1, "Not connected"}};
|
||||
}
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "ping"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(5));
|
||||
if (!result.ok()) {
|
||||
return {{}, result.error};
|
||||
}
|
||||
|
||||
NodeInfo info;
|
||||
info.timestamp = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count();
|
||||
|
||||
return {info, {}};
|
||||
}
|
||||
|
||||
Result<int64_t> ping() {
|
||||
if (!is_connected()) {
|
||||
return {0, Error{-1, "Not connected"}};
|
||||
}
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
nlohmann::json msg = {
|
||||
{"type", "ping"},
|
||||
{"request_id", next_request_id()}
|
||||
};
|
||||
|
||||
auto result = send_and_wait(msg, std::chrono::seconds(5));
|
||||
if (!result.ok()) {
|
||||
return {0, result.error};
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto latency = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
|
||||
|
||||
metrics_.last_latency_us = latency;
|
||||
|
||||
return {latency, {}};
|
||||
}
|
||||
|
||||
void on_error(ErrorCallback callback) {
|
||||
error_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void on_order(OrderCallback callback) {
|
||||
order_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void on_trade(TradeCallback callback) {
|
||||
trade_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void on_connection(ConnectionCallback callback) {
|
||||
connection_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void on_message(MessageCallback callback) {
|
||||
message_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
OrderBookManager& orderbooks() noexcept {
|
||||
return orderbook_manager_;
|
||||
}
|
||||
|
||||
OrderTracker& orders() noexcept {
|
||||
return order_tracker_;
|
||||
}
|
||||
|
||||
TradeTracker& trades() noexcept {
|
||||
return trade_tracker_;
|
||||
}
|
||||
|
||||
ClientMetrics metrics() const noexcept {
|
||||
return metrics_;
|
||||
}
|
||||
|
||||
void reset_metrics() {
|
||||
metrics_ = {};
|
||||
}
|
||||
|
||||
static std::string generate_client_id() {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<uint64_t> dis;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "cpp_" << std::hex << std::setfill('0') << std::setw(16) << dis(gen);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
private:
|
||||
void on_open(ConnectionHdl hdl) {
|
||||
state_ = ConnectionState::Connected;
|
||||
connect_cv_.notify_all();
|
||||
}
|
||||
|
||||
void on_close(ConnectionHdl hdl) {
|
||||
state_ = ConnectionState::Disconnected;
|
||||
authenticated_ = false;
|
||||
connect_cv_.notify_all();
|
||||
|
||||
if (connection_callback_) {
|
||||
connection_callback_(state_);
|
||||
}
|
||||
|
||||
// Auto reconnect
|
||||
if (config_.auto_reconnect && running_) {
|
||||
attempt_reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void on_fail(ConnectionHdl hdl) {
|
||||
state_ = ConnectionState::Failed;
|
||||
connect_cv_.notify_all();
|
||||
|
||||
if (connection_callback_) {
|
||||
connection_callback_(state_);
|
||||
}
|
||||
}
|
||||
|
||||
void on_message(ConnectionHdl hdl, MessagePtr msg) {
|
||||
metrics_.messages_received++;
|
||||
|
||||
try {
|
||||
auto json = nlohmann::json::parse(msg->get_payload());
|
||||
|
||||
// Handle request responses
|
||||
if (json.contains("request_id")) {
|
||||
std::string req_id = json["request_id"];
|
||||
std::lock_guard<std::mutex> lock(pending_mutex_);
|
||||
auto it = pending_requests_.find(req_id);
|
||||
if (it != pending_requests_.end()) {
|
||||
it->second.set_value(json);
|
||||
pending_requests_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle specific message types
|
||||
if (json.contains("type")) {
|
||||
std::string type = json["type"];
|
||||
|
||||
if (type == "orderbook_update" && json.contains("data")) {
|
||||
handle_orderbook_update(json["data"]);
|
||||
} else if (type == "trade_update" && json.contains("data")) {
|
||||
handle_trade_update(json["data"]);
|
||||
} else if (type == "order_update" && json.contains("data")) {
|
||||
handle_order_update(json["data"]);
|
||||
} else if (type == "error") {
|
||||
handle_error(json);
|
||||
}
|
||||
}
|
||||
|
||||
// Raw message callback
|
||||
if (message_callback_) {
|
||||
Message m;
|
||||
if (json.contains("type")) m.type = json["type"];
|
||||
if (json.contains("data")) m.data = json["data"];
|
||||
if (json.contains("error")) m.error = json["error"];
|
||||
if (json.contains("request_id")) m.request_id = json["request_id"];
|
||||
if (json.contains("timestamp")) m.timestamp = json["timestamp"];
|
||||
message_callback_(m);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
metrics_.error_count++;
|
||||
if (error_callback_) {
|
||||
error_callback_(Error{-1, std::string("JSON parse error: ") + e.what()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handle_orderbook_update(const nlohmann::json& data) {
|
||||
if (!data.contains("symbol")) return;
|
||||
|
||||
std::string symbol = data["symbol"];
|
||||
auto& book = orderbook_manager_.get_or_create(symbol);
|
||||
|
||||
if (data.contains("snapshot")) {
|
||||
OrderBook snapshot = data["snapshot"].get<OrderBook>();
|
||||
snapshot.symbol = symbol;
|
||||
book.apply_snapshot(snapshot);
|
||||
}
|
||||
|
||||
if (orderbook_callback_) {
|
||||
orderbook_callback_(book.get_snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
void handle_trade_update(const nlohmann::json& data) {
|
||||
if (data.contains("trade")) {
|
||||
Trade trade = data["trade"].get<Trade>();
|
||||
trade_tracker_.add(trade);
|
||||
metrics_.trades_received++;
|
||||
|
||||
if (trade_callback_) {
|
||||
trade_callback_(trade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handle_order_update(const nlohmann::json& data) {
|
||||
if (data.contains("order")) {
|
||||
Order order = data["order"].get<Order>();
|
||||
order_tracker_.upsert(order);
|
||||
|
||||
if (order_callback_) {
|
||||
order_callback_(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handle_error(const nlohmann::json& json) {
|
||||
metrics_.error_count++;
|
||||
|
||||
if (error_callback_) {
|
||||
Error err;
|
||||
if (json.contains("error")) err.message = json["error"];
|
||||
if (json.contains("request_id")) err.request_id = json["request_id"];
|
||||
err.code = -1;
|
||||
error_callback_(err);
|
||||
}
|
||||
}
|
||||
|
||||
void attempt_reconnect() {
|
||||
if (metrics_.reconnect_count >= static_cast<uint64_t>(config_.max_reconnect_attempts)) {
|
||||
state_ = ConnectionState::Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
state_ = ConnectionState::Reconnecting;
|
||||
metrics_.reconnect_count++;
|
||||
|
||||
std::this_thread::sleep_for(config_.reconnect_delay);
|
||||
|
||||
ws_client_.reset();
|
||||
ws_client_.init_asio();
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
Error send(const nlohmann::json& msg) {
|
||||
if (!is_connected()) {
|
||||
return Error{-1, "Not connected"};
|
||||
}
|
||||
|
||||
try {
|
||||
std::string payload = msg.dump();
|
||||
ws_client_.send(connection_, payload, websocketpp::frame::opcode::text);
|
||||
metrics_.messages_sent++;
|
||||
return {};
|
||||
} catch (const std::exception& e) {
|
||||
return Error{-1, std::string("Send failed: ") + e.what()};
|
||||
}
|
||||
}
|
||||
|
||||
Result<nlohmann::json> send_and_wait(
|
||||
const nlohmann::json& msg,
|
||||
std::chrono::seconds timeout
|
||||
) {
|
||||
std::string req_id = msg["request_id"];
|
||||
|
||||
std::promise<nlohmann::json> promise;
|
||||
auto future = promise.get_future();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pending_mutex_);
|
||||
pending_requests_[req_id] = std::move(promise);
|
||||
}
|
||||
|
||||
auto send_err = send(msg);
|
||||
if (send_err) {
|
||||
std::lock_guard<std::mutex> lock(pending_mutex_);
|
||||
pending_requests_.erase(req_id);
|
||||
return {{}, send_err};
|
||||
}
|
||||
|
||||
auto status = future.wait_for(timeout);
|
||||
if (status == std::future_status::timeout) {
|
||||
std::lock_guard<std::mutex> lock(pending_mutex_);
|
||||
pending_requests_.erase(req_id);
|
||||
return {{}, Error{-2, "Request timeout"}};
|
||||
}
|
||||
|
||||
try {
|
||||
return {future.get(), {}};
|
||||
} catch (const std::exception& e) {
|
||||
return {{}, Error{-3, std::string("Request failed: ") + e.what()}};
|
||||
}
|
||||
}
|
||||
|
||||
std::string next_request_id() {
|
||||
return std::to_string(++request_id_);
|
||||
}
|
||||
|
||||
ClientConfig config_;
|
||||
WsClient ws_client_;
|
||||
ConnectionHdl connection_;
|
||||
std::thread io_thread_;
|
||||
|
||||
std::atomic<ConnectionState> state_;
|
||||
std::atomic<bool> authenticated_;
|
||||
std::atomic<uint64_t> request_id_;
|
||||
std::atomic<bool> running_;
|
||||
|
||||
std::mutex connect_mutex_;
|
||||
std::condition_variable connect_cv_;
|
||||
|
||||
std::mutex pending_mutex_;
|
||||
std::unordered_map<std::string, std::promise<nlohmann::json>> pending_requests_;
|
||||
|
||||
// Callbacks
|
||||
ErrorCallback error_callback_;
|
||||
OrderCallback order_callback_;
|
||||
TradeCallback trade_callback_;
|
||||
OrderBookCallback orderbook_callback_;
|
||||
ConnectionCallback connection_callback_;
|
||||
MessageCallback message_callback_;
|
||||
|
||||
// Local data
|
||||
OrderBookManager orderbook_manager_;
|
||||
OrderTracker order_tracker_;
|
||||
TradeTracker trade_tracker_;
|
||||
|
||||
// Metrics
|
||||
mutable ClientMetrics metrics_;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Client public interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
Client::Client(ClientConfig config)
|
||||
: impl_(std::make_unique<Impl>(std::move(config)))
|
||||
{}
|
||||
|
||||
Client::~Client() = default;
|
||||
|
||||
Error Client::connect() {
|
||||
return impl_->connect();
|
||||
}
|
||||
|
||||
void Client::disconnect() {
|
||||
impl_->disconnect();
|
||||
}
|
||||
|
||||
bool Client::is_connected() const noexcept {
|
||||
return impl_->is_connected();
|
||||
}
|
||||
|
||||
ConnectionState Client::state() const noexcept {
|
||||
return impl_->state();
|
||||
}
|
||||
|
||||
Error Client::authenticate() {
|
||||
return impl_->authenticate();
|
||||
}
|
||||
|
||||
bool Client::is_authenticated() const noexcept {
|
||||
return impl_->is_authenticated();
|
||||
}
|
||||
|
||||
Result<OrderResponse> Client::place_order(const Order& order) {
|
||||
return impl_->place_order(order);
|
||||
}
|
||||
|
||||
std::future<Result<OrderResponse>> Client::place_order_async(const Order& order) {
|
||||
return impl_->place_order_async(order);
|
||||
}
|
||||
|
||||
Error Client::cancel_order(uint64_t order_id) {
|
||||
return impl_->cancel_order(order_id);
|
||||
}
|
||||
|
||||
std::future<Error> Client::cancel_order_async(uint64_t order_id) {
|
||||
return impl_->cancel_order_async(order_id);
|
||||
}
|
||||
|
||||
Error Client::modify_order(uint64_t order_id, double new_price, double new_size) {
|
||||
return impl_->modify_order(order_id, new_price, new_size);
|
||||
}
|
||||
|
||||
Result<int> Client::cancel_all_orders(const std::string& symbol) {
|
||||
return impl_->cancel_all_orders(symbol);
|
||||
}
|
||||
|
||||
Result<OrderBook> Client::get_orderbook(const std::string& symbol, int32_t depth) {
|
||||
return impl_->get_orderbook(symbol, depth);
|
||||
}
|
||||
|
||||
Result<std::vector<Trade>> Client::get_trades(const std::string& symbol, int32_t limit) {
|
||||
return impl_->get_trades(symbol, limit);
|
||||
}
|
||||
|
||||
Error Client::subscribe_orderbook(
|
||||
const std::vector<std::string>& symbols,
|
||||
OrderBookCallback callback
|
||||
) {
|
||||
return impl_->subscribe_orderbook(symbols, std::move(callback));
|
||||
}
|
||||
|
||||
Error Client::subscribe_trades(
|
||||
const std::vector<std::string>& symbols,
|
||||
TradeCallback callback
|
||||
) {
|
||||
return impl_->subscribe_trades(symbols, std::move(callback));
|
||||
}
|
||||
|
||||
Error Client::unsubscribe(
|
||||
const std::string& channel,
|
||||
const std::vector<std::string>& symbols
|
||||
) {
|
||||
return impl_->unsubscribe(channel, symbols);
|
||||
}
|
||||
|
||||
Result<std::vector<Balance>> Client::get_balances() {
|
||||
return impl_->get_balances();
|
||||
}
|
||||
|
||||
Result<std::vector<Position>> Client::get_positions() {
|
||||
return impl_->get_positions();
|
||||
}
|
||||
|
||||
Result<std::vector<Order>> Client::get_orders() {
|
||||
return impl_->get_orders();
|
||||
}
|
||||
|
||||
Result<NodeInfo> Client::get_info() {
|
||||
return impl_->get_info();
|
||||
}
|
||||
|
||||
void Client::on_error(ErrorCallback callback) {
|
||||
impl_->on_error(std::move(callback));
|
||||
}
|
||||
|
||||
void Client::on_order(OrderCallback callback) {
|
||||
impl_->on_order(std::move(callback));
|
||||
}
|
||||
|
||||
void Client::on_trade(TradeCallback callback) {
|
||||
impl_->on_trade(std::move(callback));
|
||||
}
|
||||
|
||||
void Client::on_connection(ConnectionCallback callback) {
|
||||
impl_->on_connection(std::move(callback));
|
||||
}
|
||||
|
||||
void Client::on_message(MessageCallback callback) {
|
||||
impl_->on_message(std::move(callback));
|
||||
}
|
||||
|
||||
OrderBookManager& Client::orderbooks() noexcept {
|
||||
return impl_->orderbooks();
|
||||
}
|
||||
|
||||
OrderTracker& Client::orders() noexcept {
|
||||
return impl_->orders();
|
||||
}
|
||||
|
||||
TradeTracker& Client::trades() noexcept {
|
||||
return impl_->trades();
|
||||
}
|
||||
|
||||
ClientMetrics Client::metrics() const noexcept {
|
||||
return impl_->metrics();
|
||||
}
|
||||
|
||||
void Client::reset_metrics() {
|
||||
impl_->reset_metrics();
|
||||
}
|
||||
|
||||
Result<int64_t> Client::ping() {
|
||||
return impl_->ping();
|
||||
}
|
||||
|
||||
std::string Client::generate_client_id() {
|
||||
return Impl::generate_client_id();
|
||||
}
|
||||
|
||||
} // namespace lxdex
|
||||
@@ -1,361 +0,0 @@
|
||||
// LX C++ SDK - Orderbook Implementation
|
||||
// Copyright (c) 2025 Lux Partners Limited
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "lxdex/orderbook.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
namespace lxdex {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// LocalOrderBook
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
LocalOrderBook::LocalOrderBook(std::string symbol)
|
||||
: symbol_(std::move(symbol))
|
||||
{}
|
||||
|
||||
void LocalOrderBook::apply_snapshot(const OrderBook& snapshot) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
bids_.clear();
|
||||
asks_.clear();
|
||||
|
||||
for (const auto& level : snapshot.bids) {
|
||||
if (level.size > 0) {
|
||||
bids_[level.price] = level;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& level : snapshot.asks) {
|
||||
if (level.size > 0) {
|
||||
asks_[level.price] = level;
|
||||
}
|
||||
}
|
||||
|
||||
last_update_.store(snapshot.timestamp > 0
|
||||
? snapshot.timestamp
|
||||
: std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count()
|
||||
);
|
||||
|
||||
notify_update();
|
||||
}
|
||||
|
||||
void LocalOrderBook::update_level(Side side, double price, double size) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (side == Side::Buy) {
|
||||
if (size > 0) {
|
||||
bids_[price] = PriceLevel{price, size, 1};
|
||||
} else {
|
||||
bids_.erase(price);
|
||||
}
|
||||
} else {
|
||||
if (size > 0) {
|
||||
asks_[price] = PriceLevel{price, size, 1};
|
||||
} else {
|
||||
asks_.erase(price);
|
||||
}
|
||||
}
|
||||
|
||||
last_update_.store(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count());
|
||||
|
||||
notify_update();
|
||||
}
|
||||
|
||||
void LocalOrderBook::remove_level(Side side, double price) {
|
||||
update_level(side, price, 0);
|
||||
}
|
||||
|
||||
OrderBook LocalOrderBook::get_snapshot(int32_t depth) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
OrderBook ob;
|
||||
ob.symbol = symbol_;
|
||||
ob.timestamp = last_update_.load();
|
||||
|
||||
size_t limit = depth > 0 ? static_cast<size_t>(depth) : SIZE_MAX;
|
||||
|
||||
ob.bids.reserve(std::min(bids_.size(), limit));
|
||||
size_t count = 0;
|
||||
for (const auto& [price, level] : bids_) {
|
||||
if (count >= limit) break;
|
||||
ob.bids.push_back(level);
|
||||
++count;
|
||||
}
|
||||
|
||||
ob.asks.reserve(std::min(asks_.size(), limit));
|
||||
count = 0;
|
||||
for (const auto& [price, level] : asks_) {
|
||||
if (count >= limit) break;
|
||||
ob.asks.push_back(level);
|
||||
++count;
|
||||
}
|
||||
|
||||
return ob;
|
||||
}
|
||||
|
||||
std::optional<PriceLevel> LocalOrderBook::best_bid() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (bids_.empty()) return std::nullopt;
|
||||
return bids_.begin()->second;
|
||||
}
|
||||
|
||||
std::optional<PriceLevel> LocalOrderBook::best_ask() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (asks_.empty()) return std::nullopt;
|
||||
return asks_.begin()->second;
|
||||
}
|
||||
|
||||
double LocalOrderBook::mid_price() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (bids_.empty() || asks_.empty()) return 0.0;
|
||||
return (bids_.begin()->first + asks_.begin()->first) / 2.0;
|
||||
}
|
||||
|
||||
double LocalOrderBook::spread() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (bids_.empty() || asks_.empty()) return 0.0;
|
||||
return asks_.begin()->first - bids_.begin()->first;
|
||||
}
|
||||
|
||||
double LocalOrderBook::bid_depth() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
double total = 0.0;
|
||||
for (const auto& [price, level] : bids_) {
|
||||
total += level.size;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
double LocalOrderBook::ask_depth() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
double total = 0.0;
|
||||
for (const auto& [price, level] : asks_) {
|
||||
total += level.size;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void LocalOrderBook::clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
bids_.clear();
|
||||
asks_.clear();
|
||||
last_update_.store(0);
|
||||
}
|
||||
|
||||
void LocalOrderBook::on_update(std::function<void(const OrderBook&)> callback) {
|
||||
update_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void LocalOrderBook::notify_update() {
|
||||
if (update_callback_) {
|
||||
// Create snapshot without lock (already held by caller)
|
||||
OrderBook ob;
|
||||
ob.symbol = symbol_;
|
||||
ob.timestamp = last_update_.load();
|
||||
|
||||
for (const auto& [price, level] : bids_) {
|
||||
ob.bids.push_back(level);
|
||||
}
|
||||
for (const auto& [price, level] : asks_) {
|
||||
ob.asks.push_back(level);
|
||||
}
|
||||
|
||||
update_callback_(ob);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// OrderTracker
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void OrderTracker::upsert(const Order& order) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
orders_[order.order_id] = order;
|
||||
}
|
||||
|
||||
bool OrderTracker::remove(uint64_t order_id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return orders_.erase(order_id) > 0;
|
||||
}
|
||||
|
||||
std::optional<Order> OrderTracker::get(uint64_t order_id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = orders_.find(order_id);
|
||||
if (it == orders_.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::vector<Order> OrderTracker::get_by_symbol(const std::string& symbol) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<Order> result;
|
||||
for (const auto& [id, order] : orders_) {
|
||||
if (order.symbol == symbol) {
|
||||
result.push_back(order);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Order> OrderTracker::get_open() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<Order> result;
|
||||
for (const auto& [id, order] : orders_) {
|
||||
if (order.is_open()) {
|
||||
result.push_back(order);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Order> OrderTracker::get_all() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<Order> result;
|
||||
result.reserve(orders_.size());
|
||||
for (const auto& [id, order] : orders_) {
|
||||
result.push_back(order);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void OrderTracker::clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
orders_.clear();
|
||||
}
|
||||
|
||||
size_t OrderTracker::count() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return orders_.size();
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TradeTracker
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
TradeTracker::TradeTracker(size_t max_trades)
|
||||
: max_trades_(max_trades)
|
||||
{
|
||||
trades_.reserve(max_trades_);
|
||||
}
|
||||
|
||||
void TradeTracker::add(const Trade& trade) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
trades_.push_back(trade);
|
||||
|
||||
// Trim if over limit
|
||||
if (trades_.size() > max_trades_) {
|
||||
trades_.erase(trades_.begin(), trades_.begin() + (trades_.size() - max_trades_));
|
||||
}
|
||||
|
||||
if (trade_callback_) {
|
||||
trade_callback_(trade);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Trade> TradeTracker::get_by_symbol(
|
||||
const std::string& symbol,
|
||||
size_t limit
|
||||
) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
std::vector<Trade> result;
|
||||
result.reserve(std::min(limit, trades_.size()));
|
||||
|
||||
// Iterate in reverse (most recent first)
|
||||
for (auto it = trades_.rbegin(); it != trades_.rend() && result.size() < limit; ++it) {
|
||||
if (it->symbol == symbol) {
|
||||
result.push_back(*it);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Trade> TradeTracker::get_recent(size_t limit) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
size_t count = std::min(limit, trades_.size());
|
||||
std::vector<Trade> result;
|
||||
result.reserve(count);
|
||||
|
||||
// Get most recent trades
|
||||
auto start = trades_.size() > limit ? trades_.end() - limit : trades_.begin();
|
||||
for (auto it = trades_.rbegin(); it != trades_.rend() && result.size() < limit; ++it) {
|
||||
result.push_back(*it);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void TradeTracker::clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
trades_.clear();
|
||||
}
|
||||
|
||||
size_t TradeTracker::count() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return trades_.size();
|
||||
}
|
||||
|
||||
void TradeTracker::on_trade(std::function<void(const Trade&)> callback) {
|
||||
trade_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// OrderBookManager
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
LocalOrderBook& OrderBookManager::get_or_create(const std::string& symbol) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
auto it = books_.find(symbol);
|
||||
if (it != books_.end()) {
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
auto [new_it, inserted] = books_.emplace(
|
||||
symbol,
|
||||
std::make_unique<LocalOrderBook>(symbol)
|
||||
);
|
||||
return *new_it->second;
|
||||
}
|
||||
|
||||
LocalOrderBook* OrderBookManager::get(const std::string& symbol) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = books_.find(symbol);
|
||||
return it != books_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
bool OrderBookManager::has(const std::string& symbol) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return books_.find(symbol) != books_.end();
|
||||
}
|
||||
|
||||
bool OrderBookManager::remove(const std::string& symbol) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return books_.erase(symbol) > 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> OrderBookManager::symbols() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::vector<std::string> result;
|
||||
result.reserve(books_.size());
|
||||
for (const auto& [symbol, book] : books_) {
|
||||
result.push_back(symbol);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void OrderBookManager::clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
books_.clear();
|
||||
}
|
||||
|
||||
} // namespace lxdex
|
||||
@@ -1,86 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(lx_trading_c VERSION 1.0.0 LANGUAGES C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Options
|
||||
option(LX_TRADING_C_BUILD_TESTS "Build tests" ON)
|
||||
option(LX_TRADING_C_USE_SIMD "Enable SIMD optimizations" OFF)
|
||||
|
||||
# Compiler flags
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
if(LX_TRADING_C_USE_SIMD)
|
||||
add_compile_options(-march=native)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Find dependencies
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# Math library (required on Linux, optional on macOS/Windows)
|
||||
find_library(MATH_LIBRARY m)
|
||||
|
||||
# Library sources
|
||||
set(LX_TRADING_C_SOURCES
|
||||
src/types.c
|
||||
src/orderbook.c
|
||||
src/risk.c
|
||||
src/math.c
|
||||
)
|
||||
|
||||
# Static library
|
||||
add_library(lx_trading_c STATIC ${LX_TRADING_C_SOURCES})
|
||||
|
||||
target_include_directories(lx_trading_c
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
target_link_libraries(lx_trading_c
|
||||
PUBLIC
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
if(MATH_LIBRARY)
|
||||
target_link_libraries(lx_trading_c PUBLIC ${MATH_LIBRARY})
|
||||
endif()
|
||||
|
||||
# Tests
|
||||
if(LX_TRADING_C_BUILD_TESTS)
|
||||
enable_testing()
|
||||
|
||||
# Test executable
|
||||
add_executable(lx_trading_c_tests
|
||||
tests/test_main.c
|
||||
tests/test_types.c
|
||||
tests/test_orderbook.c
|
||||
tests/test_risk.c
|
||||
tests/test_math.c
|
||||
)
|
||||
|
||||
target_link_libraries(lx_trading_c_tests
|
||||
PRIVATE
|
||||
lx_trading_c
|
||||
)
|
||||
|
||||
# Register tests with CTest
|
||||
add_test(NAME types_test COMMAND lx_trading_c_tests types)
|
||||
add_test(NAME orderbook_test COMMAND lx_trading_c_tests orderbook)
|
||||
add_test(NAME risk_test COMMAND lx_trading_c_tests risk)
|
||||
add_test(NAME math_test COMMAND lx_trading_c_tests math)
|
||||
add_test(NAME all_tests COMMAND lx_trading_c_tests all)
|
||||
endif()
|
||||
|
||||
# Install
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS lx_trading_c
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
@@ -1,304 +0,0 @@
|
||||
/**
|
||||
* LX Trading SDK - Financial Mathematics
|
||||
* Options pricing, AMM math, and risk metrics
|
||||
*/
|
||||
|
||||
#ifndef LX_TRADING_MATH_H
|
||||
#define LX_TRADING_MATH_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================================
|
||||
* Constants
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_MATH_PI 3.14159265358979323846
|
||||
#define LX_MATH_SQRT_2PI 2.506628274631000502
|
||||
#define LX_MATH_SQRT_2 1.41421356237309504880
|
||||
|
||||
/* ============================================================================
|
||||
* Greeks Structure
|
||||
* ============================================================================ */
|
||||
|
||||
typedef struct {
|
||||
double delta;
|
||||
double gamma;
|
||||
double theta;
|
||||
double vega;
|
||||
double rho;
|
||||
} LxGreeks;
|
||||
|
||||
/* ============================================================================
|
||||
* Statistical Functions
|
||||
* ============================================================================ */
|
||||
|
||||
/* Standard normal CDF (Abramowitz & Stegun approximation) */
|
||||
double lx_norm_cdf(double x);
|
||||
|
||||
/* Standard normal PDF */
|
||||
double lx_norm_pdf(double x);
|
||||
|
||||
/* ============================================================================
|
||||
* Black-Scholes Options Pricing
|
||||
* ============================================================================ */
|
||||
|
||||
/**
|
||||
* Calculate Black-Scholes option price
|
||||
* @param S Spot price
|
||||
* @param K Strike price
|
||||
* @param T Time to expiration (years)
|
||||
* @param r Risk-free rate
|
||||
* @param sigma Volatility
|
||||
* @param is_call true for call, false for put
|
||||
* @return Option price
|
||||
*/
|
||||
double lx_black_scholes(double S, double K, double T, double r, double sigma, bool is_call);
|
||||
|
||||
/**
|
||||
* Calculate implied volatility from option price (Newton-Raphson)
|
||||
* @param price Option price
|
||||
* @param S Spot price
|
||||
* @param K Strike price
|
||||
* @param T Time to expiration (years)
|
||||
* @param r Risk-free rate
|
||||
* @param is_call true for call, false for put
|
||||
* @param tol Tolerance (default 1e-6)
|
||||
* @param max_iter Maximum iterations (default 100)
|
||||
* @return Implied volatility, or -1 if not found
|
||||
*/
|
||||
double lx_implied_volatility(double price, double S, double K, double T, double r,
|
||||
bool is_call, double tol, int max_iter);
|
||||
|
||||
/**
|
||||
* Calculate all Greeks
|
||||
* @param S Spot price
|
||||
* @param K Strike price
|
||||
* @param T Time to expiration (years)
|
||||
* @param r Risk-free rate
|
||||
* @param sigma Volatility
|
||||
* @param is_call true for call, false for put
|
||||
* @return Greeks structure
|
||||
*/
|
||||
LxGreeks lx_greeks(double S, double K, double T, double r, double sigma, bool is_call);
|
||||
|
||||
/* ============================================================================
|
||||
* AMM Pricing - Constant Product (Uniswap V2)
|
||||
* ============================================================================ */
|
||||
|
||||
/**
|
||||
* Calculate constant product AMM swap output
|
||||
* @param reserve_x Reserve of token X
|
||||
* @param reserve_y Reserve of token Y
|
||||
* @param amount_in Amount of input token
|
||||
* @param fee_rate Fee rate (e.g., 0.003 for 0.3%)
|
||||
* @param is_x_to_y true if swapping X for Y, false for Y to X
|
||||
* @param output_amount Output: amount of output token
|
||||
* @param effective_price Output: effective price
|
||||
*/
|
||||
void lx_constant_product_price(double reserve_x, double reserve_y,
|
||||
double amount_in, double fee_rate, bool is_x_to_y,
|
||||
double* output_amount, double* effective_price);
|
||||
|
||||
/* ============================================================================
|
||||
* AMM Pricing - Concentrated Liquidity (Uniswap V3)
|
||||
* ============================================================================ */
|
||||
|
||||
/**
|
||||
* Calculate concentrated liquidity swap output
|
||||
* @param liquidity Liquidity in the range
|
||||
* @param sqrt_price_current Current sqrt price
|
||||
* @param sqrt_price_lower Lower sqrt price bound
|
||||
* @param sqrt_price_upper Upper sqrt price bound
|
||||
* @param amount_in Amount of input token
|
||||
* @param fee_rate Fee rate
|
||||
* @param is_token0_in true if token0 is input
|
||||
* @param output_amount Output: amount of output token
|
||||
* @param new_sqrt_price Output: new sqrt price after swap
|
||||
* @param price_impact Output: price impact percentage
|
||||
*/
|
||||
void lx_concentrated_liquidity_price(double liquidity,
|
||||
double sqrt_price_current,
|
||||
double sqrt_price_lower,
|
||||
double sqrt_price_upper,
|
||||
double amount_in,
|
||||
double fee_rate,
|
||||
bool is_token0_in,
|
||||
double* output_amount,
|
||||
double* new_sqrt_price,
|
||||
double* price_impact);
|
||||
|
||||
/**
|
||||
* Calculate liquidity for a concentrated position
|
||||
*/
|
||||
double lx_calculate_liquidity(double amount_x, double amount_y,
|
||||
double sqrt_price_current,
|
||||
double sqrt_price_lower,
|
||||
double sqrt_price_upper);
|
||||
|
||||
/* ============================================================================
|
||||
* Price Conversions
|
||||
* ============================================================================ */
|
||||
|
||||
double lx_price_to_sqrt_price(double price);
|
||||
double lx_sqrt_price_to_price(double sqrt_price);
|
||||
double lx_tick_to_sqrt_price(int tick);
|
||||
int lx_sqrt_price_to_tick(double sqrt_price, int tick_spacing);
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Metrics
|
||||
* ============================================================================ */
|
||||
|
||||
/**
|
||||
* Calculate historical volatility
|
||||
* @param returns Array of returns
|
||||
* @param count Number of returns
|
||||
* @param annualize Whether to annualize
|
||||
* @param periods_per_year Periods per year (e.g., 252 for daily)
|
||||
* @return Volatility
|
||||
*/
|
||||
double lx_volatility(const double* returns, size_t count,
|
||||
bool annualize, int periods_per_year);
|
||||
|
||||
/**
|
||||
* Calculate Sharpe ratio
|
||||
* @param returns Array of returns
|
||||
* @param count Number of returns
|
||||
* @param risk_free_rate Risk-free rate (period, not annual)
|
||||
* @param periods_per_year Periods per year
|
||||
* @return Sharpe ratio
|
||||
*/
|
||||
double lx_sharpe_ratio(const double* returns, size_t count,
|
||||
double risk_free_rate, int periods_per_year);
|
||||
|
||||
/**
|
||||
* Calculate Sortino ratio (uses downside deviation)
|
||||
* @param returns Array of returns
|
||||
* @param count Number of returns
|
||||
* @param risk_free_rate Risk-free rate
|
||||
* @param target_return Target return (for downside calculation)
|
||||
* @param periods_per_year Periods per year
|
||||
* @return Sortino ratio
|
||||
*/
|
||||
double lx_sortino_ratio(const double* returns, size_t count,
|
||||
double risk_free_rate, double target_return,
|
||||
int periods_per_year);
|
||||
|
||||
/**
|
||||
* Calculate maximum drawdown
|
||||
* @param prices Array of prices
|
||||
* @param count Number of prices
|
||||
* @param peak_idx Output: index of peak
|
||||
* @param trough_idx Output: index of trough
|
||||
* @return Maximum drawdown as a fraction (0.0 to 1.0)
|
||||
*/
|
||||
double lx_max_drawdown(const double* prices, size_t count,
|
||||
size_t* peak_idx, size_t* trough_idx);
|
||||
|
||||
/**
|
||||
* Calculate Value at Risk
|
||||
* @param returns Array of returns
|
||||
* @param count Number of returns
|
||||
* @param confidence Confidence level (e.g., 0.95)
|
||||
* @param parametric Use parametric (true) or historical (false)
|
||||
* @return VaR as positive number (loss)
|
||||
*/
|
||||
double lx_var(const double* returns, size_t count,
|
||||
double confidence, bool parametric);
|
||||
|
||||
/**
|
||||
* Calculate Conditional VaR (Expected Shortfall)
|
||||
* @param returns Array of returns
|
||||
* @param count Number of returns
|
||||
* @param confidence Confidence level
|
||||
* @return CVaR as positive number
|
||||
*/
|
||||
double lx_cvar(const double* returns, size_t count, double confidence);
|
||||
|
||||
/* ============================================================================
|
||||
* Statistical Utilities
|
||||
* ============================================================================ */
|
||||
|
||||
/**
|
||||
* Calculate returns from prices
|
||||
* @param prices Array of prices
|
||||
* @param price_count Number of prices
|
||||
* @param returns Output: array of returns (must have space for price_count - 1)
|
||||
* @return Number of returns calculated
|
||||
*/
|
||||
size_t lx_calculate_returns(const double* prices, size_t price_count, double* returns);
|
||||
|
||||
/**
|
||||
* Calculate rolling mean
|
||||
* @param data Input data
|
||||
* @param count Number of data points
|
||||
* @param window Window size
|
||||
* @param result Output: rolling means (must have space for count - window + 1)
|
||||
* @return Number of results
|
||||
*/
|
||||
size_t lx_rolling_mean(const double* data, size_t count, size_t window, double* result);
|
||||
|
||||
/**
|
||||
* Calculate rolling standard deviation
|
||||
* @param data Input data
|
||||
* @param count Number of data points
|
||||
* @param window Window size
|
||||
* @param result Output: rolling stds (must have space for count - window + 1)
|
||||
* @return Number of results
|
||||
*/
|
||||
size_t lx_rolling_std(const double* data, size_t count, size_t window, double* result);
|
||||
|
||||
/**
|
||||
* Calculate exponential moving average
|
||||
* @param data Input data
|
||||
* @param count Number of data points
|
||||
* @param alpha Smoothing factor (0 < alpha <= 1)
|
||||
* @param result Output: EMA values (must have space for count)
|
||||
*/
|
||||
void lx_ema(const double* data, size_t count, double alpha, double* result);
|
||||
|
||||
/**
|
||||
* Calculate correlation coefficient
|
||||
* @param x First array
|
||||
* @param y Second array
|
||||
* @param count Number of elements
|
||||
* @return Correlation coefficient (-1 to 1)
|
||||
*/
|
||||
double lx_correlation(const double* x, const double* y, size_t count);
|
||||
|
||||
/**
|
||||
* Calculate covariance
|
||||
* @param x First array
|
||||
* @param y Second array
|
||||
* @param count Number of elements
|
||||
* @return Covariance
|
||||
*/
|
||||
double lx_covariance(const double* x, const double* y, size_t count);
|
||||
|
||||
/**
|
||||
* Calculate beta coefficient
|
||||
* @param asset_returns Asset returns
|
||||
* @param market_returns Market returns
|
||||
* @param count Number of returns
|
||||
* @return Beta coefficient
|
||||
*/
|
||||
double lx_beta(const double* asset_returns, const double* market_returns, size_t count);
|
||||
|
||||
/* ============================================================================
|
||||
* Basic Statistics
|
||||
* ============================================================================ */
|
||||
|
||||
double lx_mean(const double* data, size_t count);
|
||||
double lx_variance(const double* data, size_t count);
|
||||
double lx_std_dev(const double* data, size_t count);
|
||||
double lx_sum(const double* data, size_t count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LX_TRADING_MATH_H */
|
||||
@@ -1,220 +0,0 @@
|
||||
/**
|
||||
* LX Trading SDK - Orderbook
|
||||
* Thread-safe orderbook with VWAP, slippage, and liquidity calculations
|
||||
*/
|
||||
|
||||
#ifndef LX_TRADING_ORDERBOOK_H
|
||||
#define LX_TRADING_ORDERBOOK_H
|
||||
|
||||
#include "types.h"
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================================
|
||||
* Orderbook
|
||||
* ============================================================================
|
||||
* Single-venue orderbook with thread-safe operations.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
char symbol[LX_SYMBOL_MAX_LEN * 2 + 2];
|
||||
char venue[LX_VENUE_MAX_LEN];
|
||||
|
||||
/* Atomic timestamp and sequence */
|
||||
atomic_int_fast64_t timestamp;
|
||||
atomic_uint_fast64_t sequence;
|
||||
|
||||
/* Price levels (dynamically allocated) */
|
||||
LxPriceLevel* bids;
|
||||
size_t bid_count;
|
||||
size_t bid_capacity;
|
||||
|
||||
LxPriceLevel* asks;
|
||||
size_t ask_count;
|
||||
size_t ask_capacity;
|
||||
|
||||
/* Thread safety */
|
||||
pthread_rwlock_t lock;
|
||||
} LxOrderbook;
|
||||
|
||||
/* ============================================================================
|
||||
* Orderbook Lifecycle
|
||||
* ============================================================================ */
|
||||
|
||||
/* Initialize orderbook (returns 0 on success, -1 on error) */
|
||||
int lx_orderbook_init(LxOrderbook* book, const char* symbol, const char* venue);
|
||||
|
||||
/* Cleanup orderbook (frees internal memory) */
|
||||
void lx_orderbook_cleanup(LxOrderbook* book);
|
||||
|
||||
/* Create/destroy on heap */
|
||||
LxOrderbook* lx_orderbook_create(const char* symbol, const char* venue);
|
||||
void lx_orderbook_destroy(LxOrderbook* book);
|
||||
|
||||
/* ============================================================================
|
||||
* Orderbook Accessors (thread-safe reads)
|
||||
* ============================================================================ */
|
||||
|
||||
const char* lx_orderbook_symbol(const LxOrderbook* book);
|
||||
const char* lx_orderbook_venue(const LxOrderbook* book);
|
||||
int64_t lx_orderbook_timestamp(const LxOrderbook* book);
|
||||
uint64_t lx_orderbook_sequence(const LxOrderbook* book);
|
||||
|
||||
/* ============================================================================
|
||||
* Orderbook Mutators (thread-safe writes)
|
||||
* ============================================================================ */
|
||||
|
||||
/* Add a price level (appends to list) */
|
||||
int lx_orderbook_add_bid(LxOrderbook* book, LxDecimal price, LxDecimal quantity);
|
||||
int lx_orderbook_add_ask(LxOrderbook* book, LxDecimal price, LxDecimal quantity);
|
||||
|
||||
/* Set a price level (updates existing or adds new) */
|
||||
int lx_orderbook_set_bid(LxOrderbook* book, LxDecimal price, LxDecimal quantity);
|
||||
int lx_orderbook_set_ask(LxOrderbook* book, LxDecimal price, LxDecimal quantity);
|
||||
|
||||
/* Remove a price level */
|
||||
void lx_orderbook_remove_bid(LxOrderbook* book, LxDecimal price);
|
||||
void lx_orderbook_remove_ask(LxOrderbook* book, LxDecimal price);
|
||||
|
||||
/* Clear all levels */
|
||||
void lx_orderbook_clear(LxOrderbook* book);
|
||||
|
||||
/* Sort levels (bids descending, asks ascending) and update timestamp/sequence */
|
||||
void lx_orderbook_sort(LxOrderbook* book);
|
||||
|
||||
/* Set timestamp/sequence directly */
|
||||
void lx_orderbook_set_timestamp(LxOrderbook* book, int64_t ts);
|
||||
void lx_orderbook_set_sequence(LxOrderbook* book, uint64_t seq);
|
||||
|
||||
/* ============================================================================
|
||||
* Orderbook Readers (thread-safe snapshots)
|
||||
* ============================================================================ */
|
||||
|
||||
/* Get copies of price levels (caller must free returned array) */
|
||||
LxPriceLevel* lx_orderbook_bids_copy(const LxOrderbook* book, size_t* count);
|
||||
LxPriceLevel* lx_orderbook_asks_copy(const LxOrderbook* book, size_t* count);
|
||||
|
||||
/* Get best bid/ask (returns false if empty) */
|
||||
bool lx_orderbook_best_bid(const LxOrderbook* book, LxDecimal* price);
|
||||
bool lx_orderbook_best_ask(const LxOrderbook* book, LxDecimal* price);
|
||||
|
||||
/* Get mid price (returns false if no bid or ask) */
|
||||
bool lx_orderbook_mid_price(const LxOrderbook* book, LxDecimal* price);
|
||||
|
||||
/* Get spread (returns false if no bid or ask) */
|
||||
bool lx_orderbook_spread(const LxOrderbook* book, LxDecimal* spread);
|
||||
bool lx_orderbook_spread_percent(const LxOrderbook* book, LxDecimal* spread_pct);
|
||||
|
||||
/* ============================================================================
|
||||
* Liquidity Calculations
|
||||
* ============================================================================ */
|
||||
|
||||
/* Total notional liquidity on each side */
|
||||
LxDecimal lx_orderbook_bid_liquidity(const LxOrderbook* book);
|
||||
LxDecimal lx_orderbook_ask_liquidity(const LxOrderbook* book);
|
||||
|
||||
/* Notional depth for N levels */
|
||||
LxDecimal lx_orderbook_bid_depth(const LxOrderbook* book, int levels);
|
||||
LxDecimal lx_orderbook_ask_depth(const LxOrderbook* book, int levels);
|
||||
|
||||
/* Check if sufficient liquidity exists */
|
||||
bool lx_orderbook_has_liquidity(const LxOrderbook* book, LxSide side, LxDecimal amount);
|
||||
|
||||
/* ============================================================================
|
||||
* VWAP Calculations
|
||||
* ============================================================================ */
|
||||
|
||||
/* Volume-weighted average price for buying amount (walks asks) */
|
||||
bool lx_orderbook_vwap_buy(const LxOrderbook* book, LxDecimal amount, LxDecimal* vwap);
|
||||
|
||||
/* Volume-weighted average price for selling amount (walks bids) */
|
||||
bool lx_orderbook_vwap_sell(const LxOrderbook* book, LxDecimal amount, LxDecimal* vwap);
|
||||
|
||||
/* ============================================================================
|
||||
* Slippage Calculation
|
||||
* ============================================================================ */
|
||||
|
||||
/* Calculate slippage for a given order size
|
||||
* Returns the difference between VWAP and best price as a percentage */
|
||||
bool lx_orderbook_slippage_buy(const LxOrderbook* book, LxDecimal amount, LxDecimal* slippage_pct);
|
||||
bool lx_orderbook_slippage_sell(const LxOrderbook* book, LxDecimal amount, LxDecimal* slippage_pct);
|
||||
|
||||
/* ============================================================================
|
||||
* Aggregated Orderbook
|
||||
* ============================================================================
|
||||
* Combines orderbooks from multiple venues.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
char venue[LX_VENUE_MAX_LEN];
|
||||
LxDecimal quantity;
|
||||
} LxVenueQuantity;
|
||||
|
||||
typedef struct {
|
||||
LxDecimal price;
|
||||
LxVenueQuantity* venues;
|
||||
size_t venue_count;
|
||||
size_t venue_capacity;
|
||||
} LxAggregatedLevel;
|
||||
|
||||
typedef struct {
|
||||
char symbol[LX_SYMBOL_MAX_LEN * 2 + 2];
|
||||
int64_t timestamp;
|
||||
|
||||
LxAggregatedLevel* bids;
|
||||
size_t bid_count;
|
||||
size_t bid_capacity;
|
||||
|
||||
LxAggregatedLevel* asks;
|
||||
size_t ask_count;
|
||||
size_t ask_capacity;
|
||||
} LxAggregatedOrderbook;
|
||||
|
||||
/* ============================================================================
|
||||
* Aggregated Orderbook Lifecycle
|
||||
* ============================================================================ */
|
||||
|
||||
int lx_aggregated_orderbook_init(LxAggregatedOrderbook* agg, const char* symbol);
|
||||
void lx_aggregated_orderbook_cleanup(LxAggregatedOrderbook* agg);
|
||||
LxAggregatedOrderbook* lx_aggregated_orderbook_create(const char* symbol);
|
||||
void lx_aggregated_orderbook_destroy(LxAggregatedOrderbook* agg);
|
||||
|
||||
/* ============================================================================
|
||||
* Aggregated Orderbook Operations
|
||||
* ============================================================================ */
|
||||
|
||||
/* Add an orderbook to the aggregation */
|
||||
int lx_aggregated_orderbook_add(LxAggregatedOrderbook* agg, const LxOrderbook* book);
|
||||
|
||||
/* Clear all aggregated data */
|
||||
void lx_aggregated_orderbook_clear(LxAggregatedOrderbook* agg);
|
||||
|
||||
/* Best bid across all venues (price, venue, qty) */
|
||||
bool lx_aggregated_orderbook_best_bid(const LxAggregatedOrderbook* agg,
|
||||
LxDecimal* price, char* venue, size_t venue_buf_size,
|
||||
LxDecimal* quantity);
|
||||
|
||||
/* Best ask across all venues (price, venue, qty) */
|
||||
bool lx_aggregated_orderbook_best_ask(const LxAggregatedOrderbook* agg,
|
||||
LxDecimal* price, char* venue, size_t venue_buf_size,
|
||||
LxDecimal* quantity);
|
||||
|
||||
/* Get aggregated price levels (caller must free) */
|
||||
LxPriceLevel* lx_aggregated_orderbook_aggregated_bids(const LxAggregatedOrderbook* agg, size_t* count);
|
||||
LxPriceLevel* lx_aggregated_orderbook_aggregated_asks(const LxAggregatedOrderbook* agg, size_t* count);
|
||||
|
||||
/* Find best venue for buying/selling amount */
|
||||
bool lx_aggregated_orderbook_best_venue_buy(const LxAggregatedOrderbook* agg, LxDecimal amount,
|
||||
char* venue, size_t venue_buf_size, LxDecimal* price);
|
||||
bool lx_aggregated_orderbook_best_venue_sell(const LxAggregatedOrderbook* agg, LxDecimal amount,
|
||||
char* venue, size_t venue_buf_size, LxDecimal* price);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LX_TRADING_ORDERBOOK_H */
|
||||
@@ -1,225 +0,0 @@
|
||||
/**
|
||||
* LX Trading SDK - Risk Management
|
||||
* Thread-safe risk controls and position tracking
|
||||
*/
|
||||
|
||||
#ifndef LX_TRADING_RISK_H
|
||||
#define LX_TRADING_RISK_H
|
||||
|
||||
#include "types.h"
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Configuration
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_RISK_MAX_POSITION_LIMITS 64
|
||||
|
||||
typedef struct {
|
||||
char asset[LX_ASSET_MAX_LEN];
|
||||
LxDecimal limit;
|
||||
} LxPositionLimit;
|
||||
|
||||
typedef struct {
|
||||
bool enabled;
|
||||
LxDecimal max_position_size;
|
||||
LxDecimal max_order_size;
|
||||
LxDecimal max_daily_loss;
|
||||
int max_open_orders;
|
||||
bool kill_switch_enabled;
|
||||
|
||||
/* Per-asset position limits */
|
||||
LxPositionLimit position_limits[LX_RISK_MAX_POSITION_LIMITS];
|
||||
size_t position_limit_count;
|
||||
} LxRiskConfig;
|
||||
|
||||
/* Initialize default config */
|
||||
void lx_risk_config_init(LxRiskConfig* config);
|
||||
|
||||
/* Add a position limit for a specific asset */
|
||||
int lx_risk_config_add_position_limit(LxRiskConfig* config, const char* asset, LxDecimal limit);
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Error Codes
|
||||
* ============================================================================ */
|
||||
|
||||
typedef enum {
|
||||
LX_RISK_OK = 0,
|
||||
LX_RISK_ERROR_KILLED = -1,
|
||||
LX_RISK_ERROR_ORDER_SIZE = -2,
|
||||
LX_RISK_ERROR_POSITION_LIMIT = -3,
|
||||
LX_RISK_ERROR_DAILY_LOSS = -4,
|
||||
LX_RISK_ERROR_OPEN_ORDERS = -5,
|
||||
LX_RISK_ERROR_DISABLED = -6
|
||||
} LxRiskError;
|
||||
|
||||
/* Get error message for risk error code */
|
||||
const char* lx_risk_error_message(LxRiskError error);
|
||||
|
||||
/* ============================================================================
|
||||
* Position Entry
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_RISK_MAX_POSITIONS 256
|
||||
|
||||
typedef struct {
|
||||
char asset[LX_ASSET_MAX_LEN];
|
||||
LxDecimal position;
|
||||
} LxPositionEntry;
|
||||
|
||||
/* ============================================================================
|
||||
* Open Orders Entry
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_RISK_MAX_SYMBOLS 256
|
||||
|
||||
typedef struct {
|
||||
char symbol[LX_SYMBOL_MAX_LEN * 2 + 2];
|
||||
int count;
|
||||
} LxOpenOrdersEntry;
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Manager
|
||||
* ============================================================================
|
||||
* Thread-safe risk manager with position tracking, PnL monitoring, and kill switch.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
LxRiskConfig config;
|
||||
atomic_bool kill_switch;
|
||||
|
||||
/* Position tracking */
|
||||
LxPositionEntry positions[LX_RISK_MAX_POSITIONS];
|
||||
size_t position_count;
|
||||
pthread_rwlock_t position_lock;
|
||||
|
||||
/* PnL tracking */
|
||||
LxDecimal daily_pnl;
|
||||
pthread_rwlock_t pnl_lock;
|
||||
|
||||
/* Open orders tracking */
|
||||
LxOpenOrdersEntry open_orders[LX_RISK_MAX_SYMBOLS];
|
||||
size_t open_orders_count;
|
||||
pthread_rwlock_t orders_lock;
|
||||
} LxRiskManager;
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Manager Lifecycle
|
||||
* ============================================================================ */
|
||||
|
||||
/* Initialize risk manager (returns 0 on success) */
|
||||
int lx_risk_manager_init(LxRiskManager* rm, const LxRiskConfig* config);
|
||||
|
||||
/* Cleanup risk manager */
|
||||
void lx_risk_manager_cleanup(LxRiskManager* rm);
|
||||
|
||||
/* Create/destroy on heap */
|
||||
LxRiskManager* lx_risk_manager_create(const LxRiskConfig* config);
|
||||
void lx_risk_manager_destroy(LxRiskManager* rm);
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Manager Configuration
|
||||
* ============================================================================ */
|
||||
|
||||
bool lx_risk_manager_is_enabled(const LxRiskManager* rm);
|
||||
const LxRiskConfig* lx_risk_manager_config(const LxRiskManager* rm);
|
||||
|
||||
/* ============================================================================
|
||||
* Kill Switch
|
||||
* ============================================================================ */
|
||||
|
||||
bool lx_risk_manager_is_killed(const LxRiskManager* rm);
|
||||
void lx_risk_manager_kill(LxRiskManager* rm);
|
||||
void lx_risk_manager_reset(LxRiskManager* rm);
|
||||
|
||||
/* ============================================================================
|
||||
* Order Validation
|
||||
* ============================================================================
|
||||
* Returns LX_RISK_OK if valid, or error code if invalid.
|
||||
*/
|
||||
|
||||
LxRiskError lx_risk_manager_validate_order(LxRiskManager* rm, const LxOrderRequest* request);
|
||||
|
||||
/* ============================================================================
|
||||
* Position Tracking
|
||||
* ============================================================================ */
|
||||
|
||||
/* Update position for an asset (adds delta based on side) */
|
||||
void lx_risk_manager_update_position(LxRiskManager* rm, const char* asset,
|
||||
LxDecimal quantity, LxSide side);
|
||||
|
||||
/* Get current position for an asset */
|
||||
LxDecimal lx_risk_manager_position(const LxRiskManager* rm, const char* asset);
|
||||
|
||||
/* Get all positions (caller provides buffer) */
|
||||
size_t lx_risk_manager_positions(const LxRiskManager* rm,
|
||||
LxPositionEntry* entries, size_t max_entries);
|
||||
|
||||
/* ============================================================================
|
||||
* PnL Tracking
|
||||
* ============================================================================ */
|
||||
|
||||
/* Update daily PnL (accumulates) */
|
||||
void lx_risk_manager_update_pnl(LxRiskManager* rm, LxDecimal pnl);
|
||||
|
||||
/* Get current daily PnL */
|
||||
LxDecimal lx_risk_manager_daily_pnl(const LxRiskManager* rm);
|
||||
|
||||
/* Reset daily PnL to zero */
|
||||
void lx_risk_manager_reset_daily_pnl(LxRiskManager* rm);
|
||||
|
||||
/* ============================================================================
|
||||
* Order Tracking
|
||||
* ============================================================================ */
|
||||
|
||||
/* Increment open orders for a symbol */
|
||||
void lx_risk_manager_order_opened(LxRiskManager* rm, const char* symbol);
|
||||
|
||||
/* Decrement open orders for a symbol */
|
||||
void lx_risk_manager_order_closed(LxRiskManager* rm, const char* symbol);
|
||||
|
||||
/* Get open order count for a symbol */
|
||||
int lx_risk_manager_open_orders(const LxRiskManager* rm, const char* symbol);
|
||||
|
||||
/* ============================================================================
|
||||
* Pre-Trade Checks (return bool, don't throw)
|
||||
* ============================================================================ */
|
||||
|
||||
bool lx_risk_manager_check_order_size(const LxRiskManager* rm, LxDecimal quantity);
|
||||
bool lx_risk_manager_check_position_limit(const LxRiskManager* rm, const char* asset,
|
||||
LxDecimal new_position);
|
||||
bool lx_risk_manager_check_daily_loss(const LxRiskManager* rm);
|
||||
bool lx_risk_manager_check_open_orders(const LxRiskManager* rm, const char* symbol);
|
||||
|
||||
/* ============================================================================
|
||||
* Order Tracker (RAII-style helper)
|
||||
* ============================================================================
|
||||
* Use lx_order_tracker_begin() to increment open orders, then
|
||||
* lx_order_tracker_end() to decrement (unless released).
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
LxRiskManager* risk_manager;
|
||||
char symbol[LX_SYMBOL_MAX_LEN * 2 + 2];
|
||||
bool released;
|
||||
} LxOrderTracker;
|
||||
|
||||
/* Begin tracking (increments open orders) */
|
||||
void lx_order_tracker_begin(LxOrderTracker* tracker, LxRiskManager* rm, const char* symbol);
|
||||
|
||||
/* End tracking (decrements open orders unless released) */
|
||||
void lx_order_tracker_end(LxOrderTracker* tracker);
|
||||
|
||||
/* Release tracker (prevents decrement on end) */
|
||||
void lx_order_tracker_release(LxOrderTracker* tracker);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LX_TRADING_RISK_H */
|
||||
@@ -1,345 +0,0 @@
|
||||
/**
|
||||
* LX Trading SDK - Core Types
|
||||
* Fixed-point decimal, trading structs, and enums for HFT
|
||||
*/
|
||||
|
||||
#ifndef LX_TRADING_TYPES_H
|
||||
#define LX_TRADING_TYPES_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================================
|
||||
* Fixed-Point Decimal
|
||||
* ============================================================================
|
||||
* Stores value as integer * 10^(-8) for exact financial arithmetic.
|
||||
*/
|
||||
|
||||
#define LX_DECIMAL_PRECISION 8
|
||||
#define LX_DECIMAL_SCALE 100000000LL
|
||||
|
||||
typedef struct {
|
||||
int64_t value;
|
||||
} LxDecimal;
|
||||
|
||||
/* Decimal constructors */
|
||||
LxDecimal lx_decimal_zero(void);
|
||||
LxDecimal lx_decimal_one(void);
|
||||
LxDecimal lx_decimal_from_int(int64_t value);
|
||||
LxDecimal lx_decimal_from_double(double value);
|
||||
LxDecimal lx_decimal_from_string(const char* str);
|
||||
LxDecimal lx_decimal_from_scaled(int64_t scaled_value);
|
||||
|
||||
/* Decimal conversions */
|
||||
double lx_decimal_to_double(LxDecimal d);
|
||||
int lx_decimal_to_string(LxDecimal d, char* buf, size_t buf_size);
|
||||
int64_t lx_decimal_scaled_value(LxDecimal d);
|
||||
|
||||
/* Decimal arithmetic */
|
||||
LxDecimal lx_decimal_add(LxDecimal a, LxDecimal b);
|
||||
LxDecimal lx_decimal_sub(LxDecimal a, LxDecimal b);
|
||||
LxDecimal lx_decimal_mul(LxDecimal a, LxDecimal b);
|
||||
LxDecimal lx_decimal_div(LxDecimal a, LxDecimal b);
|
||||
LxDecimal lx_decimal_abs(LxDecimal d);
|
||||
LxDecimal lx_decimal_neg(LxDecimal d);
|
||||
|
||||
/* Decimal comparisons */
|
||||
bool lx_decimal_eq(LxDecimal a, LxDecimal b);
|
||||
bool lx_decimal_ne(LxDecimal a, LxDecimal b);
|
||||
bool lx_decimal_lt(LxDecimal a, LxDecimal b);
|
||||
bool lx_decimal_le(LxDecimal a, LxDecimal b);
|
||||
bool lx_decimal_gt(LxDecimal a, LxDecimal b);
|
||||
bool lx_decimal_ge(LxDecimal a, LxDecimal b);
|
||||
|
||||
/* Decimal predicates */
|
||||
bool lx_decimal_is_zero(LxDecimal d);
|
||||
bool lx_decimal_is_positive(LxDecimal d);
|
||||
bool lx_decimal_is_negative(LxDecimal d);
|
||||
|
||||
/* ============================================================================
|
||||
* Trading Enums
|
||||
* ============================================================================ */
|
||||
|
||||
typedef enum {
|
||||
LX_SIDE_BUY = 0,
|
||||
LX_SIDE_SELL = 1
|
||||
} LxSide;
|
||||
|
||||
typedef enum {
|
||||
LX_ORDER_TYPE_MARKET = 0,
|
||||
LX_ORDER_TYPE_LIMIT = 1,
|
||||
LX_ORDER_TYPE_LIMIT_MAKER = 2,
|
||||
LX_ORDER_TYPE_STOP_LOSS = 3,
|
||||
LX_ORDER_TYPE_STOP_LOSS_LIMIT = 4,
|
||||
LX_ORDER_TYPE_TAKE_PROFIT = 5,
|
||||
LX_ORDER_TYPE_TAKE_PROFIT_LIMIT = 6
|
||||
} LxOrderType;
|
||||
|
||||
typedef enum {
|
||||
LX_TIF_GTC = 0, /* Good till cancelled */
|
||||
LX_TIF_IOC = 1, /* Immediate or cancel */
|
||||
LX_TIF_FOK = 2, /* Fill or kill */
|
||||
LX_TIF_GTD = 3, /* Good till date */
|
||||
LX_TIF_POST_ONLY = 4 /* Post only */
|
||||
} LxTimeInForce;
|
||||
|
||||
typedef enum {
|
||||
LX_ORDER_STATUS_PENDING = 0,
|
||||
LX_ORDER_STATUS_OPEN = 1,
|
||||
LX_ORDER_STATUS_PARTIALLY_FILLED = 2,
|
||||
LX_ORDER_STATUS_FILLED = 3,
|
||||
LX_ORDER_STATUS_CANCELLED = 4,
|
||||
LX_ORDER_STATUS_REJECTED = 5,
|
||||
LX_ORDER_STATUS_EXPIRED = 6
|
||||
} LxOrderStatus;
|
||||
|
||||
typedef enum {
|
||||
LX_VENUE_NATIVE = 0,
|
||||
LX_VENUE_CCXT = 1,
|
||||
LX_VENUE_HUMMINGBOT = 2,
|
||||
LX_VENUE_CUSTOM = 3
|
||||
} LxVenueType;
|
||||
|
||||
/* Enum to string conversions */
|
||||
const char* lx_side_to_string(LxSide side);
|
||||
const char* lx_order_type_to_string(LxOrderType type);
|
||||
const char* lx_tif_to_string(LxTimeInForce tif);
|
||||
const char* lx_order_status_to_string(LxOrderStatus status);
|
||||
|
||||
/* ============================================================================
|
||||
* Trading Pair
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_SYMBOL_MAX_LEN 16
|
||||
|
||||
typedef struct {
|
||||
char base[LX_SYMBOL_MAX_LEN];
|
||||
char quote[LX_SYMBOL_MAX_LEN];
|
||||
} LxTradingPair;
|
||||
|
||||
/* Parse symbol string (e.g., "BTC-USDC", "ETH/USD", "LUX_USDT") */
|
||||
bool lx_trading_pair_from_symbol(const char* symbol, LxTradingPair* pair);
|
||||
|
||||
/* Format to different conventions */
|
||||
int lx_trading_pair_to_hummingbot(const LxTradingPair* pair, char* buf, size_t buf_size);
|
||||
int lx_trading_pair_to_ccxt(const LxTradingPair* pair, char* buf, size_t buf_size);
|
||||
|
||||
/* ============================================================================
|
||||
* Price Level
|
||||
* ============================================================================ */
|
||||
|
||||
typedef struct {
|
||||
LxDecimal price;
|
||||
LxDecimal quantity;
|
||||
} LxPriceLevel;
|
||||
|
||||
/* Calculate notional value (price * quantity) */
|
||||
LxDecimal lx_price_level_value(const LxPriceLevel* level);
|
||||
|
||||
/* ============================================================================
|
||||
* Fee
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_ASSET_MAX_LEN 32
|
||||
|
||||
typedef struct {
|
||||
char asset[LX_ASSET_MAX_LEN];
|
||||
LxDecimal amount;
|
||||
bool has_rate;
|
||||
LxDecimal rate;
|
||||
} LxFee;
|
||||
|
||||
/* ============================================================================
|
||||
* Balance
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_VENUE_MAX_LEN 64
|
||||
|
||||
typedef struct {
|
||||
char asset[LX_ASSET_MAX_LEN];
|
||||
char venue[LX_VENUE_MAX_LEN];
|
||||
LxDecimal free;
|
||||
LxDecimal locked;
|
||||
} LxBalance;
|
||||
|
||||
/* Get total balance (free + locked) */
|
||||
LxDecimal lx_balance_total(const LxBalance* balance);
|
||||
|
||||
/* ============================================================================
|
||||
* Order Request
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_ORDER_ID_MAX_LEN 64
|
||||
#define LX_CLIENT_ORDER_ID_MAX_LEN 64
|
||||
|
||||
typedef struct {
|
||||
char symbol[LX_SYMBOL_MAX_LEN * 2 + 2]; /* "BASE-QUOTE" */
|
||||
LxSide side;
|
||||
LxOrderType order_type;
|
||||
LxDecimal quantity;
|
||||
bool has_price;
|
||||
LxDecimal price;
|
||||
bool has_stop_price;
|
||||
LxDecimal stop_price;
|
||||
LxTimeInForce time_in_force;
|
||||
bool reduce_only;
|
||||
bool post_only;
|
||||
bool has_venue;
|
||||
char venue[LX_VENUE_MAX_LEN];
|
||||
char client_order_id[LX_CLIENT_ORDER_ID_MAX_LEN];
|
||||
} LxOrderRequest;
|
||||
|
||||
/* Initialize an order request */
|
||||
void lx_order_request_init(LxOrderRequest* req);
|
||||
|
||||
/* Factory functions */
|
||||
LxOrderRequest lx_order_request_market(const char* symbol, LxSide side, LxDecimal quantity);
|
||||
LxOrderRequest lx_order_request_limit(const char* symbol, LxSide side, LxDecimal quantity, LxDecimal price);
|
||||
|
||||
/* Builder functions (mutate in place, return pointer for chaining) */
|
||||
LxOrderRequest* lx_order_request_with_venue(LxOrderRequest* req, const char* venue);
|
||||
LxOrderRequest* lx_order_request_with_post_only(LxOrderRequest* req);
|
||||
LxOrderRequest* lx_order_request_with_client_id(LxOrderRequest* req, const char* client_id);
|
||||
LxOrderRequest* lx_order_request_with_stop_price(LxOrderRequest* req, LxDecimal stop_price);
|
||||
|
||||
/* ============================================================================
|
||||
* Order
|
||||
* ============================================================================ */
|
||||
|
||||
typedef struct {
|
||||
char order_id[LX_ORDER_ID_MAX_LEN];
|
||||
char client_order_id[LX_CLIENT_ORDER_ID_MAX_LEN];
|
||||
char symbol[LX_SYMBOL_MAX_LEN * 2 + 2];
|
||||
char venue[LX_VENUE_MAX_LEN];
|
||||
LxSide side;
|
||||
LxOrderType order_type;
|
||||
LxOrderStatus status;
|
||||
LxDecimal quantity;
|
||||
LxDecimal filled_quantity;
|
||||
LxDecimal remaining_quantity;
|
||||
bool has_price;
|
||||
LxDecimal price;
|
||||
bool has_average_price;
|
||||
LxDecimal average_price;
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
/* Fees stored separately (typically small count) */
|
||||
LxFee* fees;
|
||||
size_t fee_count;
|
||||
} LxOrder;
|
||||
|
||||
/* Order status checks */
|
||||
bool lx_order_is_open(const LxOrder* order);
|
||||
bool lx_order_is_done(const LxOrder* order);
|
||||
LxDecimal lx_order_fill_percent(const LxOrder* order);
|
||||
|
||||
/* Initialize/cleanup */
|
||||
void lx_order_init(LxOrder* order);
|
||||
void lx_order_cleanup(LxOrder* order);
|
||||
|
||||
/* ============================================================================
|
||||
* Trade/Fill
|
||||
* ============================================================================ */
|
||||
|
||||
typedef struct {
|
||||
char trade_id[LX_ORDER_ID_MAX_LEN];
|
||||
char order_id[LX_ORDER_ID_MAX_LEN];
|
||||
char symbol[LX_SYMBOL_MAX_LEN * 2 + 2];
|
||||
char venue[LX_VENUE_MAX_LEN];
|
||||
LxSide side;
|
||||
LxDecimal price;
|
||||
LxDecimal quantity;
|
||||
LxFee fee;
|
||||
int64_t timestamp;
|
||||
bool is_maker;
|
||||
} LxTrade;
|
||||
|
||||
/* Calculate trade value (price * quantity) */
|
||||
LxDecimal lx_trade_value(const LxTrade* trade);
|
||||
|
||||
/* ============================================================================
|
||||
* Ticker
|
||||
* ============================================================================ */
|
||||
|
||||
typedef struct {
|
||||
char symbol[LX_SYMBOL_MAX_LEN * 2 + 2];
|
||||
char venue[LX_VENUE_MAX_LEN];
|
||||
bool has_bid;
|
||||
LxDecimal bid;
|
||||
bool has_ask;
|
||||
LxDecimal ask;
|
||||
bool has_last;
|
||||
LxDecimal last;
|
||||
bool has_volume_24h;
|
||||
LxDecimal volume_24h;
|
||||
bool has_high_24h;
|
||||
LxDecimal high_24h;
|
||||
bool has_low_24h;
|
||||
LxDecimal low_24h;
|
||||
bool has_change_24h;
|
||||
LxDecimal change_24h;
|
||||
int64_t timestamp;
|
||||
} LxTicker;
|
||||
|
||||
/* Ticker calculations */
|
||||
bool lx_ticker_mid_price(const LxTicker* ticker, LxDecimal* result);
|
||||
bool lx_ticker_spread(const LxTicker* ticker, LxDecimal* result);
|
||||
bool lx_ticker_spread_percent(const LxTicker* ticker, LxDecimal* result);
|
||||
|
||||
/* ============================================================================
|
||||
* Pool Info (AMM)
|
||||
* ============================================================================ */
|
||||
|
||||
#define LX_ADDRESS_MAX_LEN 64
|
||||
|
||||
typedef struct {
|
||||
char address[LX_ADDRESS_MAX_LEN];
|
||||
char base_token[LX_ASSET_MAX_LEN];
|
||||
char quote_token[LX_ASSET_MAX_LEN];
|
||||
LxDecimal base_reserve;
|
||||
LxDecimal quote_reserve;
|
||||
LxDecimal total_liquidity;
|
||||
LxDecimal fee_rate;
|
||||
bool has_apy;
|
||||
LxDecimal apy;
|
||||
} LxPoolInfo;
|
||||
|
||||
/* ============================================================================
|
||||
* Swap Quote
|
||||
* ============================================================================ */
|
||||
|
||||
typedef struct {
|
||||
char base_token[LX_ASSET_MAX_LEN];
|
||||
char quote_token[LX_ASSET_MAX_LEN];
|
||||
LxDecimal input_amount;
|
||||
LxDecimal output_amount;
|
||||
LxDecimal price;
|
||||
LxDecimal price_impact;
|
||||
LxDecimal fee;
|
||||
char** route;
|
||||
size_t route_count;
|
||||
int64_t expires_at;
|
||||
} LxSwapQuote;
|
||||
|
||||
/* Cleanup swap quote (frees route) */
|
||||
void lx_swap_quote_cleanup(LxSwapQuote* quote);
|
||||
|
||||
/* ============================================================================
|
||||
* Timestamp Utilities
|
||||
* ============================================================================ */
|
||||
|
||||
int64_t lx_now_ms(void);
|
||||
int64_t lx_now_us(void);
|
||||
int64_t lx_now_ns(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LX_TRADING_TYPES_H */
|
||||
@@ -1,650 +0,0 @@
|
||||
/**
|
||||
* LX Trading SDK - Financial Mathematics Implementation
|
||||
*/
|
||||
|
||||
#include "lx_trading/math.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ============================================================================
|
||||
* Statistical Functions
|
||||
* ============================================================================ */
|
||||
|
||||
double lx_norm_cdf(double x) {
|
||||
/* Abramowitz & Stegun approximation */
|
||||
const double a1 = 0.254829592;
|
||||
const double a2 = -0.284496736;
|
||||
const double a3 = 1.421413741;
|
||||
const double a4 = -1.453152027;
|
||||
const double a5 = 1.061405429;
|
||||
const double p = 0.3275911;
|
||||
|
||||
int sign = (x < 0) ? -1 : 1;
|
||||
x = fabs(x) / LX_MATH_SQRT_2;
|
||||
|
||||
double t = 1.0 / (1.0 + p * x);
|
||||
double y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * exp(-x * x);
|
||||
|
||||
return 0.5 * (1.0 + sign * y);
|
||||
}
|
||||
|
||||
double lx_norm_pdf(double x) {
|
||||
return exp(-0.5 * x * x) / LX_MATH_SQRT_2PI;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Black-Scholes Options Pricing
|
||||
* ============================================================================ */
|
||||
|
||||
double lx_black_scholes(double S, double K, double T, double r, double sigma, bool is_call) {
|
||||
if (T <= 0) {
|
||||
/* At expiry */
|
||||
if (is_call) {
|
||||
return (S > K) ? S - K : 0.0;
|
||||
} else {
|
||||
return (K > S) ? K - S : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
if (sigma <= 0 || S <= 0 || K <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double sqrt_T = sqrt(T);
|
||||
double d1 = (log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrt_T);
|
||||
double d2 = d1 - sigma * sqrt_T;
|
||||
|
||||
double discount = exp(-r * T);
|
||||
|
||||
if (is_call) {
|
||||
return S * lx_norm_cdf(d1) - K * discount * lx_norm_cdf(d2);
|
||||
} else {
|
||||
return K * discount * lx_norm_cdf(-d2) - S * lx_norm_cdf(-d1);
|
||||
}
|
||||
}
|
||||
|
||||
double lx_implied_volatility(double price, double S, double K, double T, double r,
|
||||
bool is_call, double tol, int max_iter) {
|
||||
if (price <= 0 || S <= 0 || K <= 0 || T <= 0) {
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
/* Newton-Raphson starting point */
|
||||
double sigma = 0.2; /* Initial guess */
|
||||
|
||||
for (int i = 0; i < max_iter; i++) {
|
||||
double calc_price = lx_black_scholes(S, K, T, r, sigma, is_call);
|
||||
double diff = calc_price - price;
|
||||
|
||||
if (fabs(diff) < tol) {
|
||||
return sigma;
|
||||
}
|
||||
|
||||
/* Calculate vega */
|
||||
double sqrt_T = sqrt(T);
|
||||
double d1 = (log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrt_T);
|
||||
double vega = S * lx_norm_pdf(d1) * sqrt_T;
|
||||
|
||||
if (vega < 1e-10) {
|
||||
/* Vega too small, can't converge */
|
||||
break;
|
||||
}
|
||||
|
||||
sigma -= diff / vega;
|
||||
|
||||
/* Bound sigma */
|
||||
if (sigma < 0.001) sigma = 0.001;
|
||||
if (sigma > 10.0) sigma = 10.0;
|
||||
}
|
||||
|
||||
return -1.0; /* Did not converge */
|
||||
}
|
||||
|
||||
LxGreeks lx_greeks(double S, double K, double T, double r, double sigma, bool is_call) {
|
||||
LxGreeks g = {0};
|
||||
|
||||
if (T <= 0 || sigma <= 0 || S <= 0 || K <= 0) {
|
||||
return g;
|
||||
}
|
||||
|
||||
double sqrt_T = sqrt(T);
|
||||
double d1 = (log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrt_T);
|
||||
double d2 = d1 - sigma * sqrt_T;
|
||||
|
||||
double nd1 = lx_norm_cdf(d1);
|
||||
double nd2 = lx_norm_cdf(d2);
|
||||
double pdf_d1 = lx_norm_pdf(d1);
|
||||
double discount = exp(-r * T);
|
||||
|
||||
/* Delta */
|
||||
if (is_call) {
|
||||
g.delta = nd1;
|
||||
} else {
|
||||
g.delta = nd1 - 1.0;
|
||||
}
|
||||
|
||||
/* Gamma */
|
||||
g.gamma = pdf_d1 / (S * sigma * sqrt_T);
|
||||
|
||||
/* Vega (per 1% change in volatility) */
|
||||
g.vega = S * pdf_d1 * sqrt_T * 0.01;
|
||||
|
||||
/* Theta (daily) */
|
||||
double theta_term1 = -S * pdf_d1 * sigma / (2.0 * sqrt_T);
|
||||
if (is_call) {
|
||||
double theta_term2 = -r * K * discount * nd2;
|
||||
g.theta = (theta_term1 + theta_term2) / 365.0;
|
||||
} else {
|
||||
double theta_term2 = r * K * discount * lx_norm_cdf(-d2);
|
||||
g.theta = (theta_term1 + theta_term2) / 365.0;
|
||||
}
|
||||
|
||||
/* Rho (per 1% change in rate) */
|
||||
if (is_call) {
|
||||
g.rho = K * T * discount * nd2 * 0.01;
|
||||
} else {
|
||||
g.rho = -K * T * discount * lx_norm_cdf(-d2) * 0.01;
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* AMM Pricing - Constant Product
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_constant_product_price(double reserve_x, double reserve_y,
|
||||
double amount_in, double fee_rate, bool is_x_to_y,
|
||||
double* output_amount, double* effective_price) {
|
||||
if (!output_amount || !effective_price) return;
|
||||
|
||||
if (reserve_x <= 0 || reserve_y <= 0 || amount_in <= 0) {
|
||||
*output_amount = 0;
|
||||
*effective_price = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double amount_in_with_fee = amount_in * (1.0 - fee_rate);
|
||||
|
||||
double input_reserve, output_reserve;
|
||||
if (is_x_to_y) {
|
||||
input_reserve = reserve_x;
|
||||
output_reserve = reserve_y;
|
||||
} else {
|
||||
input_reserve = reserve_y;
|
||||
output_reserve = reserve_x;
|
||||
}
|
||||
|
||||
/* xy = k, so output = output_reserve - k / (input_reserve + amount_in) */
|
||||
double k = input_reserve * output_reserve;
|
||||
double new_input_reserve = input_reserve + amount_in_with_fee;
|
||||
*output_amount = output_reserve - k / new_input_reserve;
|
||||
|
||||
if (*output_amount > 0) {
|
||||
*effective_price = *output_amount / amount_in;
|
||||
} else {
|
||||
*effective_price = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* AMM Pricing - Concentrated Liquidity
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_concentrated_liquidity_price(double liquidity,
|
||||
double sqrt_price_current,
|
||||
double sqrt_price_lower,
|
||||
double sqrt_price_upper,
|
||||
double amount_in,
|
||||
double fee_rate,
|
||||
bool is_token0_in,
|
||||
double* output_amount,
|
||||
double* new_sqrt_price,
|
||||
double* price_impact) {
|
||||
if (!output_amount || !new_sqrt_price || !price_impact) return;
|
||||
|
||||
if (liquidity <= 0 || amount_in <= 0 || sqrt_price_current <= 0) {
|
||||
*output_amount = 0;
|
||||
*new_sqrt_price = sqrt_price_current;
|
||||
*price_impact = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double amount_in_with_fee = amount_in * (1.0 - fee_rate);
|
||||
double initial_price = sqrt_price_current * sqrt_price_current;
|
||||
|
||||
if (is_token0_in) {
|
||||
/* Token0 in, price increases */
|
||||
double delta_sqrt_price = amount_in_with_fee / liquidity;
|
||||
double new_sp = sqrt_price_current + delta_sqrt_price;
|
||||
|
||||
/* Clamp to range */
|
||||
if (new_sp > sqrt_price_upper) new_sp = sqrt_price_upper;
|
||||
|
||||
/* Calculate token1 output */
|
||||
*output_amount = liquidity * (new_sp - sqrt_price_current);
|
||||
*new_sqrt_price = new_sp;
|
||||
} else {
|
||||
/* Token1 in, price decreases */
|
||||
double delta_1_sqrt_price = amount_in_with_fee / liquidity;
|
||||
double new_sp = sqrt_price_current - delta_1_sqrt_price / sqrt_price_current;
|
||||
|
||||
/* Clamp to range */
|
||||
if (new_sp < sqrt_price_lower) new_sp = sqrt_price_lower;
|
||||
|
||||
/* Calculate token0 output */
|
||||
*output_amount = liquidity * (1.0 / new_sp - 1.0 / sqrt_price_current);
|
||||
if (*output_amount < 0) *output_amount = -*output_amount;
|
||||
*new_sqrt_price = new_sp;
|
||||
}
|
||||
|
||||
/* Calculate price impact */
|
||||
double final_price = (*new_sqrt_price) * (*new_sqrt_price);
|
||||
if (initial_price > 0) {
|
||||
*price_impact = fabs(final_price - initial_price) / initial_price * 100.0;
|
||||
} else {
|
||||
*price_impact = 0;
|
||||
}
|
||||
}
|
||||
|
||||
double lx_calculate_liquidity(double amount_x, double amount_y,
|
||||
double sqrt_price_current,
|
||||
double sqrt_price_lower,
|
||||
double sqrt_price_upper) {
|
||||
if (sqrt_price_current <= 0 || sqrt_price_lower <= 0 || sqrt_price_upper <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sqrt_price_lower >= sqrt_price_upper) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double liquidity_x = 0;
|
||||
double liquidity_y = 0;
|
||||
|
||||
if (sqrt_price_current <= sqrt_price_lower) {
|
||||
/* All token0 */
|
||||
if (amount_x > 0) {
|
||||
liquidity_x = amount_x * sqrt_price_lower * sqrt_price_upper /
|
||||
(sqrt_price_upper - sqrt_price_lower);
|
||||
}
|
||||
return liquidity_x;
|
||||
} else if (sqrt_price_current >= sqrt_price_upper) {
|
||||
/* All token1 */
|
||||
if (amount_y > 0) {
|
||||
liquidity_y = amount_y / (sqrt_price_upper - sqrt_price_lower);
|
||||
}
|
||||
return liquidity_y;
|
||||
} else {
|
||||
/* Both tokens */
|
||||
if (amount_x > 0) {
|
||||
liquidity_x = amount_x * sqrt_price_current * sqrt_price_upper /
|
||||
(sqrt_price_upper - sqrt_price_current);
|
||||
}
|
||||
if (amount_y > 0) {
|
||||
liquidity_y = amount_y / (sqrt_price_current - sqrt_price_lower);
|
||||
}
|
||||
return (liquidity_x < liquidity_y) ? liquidity_x : liquidity_y;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Price Conversions
|
||||
* ============================================================================ */
|
||||
|
||||
double lx_price_to_sqrt_price(double price) {
|
||||
return sqrt(price);
|
||||
}
|
||||
|
||||
double lx_sqrt_price_to_price(double sqrt_price) {
|
||||
return sqrt_price * sqrt_price;
|
||||
}
|
||||
|
||||
double lx_tick_to_sqrt_price(int tick) {
|
||||
return pow(1.0001, tick / 2.0);
|
||||
}
|
||||
|
||||
int lx_sqrt_price_to_tick(double sqrt_price, int tick_spacing) {
|
||||
if (sqrt_price <= 0 || tick_spacing <= 0) return 0;
|
||||
int tick = (int)(2.0 * log(sqrt_price) / log(1.0001));
|
||||
return (tick / tick_spacing) * tick_spacing;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Basic Statistics
|
||||
* ============================================================================ */
|
||||
|
||||
double lx_sum(const double* data, size_t count) {
|
||||
if (!data || count == 0) return 0;
|
||||
|
||||
double sum = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
double lx_mean(const double* data, size_t count) {
|
||||
if (!data || count == 0) return 0;
|
||||
return lx_sum(data, count) / (double)count;
|
||||
}
|
||||
|
||||
double lx_variance(const double* data, size_t count) {
|
||||
if (!data || count < 2) return 0;
|
||||
|
||||
double m = lx_mean(data, count);
|
||||
double sum_sq = 0;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
double diff = data[i] - m;
|
||||
sum_sq += diff * diff;
|
||||
}
|
||||
|
||||
return sum_sq / (double)(count - 1); /* Sample variance */
|
||||
}
|
||||
|
||||
double lx_std_dev(const double* data, size_t count) {
|
||||
return sqrt(lx_variance(data, count));
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Metrics
|
||||
* ============================================================================ */
|
||||
|
||||
double lx_volatility(const double* returns, size_t count,
|
||||
bool annualize, int periods_per_year) {
|
||||
if (!returns || count < 2) return 0;
|
||||
|
||||
double vol = lx_std_dev(returns, count);
|
||||
|
||||
if (annualize && periods_per_year > 0) {
|
||||
vol *= sqrt((double)periods_per_year);
|
||||
}
|
||||
|
||||
return vol;
|
||||
}
|
||||
|
||||
double lx_sharpe_ratio(const double* returns, size_t count,
|
||||
double risk_free_rate, int periods_per_year) {
|
||||
if (!returns || count < 2) return 0;
|
||||
|
||||
double mean_return = lx_mean(returns, count);
|
||||
double vol = lx_std_dev(returns, count);
|
||||
|
||||
if (vol < 1e-10) return 0;
|
||||
|
||||
double excess_return = mean_return - risk_free_rate / (double)periods_per_year;
|
||||
double sharpe = excess_return / vol;
|
||||
|
||||
/* Annualize */
|
||||
if (periods_per_year > 0) {
|
||||
sharpe *= sqrt((double)periods_per_year);
|
||||
}
|
||||
|
||||
return sharpe;
|
||||
}
|
||||
|
||||
double lx_sortino_ratio(const double* returns, size_t count,
|
||||
double risk_free_rate, double target_return,
|
||||
int periods_per_year) {
|
||||
if (!returns || count < 2) return 0;
|
||||
|
||||
double mean_return = lx_mean(returns, count);
|
||||
|
||||
/* Calculate downside deviation */
|
||||
double sum_sq = 0;
|
||||
size_t downside_count = 0;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (returns[i] < target_return) {
|
||||
double diff = returns[i] - target_return;
|
||||
sum_sq += diff * diff;
|
||||
downside_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (downside_count == 0) {
|
||||
return INFINITY; /* No downside */
|
||||
}
|
||||
|
||||
double downside_dev = sqrt(sum_sq / (double)downside_count);
|
||||
|
||||
if (downside_dev < 1e-10) return 0;
|
||||
|
||||
double excess_return = mean_return - risk_free_rate / (double)periods_per_year;
|
||||
double sortino = excess_return / downside_dev;
|
||||
|
||||
/* Annualize */
|
||||
if (periods_per_year > 0) {
|
||||
sortino *= sqrt((double)periods_per_year);
|
||||
}
|
||||
|
||||
return sortino;
|
||||
}
|
||||
|
||||
double lx_max_drawdown(const double* prices, size_t count,
|
||||
size_t* peak_idx, size_t* trough_idx) {
|
||||
if (!prices || count < 2) {
|
||||
if (peak_idx) *peak_idx = 0;
|
||||
if (trough_idx) *trough_idx = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
double max_dd = 0;
|
||||
double peak = prices[0];
|
||||
size_t peak_i = 0;
|
||||
size_t best_peak_i = 0;
|
||||
size_t best_trough_i = 0;
|
||||
|
||||
for (size_t i = 1; i < count; i++) {
|
||||
if (prices[i] > peak) {
|
||||
peak = prices[i];
|
||||
peak_i = i;
|
||||
} else {
|
||||
double dd = (peak - prices[i]) / peak;
|
||||
if (dd > max_dd) {
|
||||
max_dd = dd;
|
||||
best_peak_i = peak_i;
|
||||
best_trough_i = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (peak_idx) *peak_idx = best_peak_i;
|
||||
if (trough_idx) *trough_idx = best_trough_i;
|
||||
|
||||
return max_dd;
|
||||
}
|
||||
|
||||
static int compare_doubles(const void* a, const void* b) {
|
||||
double da = *(const double*)a;
|
||||
double db = *(const double*)b;
|
||||
if (da < db) return -1;
|
||||
if (da > db) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
double lx_var(const double* returns, size_t count,
|
||||
double confidence, bool parametric) {
|
||||
if (!returns || count < 2 || confidence <= 0 || confidence >= 1) return 0;
|
||||
|
||||
if (parametric) {
|
||||
/* Parametric VaR using normal distribution */
|
||||
double m = lx_mean(returns, count);
|
||||
double s = lx_std_dev(returns, count);
|
||||
|
||||
/* Z-score for confidence level (one-tailed) */
|
||||
/* For 95%, z ~ 1.645; for 99%, z ~ 2.326 */
|
||||
/* Using inverse of norm_cdf approximation */
|
||||
double z;
|
||||
if (confidence >= 0.99) {
|
||||
z = 2.326;
|
||||
} else if (confidence >= 0.95) {
|
||||
z = 1.645;
|
||||
} else {
|
||||
/* Rough approximation */
|
||||
z = sqrt(2.0) * (confidence - 0.5) * 3.0;
|
||||
}
|
||||
|
||||
return -(m - z * s);
|
||||
} else {
|
||||
/* Historical VaR */
|
||||
double* sorted = malloc(count * sizeof(double));
|
||||
if (!sorted) return 0;
|
||||
|
||||
memcpy(sorted, returns, count * sizeof(double));
|
||||
qsort(sorted, count, sizeof(double), compare_doubles);
|
||||
|
||||
size_t idx = (size_t)((1.0 - confidence) * (double)count);
|
||||
if (idx >= count) idx = count - 1;
|
||||
|
||||
double var_val = -sorted[idx];
|
||||
free(sorted);
|
||||
|
||||
return var_val;
|
||||
}
|
||||
}
|
||||
|
||||
double lx_cvar(const double* returns, size_t count, double confidence) {
|
||||
if (!returns || count < 2 || confidence <= 0 || confidence >= 1) return 0;
|
||||
|
||||
double* sorted = malloc(count * sizeof(double));
|
||||
if (!sorted) return 0;
|
||||
|
||||
memcpy(sorted, returns, count * sizeof(double));
|
||||
qsort(sorted, count, sizeof(double), compare_doubles);
|
||||
|
||||
size_t cutoff = (size_t)((1.0 - confidence) * (double)count);
|
||||
if (cutoff == 0) cutoff = 1;
|
||||
if (cutoff > count) cutoff = count;
|
||||
|
||||
double sum = 0;
|
||||
for (size_t i = 0; i < cutoff; i++) {
|
||||
sum += sorted[i];
|
||||
}
|
||||
|
||||
double cvar_val = -sum / (double)cutoff;
|
||||
free(sorted);
|
||||
|
||||
return cvar_val;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Statistical Utilities
|
||||
* ============================================================================ */
|
||||
|
||||
size_t lx_calculate_returns(const double* prices, size_t price_count, double* returns) {
|
||||
if (!prices || !returns || price_count < 2) return 0;
|
||||
|
||||
for (size_t i = 1; i < price_count; i++) {
|
||||
if (prices[i - 1] != 0) {
|
||||
returns[i - 1] = (prices[i] - prices[i - 1]) / prices[i - 1];
|
||||
} else {
|
||||
returns[i - 1] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return price_count - 1;
|
||||
}
|
||||
|
||||
size_t lx_rolling_mean(const double* data, size_t count, size_t window, double* result) {
|
||||
if (!data || !result || count < window || window == 0) return 0;
|
||||
|
||||
size_t result_count = count - window + 1;
|
||||
|
||||
for (size_t i = 0; i < result_count; i++) {
|
||||
double sum = 0;
|
||||
for (size_t j = 0; j < window; j++) {
|
||||
sum += data[i + j];
|
||||
}
|
||||
result[i] = sum / (double)window;
|
||||
}
|
||||
|
||||
return result_count;
|
||||
}
|
||||
|
||||
size_t lx_rolling_std(const double* data, size_t count, size_t window, double* result) {
|
||||
if (!data || !result || count < window || window < 2) return 0;
|
||||
|
||||
size_t result_count = count - window + 1;
|
||||
|
||||
for (size_t i = 0; i < result_count; i++) {
|
||||
/* Calculate mean */
|
||||
double sum = 0;
|
||||
for (size_t j = 0; j < window; j++) {
|
||||
sum += data[i + j];
|
||||
}
|
||||
double mean = sum / (double)window;
|
||||
|
||||
/* Calculate variance */
|
||||
double sum_sq = 0;
|
||||
for (size_t j = 0; j < window; j++) {
|
||||
double diff = data[i + j] - mean;
|
||||
sum_sq += diff * diff;
|
||||
}
|
||||
|
||||
result[i] = sqrt(sum_sq / (double)(window - 1));
|
||||
}
|
||||
|
||||
return result_count;
|
||||
}
|
||||
|
||||
void lx_ema(const double* data, size_t count, double alpha, double* result) {
|
||||
if (!data || !result || count == 0 || alpha <= 0 || alpha > 1) return;
|
||||
|
||||
result[0] = data[0];
|
||||
|
||||
for (size_t i = 1; i < count; i++) {
|
||||
result[i] = alpha * data[i] + (1.0 - alpha) * result[i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
double lx_correlation(const double* x, const double* y, size_t count) {
|
||||
if (!x || !y || count < 2) return 0;
|
||||
|
||||
double mean_x = lx_mean(x, count);
|
||||
double mean_y = lx_mean(y, count);
|
||||
|
||||
double cov = 0;
|
||||
double var_x = 0;
|
||||
double var_y = 0;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
double dx = x[i] - mean_x;
|
||||
double dy = y[i] - mean_y;
|
||||
cov += dx * dy;
|
||||
var_x += dx * dx;
|
||||
var_y += dy * dy;
|
||||
}
|
||||
|
||||
double denom = sqrt(var_x * var_y);
|
||||
if (denom < 1e-10) return 0;
|
||||
|
||||
return cov / denom;
|
||||
}
|
||||
|
||||
double lx_covariance(const double* x, const double* y, size_t count) {
|
||||
if (!x || !y || count < 2) return 0;
|
||||
|
||||
double mean_x = lx_mean(x, count);
|
||||
double mean_y = lx_mean(y, count);
|
||||
|
||||
double cov = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
cov += (x[i] - mean_x) * (y[i] - mean_y);
|
||||
}
|
||||
|
||||
return cov / (double)(count - 1);
|
||||
}
|
||||
|
||||
double lx_beta(const double* asset_returns, const double* market_returns, size_t count) {
|
||||
if (!asset_returns || !market_returns || count < 2) return 0;
|
||||
|
||||
double cov = lx_covariance(asset_returns, market_returns, count);
|
||||
double var_market = lx_variance(market_returns, count);
|
||||
|
||||
if (var_market < 1e-10) return 0;
|
||||
|
||||
return cov / var_market;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,525 +0,0 @@
|
||||
/**
|
||||
* LX Trading SDK - Risk Manager Implementation
|
||||
*/
|
||||
|
||||
#include "lx_trading/risk.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Configuration
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_risk_config_init(LxRiskConfig* config) {
|
||||
if (!config) return;
|
||||
memset(config, 0, sizeof(LxRiskConfig));
|
||||
config->enabled = true;
|
||||
config->max_open_orders = 100;
|
||||
}
|
||||
|
||||
int lx_risk_config_add_position_limit(LxRiskConfig* config, const char* asset, LxDecimal limit) {
|
||||
if (!config || !asset) return -1;
|
||||
if (config->position_limit_count >= LX_RISK_MAX_POSITION_LIMITS) return -1;
|
||||
|
||||
strncpy(config->position_limits[config->position_limit_count].asset,
|
||||
asset, LX_ASSET_MAX_LEN - 1);
|
||||
config->position_limits[config->position_limit_count].limit = limit;
|
||||
config->position_limit_count++;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Error Messages
|
||||
* ============================================================================ */
|
||||
|
||||
const char* lx_risk_error_message(LxRiskError error) {
|
||||
switch (error) {
|
||||
case LX_RISK_OK: return "OK";
|
||||
case LX_RISK_ERROR_KILLED: return "Kill switch activated";
|
||||
case LX_RISK_ERROR_ORDER_SIZE: return "Order size exceeds limit";
|
||||
case LX_RISK_ERROR_POSITION_LIMIT: return "Position limit exceeded";
|
||||
case LX_RISK_ERROR_DAILY_LOSS: return "Daily loss limit exceeded";
|
||||
case LX_RISK_ERROR_OPEN_ORDERS: return "Maximum open orders exceeded";
|
||||
case LX_RISK_ERROR_DISABLED: return "Risk management disabled";
|
||||
default: return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Manager Lifecycle
|
||||
* ============================================================================ */
|
||||
|
||||
int lx_risk_manager_init(LxRiskManager* rm, const LxRiskConfig* config) {
|
||||
if (!rm) return -1;
|
||||
|
||||
memset(rm, 0, sizeof(LxRiskManager));
|
||||
|
||||
if (config) {
|
||||
rm->config = *config;
|
||||
} else {
|
||||
lx_risk_config_init(&rm->config);
|
||||
}
|
||||
|
||||
atomic_store(&rm->kill_switch, false);
|
||||
rm->daily_pnl = lx_decimal_zero();
|
||||
|
||||
if (pthread_rwlock_init(&rm->position_lock, NULL) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pthread_rwlock_init(&rm->pnl_lock, NULL) != 0) {
|
||||
pthread_rwlock_destroy(&rm->position_lock);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pthread_rwlock_init(&rm->orders_lock, NULL) != 0) {
|
||||
pthread_rwlock_destroy(&rm->position_lock);
|
||||
pthread_rwlock_destroy(&rm->pnl_lock);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void lx_risk_manager_cleanup(LxRiskManager* rm) {
|
||||
if (!rm) return;
|
||||
|
||||
pthread_rwlock_destroy(&rm->position_lock);
|
||||
pthread_rwlock_destroy(&rm->pnl_lock);
|
||||
pthread_rwlock_destroy(&rm->orders_lock);
|
||||
}
|
||||
|
||||
LxRiskManager* lx_risk_manager_create(const LxRiskConfig* config) {
|
||||
LxRiskManager* rm = malloc(sizeof(LxRiskManager));
|
||||
if (!rm) return NULL;
|
||||
|
||||
if (lx_risk_manager_init(rm, config) != 0) {
|
||||
free(rm);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return rm;
|
||||
}
|
||||
|
||||
void lx_risk_manager_destroy(LxRiskManager* rm) {
|
||||
if (!rm) return;
|
||||
lx_risk_manager_cleanup(rm);
|
||||
free(rm);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Risk Manager Configuration
|
||||
* ============================================================================ */
|
||||
|
||||
bool lx_risk_manager_is_enabled(const LxRiskManager* rm) {
|
||||
return rm ? rm->config.enabled : false;
|
||||
}
|
||||
|
||||
const LxRiskConfig* lx_risk_manager_config(const LxRiskManager* rm) {
|
||||
return rm ? &rm->config : NULL;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Kill Switch
|
||||
* ============================================================================ */
|
||||
|
||||
bool lx_risk_manager_is_killed(const LxRiskManager* rm) {
|
||||
return rm ? atomic_load(&rm->kill_switch) : false;
|
||||
}
|
||||
|
||||
void lx_risk_manager_kill(LxRiskManager* rm) {
|
||||
if (rm) atomic_store(&rm->kill_switch, true);
|
||||
}
|
||||
|
||||
void lx_risk_manager_reset(LxRiskManager* rm) {
|
||||
if (rm) atomic_store(&rm->kill_switch, false);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Helper: Extract asset from symbol
|
||||
* ============================================================================ */
|
||||
|
||||
static void extract_base_asset(const char* symbol, char* asset, size_t asset_size) {
|
||||
if (!symbol || !asset || asset_size == 0) return;
|
||||
|
||||
const char* sep = strchr(symbol, '-');
|
||||
if (!sep) sep = strchr(symbol, '/');
|
||||
if (!sep) sep = strchr(symbol, '_');
|
||||
|
||||
if (sep) {
|
||||
size_t len = sep - symbol;
|
||||
if (len >= asset_size) len = asset_size - 1;
|
||||
strncpy(asset, symbol, len);
|
||||
asset[len] = '\0';
|
||||
} else {
|
||||
strncpy(asset, symbol, asset_size - 1);
|
||||
asset[asset_size - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Order Validation
|
||||
* ============================================================================ */
|
||||
|
||||
LxRiskError lx_risk_manager_validate_order(LxRiskManager* rm, const LxOrderRequest* request) {
|
||||
if (!rm || !request) return LX_RISK_ERROR_DISABLED;
|
||||
|
||||
/* Check if disabled */
|
||||
if (!rm->config.enabled) {
|
||||
return LX_RISK_OK;
|
||||
}
|
||||
|
||||
/* Check kill switch */
|
||||
if (atomic_load(&rm->kill_switch)) {
|
||||
return LX_RISK_ERROR_KILLED;
|
||||
}
|
||||
|
||||
/* Check order size */
|
||||
if (!lx_decimal_is_zero(rm->config.max_order_size)) {
|
||||
if (lx_decimal_gt(request->quantity, rm->config.max_order_size)) {
|
||||
return LX_RISK_ERROR_ORDER_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check position limits */
|
||||
char asset[LX_ASSET_MAX_LEN];
|
||||
extract_base_asset(request->symbol, asset, sizeof(asset));
|
||||
|
||||
if (!lx_decimal_is_zero(rm->config.max_position_size)) {
|
||||
pthread_rwlock_rdlock(&rm->position_lock);
|
||||
|
||||
LxDecimal current_pos = lx_decimal_zero();
|
||||
for (size_t i = 0; i < rm->position_count; i++) {
|
||||
if (strcmp(rm->positions[i].asset, asset) == 0) {
|
||||
current_pos = rm->positions[i].position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&rm->position_lock);
|
||||
|
||||
/* Calculate new position */
|
||||
LxDecimal new_pos;
|
||||
if (request->side == LX_SIDE_BUY) {
|
||||
new_pos = lx_decimal_add(current_pos, request->quantity);
|
||||
} else {
|
||||
new_pos = lx_decimal_sub(current_pos, request->quantity);
|
||||
}
|
||||
new_pos = lx_decimal_abs(new_pos);
|
||||
|
||||
/* Check global limit */
|
||||
if (lx_decimal_gt(new_pos, rm->config.max_position_size)) {
|
||||
return LX_RISK_ERROR_POSITION_LIMIT;
|
||||
}
|
||||
|
||||
/* Check asset-specific limit */
|
||||
for (size_t i = 0; i < rm->config.position_limit_count; i++) {
|
||||
if (strcmp(rm->config.position_limits[i].asset, asset) == 0) {
|
||||
if (lx_decimal_gt(new_pos, rm->config.position_limits[i].limit)) {
|
||||
return LX_RISK_ERROR_POSITION_LIMIT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check daily loss */
|
||||
if (!lx_decimal_is_zero(rm->config.max_daily_loss)) {
|
||||
pthread_rwlock_rdlock(&rm->pnl_lock);
|
||||
LxDecimal daily_pnl = rm->daily_pnl;
|
||||
pthread_rwlock_unlock(&rm->pnl_lock);
|
||||
|
||||
if (lx_decimal_is_negative(daily_pnl)) {
|
||||
LxDecimal loss = lx_decimal_neg(daily_pnl);
|
||||
if (lx_decimal_gt(loss, rm->config.max_daily_loss)) {
|
||||
return LX_RISK_ERROR_DAILY_LOSS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check open orders */
|
||||
if (rm->config.max_open_orders > 0) {
|
||||
pthread_rwlock_rdlock(&rm->orders_lock);
|
||||
|
||||
int open_count = 0;
|
||||
for (size_t i = 0; i < rm->open_orders_count; i++) {
|
||||
if (strcmp(rm->open_orders[i].symbol, request->symbol) == 0) {
|
||||
open_count = rm->open_orders[i].count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&rm->orders_lock);
|
||||
|
||||
if (open_count >= rm->config.max_open_orders) {
|
||||
return LX_RISK_ERROR_OPEN_ORDERS;
|
||||
}
|
||||
}
|
||||
|
||||
return LX_RISK_OK;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Position Tracking
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_risk_manager_update_position(LxRiskManager* rm, const char* asset,
|
||||
LxDecimal quantity, LxSide side) {
|
||||
if (!rm || !asset) return;
|
||||
|
||||
pthread_rwlock_wrlock(&rm->position_lock);
|
||||
|
||||
/* Find existing position */
|
||||
size_t idx = rm->position_count;
|
||||
for (size_t i = 0; i < rm->position_count; i++) {
|
||||
if (strcmp(rm->positions[i].asset, asset) == 0) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Create new position entry if needed */
|
||||
if (idx == rm->position_count) {
|
||||
if (rm->position_count < LX_RISK_MAX_POSITIONS) {
|
||||
strncpy(rm->positions[idx].asset, asset, LX_ASSET_MAX_LEN - 1);
|
||||
rm->positions[idx].position = lx_decimal_zero();
|
||||
rm->position_count++;
|
||||
} else {
|
||||
pthread_rwlock_unlock(&rm->position_lock);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Update position */
|
||||
if (side == LX_SIDE_BUY) {
|
||||
rm->positions[idx].position = lx_decimal_add(rm->positions[idx].position, quantity);
|
||||
} else {
|
||||
rm->positions[idx].position = lx_decimal_sub(rm->positions[idx].position, quantity);
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&rm->position_lock);
|
||||
}
|
||||
|
||||
LxDecimal lx_risk_manager_position(const LxRiskManager* rm, const char* asset) {
|
||||
if (!rm || !asset) return lx_decimal_zero();
|
||||
|
||||
pthread_rwlock_rdlock((pthread_rwlock_t*)&rm->position_lock);
|
||||
|
||||
LxDecimal pos = lx_decimal_zero();
|
||||
for (size_t i = 0; i < rm->position_count; i++) {
|
||||
if (strcmp(rm->positions[i].asset, asset) == 0) {
|
||||
pos = rm->positions[i].position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock((pthread_rwlock_t*)&rm->position_lock);
|
||||
return pos;
|
||||
}
|
||||
|
||||
size_t lx_risk_manager_positions(const LxRiskManager* rm,
|
||||
LxPositionEntry* entries, size_t max_entries) {
|
||||
if (!rm || !entries || max_entries == 0) return 0;
|
||||
|
||||
pthread_rwlock_rdlock((pthread_rwlock_t*)&rm->position_lock);
|
||||
|
||||
size_t count = rm->position_count < max_entries ? rm->position_count : max_entries;
|
||||
memcpy(entries, rm->positions, count * sizeof(LxPositionEntry));
|
||||
|
||||
pthread_rwlock_unlock((pthread_rwlock_t*)&rm->position_lock);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* PnL Tracking
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_risk_manager_update_pnl(LxRiskManager* rm, LxDecimal pnl) {
|
||||
if (!rm) return;
|
||||
|
||||
pthread_rwlock_wrlock(&rm->pnl_lock);
|
||||
rm->daily_pnl = lx_decimal_add(rm->daily_pnl, pnl);
|
||||
pthread_rwlock_unlock(&rm->pnl_lock);
|
||||
|
||||
/* Check for kill switch trigger */
|
||||
if (rm->config.kill_switch_enabled && !lx_decimal_is_zero(rm->config.max_daily_loss)) {
|
||||
pthread_rwlock_rdlock(&rm->pnl_lock);
|
||||
LxDecimal daily_pnl = rm->daily_pnl;
|
||||
pthread_rwlock_unlock(&rm->pnl_lock);
|
||||
|
||||
if (lx_decimal_is_negative(daily_pnl)) {
|
||||
LxDecimal loss = lx_decimal_neg(daily_pnl);
|
||||
if (lx_decimal_gt(loss, rm->config.max_daily_loss)) {
|
||||
atomic_store(&rm->kill_switch, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LxDecimal lx_risk_manager_daily_pnl(const LxRiskManager* rm) {
|
||||
if (!rm) return lx_decimal_zero();
|
||||
|
||||
pthread_rwlock_rdlock((pthread_rwlock_t*)&rm->pnl_lock);
|
||||
LxDecimal pnl = rm->daily_pnl;
|
||||
pthread_rwlock_unlock((pthread_rwlock_t*)&rm->pnl_lock);
|
||||
|
||||
return pnl;
|
||||
}
|
||||
|
||||
void lx_risk_manager_reset_daily_pnl(LxRiskManager* rm) {
|
||||
if (!rm) return;
|
||||
|
||||
pthread_rwlock_wrlock(&rm->pnl_lock);
|
||||
rm->daily_pnl = lx_decimal_zero();
|
||||
pthread_rwlock_unlock(&rm->pnl_lock);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Order Tracking
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_risk_manager_order_opened(LxRiskManager* rm, const char* symbol) {
|
||||
if (!rm || !symbol) return;
|
||||
|
||||
pthread_rwlock_wrlock(&rm->orders_lock);
|
||||
|
||||
/* Find existing entry */
|
||||
for (size_t i = 0; i < rm->open_orders_count; i++) {
|
||||
if (strcmp(rm->open_orders[i].symbol, symbol) == 0) {
|
||||
rm->open_orders[i].count++;
|
||||
pthread_rwlock_unlock(&rm->orders_lock);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Create new entry */
|
||||
if (rm->open_orders_count < LX_RISK_MAX_SYMBOLS) {
|
||||
strncpy(rm->open_orders[rm->open_orders_count].symbol, symbol,
|
||||
sizeof(rm->open_orders[rm->open_orders_count].symbol) - 1);
|
||||
rm->open_orders[rm->open_orders_count].count = 1;
|
||||
rm->open_orders_count++;
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&rm->orders_lock);
|
||||
}
|
||||
|
||||
void lx_risk_manager_order_closed(LxRiskManager* rm, const char* symbol) {
|
||||
if (!rm || !symbol) return;
|
||||
|
||||
pthread_rwlock_wrlock(&rm->orders_lock);
|
||||
|
||||
for (size_t i = 0; i < rm->open_orders_count; i++) {
|
||||
if (strcmp(rm->open_orders[i].symbol, symbol) == 0) {
|
||||
if (rm->open_orders[i].count > 0) {
|
||||
rm->open_orders[i].count--;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&rm->orders_lock);
|
||||
}
|
||||
|
||||
int lx_risk_manager_open_orders(const LxRiskManager* rm, const char* symbol) {
|
||||
if (!rm || !symbol) return 0;
|
||||
|
||||
pthread_rwlock_rdlock((pthread_rwlock_t*)&rm->orders_lock);
|
||||
|
||||
int count = 0;
|
||||
for (size_t i = 0; i < rm->open_orders_count; i++) {
|
||||
if (strcmp(rm->open_orders[i].symbol, symbol) == 0) {
|
||||
count = rm->open_orders[i].count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock((pthread_rwlock_t*)&rm->orders_lock);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Pre-Trade Checks
|
||||
* ============================================================================ */
|
||||
|
||||
bool lx_risk_manager_check_order_size(const LxRiskManager* rm, LxDecimal quantity) {
|
||||
if (!rm || !rm->config.enabled) return true;
|
||||
if (lx_decimal_is_zero(rm->config.max_order_size)) return true;
|
||||
return lx_decimal_le(quantity, rm->config.max_order_size);
|
||||
}
|
||||
|
||||
bool lx_risk_manager_check_position_limit(const LxRiskManager* rm, const char* asset,
|
||||
LxDecimal new_position) {
|
||||
if (!rm || !rm->config.enabled || !asset) return true;
|
||||
if (lx_decimal_is_zero(rm->config.max_position_size)) return true;
|
||||
|
||||
LxDecimal abs_pos = lx_decimal_abs(new_position);
|
||||
|
||||
/* Check global limit */
|
||||
if (lx_decimal_gt(abs_pos, rm->config.max_position_size)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check asset-specific limit */
|
||||
for (size_t i = 0; i < rm->config.position_limit_count; i++) {
|
||||
if (strcmp(rm->config.position_limits[i].asset, asset) == 0) {
|
||||
if (lx_decimal_gt(abs_pos, rm->config.position_limits[i].limit)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool lx_risk_manager_check_daily_loss(const LxRiskManager* rm) {
|
||||
if (!rm || !rm->config.enabled) return true;
|
||||
if (lx_decimal_is_zero(rm->config.max_daily_loss)) return true;
|
||||
|
||||
pthread_rwlock_rdlock((pthread_rwlock_t*)&rm->pnl_lock);
|
||||
LxDecimal daily_pnl = rm->daily_pnl;
|
||||
pthread_rwlock_unlock((pthread_rwlock_t*)&rm->pnl_lock);
|
||||
|
||||
if (lx_decimal_is_negative(daily_pnl)) {
|
||||
LxDecimal loss = lx_decimal_neg(daily_pnl);
|
||||
if (lx_decimal_gt(loss, rm->config.max_daily_loss)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool lx_risk_manager_check_open_orders(const LxRiskManager* rm, const char* symbol) {
|
||||
if (!rm || !rm->config.enabled || !symbol) return true;
|
||||
if (rm->config.max_open_orders <= 0) return true;
|
||||
|
||||
int count = lx_risk_manager_open_orders(rm, symbol);
|
||||
return count < rm->config.max_open_orders;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Order Tracker
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_order_tracker_begin(LxOrderTracker* tracker, LxRiskManager* rm, const char* symbol) {
|
||||
if (!tracker || !rm || !symbol) return;
|
||||
|
||||
tracker->risk_manager = rm;
|
||||
strncpy(tracker->symbol, symbol, sizeof(tracker->symbol) - 1);
|
||||
tracker->symbol[sizeof(tracker->symbol) - 1] = '\0';
|
||||
tracker->released = false;
|
||||
|
||||
lx_risk_manager_order_opened(rm, symbol);
|
||||
}
|
||||
|
||||
void lx_order_tracker_end(LxOrderTracker* tracker) {
|
||||
if (!tracker || tracker->released) return;
|
||||
|
||||
lx_risk_manager_order_closed(tracker->risk_manager, tracker->symbol);
|
||||
}
|
||||
|
||||
void lx_order_tracker_release(LxOrderTracker* tracker) {
|
||||
if (tracker) tracker->released = true;
|
||||
}
|
||||
@@ -1,514 +0,0 @@
|
||||
/**
|
||||
* LX Trading SDK - Types Implementation
|
||||
*/
|
||||
|
||||
#include "lx_trading/types.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
/* ============================================================================
|
||||
* Decimal Implementation
|
||||
* ============================================================================ */
|
||||
|
||||
LxDecimal lx_decimal_zero(void) {
|
||||
LxDecimal d = {0};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_one(void) {
|
||||
LxDecimal d = {LX_DECIMAL_SCALE};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_from_int(int64_t value) {
|
||||
LxDecimal d = {value * LX_DECIMAL_SCALE};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_from_double(double value) {
|
||||
LxDecimal d = {(int64_t)(value * LX_DECIMAL_SCALE)};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_from_string(const char* str) {
|
||||
if (!str) return lx_decimal_zero();
|
||||
|
||||
int negative = 0;
|
||||
const char* p = str;
|
||||
|
||||
/* Skip whitespace */
|
||||
while (*p && isspace((unsigned char)*p)) p++;
|
||||
|
||||
/* Handle sign */
|
||||
if (*p == '-') {
|
||||
negative = 1;
|
||||
p++;
|
||||
} else if (*p == '+') {
|
||||
p++;
|
||||
}
|
||||
|
||||
int64_t integer_part = 0;
|
||||
int64_t decimal_part = 0;
|
||||
int decimal_places = 0;
|
||||
|
||||
/* Parse integer part */
|
||||
while (*p && isdigit((unsigned char)*p)) {
|
||||
integer_part = integer_part * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
|
||||
/* Parse decimal part */
|
||||
if (*p == '.') {
|
||||
p++;
|
||||
while (*p && isdigit((unsigned char)*p) && decimal_places < LX_DECIMAL_PRECISION) {
|
||||
decimal_part = decimal_part * 10 + (*p - '0');
|
||||
decimal_places++;
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scale decimal part */
|
||||
int64_t scale_factor = LX_DECIMAL_SCALE;
|
||||
for (int i = 0; i < decimal_places; i++) {
|
||||
scale_factor /= 10;
|
||||
}
|
||||
|
||||
int64_t value = integer_part * LX_DECIMAL_SCALE + decimal_part * scale_factor;
|
||||
if (negative) value = -value;
|
||||
|
||||
LxDecimal d = {value};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_from_scaled(int64_t scaled_value) {
|
||||
LxDecimal d = {scaled_value};
|
||||
return d;
|
||||
}
|
||||
|
||||
double lx_decimal_to_double(LxDecimal d) {
|
||||
return (double)d.value / LX_DECIMAL_SCALE;
|
||||
}
|
||||
|
||||
int lx_decimal_to_string(LxDecimal d, char* buf, size_t buf_size) {
|
||||
if (!buf || buf_size == 0) return -1;
|
||||
|
||||
int negative = d.value < 0;
|
||||
int64_t abs_value = negative ? -d.value : d.value;
|
||||
|
||||
int64_t integer_part = abs_value / LX_DECIMAL_SCALE;
|
||||
int64_t decimal_part = abs_value % LX_DECIMAL_SCALE;
|
||||
|
||||
/* Remove trailing zeros */
|
||||
int decimal_digits = LX_DECIMAL_PRECISION;
|
||||
while (decimal_digits > 1 && decimal_part % 10 == 0) {
|
||||
decimal_part /= 10;
|
||||
decimal_digits--;
|
||||
}
|
||||
|
||||
int written;
|
||||
if (decimal_part == 0) {
|
||||
written = snprintf(buf, buf_size, "%s%lld",
|
||||
negative ? "-" : "",
|
||||
(long long)integer_part);
|
||||
} else {
|
||||
written = snprintf(buf, buf_size, "%s%lld.%0*lld",
|
||||
negative ? "-" : "",
|
||||
(long long)integer_part,
|
||||
decimal_digits,
|
||||
(long long)decimal_part);
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
int64_t lx_decimal_scaled_value(LxDecimal d) {
|
||||
return d.value;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_add(LxDecimal a, LxDecimal b) {
|
||||
LxDecimal d = {a.value + b.value};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_sub(LxDecimal a, LxDecimal b) {
|
||||
LxDecimal d = {a.value - b.value};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_mul(LxDecimal a, LxDecimal b) {
|
||||
LxDecimal d = {(a.value * b.value) / LX_DECIMAL_SCALE};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_div(LxDecimal a, LxDecimal b) {
|
||||
if (b.value == 0) {
|
||||
LxDecimal d = {0};
|
||||
return d;
|
||||
}
|
||||
LxDecimal d = {(a.value * LX_DECIMAL_SCALE) / b.value};
|
||||
return d;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_abs(LxDecimal d) {
|
||||
LxDecimal result = {d.value < 0 ? -d.value : d.value};
|
||||
return result;
|
||||
}
|
||||
|
||||
LxDecimal lx_decimal_neg(LxDecimal d) {
|
||||
LxDecimal result = {-d.value};
|
||||
return result;
|
||||
}
|
||||
|
||||
bool lx_decimal_eq(LxDecimal a, LxDecimal b) { return a.value == b.value; }
|
||||
bool lx_decimal_ne(LxDecimal a, LxDecimal b) { return a.value != b.value; }
|
||||
bool lx_decimal_lt(LxDecimal a, LxDecimal b) { return a.value < b.value; }
|
||||
bool lx_decimal_le(LxDecimal a, LxDecimal b) { return a.value <= b.value; }
|
||||
bool lx_decimal_gt(LxDecimal a, LxDecimal b) { return a.value > b.value; }
|
||||
bool lx_decimal_ge(LxDecimal a, LxDecimal b) { return a.value >= b.value; }
|
||||
|
||||
bool lx_decimal_is_zero(LxDecimal d) { return d.value == 0; }
|
||||
bool lx_decimal_is_positive(LxDecimal d) { return d.value > 0; }
|
||||
bool lx_decimal_is_negative(LxDecimal d) { return d.value < 0; }
|
||||
|
||||
/* ============================================================================
|
||||
* Enum to String
|
||||
* ============================================================================ */
|
||||
|
||||
const char* lx_side_to_string(LxSide side) {
|
||||
return side == LX_SIDE_BUY ? "buy" : "sell";
|
||||
}
|
||||
|
||||
const char* lx_order_type_to_string(LxOrderType type) {
|
||||
switch (type) {
|
||||
case LX_ORDER_TYPE_MARKET: return "market";
|
||||
case LX_ORDER_TYPE_LIMIT: return "limit";
|
||||
case LX_ORDER_TYPE_LIMIT_MAKER: return "limit_maker";
|
||||
case LX_ORDER_TYPE_STOP_LOSS: return "stop_loss";
|
||||
case LX_ORDER_TYPE_STOP_LOSS_LIMIT: return "stop_loss_limit";
|
||||
case LX_ORDER_TYPE_TAKE_PROFIT: return "take_profit";
|
||||
case LX_ORDER_TYPE_TAKE_PROFIT_LIMIT: return "take_profit_limit";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* lx_tif_to_string(LxTimeInForce tif) {
|
||||
switch (tif) {
|
||||
case LX_TIF_GTC: return "GTC";
|
||||
case LX_TIF_IOC: return "IOC";
|
||||
case LX_TIF_FOK: return "FOK";
|
||||
case LX_TIF_GTD: return "GTD";
|
||||
case LX_TIF_POST_ONLY: return "POST_ONLY";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char* lx_order_status_to_string(LxOrderStatus status) {
|
||||
switch (status) {
|
||||
case LX_ORDER_STATUS_PENDING: return "pending";
|
||||
case LX_ORDER_STATUS_OPEN: return "open";
|
||||
case LX_ORDER_STATUS_PARTIALLY_FILLED: return "partially_filled";
|
||||
case LX_ORDER_STATUS_FILLED: return "filled";
|
||||
case LX_ORDER_STATUS_CANCELLED: return "cancelled";
|
||||
case LX_ORDER_STATUS_REJECTED: return "rejected";
|
||||
case LX_ORDER_STATUS_EXPIRED: return "expired";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Trading Pair
|
||||
* ============================================================================ */
|
||||
|
||||
bool lx_trading_pair_from_symbol(const char* symbol, LxTradingPair* pair) {
|
||||
if (!symbol || !pair) return false;
|
||||
|
||||
memset(pair, 0, sizeof(LxTradingPair));
|
||||
|
||||
/* Find separator */
|
||||
const char* sep = strchr(symbol, '-');
|
||||
if (!sep) sep = strchr(symbol, '/');
|
||||
if (!sep) sep = strchr(symbol, '_');
|
||||
if (!sep) return false;
|
||||
|
||||
size_t base_len = sep - symbol;
|
||||
size_t quote_len = strlen(sep + 1);
|
||||
|
||||
if (base_len >= LX_SYMBOL_MAX_LEN || quote_len >= LX_SYMBOL_MAX_LEN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
strncpy(pair->base, symbol, base_len);
|
||||
pair->base[base_len] = '\0';
|
||||
strncpy(pair->quote, sep + 1, quote_len);
|
||||
pair->quote[quote_len] = '\0';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int lx_trading_pair_to_hummingbot(const LxTradingPair* pair, char* buf, size_t buf_size) {
|
||||
if (!pair || !buf || buf_size == 0) return -1;
|
||||
return snprintf(buf, buf_size, "%s-%s", pair->base, pair->quote);
|
||||
}
|
||||
|
||||
int lx_trading_pair_to_ccxt(const LxTradingPair* pair, char* buf, size_t buf_size) {
|
||||
if (!pair || !buf || buf_size == 0) return -1;
|
||||
return snprintf(buf, buf_size, "%s/%s", pair->base, pair->quote);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Price Level
|
||||
* ============================================================================ */
|
||||
|
||||
LxDecimal lx_price_level_value(const LxPriceLevel* level) {
|
||||
if (!level) return lx_decimal_zero();
|
||||
return lx_decimal_mul(level->price, level->quantity);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Balance
|
||||
* ============================================================================ */
|
||||
|
||||
LxDecimal lx_balance_total(const LxBalance* balance) {
|
||||
if (!balance) return lx_decimal_zero();
|
||||
return lx_decimal_add(balance->free, balance->locked);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Order Request
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_order_request_init(LxOrderRequest* req) {
|
||||
if (!req) return;
|
||||
memset(req, 0, sizeof(LxOrderRequest));
|
||||
req->side = LX_SIDE_BUY;
|
||||
req->order_type = LX_ORDER_TYPE_MARKET;
|
||||
req->time_in_force = LX_TIF_GTC;
|
||||
}
|
||||
|
||||
LxOrderRequest lx_order_request_market(const char* symbol, LxSide side, LxDecimal quantity) {
|
||||
LxOrderRequest req;
|
||||
lx_order_request_init(&req);
|
||||
|
||||
if (symbol) {
|
||||
strncpy(req.symbol, symbol, sizeof(req.symbol) - 1);
|
||||
}
|
||||
req.side = side;
|
||||
req.order_type = LX_ORDER_TYPE_MARKET;
|
||||
req.quantity = quantity;
|
||||
req.time_in_force = LX_TIF_IOC;
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
LxOrderRequest lx_order_request_limit(const char* symbol, LxSide side,
|
||||
LxDecimal quantity, LxDecimal price) {
|
||||
LxOrderRequest req;
|
||||
lx_order_request_init(&req);
|
||||
|
||||
if (symbol) {
|
||||
strncpy(req.symbol, symbol, sizeof(req.symbol) - 1);
|
||||
}
|
||||
req.side = side;
|
||||
req.order_type = LX_ORDER_TYPE_LIMIT;
|
||||
req.quantity = quantity;
|
||||
req.has_price = true;
|
||||
req.price = price;
|
||||
req.time_in_force = LX_TIF_GTC;
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
LxOrderRequest* lx_order_request_with_venue(LxOrderRequest* req, const char* venue) {
|
||||
if (!req) return NULL;
|
||||
if (venue) {
|
||||
req->has_venue = true;
|
||||
strncpy(req->venue, venue, sizeof(req->venue) - 1);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
LxOrderRequest* lx_order_request_with_post_only(LxOrderRequest* req) {
|
||||
if (!req) return NULL;
|
||||
req->post_only = true;
|
||||
req->time_in_force = LX_TIF_POST_ONLY;
|
||||
return req;
|
||||
}
|
||||
|
||||
LxOrderRequest* lx_order_request_with_client_id(LxOrderRequest* req, const char* client_id) {
|
||||
if (!req) return NULL;
|
||||
if (client_id) {
|
||||
strncpy(req->client_order_id, client_id, sizeof(req->client_order_id) - 1);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
LxOrderRequest* lx_order_request_with_stop_price(LxOrderRequest* req, LxDecimal stop_price) {
|
||||
if (!req) return NULL;
|
||||
req->has_stop_price = true;
|
||||
req->stop_price = stop_price;
|
||||
return req;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Order
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_order_init(LxOrder* order) {
|
||||
if (!order) return;
|
||||
memset(order, 0, sizeof(LxOrder));
|
||||
order->side = LX_SIDE_BUY;
|
||||
order->order_type = LX_ORDER_TYPE_LIMIT;
|
||||
order->status = LX_ORDER_STATUS_PENDING;
|
||||
}
|
||||
|
||||
void lx_order_cleanup(LxOrder* order) {
|
||||
if (!order) return;
|
||||
if (order->fees) {
|
||||
free(order->fees);
|
||||
order->fees = NULL;
|
||||
}
|
||||
order->fee_count = 0;
|
||||
}
|
||||
|
||||
bool lx_order_is_open(const LxOrder* order) {
|
||||
if (!order) return false;
|
||||
return order->status == LX_ORDER_STATUS_OPEN ||
|
||||
order->status == LX_ORDER_STATUS_PARTIALLY_FILLED ||
|
||||
order->status == LX_ORDER_STATUS_PENDING;
|
||||
}
|
||||
|
||||
bool lx_order_is_done(const LxOrder* order) {
|
||||
if (!order) return false;
|
||||
return order->status == LX_ORDER_STATUS_FILLED ||
|
||||
order->status == LX_ORDER_STATUS_CANCELLED ||
|
||||
order->status == LX_ORDER_STATUS_REJECTED ||
|
||||
order->status == LX_ORDER_STATUS_EXPIRED;
|
||||
}
|
||||
|
||||
LxDecimal lx_order_fill_percent(const LxOrder* order) {
|
||||
if (!order || lx_decimal_is_zero(order->quantity)) {
|
||||
return lx_decimal_zero();
|
||||
}
|
||||
LxDecimal ratio = lx_decimal_div(order->filled_quantity, order->quantity);
|
||||
return lx_decimal_mul(ratio, lx_decimal_from_double(100.0));
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Trade
|
||||
* ============================================================================ */
|
||||
|
||||
LxDecimal lx_trade_value(const LxTrade* trade) {
|
||||
if (!trade) return lx_decimal_zero();
|
||||
return lx_decimal_mul(trade->price, trade->quantity);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Ticker
|
||||
* ============================================================================ */
|
||||
|
||||
bool lx_ticker_mid_price(const LxTicker* ticker, LxDecimal* result) {
|
||||
if (!ticker || !result) return false;
|
||||
if (!ticker->has_bid || !ticker->has_ask) {
|
||||
if (ticker->has_last) {
|
||||
*result = ticker->last;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
*result = lx_decimal_div(lx_decimal_add(ticker->bid, ticker->ask),
|
||||
lx_decimal_from_double(2.0));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool lx_ticker_spread(const LxTicker* ticker, LxDecimal* result) {
|
||||
if (!ticker || !result) return false;
|
||||
if (!ticker->has_bid || !ticker->has_ask) return false;
|
||||
*result = lx_decimal_sub(ticker->ask, ticker->bid);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool lx_ticker_spread_percent(const LxTicker* ticker, LxDecimal* result) {
|
||||
if (!ticker || !result) return false;
|
||||
if (!ticker->has_bid || !ticker->has_ask) return false;
|
||||
if (lx_decimal_is_zero(ticker->bid)) return false;
|
||||
|
||||
LxDecimal spread = lx_decimal_sub(ticker->ask, ticker->bid);
|
||||
*result = lx_decimal_mul(lx_decimal_div(spread, ticker->bid),
|
||||
lx_decimal_from_double(100.0));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Swap Quote
|
||||
* ============================================================================ */
|
||||
|
||||
void lx_swap_quote_cleanup(LxSwapQuote* quote) {
|
||||
if (!quote) return;
|
||||
if (quote->route) {
|
||||
for (size_t i = 0; i < quote->route_count; i++) {
|
||||
free(quote->route[i]);
|
||||
}
|
||||
free(quote->route);
|
||||
quote->route = NULL;
|
||||
}
|
||||
quote->route_count = 0;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* Timestamp Utilities
|
||||
* ============================================================================ */
|
||||
|
||||
int64_t lx_now_ms(void) {
|
||||
#ifdef _WIN32
|
||||
FILETIME ft;
|
||||
GetSystemTimeAsFileTime(&ft);
|
||||
ULARGE_INTEGER ull;
|
||||
ull.LowPart = ft.dwLowDateTime;
|
||||
ull.HighPart = ft.dwHighDateTime;
|
||||
return (int64_t)((ull.QuadPart - 116444736000000000ULL) / 10000);
|
||||
#else
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return (int64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
|
||||
#endif
|
||||
}
|
||||
|
||||
int64_t lx_now_us(void) {
|
||||
#ifdef _WIN32
|
||||
FILETIME ft;
|
||||
GetSystemTimeAsFileTime(&ft);
|
||||
ULARGE_INTEGER ull;
|
||||
ull.LowPart = ft.dwLowDateTime;
|
||||
ull.HighPart = ft.dwHighDateTime;
|
||||
return (int64_t)((ull.QuadPart - 116444736000000000ULL) / 10);
|
||||
#else
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return (int64_t)tv.tv_sec * 1000000 + tv.tv_usec;
|
||||
#endif
|
||||
}
|
||||
|
||||
int64_t lx_now_ns(void) {
|
||||
#ifdef _WIN32
|
||||
FILETIME ft;
|
||||
GetSystemTimeAsFileTime(&ft);
|
||||
ULARGE_INTEGER ull;
|
||||
ull.LowPart = ft.dwLowDateTime;
|
||||
ull.HighPart = ft.dwHighDateTime;
|
||||
return (int64_t)((ull.QuadPart - 116444736000000000ULL) * 100);
|
||||
#else
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
|
||||
#endif
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(lx_trading VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Allow older CMake policies for dependencies
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||
|
||||
# Options
|
||||
option(LX_TRADING_BUILD_TESTS "Build tests" ON)
|
||||
option(LX_TRADING_BUILD_EXAMPLES "Build examples" ON)
|
||||
option(LX_TRADING_USE_SIMD "Enable SIMD optimizations" ON)
|
||||
|
||||
# Compiler flags
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
if(LX_TRADING_USE_SIMD)
|
||||
add_compile_options(-march=native)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Find dependencies
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# FetchContent for dependencies
|
||||
include(FetchContent)
|
||||
|
||||
# nlohmann/json
|
||||
FetchContent_Declare(
|
||||
json
|
||||
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||
GIT_TAG v3.11.3
|
||||
)
|
||||
|
||||
# cpr (HTTP client)
|
||||
FetchContent_Declare(
|
||||
cpr
|
||||
GIT_REPOSITORY https://github.com/libcpr/cpr.git
|
||||
GIT_TAG 1.10.5
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(json cpr)
|
||||
|
||||
# Library
|
||||
add_library(lx_trading
|
||||
src/types.cpp
|
||||
src/config.cpp
|
||||
src/adapter.cpp
|
||||
src/native_adapter.cpp
|
||||
src/ccxt_adapter.cpp
|
||||
src/hummingbot_adapter.cpp
|
||||
src/orderbook.cpp
|
||||
src/client.cpp
|
||||
src/risk.cpp
|
||||
src/execution.cpp
|
||||
src/math.cpp
|
||||
# Arbitrage module
|
||||
src/arbitrage/types.cpp
|
||||
src/arbitrage/scanner.cpp
|
||||
src/arbitrage/lx_first.cpp
|
||||
src/arbitrage/unified.cpp
|
||||
src/arbitrage/cross_chain.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lx_trading
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
target_link_libraries(lx_trading
|
||||
PUBLIC
|
||||
nlohmann_json::nlohmann_json
|
||||
cpr::cpr
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
# Tests
|
||||
if(LX_TRADING_BUILD_TESTS)
|
||||
enable_testing()
|
||||
|
||||
FetchContent_Declare(
|
||||
Catch2
|
||||
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
||||
GIT_TAG v3.5.0
|
||||
)
|
||||
FetchContent_MakeAvailable(Catch2)
|
||||
|
||||
add_executable(lx_trading_tests
|
||||
tests/test_types.cpp
|
||||
tests/test_orderbook.cpp
|
||||
tests/test_math.cpp
|
||||
tests/test_risk.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(lx_trading_tests
|
||||
PRIVATE
|
||||
lx_trading
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
include(CTest)
|
||||
include(Catch)
|
||||
catch_discover_tests(lx_trading_tests)
|
||||
endif()
|
||||
|
||||
# Examples
|
||||
if(LX_TRADING_BUILD_EXAMPLES)
|
||||
add_executable(example_basic examples/basic.cpp)
|
||||
target_link_libraries(example_basic PRIVATE lx_trading)
|
||||
|
||||
add_executable(example_orderbook examples/orderbook_demo.cpp)
|
||||
target_link_libraries(example_orderbook PRIVATE lx_trading)
|
||||
endif()
|
||||
|
||||
# Install (simplified - dependencies handled separately)
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS lx_trading
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
@@ -1,291 +0,0 @@
|
||||
# LX Trading SDK for C++
|
||||
|
||||
Modern C++17/20 high-frequency trading library for LX Exchange.
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero-copy where possible**: Fixed-point decimal, inline string storage
|
||||
- **Lock-free orderbook**: Thread-safe reads with minimal contention
|
||||
- **SIMD optimizations**: AVX2/SSE4 for financial math when available
|
||||
- **Smart order routing**: Aggregate orderbooks across multiple venues
|
||||
- **Execution algorithms**: TWAP, VWAP, Iceberg, Sniper, POV
|
||||
- **Risk management**: Thread-safe position and PnL tracking
|
||||
- **Multi-venue support**: Native LX DEX/AMM, CCXT, Hummingbot Gateway
|
||||
|
||||
## Quick Start
|
||||
|
||||
```cpp
|
||||
#include <lx/trading/client.hpp>
|
||||
#include <lx/trading/config.hpp>
|
||||
|
||||
using namespace lx::trading;
|
||||
|
||||
int main() {
|
||||
// Configure venues
|
||||
Config config;
|
||||
config.with_native("lx_dex",
|
||||
NativeVenueConfig::lx_dex("https://api.lx.exchange")
|
||||
.with_credentials("key", "secret"));
|
||||
|
||||
// Create client
|
||||
Client client(config);
|
||||
client.connect().get();
|
||||
|
||||
// Get aggregated orderbook
|
||||
auto book = client.aggregated_orderbook("BTC-USDC").get();
|
||||
|
||||
// Place order with smart routing
|
||||
auto order = client.buy("BTC-USDC", Decimal::from_double(0.1)).get();
|
||||
|
||||
client.disconnect().get();
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Requirements:
|
||||
- CMake 3.16+
|
||||
- C++20 compiler (GCC 10+, Clang 12+, MSVC 2019+)
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build . -j$(nproc)
|
||||
|
||||
# Run tests
|
||||
ctest --output-on-failure
|
||||
```
|
||||
|
||||
### CMake Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `LX_TRADING_BUILD_TESTS` | ON | Build test suite |
|
||||
| `LX_TRADING_BUILD_EXAMPLES` | ON | Build examples |
|
||||
| `LX_TRADING_USE_SIMD` | ON | Enable SIMD optimizations |
|
||||
|
||||
## API Overview
|
||||
|
||||
### Configuration
|
||||
|
||||
```cpp
|
||||
Config config;
|
||||
|
||||
// Native venues
|
||||
config.with_native("lx_dex",
|
||||
NativeVenueConfig::lx_dex("https://api.lx.exchange")
|
||||
.with_credentials("key", "secret")
|
||||
.with_wallet("0x...", "private_key"));
|
||||
|
||||
// CCXT exchanges (via REST proxy)
|
||||
config.with_ccxt("binance",
|
||||
CcxtConfig::create("binance")
|
||||
.with_credentials("key", "secret")
|
||||
.enable_sandbox());
|
||||
|
||||
// Hummingbot Gateway
|
||||
config.with_hummingbot("uniswap",
|
||||
HummingbotConfig::create("uniswap_lux_mainnet")
|
||||
.with_wallet("0x...")
|
||||
.with_endpoint("localhost", 15888));
|
||||
|
||||
// Risk management
|
||||
config.set_max_order_size(Decimal::from_double(10.0))
|
||||
.set_max_position_size(Decimal::from_double(100.0))
|
||||
.set_max_daily_loss(Decimal::from_double(1000.0));
|
||||
```
|
||||
|
||||
### Decimal Arithmetic
|
||||
|
||||
Fixed-point decimal with 8 decimal places for exact financial math:
|
||||
|
||||
```cpp
|
||||
Decimal a = Decimal::from_double(100.5);
|
||||
Decimal b = Decimal::from_string("50.25");
|
||||
Decimal c = a + b; // 150.75
|
||||
Decimal d = a * b; // 5050.125
|
||||
std::string s = c.to_string(); // "150.75"
|
||||
```
|
||||
|
||||
### Orderbook
|
||||
|
||||
```cpp
|
||||
// Single venue
|
||||
Orderbook book("BTC-USDC", "lx_dex");
|
||||
book.add_bid(Decimal::from_double(50000), Decimal::from_double(1.5));
|
||||
book.add_ask(Decimal::from_double(50010), Decimal::from_double(1.0));
|
||||
book.sort();
|
||||
|
||||
auto vwap = book.vwap_buy(Decimal::from_double(2.0)); // VWAP for buying 2 BTC
|
||||
|
||||
// Aggregated across venues
|
||||
AggregatedOrderbook agg("BTC-USDC");
|
||||
agg.add_orderbook(book1);
|
||||
agg.add_orderbook(book2);
|
||||
|
||||
auto best = agg.best_venue_buy(Decimal::from_double(1.0)); // Find best execution venue
|
||||
```
|
||||
|
||||
### Order Placement
|
||||
|
||||
```cpp
|
||||
// Market orders
|
||||
auto order = client.buy("BTC-USDC", Decimal::from_double(0.1)).get();
|
||||
auto order = client.sell("BTC-USDC", Decimal::from_double(0.1)).get();
|
||||
|
||||
// Limit orders
|
||||
auto order = client.limit_buy("BTC-USDC",
|
||||
Decimal::from_double(0.1),
|
||||
Decimal::from_double(50000.0)).get();
|
||||
|
||||
// Full control
|
||||
OrderRequest req = OrderRequest::limit("BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(0.1), Decimal::from_double(50000.0))
|
||||
.with_venue("lx_dex")
|
||||
.with_post_only()
|
||||
.with_client_id("my-order-123");
|
||||
|
||||
auto order = client.place_order(req).get();
|
||||
```
|
||||
|
||||
### AMM Operations
|
||||
|
||||
```cpp
|
||||
// Get quote
|
||||
auto quote = client.quote("ETH", "USDC",
|
||||
Decimal::from_double(1.0), true, "lx_amm").get();
|
||||
|
||||
// Execute swap
|
||||
auto trade = client.swap("ETH", "USDC",
|
||||
Decimal::from_double(1.0), true, 0.01, "lx_amm").get();
|
||||
|
||||
// Liquidity management
|
||||
auto result = client.add_liquidity("ETH", "USDC",
|
||||
Decimal::from_double(1.0),
|
||||
Decimal::from_double(2000.0),
|
||||
0.01, "lx_amm").get();
|
||||
```
|
||||
|
||||
### Execution Algorithms
|
||||
|
||||
```cpp
|
||||
#include <lx/trading/execution.hpp>
|
||||
|
||||
// TWAP - spread order over time
|
||||
auto twap = make_twap(client, "BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(10.0),
|
||||
std::chrono::minutes(30),
|
||||
10); // 10 slices
|
||||
auto result = twap->execute().get();
|
||||
|
||||
// VWAP - participate in market volume
|
||||
auto vwap = make_vwap(client, "BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(10.0),
|
||||
Decimal::from_double(0.1), // 10% participation
|
||||
std::chrono::hours(1));
|
||||
|
||||
// Iceberg - hidden large order
|
||||
auto iceberg = make_iceberg(client, "BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(100.0), // total
|
||||
Decimal::from_double(5.0), // visible
|
||||
Decimal::from_double(50000.0));
|
||||
|
||||
// Sniper - wait for price then execute
|
||||
auto sniper = make_sniper(client, "BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(1.0),
|
||||
Decimal::from_double(49000.0), // target price
|
||||
std::chrono::seconds(60));
|
||||
```
|
||||
|
||||
### Risk Management
|
||||
|
||||
```cpp
|
||||
auto& risk = client.risk_manager();
|
||||
|
||||
// Check before trading
|
||||
if (!risk.check_order_size(qty)) {
|
||||
// Order too large
|
||||
}
|
||||
|
||||
// Manual control
|
||||
risk.kill(); // Stop all trading
|
||||
risk.reset(); // Resume trading
|
||||
|
||||
// Position tracking
|
||||
risk.update_position("BTC", Decimal::from_double(1.0), Side::Buy);
|
||||
Decimal pos = risk.position("BTC");
|
||||
```
|
||||
|
||||
### Financial Math
|
||||
|
||||
```cpp
|
||||
#include <lx/trading/math.hpp>
|
||||
|
||||
using namespace lx::trading::math;
|
||||
|
||||
// Options pricing
|
||||
double call_price = black_scholes(100, 100, 1.0, 0.05, 0.2, true);
|
||||
double iv = implied_volatility(10.45, 100, 100, 1.0, 0.05, true);
|
||||
Greeks g = greeks(100, 100, 1.0, 0.05, 0.2, true);
|
||||
|
||||
// AMM math
|
||||
auto [out, price] = constant_product_price(1000, 1000, 10, 0.003, true);
|
||||
auto [out, new_sqrt_p, impact] = concentrated_liquidity_price(...);
|
||||
|
||||
// Risk metrics
|
||||
double vol = volatility(returns, true, 252);
|
||||
double sharpe = sharpe_ratio(returns, 0.05);
|
||||
auto [dd, peak, trough] = max_drawdown(prices);
|
||||
double var95 = var(returns, 0.95);
|
||||
double cvar95 = cvar(returns, 0.95);
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
lx-trading-cpp/
|
||||
├── include/lx/trading/
|
||||
│ ├── types.hpp # Core types (Decimal, Side, Order, etc.)
|
||||
│ ├── config.hpp # Configuration with builder pattern
|
||||
│ ├── adapter.hpp # VenueAdapter interface
|
||||
│ ├── adapters/
|
||||
│ │ ├── native.hpp # LX DEX and AMM adapters
|
||||
│ │ ├── ccxt.hpp # CCXT REST proxy adapter
|
||||
│ │ └── hummingbot.hpp # Hummingbot Gateway adapter
|
||||
│ ├── orderbook.hpp # Lock-free orderbook
|
||||
│ ├── client.hpp # Unified trading client
|
||||
│ ├── risk.hpp # Risk management
|
||||
│ ├── execution.hpp # Execution algorithms
|
||||
│ └── math.hpp # Financial mathematics
|
||||
├── src/ # Implementation
|
||||
├── tests/ # Catch2 tests
|
||||
└── examples/ # Usage examples
|
||||
```
|
||||
|
||||
## Thread Safety
|
||||
|
||||
- `Orderbook`: Thread-safe reads/writes via `std::shared_mutex`
|
||||
- `RiskManager`: Thread-safe via atomic operations and locks
|
||||
- `Client`: Future-based async API, safe for concurrent calls
|
||||
- `Decimal`: Immutable, thread-safe by design
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Fixed-point arithmetic**: `Decimal` uses int64 internally for exact math
|
||||
2. **Inline storage**: `TradingPair` uses fixed arrays, no heap allocation
|
||||
3. **Lock-free reads**: Orderbook allows concurrent readers
|
||||
4. **SIMD**: Batch operations use AVX2 when available
|
||||
5. **Zero-copy**: Futures return by value, move semantics throughout
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [nlohmann/json](https://github.com/nlohmann/json) - JSON parsing
|
||||
- [cpr](https://github.com/libcpr/cpr) - HTTP client
|
||||
- [websocketpp](https://github.com/zaphoyd/websocketpp) - WebSocket (streaming)
|
||||
- [Catch2](https://github.com/catchorg/Catch2) - Testing (optional)
|
||||
|
||||
All dependencies are fetched via CMake FetchContent.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1,380 +0,0 @@
|
||||
/**
|
||||
* LX-First Arbitrage Bot Example
|
||||
*
|
||||
* This bot uses the LX-First strategy where LX DEX prices are treated
|
||||
* as the "truth" (fastest venue with nanosecond updates, 200ms blocks).
|
||||
* Other venues are always stale by comparison.
|
||||
*
|
||||
* Arbitrage = exploiting stale venues before they catch up to LX prices.
|
||||
*
|
||||
* Cross-chain transport options:
|
||||
* - Warp: For Lux subnet communication (<500ms)
|
||||
* - Teleport: For EVM chain bridging (~30s)
|
||||
* - CEX API: Direct trading (instant)
|
||||
*
|
||||
* NO SMART CONTRACTS - just coordinated trades through unified SDK.
|
||||
*/
|
||||
|
||||
#include <lx/trading/lx_dex.hpp>
|
||||
#include <lx/trading/arbitrage/arbitrage.hpp>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
#include <csignal>
|
||||
#include <mutex>
|
||||
|
||||
using namespace lx::trading;
|
||||
using namespace lx::trading::arbitrage;
|
||||
|
||||
// ============================================
|
||||
// Global state
|
||||
// ============================================
|
||||
|
||||
std::atomic<bool> g_running{false};
|
||||
std::mutex g_stats_mutex;
|
||||
|
||||
struct Stats {
|
||||
uint64_t total_opportunities = 0;
|
||||
uint64_t total_executions = 0;
|
||||
Decimal total_pnl{"0"};
|
||||
};
|
||||
|
||||
Stats g_stats;
|
||||
|
||||
// ============================================
|
||||
// Arbitrage Bot Class
|
||||
// ============================================
|
||||
|
||||
class ArbitrageBot {
|
||||
public:
|
||||
ArbitrageBot() = default;
|
||||
|
||||
bool start() {
|
||||
std::cout << std::string(60, '=') << std::endl;
|
||||
std::cout << "LX-FIRST ARBITRAGE BOT" << std::endl;
|
||||
std::cout << std::string(60, '=') << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// Initialize DEX client
|
||||
LxDexConfig dex_config;
|
||||
dex_config.endpoint = get_env("LX_DEX_ENDPOINT", "wss://dex.lux.network/ws");
|
||||
dex_config.api_key = get_env("LX_API_KEY", "");
|
||||
|
||||
dex_ = std::make_shared<LxDex>(dex_config);
|
||||
if (!dex_->connect()) {
|
||||
std::cerr << "Failed to connect to LX DEX" << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::cout << "[OK] Connected to LX DEX" << std::endl;
|
||||
|
||||
// Initialize LX-First strategy
|
||||
LxFirstConfig lx_config;
|
||||
lx_config.max_staleness_ms = 2000;
|
||||
lx_config.min_divergence_bps = Decimal("10");
|
||||
lx_config.min_profit = Decimal("5");
|
||||
lx_config.max_position_size = Decimal("10000");
|
||||
lx_config.symbols = {"BTC-USDC", "ETH-USDC", "LUX-USDC"};
|
||||
lx_config.venue_latencies = {
|
||||
{"binance", 50},
|
||||
{"mexc", 100},
|
||||
{"okx", 80},
|
||||
{"uniswap", 12000},
|
||||
{"pancakeswap", 3000}
|
||||
};
|
||||
|
||||
lx_first_ = std::make_shared<LxFirstArbitrage>(lx_config);
|
||||
lx_first_->on_opportunity([this](const LxFirstOpportunity& opp) {
|
||||
on_lx_first_opportunity(opp);
|
||||
});
|
||||
std::cout << "[OK] LX-First strategy initialized" << std::endl;
|
||||
|
||||
// Initialize Scanner
|
||||
ScannerConfig scanner_config;
|
||||
scanner_config.min_spread_bps = Decimal("10");
|
||||
scanner_config.min_profit_usd = Decimal("10");
|
||||
scanner_config.max_price_age_ms = 5000;
|
||||
scanner_config.symbols = {"BTC", "ETH", "LUX", "SOL", "AVAX"};
|
||||
scanner_config.chain_ids = {"lux", "ethereum", "bsc", "arbitrum", "polygon"};
|
||||
scanner_config.scan_interval_ms = 100;
|
||||
|
||||
scanner_ = std::make_shared<Scanner>(scanner_config);
|
||||
scanner_->on_opportunity([](const ArbitrageOpportunity& opp) {
|
||||
std::cout << "[SCANNER] " << to_string(opp.type) << ": "
|
||||
<< opp.buy_source.venue << " -> " << opp.sell_source.venue
|
||||
<< " | Spread: " << opp.spread_bps.to_string() << " bps"
|
||||
<< " | Net PnL: $" << opp.net_pnl.to_string()
|
||||
<< std::endl;
|
||||
});
|
||||
std::cout << "[OK] Scanner initialized" << std::endl;
|
||||
|
||||
// Initialize Cross-Chain Router
|
||||
router_ = std::make_shared<CrossChainRouter>(default_cross_chain_config());
|
||||
std::cout << "[OK] Cross-chain router initialized" << std::endl;
|
||||
|
||||
// Start all systems
|
||||
lx_first_->start();
|
||||
scanner_->start();
|
||||
g_running = true;
|
||||
|
||||
std::cout << std::endl;
|
||||
std::cout << std::string(60, '=') << std::endl;
|
||||
std::cout << "BOT RUNNING - Press Ctrl+C to stop" << std::endl;
|
||||
std::cout << std::string(60, '=') << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Monitoring symbols: BTC-USDC, ETH-USDC, LUX-USDC" << std::endl;
|
||||
std::cout << "Min divergence: 10 bps" << std::endl;
|
||||
std::cout << "Min profit: $5" << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// Start price feed simulator thread
|
||||
price_feed_thread_ = std::thread([this]() {
|
||||
simulate_price_feeds();
|
||||
});
|
||||
|
||||
// Start stats reporter thread
|
||||
stats_thread_ = std::thread([this]() {
|
||||
report_stats();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
std::cout << "\nShutting down..." << std::endl;
|
||||
g_running = false;
|
||||
|
||||
lx_first_->stop();
|
||||
scanner_->stop();
|
||||
|
||||
if (price_feed_thread_.joinable()) {
|
||||
price_feed_thread_.join();
|
||||
}
|
||||
if (stats_thread_.joinable()) {
|
||||
stats_thread_.join();
|
||||
}
|
||||
|
||||
print_final_stats();
|
||||
}
|
||||
|
||||
private:
|
||||
void on_lx_first_opportunity(const LxFirstOpportunity& opp) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_stats_mutex);
|
||||
g_stats.total_opportunities++;
|
||||
}
|
||||
|
||||
std::cout << std::endl;
|
||||
std::cout << std::string(50, '=') << std::endl;
|
||||
std::cout << "LX-FIRST OPPORTUNITY DETECTED" << std::endl;
|
||||
std::cout << std::string(50, '=') << std::endl;
|
||||
std::cout << "Symbol: " << opp.symbol << std::endl;
|
||||
std::cout << "LX Price: $" << opp.lx_price.mid.to_string() << std::endl;
|
||||
std::cout << "Stale Venue: " << opp.stale_venue << std::endl;
|
||||
std::cout << "Stale Bid/Ask: $" << opp.stale_price.bid.to_string()
|
||||
<< " / $" << opp.stale_price.ask.to_string() << std::endl;
|
||||
std::cout << "Staleness: " << opp.staleness << "ms" << std::endl;
|
||||
std::cout << "Side: " << opp.side << std::endl;
|
||||
std::cout << "Divergence: " << opp.divergence_bps.to_string() << " bps" << std::endl;
|
||||
std::cout << "Expected Profit: $" << opp.expected_profit.to_string() << std::endl;
|
||||
std::cout << "Confidence: " << (opp.confidence * 100) << "%" << std::endl;
|
||||
std::cout << std::string(50, '=') << std::endl;
|
||||
|
||||
// Execute if confidence is high enough
|
||||
if (opp.confidence > 0.8) {
|
||||
execute_lx_first(opp);
|
||||
}
|
||||
}
|
||||
|
||||
void execute_lx_first(const LxFirstOpportunity& opp) {
|
||||
std::cout << "\n[EXECUTING] " << opp.id << "..." << std::endl;
|
||||
|
||||
// Determine cross-chain transport
|
||||
std::string buy_chain = router_->venue_to_chain(opp.stale_venue);
|
||||
std::string sell_chain = "lux_mainnet";
|
||||
auto transport = router_->determine_transport(buy_chain, sell_chain);
|
||||
auto latency = router_->estimate_latency(buy_chain, sell_chain);
|
||||
|
||||
std::cout << " Transport: " << to_string(transport) << std::endl;
|
||||
std::cout << " Est. Latency: " << latency << "ms" << std::endl;
|
||||
|
||||
if (opp.side == "buy") {
|
||||
std::cout << " Buying on " << opp.stale_venue << "..." << std::endl;
|
||||
// In production: place actual order
|
||||
// auto order = cex_client->place_order(...)
|
||||
|
||||
std::cout << " Hedging on LX DEX..." << std::endl;
|
||||
// In production: place hedge order
|
||||
// auto hedge = dex_->spot()->sell(...)
|
||||
} else {
|
||||
std::cout << " Selling on " << opp.stale_venue << "..." << std::endl;
|
||||
// In production: place actual order
|
||||
|
||||
std::cout << " Hedging on LX DEX..." << std::endl;
|
||||
// In production: place hedge order
|
||||
}
|
||||
|
||||
// Simulate successful execution
|
||||
Decimal profit = opp.expected_profit * Decimal("0.8"); // Simulate slippage
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_stats_mutex);
|
||||
g_stats.total_executions++;
|
||||
g_stats.total_pnl = g_stats.total_pnl + profit;
|
||||
}
|
||||
|
||||
std::cout << "[SUCCESS] Executed " << opp.id
|
||||
<< " | Profit: $" << profit.to_string() << std::endl;
|
||||
}
|
||||
|
||||
void simulate_price_feeds() {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_real_distribution<> price_dist(-0.001, 0.001);
|
||||
std::uniform_real_distribution<> divergence_dist(-0.002, 0.002);
|
||||
std::uniform_int_distribution<> block_dist(1000000, 2000000);
|
||||
|
||||
struct BasePair {
|
||||
std::string symbol;
|
||||
Decimal base;
|
||||
};
|
||||
|
||||
std::vector<BasePair> base_prices = {
|
||||
{"BTC-USDC", Decimal("50000")},
|
||||
{"ETH-USDC", Decimal("3000")},
|
||||
{"LUX-USDC", Decimal("25")}
|
||||
};
|
||||
|
||||
while (g_running) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch()).count();
|
||||
|
||||
for (const auto& pair : base_prices) {
|
||||
// Simulate LX DEX price (the oracle)
|
||||
double variance = price_dist(gen);
|
||||
Decimal lx_mid = pair.base * Decimal(std::to_string(1.0 + variance));
|
||||
|
||||
LxPrice lx_price;
|
||||
lx_price.symbol = pair.symbol;
|
||||
lx_price.bid = lx_mid * Decimal("0.9999");
|
||||
lx_price.ask = lx_mid * Decimal("1.0001");
|
||||
lx_price.mid = lx_mid;
|
||||
lx_price.timestamp = timestamp;
|
||||
lx_price.block_num = block_dist(gen);
|
||||
|
||||
lx_first_->update_lx_price(lx_price);
|
||||
|
||||
// Simulate stale CEX prices
|
||||
std::vector<std::pair<std::string, int>> venues = {
|
||||
{"binance", 50},
|
||||
{"mexc", 100}
|
||||
};
|
||||
|
||||
for (const auto& [venue, latency] : venues) {
|
||||
double divergence = divergence_dist(gen);
|
||||
Decimal venue_mid = pair.base * Decimal(std::to_string(1.0 + divergence));
|
||||
|
||||
VenuePrice venue_price;
|
||||
venue_price.venue = venue;
|
||||
venue_price.symbol = pair.symbol;
|
||||
venue_price.bid = venue_mid * Decimal("0.9998");
|
||||
venue_price.ask = venue_mid * Decimal("1.0002");
|
||||
venue_price.timestamp = timestamp - latency;
|
||||
venue_price.latency = latency;
|
||||
venue_price.stale = false;
|
||||
|
||||
lx_first_->update_venue_price(venue_price);
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
}
|
||||
|
||||
void report_stats() {
|
||||
while (g_running) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(30));
|
||||
|
||||
if (!g_running) break;
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_stats_mutex);
|
||||
std::cout << std::endl;
|
||||
std::cout << std::string(40, '-') << std::endl;
|
||||
std::cout << "STATS" << std::endl;
|
||||
std::cout << " Opportunities: " << g_stats.total_opportunities << std::endl;
|
||||
std::cout << " Executions: " << g_stats.total_executions << std::endl;
|
||||
std::cout << " Total PnL: $" << g_stats.total_pnl.to_string() << std::endl;
|
||||
if (g_stats.total_executions > 0) {
|
||||
Decimal avg_pnl = g_stats.total_pnl / Decimal(std::to_string(g_stats.total_executions));
|
||||
std::cout << " Avg PnL: $" << avg_pnl.to_string() << std::endl;
|
||||
}
|
||||
std::cout << std::string(40, '-') << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void print_final_stats() {
|
||||
std::lock_guard<std::mutex> lock(g_stats_mutex);
|
||||
std::cout << std::endl;
|
||||
std::cout << std::string(50, '=') << std::endl;
|
||||
std::cout << "FINAL STATISTICS" << std::endl;
|
||||
std::cout << std::string(50, '=') << std::endl;
|
||||
std::cout << "Total Opportunities: " << g_stats.total_opportunities << std::endl;
|
||||
std::cout << "Total Executions: " << g_stats.total_executions << std::endl;
|
||||
std::cout << "Total PnL: $" << g_stats.total_pnl.to_string() << std::endl;
|
||||
if (g_stats.total_executions > 0 && g_stats.total_opportunities > 0) {
|
||||
double win_rate = (static_cast<double>(g_stats.total_executions) /
|
||||
static_cast<double>(g_stats.total_opportunities)) * 100.0;
|
||||
Decimal avg_pnl = g_stats.total_pnl / Decimal(std::to_string(g_stats.total_executions));
|
||||
std::cout << "Execution Rate: " << win_rate << "%" << std::endl;
|
||||
std::cout << "Avg PnL per Trade: $" << avg_pnl.to_string() << std::endl;
|
||||
}
|
||||
std::cout << std::string(50, '=') << std::endl;
|
||||
}
|
||||
|
||||
static std::string get_env(const std::string& key, const std::string& default_value) {
|
||||
const char* value = std::getenv(key.c_str());
|
||||
return value ? value : default_value;
|
||||
}
|
||||
|
||||
std::shared_ptr<LxDex> dex_;
|
||||
std::shared_ptr<LxFirstArbitrage> lx_first_;
|
||||
std::shared_ptr<Scanner> scanner_;
|
||||
std::shared_ptr<CrossChainRouter> router_;
|
||||
std::thread price_feed_thread_;
|
||||
std::thread stats_thread_;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Main Entry Point
|
||||
// ============================================
|
||||
|
||||
ArbitrageBot* g_bot = nullptr;
|
||||
|
||||
void signal_handler(int signal) {
|
||||
if (g_bot) {
|
||||
g_bot->stop();
|
||||
}
|
||||
std::exit(0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Set up signal handlers
|
||||
std::signal(SIGINT, signal_handler);
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
|
||||
ArbitrageBot bot;
|
||||
g_bot = ⊥
|
||||
|
||||
if (!bot.start()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Keep running until signal
|
||||
while (g_running) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// LX Trading SDK - Basic Example
|
||||
// Demonstrates client setup, market data, and order placement
|
||||
|
||||
#include <lx/trading/client.hpp>
|
||||
#include <lx/trading/config.hpp>
|
||||
#include <iostream>
|
||||
|
||||
using namespace lx::trading;
|
||||
|
||||
int main() {
|
||||
// Build configuration
|
||||
Config config;
|
||||
config.general.smart_routing = true;
|
||||
config.general.timeout_ms = 30000;
|
||||
|
||||
// Add LX DEX
|
||||
config.with_native("lx_dex",
|
||||
NativeVenueConfig::lx_dex("https://api.lx.exchange")
|
||||
.with_credentials("your-api-key", "your-api-secret"));
|
||||
|
||||
// Add LX AMM
|
||||
config.with_native("lx_amm",
|
||||
NativeVenueConfig::lx_amm("https://amm.lx.exchange"));
|
||||
|
||||
// Risk management
|
||||
config.set_max_order_size(Decimal::from_double(10.0))
|
||||
.set_max_position_size(Decimal::from_double(100.0))
|
||||
.set_max_daily_loss(Decimal::from_double(1000.0));
|
||||
|
||||
// Create client
|
||||
Client client(config);
|
||||
|
||||
try {
|
||||
// Connect to all venues
|
||||
std::cout << "Connecting to venues...\n";
|
||||
client.connect().get();
|
||||
|
||||
// List connected venues
|
||||
for (const auto& venue : client.venues()) {
|
||||
std::cout << "Connected: " << venue.name
|
||||
<< " (latency: " << venue.latency_ms.value_or(0) << "ms)\n";
|
||||
}
|
||||
|
||||
// Get aggregated orderbook
|
||||
std::cout << "\nFetching BTC-USDC orderbook...\n";
|
||||
auto book = client.aggregated_orderbook("BTC-USDC").get();
|
||||
|
||||
if (auto best_bid = book.best_bid()) {
|
||||
auto [price, venue, qty] = *best_bid;
|
||||
std::cout << "Best bid: " << price.to_string()
|
||||
<< " @ " << venue << "\n";
|
||||
}
|
||||
|
||||
if (auto best_ask = book.best_ask()) {
|
||||
auto [price, venue, qty] = *best_ask;
|
||||
std::cout << "Best ask: " << price.to_string()
|
||||
<< " @ " << venue << "\n";
|
||||
}
|
||||
|
||||
// Get ticker
|
||||
std::cout << "\nFetching ticker...\n";
|
||||
auto ticker = client.ticker("BTC-USDC").get();
|
||||
std::cout << "Mid price: " << ticker.mid_price().value_or(Decimal::zero()).to_string() << "\n";
|
||||
std::cout << "Spread: " << ticker.spread().value_or(Decimal::zero()).to_string() << "\n";
|
||||
|
||||
// Get balances
|
||||
std::cout << "\nFetching balances...\n";
|
||||
auto balances = client.balances().get();
|
||||
for (const auto& bal : balances) {
|
||||
std::cout << bal.asset << ": " << bal.total().to_string() << "\n";
|
||||
}
|
||||
|
||||
// Place a limit order
|
||||
std::cout << "\nPlacing limit buy order...\n";
|
||||
auto order = client.limit_buy(
|
||||
"BTC-USDC",
|
||||
Decimal::from_double(0.1),
|
||||
Decimal::from_double(40000.0)).get();
|
||||
|
||||
std::cout << "Order placed: " << order.order_id << "\n";
|
||||
std::cout << "Status: " << to_string(order.status) << "\n";
|
||||
|
||||
// Cancel the order
|
||||
std::cout << "\nCancelling order...\n";
|
||||
auto cancelled = client.cancel_order(order.order_id, order.symbol, order.venue).get();
|
||||
std::cout << "Cancelled: " << to_string(cancelled.status) << "\n";
|
||||
|
||||
// Disconnect
|
||||
client.disconnect().get();
|
||||
std::cout << "\nDisconnected.\n";
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
// LX Trading SDK - Orderbook Demo
|
||||
// Demonstrates lock-free orderbook and aggregation
|
||||
|
||||
#include <lx/trading/orderbook.hpp>
|
||||
#include <lx/trading/math.hpp>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace lx::trading;
|
||||
using namespace lx::trading::math;
|
||||
|
||||
void print_orderbook(const Orderbook& book) {
|
||||
std::cout << "\n=== " << book.symbol() << " @ " << book.venue() << " ===\n";
|
||||
std::cout << "Timestamp: " << book.timestamp() << "\n";
|
||||
|
||||
auto bids = book.bids();
|
||||
auto asks = book.asks();
|
||||
|
||||
std::cout << "\nAsks:\n";
|
||||
for (auto it = asks.rbegin(); it != asks.rend(); ++it) {
|
||||
std::cout << " " << it->price.to_string() << " x " << it->quantity.to_string() << "\n";
|
||||
}
|
||||
|
||||
std::cout << "---\n";
|
||||
|
||||
std::cout << "Bids:\n";
|
||||
for (const auto& bid : bids) {
|
||||
std::cout << " " << bid.price.to_string() << " x " << bid.quantity.to_string() << "\n";
|
||||
}
|
||||
|
||||
if (auto mid = book.mid_price()) {
|
||||
std::cout << "\nMid: " << mid->to_string() << "\n";
|
||||
}
|
||||
if (auto spread = book.spread_percent()) {
|
||||
std::cout << "Spread: " << spread->to_string() << "%\n";
|
||||
}
|
||||
}
|
||||
|
||||
void concurrent_update_demo() {
|
||||
std::cout << "\n=== Concurrent Update Demo ===\n";
|
||||
|
||||
Orderbook book("BTC-USDC", "test");
|
||||
|
||||
// Multiple threads updating
|
||||
std::vector<std::thread> writers;
|
||||
|
||||
for (int t = 0; t < 4; ++t) {
|
||||
writers.emplace_back([&book, t]() {
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
double price = 100.0 + t * 10 + (i % 10);
|
||||
double qty = 1.0 + (i % 5) * 0.1;
|
||||
|
||||
if (t % 2 == 0) {
|
||||
book.add_bid(Decimal::from_double(price), Decimal::from_double(qty));
|
||||
} else {
|
||||
book.add_ask(Decimal::from_double(price + 50), Decimal::from_double(qty));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reader thread
|
||||
std::thread reader([&book]() {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
auto bids = book.bids();
|
||||
auto asks = book.asks();
|
||||
std::cout << "Read: " << bids.size() << " bids, " << asks.size() << " asks\n";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
});
|
||||
|
||||
for (auto& w : writers) w.join();
|
||||
reader.join();
|
||||
|
||||
book.sort();
|
||||
std::cout << "Final: " << book.bids().size() << " bids, " << book.asks().size() << " asks\n";
|
||||
}
|
||||
|
||||
void aggregation_demo() {
|
||||
std::cout << "\n=== Aggregation Demo ===\n";
|
||||
|
||||
// Simulate orderbooks from multiple venues
|
||||
Orderbook binance("BTC-USDC", "binance");
|
||||
binance.add_bid(Decimal::from_double(50000.0), Decimal::from_double(1.5));
|
||||
binance.add_bid(Decimal::from_double(49990.0), Decimal::from_double(2.0));
|
||||
binance.add_ask(Decimal::from_double(50010.0), Decimal::from_double(1.0));
|
||||
binance.add_ask(Decimal::from_double(50020.0), Decimal::from_double(1.5));
|
||||
binance.sort();
|
||||
|
||||
Orderbook lx_dex("BTC-USDC", "lx_dex");
|
||||
lx_dex.add_bid(Decimal::from_double(50005.0), Decimal::from_double(0.8)); // Best bid!
|
||||
lx_dex.add_bid(Decimal::from_double(49995.0), Decimal::from_double(1.2));
|
||||
lx_dex.add_ask(Decimal::from_double(50008.0), Decimal::from_double(0.5)); // Best ask!
|
||||
lx_dex.add_ask(Decimal::from_double(50015.0), Decimal::from_double(1.0));
|
||||
lx_dex.sort();
|
||||
|
||||
print_orderbook(binance);
|
||||
print_orderbook(lx_dex);
|
||||
|
||||
// Aggregate
|
||||
AggregatedOrderbook agg("BTC-USDC");
|
||||
agg.add_orderbook(binance);
|
||||
agg.add_orderbook(lx_dex);
|
||||
|
||||
std::cout << "\n=== Aggregated ===\n";
|
||||
|
||||
if (auto best = agg.best_bid()) {
|
||||
auto [price, venue, qty] = *best;
|
||||
std::cout << "Best bid: " << price.to_string() << " @ " << venue << "\n";
|
||||
}
|
||||
|
||||
if (auto best = agg.best_ask()) {
|
||||
auto [price, venue, qty] = *best;
|
||||
std::cout << "Best ask: " << price.to_string() << " @ " << venue << "\n";
|
||||
}
|
||||
|
||||
// Best venue for buying 1 BTC
|
||||
if (auto best = agg.best_venue_buy(Decimal::from_double(1.0))) {
|
||||
auto [venue, price] = *best;
|
||||
std::cout << "Buy 1 BTC best at: " << venue << " @ " << price.to_string() << "\n";
|
||||
}
|
||||
|
||||
// Best venue for selling 1 BTC
|
||||
if (auto best = agg.best_venue_sell(Decimal::from_double(1.0))) {
|
||||
auto [venue, price] = *best;
|
||||
std::cout << "Sell 1 BTC best at: " << venue << " @ " << price.to_string() << "\n";
|
||||
}
|
||||
|
||||
std::cout << "\nAggregated asks:\n";
|
||||
for (const auto& level : agg.aggregated_asks()) {
|
||||
std::cout << " " << level.price.to_string() << " x " << level.quantity.to_string() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void vwap_demo() {
|
||||
std::cout << "\n=== VWAP Demo ===\n";
|
||||
|
||||
Orderbook book("ETH-USDC", "test");
|
||||
|
||||
// Create realistic orderbook
|
||||
book.add_ask(Decimal::from_double(2000.0), Decimal::from_double(5.0));
|
||||
book.add_ask(Decimal::from_double(2001.0), Decimal::from_double(10.0));
|
||||
book.add_ask(Decimal::from_double(2002.0), Decimal::from_double(15.0));
|
||||
book.add_ask(Decimal::from_double(2005.0), Decimal::from_double(20.0));
|
||||
book.add_ask(Decimal::from_double(2010.0), Decimal::from_double(50.0));
|
||||
book.sort();
|
||||
|
||||
// Calculate VWAP for different sizes
|
||||
std::cout << "VWAP for buying:\n";
|
||||
for (double size : {1.0, 5.0, 10.0, 25.0, 50.0}) {
|
||||
auto vwap = book.vwap_buy(Decimal::from_double(size));
|
||||
if (vwap) {
|
||||
std::cout << " " << size << " ETH: " << vwap->to_string() << "\n";
|
||||
} else {
|
||||
std::cout << " " << size << " ETH: insufficient liquidity\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void amm_math_demo() {
|
||||
std::cout << "\n=== AMM Math Demo ===\n";
|
||||
|
||||
// Constant product (Uniswap V2)
|
||||
double reserve_eth = 1000.0;
|
||||
double reserve_usdc = 2000000.0; // Price = 2000
|
||||
|
||||
std::cout << "Constant Product Pool:\n";
|
||||
std::cout << "ETH reserve: " << reserve_eth << "\n";
|
||||
std::cout << "USDC reserve: " << reserve_usdc << "\n";
|
||||
std::cout << "Implied price: " << (reserve_usdc / reserve_eth) << "\n\n";
|
||||
|
||||
for (double amt : {1.0, 10.0, 50.0, 100.0}) {
|
||||
auto [out, price] = constant_product_price(reserve_eth, reserve_usdc, amt, 0.003, true);
|
||||
double slippage = (2000.0 - price * 1000) / 2000.0 * 100;
|
||||
std::cout << "Buy " << amt << " ETH -> " << out << " USDC"
|
||||
<< " (price: " << (out / amt) << ", slippage: " << slippage << "%)\n";
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "LX Trading SDK - Orderbook Demo\n";
|
||||
std::cout << "================================\n";
|
||||
|
||||
concurrent_update_demo();
|
||||
aggregation_demo();
|
||||
vwap_demo();
|
||||
amm_math_demo();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
// LX Trading SDK - Venue Adapter Interface
|
||||
// Abstract interface for all trading venues
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Forward declarations
|
||||
class Orderbook;
|
||||
|
||||
// Venue capabilities
|
||||
struct VenueCapabilities {
|
||||
bool limit_orders = false;
|
||||
bool market_orders = false;
|
||||
bool stop_orders = false;
|
||||
bool post_only = false;
|
||||
bool cancel_orders = false;
|
||||
bool batch_orders = false;
|
||||
bool streaming = false;
|
||||
bool orderbook = false;
|
||||
bool trades = false;
|
||||
bool amm_swap = false;
|
||||
bool add_liquidity = false;
|
||||
bool remove_liquidity = false;
|
||||
bool lp_positions = false;
|
||||
int max_batch_size = 1;
|
||||
std::set<std::string> supported_pairs;
|
||||
|
||||
static VenueCapabilities clob() {
|
||||
VenueCapabilities caps;
|
||||
caps.limit_orders = true;
|
||||
caps.market_orders = true;
|
||||
caps.stop_orders = true;
|
||||
caps.post_only = true;
|
||||
caps.cancel_orders = true;
|
||||
caps.batch_orders = true;
|
||||
caps.streaming = true;
|
||||
caps.orderbook = true;
|
||||
caps.trades = true;
|
||||
caps.max_batch_size = 10;
|
||||
return caps;
|
||||
}
|
||||
|
||||
static VenueCapabilities amm() {
|
||||
VenueCapabilities caps;
|
||||
caps.market_orders = true;
|
||||
caps.streaming = true;
|
||||
caps.trades = true;
|
||||
caps.amm_swap = true;
|
||||
caps.add_liquidity = true;
|
||||
caps.remove_liquidity = true;
|
||||
caps.lp_positions = true;
|
||||
return caps;
|
||||
}
|
||||
};
|
||||
|
||||
// Adapter error
|
||||
class AdapterError : public std::runtime_error {
|
||||
public:
|
||||
explicit AdapterError(const std::string& msg) : std::runtime_error(msg) {}
|
||||
};
|
||||
|
||||
// Base adapter interface
|
||||
class VenueAdapter {
|
||||
public:
|
||||
virtual ~VenueAdapter() = default;
|
||||
|
||||
// Properties
|
||||
[[nodiscard]] virtual std::string_view name() const = 0;
|
||||
[[nodiscard]] virtual VenueType venue_type() const = 0;
|
||||
[[nodiscard]] virtual const VenueCapabilities& capabilities() const = 0;
|
||||
[[nodiscard]] virtual bool is_connected() const = 0;
|
||||
[[nodiscard]] virtual std::optional<int> latency_ms() const { return std::nullopt; }
|
||||
|
||||
// Venue info
|
||||
[[nodiscard]] VenueInfo info() const {
|
||||
VenueInfo vi;
|
||||
vi.name = std::string(name());
|
||||
vi.venue_type = venue_type();
|
||||
vi.connected = is_connected();
|
||||
vi.latency_ms = latency_ms();
|
||||
const auto& caps = capabilities();
|
||||
vi.supported_pairs = std::vector<std::string>(caps.supported_pairs.begin(),
|
||||
caps.supported_pairs.end());
|
||||
vi.maker_fee = Decimal::from_double(0.001);
|
||||
vi.taker_fee = Decimal::from_double(0.002);
|
||||
return vi;
|
||||
}
|
||||
|
||||
// Connection
|
||||
virtual std::future<void> connect() = 0;
|
||||
virtual std::future<void> disconnect() = 0;
|
||||
|
||||
// Market data
|
||||
virtual std::future<std::vector<MarketInfo>> get_markets() = 0;
|
||||
virtual std::future<Ticker> get_ticker(const std::string& symbol) = 0;
|
||||
virtual std::future<std::unique_ptr<Orderbook>> get_orderbook(
|
||||
const std::string& symbol,
|
||||
std::optional<int> depth = std::nullopt) = 0;
|
||||
virtual std::future<std::vector<Trade>> get_trades(
|
||||
const std::string& symbol,
|
||||
std::optional<int> limit = std::nullopt) = 0;
|
||||
|
||||
// Account
|
||||
virtual std::future<std::vector<Balance>> get_balances() = 0;
|
||||
virtual std::future<Balance> get_balance(const std::string& asset) = 0;
|
||||
virtual std::future<std::vector<Order>> get_open_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) = 0;
|
||||
|
||||
// Orders
|
||||
virtual std::future<Order> place_order(const OrderRequest& request) = 0;
|
||||
virtual std::future<Order> cancel_order(const std::string& order_id,
|
||||
const std::string& symbol) = 0;
|
||||
virtual std::future<std::vector<Order>> cancel_all_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) = 0;
|
||||
|
||||
// AMM operations (optional - throw AdapterError if not supported)
|
||||
virtual std::future<SwapQuote> get_swap_quote(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy) {
|
||||
return std::async(std::launch::deferred, []() -> SwapQuote {
|
||||
throw AdapterError("AMM swap not supported");
|
||||
});
|
||||
}
|
||||
|
||||
virtual std::future<Trade> execute_swap(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::deferred, []() -> Trade {
|
||||
throw AdapterError("AMM swap not supported");
|
||||
});
|
||||
}
|
||||
|
||||
virtual std::future<PoolInfo> get_pool_info(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token) {
|
||||
return std::async(std::launch::deferred, []() -> PoolInfo {
|
||||
throw AdapterError("Pool info not supported");
|
||||
});
|
||||
}
|
||||
|
||||
virtual std::future<LiquidityResult> add_liquidity(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal base_amount,
|
||||
Decimal quote_amount,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::deferred, []() -> LiquidityResult {
|
||||
throw AdapterError("Add liquidity not supported");
|
||||
});
|
||||
}
|
||||
|
||||
virtual std::future<LiquidityResult> remove_liquidity(
|
||||
const std::string& pool_address,
|
||||
Decimal liquidity_amount,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::deferred, []() -> LiquidityResult {
|
||||
throw AdapterError("Remove liquidity not supported");
|
||||
});
|
||||
}
|
||||
|
||||
virtual std::future<std::vector<LpPosition>> get_lp_positions() {
|
||||
return std::async(std::launch::deferred, []() -> std::vector<LpPosition> {
|
||||
throw AdapterError("LP positions not supported");
|
||||
});
|
||||
}
|
||||
|
||||
// Streaming callbacks
|
||||
using TickerCallback = std::function<void(const Ticker&)>;
|
||||
using TradeCallback = std::function<void(const Trade&)>;
|
||||
using OrderbookCallback = std::function<void(const Orderbook&)>;
|
||||
using OrderCallback = std::function<void(const Order&)>;
|
||||
|
||||
virtual void subscribe_ticker(const std::string& symbol, TickerCallback cb) {
|
||||
(void)symbol; (void)cb; // Default: no streaming
|
||||
}
|
||||
virtual void subscribe_trades(const std::string& symbol, TradeCallback cb) {
|
||||
(void)symbol; (void)cb;
|
||||
}
|
||||
virtual void subscribe_orderbook(const std::string& symbol, OrderbookCallback cb) {
|
||||
(void)symbol; (void)cb;
|
||||
}
|
||||
virtual void subscribe_orders(OrderCallback cb) {
|
||||
(void)cb;
|
||||
}
|
||||
virtual void unsubscribe_all() {}
|
||||
};
|
||||
|
||||
// Adapter factory type
|
||||
using AdapterFactory = std::function<std::unique_ptr<VenueAdapter>()>;
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,69 +0,0 @@
|
||||
// LX Trading SDK - CCXT Adapter
|
||||
// REST proxy to CCXT service for 100+ exchanges
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/adapter.hpp>
|
||||
#include <lx/trading/config.hpp>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// CCXT Adapter - connects to a CCXT REST service
|
||||
// This allows C++ to leverage CCXT's exchange coverage via a REST API
|
||||
class CcxtAdapter : public VenueAdapter {
|
||||
public:
|
||||
CcxtAdapter(std::string_view name, const CcxtConfig& config);
|
||||
~CcxtAdapter() override;
|
||||
|
||||
// VenueAdapter interface
|
||||
[[nodiscard]] std::string_view name() const override { return name_; }
|
||||
[[nodiscard]] VenueType venue_type() const override { return VenueType::Ccxt; }
|
||||
[[nodiscard]] const VenueCapabilities& capabilities() const override { return capabilities_; }
|
||||
[[nodiscard]] bool is_connected() const override { return connected_.load(); }
|
||||
[[nodiscard]] std::optional<int> latency_ms() const override {
|
||||
int lat = latency_.load();
|
||||
return lat > 0 ? std::optional<int>(lat) : std::nullopt;
|
||||
}
|
||||
|
||||
std::future<void> connect() override;
|
||||
std::future<void> disconnect() override;
|
||||
|
||||
std::future<std::vector<MarketInfo>> get_markets() override;
|
||||
std::future<Ticker> get_ticker(const std::string& symbol) override;
|
||||
std::future<std::unique_ptr<Orderbook>> get_orderbook(
|
||||
const std::string& symbol,
|
||||
std::optional<int> depth = std::nullopt) override;
|
||||
std::future<std::vector<Trade>> get_trades(
|
||||
const std::string& symbol,
|
||||
std::optional<int> limit = std::nullopt) override;
|
||||
|
||||
std::future<std::vector<Balance>> get_balances() override;
|
||||
std::future<Balance> get_balance(const std::string& asset) override;
|
||||
std::future<std::vector<Order>> get_open_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) override;
|
||||
|
||||
std::future<Order> place_order(const OrderRequest& request) override;
|
||||
std::future<Order> cancel_order(const std::string& order_id,
|
||||
const std::string& symbol) override;
|
||||
std::future<std::vector<Order>> cancel_all_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) override;
|
||||
|
||||
// Set the CCXT service URL (default: http://localhost:3000)
|
||||
void set_service_url(std::string_view url);
|
||||
|
||||
private:
|
||||
Order convert_order(const nlohmann::json& json);
|
||||
void update_latency(int64_t start_ns);
|
||||
|
||||
std::string name_;
|
||||
CcxtConfig config_;
|
||||
std::string service_url_ = "http://localhost:3000";
|
||||
VenueCapabilities capabilities_;
|
||||
std::atomic<bool> connected_{false};
|
||||
std::atomic<int> latency_{0};
|
||||
};
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,97 +0,0 @@
|
||||
// LX Trading SDK - Hummingbot Gateway Adapter
|
||||
// Connect to Hummingbot Gateway for DEX trading
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/adapter.hpp>
|
||||
#include <lx/trading/config.hpp>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Hummingbot Gateway Adapter
|
||||
// Connects to Hummingbot Gateway REST API for DEX operations
|
||||
class HummingbotAdapter : public VenueAdapter {
|
||||
public:
|
||||
HummingbotAdapter(std::string_view name, const HummingbotConfig& config);
|
||||
~HummingbotAdapter() override;
|
||||
|
||||
// VenueAdapter interface
|
||||
[[nodiscard]] std::string_view name() const override { return name_; }
|
||||
[[nodiscard]] VenueType venue_type() const override { return VenueType::Hummingbot; }
|
||||
[[nodiscard]] const VenueCapabilities& capabilities() const override { return capabilities_; }
|
||||
[[nodiscard]] bool is_connected() const override { return connected_.load(); }
|
||||
[[nodiscard]] std::optional<int> latency_ms() const override {
|
||||
int lat = latency_.load();
|
||||
return lat > 0 ? std::optional<int>(lat) : std::nullopt;
|
||||
}
|
||||
|
||||
std::future<void> connect() override;
|
||||
std::future<void> disconnect() override;
|
||||
|
||||
std::future<std::vector<MarketInfo>> get_markets() override;
|
||||
std::future<Ticker> get_ticker(const std::string& symbol) override;
|
||||
std::future<std::unique_ptr<Orderbook>> get_orderbook(
|
||||
const std::string& symbol,
|
||||
std::optional<int> depth = std::nullopt) override;
|
||||
std::future<std::vector<Trade>> get_trades(
|
||||
const std::string& symbol,
|
||||
std::optional<int> limit = std::nullopt) override;
|
||||
|
||||
std::future<std::vector<Balance>> get_balances() override;
|
||||
std::future<Balance> get_balance(const std::string& asset) override;
|
||||
std::future<std::vector<Order>> get_open_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) override;
|
||||
|
||||
std::future<Order> place_order(const OrderRequest& request) override;
|
||||
std::future<Order> cancel_order(const std::string& order_id,
|
||||
const std::string& symbol) override;
|
||||
std::future<std::vector<Order>> cancel_all_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) override;
|
||||
|
||||
// AMM operations
|
||||
std::future<SwapQuote> get_swap_quote(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy) override;
|
||||
|
||||
std::future<Trade> execute_swap(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy,
|
||||
Decimal slippage) override;
|
||||
|
||||
std::future<PoolInfo> get_pool_info(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token) override;
|
||||
|
||||
std::future<LiquidityResult> add_liquidity(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal base_amount,
|
||||
Decimal quote_amount,
|
||||
Decimal slippage) override;
|
||||
|
||||
std::future<LiquidityResult> remove_liquidity(
|
||||
const std::string& pool_address,
|
||||
Decimal liquidity_amount,
|
||||
Decimal slippage) override;
|
||||
|
||||
std::future<std::vector<LpPosition>> get_lp_positions() override;
|
||||
|
||||
private:
|
||||
nlohmann::json build_request_body();
|
||||
void update_latency(int64_t start_ns);
|
||||
|
||||
std::string name_;
|
||||
HummingbotConfig config_;
|
||||
VenueCapabilities capabilities_;
|
||||
std::atomic<bool> connected_{false};
|
||||
std::atomic<int> latency_{0};
|
||||
};
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,153 +0,0 @@
|
||||
// LX Trading SDK - Native Adapters
|
||||
// LX DEX (CLOB) and LX AMM adapters
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/adapter.hpp>
|
||||
#include <lx/trading/config.hpp>
|
||||
#include <lx/trading/orderbook.hpp>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// HTTP client abstraction (implemented with cpr)
|
||||
class HttpClient;
|
||||
|
||||
// LX DEX Adapter - Central Limit Order Book
|
||||
class LxDexAdapter : public VenueAdapter {
|
||||
public:
|
||||
LxDexAdapter(std::string_view name, const NativeVenueConfig& config);
|
||||
~LxDexAdapter() override;
|
||||
|
||||
// VenueAdapter interface
|
||||
[[nodiscard]] std::string_view name() const override { return name_; }
|
||||
[[nodiscard]] VenueType venue_type() const override { return VenueType::Native; }
|
||||
[[nodiscard]] const VenueCapabilities& capabilities() const override { return capabilities_; }
|
||||
[[nodiscard]] bool is_connected() const override { return connected_.load(); }
|
||||
[[nodiscard]] std::optional<int> latency_ms() const override {
|
||||
int lat = latency_.load();
|
||||
return lat > 0 ? std::optional<int>(lat) : std::nullopt;
|
||||
}
|
||||
|
||||
std::future<void> connect() override;
|
||||
std::future<void> disconnect() override;
|
||||
|
||||
std::future<std::vector<MarketInfo>> get_markets() override;
|
||||
std::future<Ticker> get_ticker(const std::string& symbol) override;
|
||||
std::future<std::unique_ptr<Orderbook>> get_orderbook(
|
||||
const std::string& symbol,
|
||||
std::optional<int> depth = std::nullopt) override;
|
||||
std::future<std::vector<Trade>> get_trades(
|
||||
const std::string& symbol,
|
||||
std::optional<int> limit = std::nullopt) override;
|
||||
|
||||
std::future<std::vector<Balance>> get_balances() override;
|
||||
std::future<Balance> get_balance(const std::string& asset) override;
|
||||
std::future<std::vector<Order>> get_open_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) override;
|
||||
|
||||
std::future<Order> place_order(const OrderRequest& request) override;
|
||||
std::future<Order> cancel_order(const std::string& order_id,
|
||||
const std::string& symbol) override;
|
||||
std::future<std::vector<Order>> cancel_all_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) override;
|
||||
|
||||
private:
|
||||
Order convert_order(const nlohmann::json& json);
|
||||
void update_latency(int64_t start_ns);
|
||||
|
||||
std::string name_;
|
||||
NativeVenueConfig config_;
|
||||
VenueCapabilities capabilities_;
|
||||
std::unique_ptr<HttpClient> http_;
|
||||
std::atomic<bool> connected_{false};
|
||||
std::atomic<int> latency_{0};
|
||||
};
|
||||
|
||||
// LX AMM Adapter - Automated Market Maker
|
||||
class LxAmmAdapter : public VenueAdapter {
|
||||
public:
|
||||
LxAmmAdapter(std::string_view name, const NativeVenueConfig& config);
|
||||
~LxAmmAdapter() override;
|
||||
|
||||
// VenueAdapter interface
|
||||
[[nodiscard]] std::string_view name() const override { return name_; }
|
||||
[[nodiscard]] VenueType venue_type() const override { return VenueType::Native; }
|
||||
[[nodiscard]] const VenueCapabilities& capabilities() const override { return capabilities_; }
|
||||
[[nodiscard]] bool is_connected() const override { return connected_.load(); }
|
||||
[[nodiscard]] std::optional<int> latency_ms() const override {
|
||||
int lat = latency_.load();
|
||||
return lat > 0 ? std::optional<int>(lat) : std::nullopt;
|
||||
}
|
||||
|
||||
std::future<void> connect() override;
|
||||
std::future<void> disconnect() override;
|
||||
|
||||
std::future<std::vector<MarketInfo>> get_markets() override;
|
||||
std::future<Ticker> get_ticker(const std::string& symbol) override;
|
||||
std::future<std::unique_ptr<Orderbook>> get_orderbook(
|
||||
const std::string& symbol,
|
||||
std::optional<int> depth = std::nullopt) override;
|
||||
std::future<std::vector<Trade>> get_trades(
|
||||
const std::string& symbol,
|
||||
std::optional<int> limit = std::nullopt) override;
|
||||
|
||||
std::future<std::vector<Balance>> get_balances() override;
|
||||
std::future<Balance> get_balance(const std::string& asset) override;
|
||||
std::future<std::vector<Order>> get_open_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) override;
|
||||
|
||||
std::future<Order> place_order(const OrderRequest& request) override;
|
||||
std::future<Order> cancel_order(const std::string& order_id,
|
||||
const std::string& symbol) override;
|
||||
std::future<std::vector<Order>> cancel_all_orders(
|
||||
const std::optional<std::string>& symbol = std::nullopt) override;
|
||||
|
||||
// AMM-specific operations
|
||||
std::future<SwapQuote> get_swap_quote(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy) override;
|
||||
|
||||
std::future<Trade> execute_swap(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy,
|
||||
Decimal slippage) override;
|
||||
|
||||
std::future<PoolInfo> get_pool_info(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token) override;
|
||||
|
||||
std::future<LiquidityResult> add_liquidity(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal base_amount,
|
||||
Decimal quote_amount,
|
||||
Decimal slippage) override;
|
||||
|
||||
std::future<LiquidityResult> remove_liquidity(
|
||||
const std::string& pool_address,
|
||||
Decimal liquidity_amount,
|
||||
Decimal slippage) override;
|
||||
|
||||
std::future<std::vector<LpPosition>> get_lp_positions() override;
|
||||
|
||||
private:
|
||||
void update_latency(int64_t start_ns);
|
||||
|
||||
std::string name_;
|
||||
NativeVenueConfig config_;
|
||||
VenueCapabilities capabilities_;
|
||||
std::unique_ptr<HttpClient> http_;
|
||||
std::atomic<bool> connected_{false};
|
||||
std::atomic<int> latency_{0};
|
||||
};
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,24 +0,0 @@
|
||||
// LX Trading SDK - Arbitrage Module
|
||||
//
|
||||
// LX-First Arbitrage Strategy:
|
||||
// - LX DEX is the FASTEST venue (nanosecond price updates, 200ms blocks)
|
||||
// - By the time other venues update, LX has already moved
|
||||
// - LX DEX price is the "TRUTH" (most current)
|
||||
// - Other venues are always STALE by comparison
|
||||
// - Arbitrage = correcting stale venues to match LX
|
||||
//
|
||||
// Key Concepts:
|
||||
// 1. LX as Oracle: LX DEX provides the reference price
|
||||
// 2. Stale Venues: CEX and external DEX are behind by 50ms-12s
|
||||
// 3. Front-running: Trade on stale venues before they catch up
|
||||
// 4. Cross-chain: Warp for Lux subnets, Teleport for EVM
|
||||
//
|
||||
// NO SMART CONTRACTS - All arbitrage is executed through native RPC and the unified SDK.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/arbitrage/types.hpp>
|
||||
#include <lx/trading/arbitrage/scanner.hpp>
|
||||
#include <lx/trading/arbitrage/lx_first.hpp>
|
||||
#include <lx/trading/arbitrage/unified.hpp>
|
||||
#include <lx/trading/arbitrage/cross_chain.hpp>
|
||||
@@ -1,121 +0,0 @@
|
||||
// LX Trading SDK - Cross-Chain Arbitrage Transports
|
||||
//
|
||||
// 1. WARP (Lux Native)
|
||||
// - Only works WITHIN Lux ecosystem (between subnets)
|
||||
// - Sub-second message delivery (<500ms)
|
||||
// - Use for: LX DEX <-> LX AMM <-> Other Lux subnets
|
||||
// - Cannot reach external chains
|
||||
//
|
||||
// 2. TELEPORT (EVM Bridge)
|
||||
// - Works with ANY EVM-compatible chain
|
||||
// - Lux <-> Ethereum, BSC, Arbitrum, Polygon, etc.
|
||||
// - ~30 second finality (depends on source chain)
|
||||
// - Uses validator attestations
|
||||
//
|
||||
// 3. CEX API
|
||||
// - No bridging needed - just API calls
|
||||
// - Sub-second execution
|
||||
// - Settlement via withdraw/deposit (slow but doesn't block arb)
|
||||
//
|
||||
// 4. FOR OMNICHAIN ARBITRAGE:
|
||||
// - Lux internal: Warp (instant)
|
||||
// - External EVM: Teleport (~30s)
|
||||
// - CEX: Direct API (instant trade, later settle)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/arbitrage/types.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
/// Warp client interface for Lux-native messaging
|
||||
class WarpClient {
|
||||
public:
|
||||
virtual ~WarpClient() = default;
|
||||
|
||||
/// Send a Warp message to another Lux subnet
|
||||
virtual std::string send_message(const std::string& dest_subnet,
|
||||
const std::vector<uint8_t>& payload) = 0;
|
||||
|
||||
/// Receive a Warp message
|
||||
virtual std::vector<uint8_t> receive_message(const std::string& message_id) = 0;
|
||||
|
||||
/// Get this subnet's ID
|
||||
virtual std::string get_blockchain_id() const = 0;
|
||||
};
|
||||
|
||||
/// Teleport client interface for EVM bridging
|
||||
class TeleportClient {
|
||||
public:
|
||||
virtual ~TeleportClient() = default;
|
||||
|
||||
/// Bridge assets to another EVM chain
|
||||
virtual std::string bridge(const std::string& dest_chain,
|
||||
const std::string& token,
|
||||
Decimal amount) = 0;
|
||||
|
||||
/// Get bridge transaction status
|
||||
virtual BridgeStatus get_bridge_status(const std::string& tx_id) = 0;
|
||||
|
||||
/// Estimate bridge fee
|
||||
virtual Decimal estimate_bridge_fee(const std::string& dest_chain,
|
||||
const std::string& token,
|
||||
Decimal amount) = 0;
|
||||
};
|
||||
|
||||
/// Cross-chain router for determining optimal transport
|
||||
class CrossChainRouter {
|
||||
public:
|
||||
explicit CrossChainRouter(CrossChainConfig config);
|
||||
~CrossChainRouter() = default;
|
||||
|
||||
// Non-copyable
|
||||
CrossChainRouter(const CrossChainRouter&) = delete;
|
||||
CrossChainRouter& operator=(const CrossChainRouter&) = delete;
|
||||
|
||||
/// Set the Warp client
|
||||
void set_warp_client(std::shared_ptr<WarpClient> client);
|
||||
|
||||
/// Set the Teleport client
|
||||
void set_teleport_client(std::shared_ptr<TeleportClient> client);
|
||||
|
||||
/// Get the Warp client
|
||||
[[nodiscard]] std::shared_ptr<WarpClient> warp() const;
|
||||
|
||||
/// Get the Teleport client
|
||||
[[nodiscard]] std::shared_ptr<TeleportClient> teleport() const;
|
||||
|
||||
/// Determine the best transport between two chains
|
||||
[[nodiscard]] CrossChainTransport determine_transport(const std::string& source_chain,
|
||||
const std::string& dest_chain) const;
|
||||
|
||||
/// Estimate latency for cross-chain message (ms)
|
||||
[[nodiscard]] int64_t estimate_latency(const std::string& source_chain,
|
||||
const std::string& dest_chain) const;
|
||||
|
||||
/// Estimate cost for cross-chain transfer
|
||||
[[nodiscard]] Decimal estimate_cost(const std::string& source_chain,
|
||||
const std::string& dest_chain,
|
||||
const std::string& token,
|
||||
Decimal amount) const;
|
||||
|
||||
/// Get chain ID from venue name
|
||||
[[nodiscard]] std::string venue_to_chain(const std::string& venue) const;
|
||||
|
||||
/// Enhance an opportunity with routing information
|
||||
[[nodiscard]] EnhancedOpportunity enhance_opportunity(const UnifiedOpportunity& opp) const;
|
||||
|
||||
/// Get current config
|
||||
[[nodiscard]] const CrossChainConfig& config() const noexcept { return config_; }
|
||||
|
||||
private:
|
||||
CrossChainConfig config_;
|
||||
std::shared_ptr<WarpClient> warp_client_;
|
||||
std::shared_ptr<TeleportClient> teleport_client_;
|
||||
mutable std::mutex warp_mutex_;
|
||||
mutable std::mutex teleport_mutex_;
|
||||
};
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,128 +0,0 @@
|
||||
// LX Trading SDK - LX-First Arbitrage Strategy
|
||||
//
|
||||
// Key Insight: LX DEX is the FASTEST venue (nanosecond price updates, 200ms blocks).
|
||||
// By the time other venues update, LX has already moved.
|
||||
//
|
||||
// This means:
|
||||
// 1. LX DEX price is the "TRUE" price (most current)
|
||||
// 2. Other venues are always STALE by comparison
|
||||
// 3. Arbitrage = correcting stale venues to match LX
|
||||
// 4. LX DEX is the ORACLE, not just another venue
|
||||
//
|
||||
// Strategy:
|
||||
// 1. Watch LX DEX prices (the reference)
|
||||
// 2. Compare against "slow" venues (CEX, external DEX)
|
||||
// 3. When slow venue diverges from LX, trade on SLOW venue
|
||||
// 4. You're essentially front-running slow venues with LX information
|
||||
//
|
||||
// Example:
|
||||
// - LX DEX BTC: $50,000 (current, true)
|
||||
// - Binance BTC: $49,990 (stale, 50ms behind)
|
||||
// - Uniswap BTC: $50,020 (stale, 12s behind)
|
||||
//
|
||||
// Action:
|
||||
// - Buy on Binance at $49,990 (they haven't caught up yet)
|
||||
// - Sell on Uniswap at $50,020 (they haven't corrected yet)
|
||||
// - Net: $30 profit per BTC
|
||||
//
|
||||
// Why LX wins: By the time Binance/Uniswap update, we've already executed.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/arbitrage/types.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
/// LX-first arbitrage using LX DEX as the price oracle
|
||||
class LxFirstArbitrage {
|
||||
public:
|
||||
explicit LxFirstArbitrage(LxFirstConfig config);
|
||||
~LxFirstArbitrage() = default;
|
||||
|
||||
// Non-copyable
|
||||
LxFirstArbitrage(const LxFirstArbitrage&) = delete;
|
||||
LxFirstArbitrage& operator=(const LxFirstArbitrage&) = delete;
|
||||
|
||||
/// Update the LX DEX price (the oracle)
|
||||
/// Immediately checks for opportunities against stale venues
|
||||
void update_lx_price(const LxPrice& price);
|
||||
|
||||
/// Update a price from a 'slow' venue
|
||||
void update_venue_price(const VenuePrice& price);
|
||||
|
||||
/// Subscribe to opportunity events
|
||||
void on_opportunity(LxFirstCallback callback);
|
||||
|
||||
/// Start the arbitrage system
|
||||
void start();
|
||||
|
||||
/// Stop the arbitrage system
|
||||
void stop();
|
||||
|
||||
/// Check if system is running
|
||||
[[nodiscard]] bool is_running() const noexcept { return running_.load(); }
|
||||
|
||||
/// Get current config
|
||||
[[nodiscard]] const LxFirstConfig& config() const noexcept { return config_; }
|
||||
|
||||
private:
|
||||
void check_opportunities(const std::string& symbol);
|
||||
LxFirstOpportunity create_opportunity(
|
||||
const std::string& symbol,
|
||||
const LxPrice& lx_price,
|
||||
const VenuePrice& vp,
|
||||
int64_t staleness,
|
||||
const std::string& side,
|
||||
Decimal divergence,
|
||||
Decimal divergence_bps) const;
|
||||
double calculate_confidence(int64_t staleness, Decimal divergence_bps) const;
|
||||
|
||||
LxFirstConfig config_;
|
||||
std::unordered_map<std::string, LxPrice> lx_prices_;
|
||||
std::unordered_map<std::string, std::vector<VenuePrice>> venue_prices_;
|
||||
std::vector<LxFirstCallback> callbacks_;
|
||||
mutable std::mutex lx_prices_mutex_;
|
||||
mutable std::mutex venue_prices_mutex_;
|
||||
mutable std::mutex callbacks_mutex_;
|
||||
std::atomic<bool> running_{false};
|
||||
};
|
||||
|
||||
/*
|
||||
TRADING EXECUTION STRATEGY
|
||||
|
||||
When an LxFirstOpportunity is detected:
|
||||
|
||||
1. DO NOT trade on LX DEX (it's the reference, not the opportunity)
|
||||
|
||||
2. Trade on the STALE venue:
|
||||
- If Side="buy": Buy on stale venue (their ask is behind LX)
|
||||
- If Side="sell": Sell on stale venue (their bid is behind LX)
|
||||
|
||||
3. Settlement options:
|
||||
a) Hold position until venues converge (market neutral)
|
||||
b) Immediately hedge on LX DEX (lock in profit)
|
||||
c) Bridge and sell on another venue (more complex)
|
||||
|
||||
4. The key insight:
|
||||
- You're NOT arbitraging between two venues
|
||||
- You're front-running the slow venue with LX information
|
||||
- LX price is where the slow venue WILL BE, you just got there first
|
||||
|
||||
Example execution:
|
||||
|
||||
LX DEX shows BTC = $50,000 (current, true price)
|
||||
Binance shows BTC = $49,950 (50ms stale)
|
||||
|
||||
Action: BUY on Binance at $49,950
|
||||
Why: Binance WILL update to ~$50,000, we bought before they did
|
||||
Profit: ~$50 per BTC (0.1%)
|
||||
|
||||
Optional hedge: SELL on LX DEX at $50,000 to lock in profit immediately
|
||||
*/
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,82 +0,0 @@
|
||||
// LX Trading SDK - Arbitrage Scanner
|
||||
// Continuously scans for arbitrage opportunities across all venues.
|
||||
// Supports simple, triangular, and CEX-DEX arbitrage detection.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/arbitrage/types.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
/// Known CEX venues
|
||||
inline const std::set<std::string> CEX_VENUES = {
|
||||
"binance", "coinbase", "kraken", "okx", "bybit",
|
||||
"kucoin", "mexc", "gate", "huobi"
|
||||
};
|
||||
|
||||
/// Arbitrage scanner for detecting cross-venue opportunities
|
||||
class Scanner {
|
||||
public:
|
||||
explicit Scanner(ScannerConfig config);
|
||||
~Scanner();
|
||||
|
||||
// Non-copyable
|
||||
Scanner(const Scanner&) = delete;
|
||||
Scanner& operator=(const Scanner&) = delete;
|
||||
|
||||
/// Add a chain configuration
|
||||
void add_chain(const CrossChainInfo& info);
|
||||
|
||||
/// Update a price feed
|
||||
void update_price(const PriceSource& source);
|
||||
|
||||
/// Subscribe to opportunity events
|
||||
void on_opportunity(OpportunityCallback callback);
|
||||
|
||||
/// Start scanning for opportunities
|
||||
void start();
|
||||
|
||||
/// Stop scanning
|
||||
void stop();
|
||||
|
||||
/// Check if scanner is running
|
||||
[[nodiscard]] bool is_running() const noexcept { return running_.load(); }
|
||||
|
||||
/// Get current config
|
||||
[[nodiscard]] const ScannerConfig& config() const noexcept { return config_; }
|
||||
|
||||
private:
|
||||
void scan_loop();
|
||||
std::vector<ArbitrageOpportunity> scan();
|
||||
std::vector<ArbitrageOpportunity> find_simple_arb(
|
||||
const std::string& symbol,
|
||||
const std::vector<PriceSource>& sources) const;
|
||||
std::vector<ArbitrageOpportunity> find_cex_dex_arb(
|
||||
const std::string& symbol,
|
||||
const std::vector<PriceSource>& sources) const;
|
||||
std::pair<Decimal, Decimal> calculate_costs(
|
||||
const std::string& source_chain,
|
||||
const std::string& dest_chain) const;
|
||||
double calculate_confidence(
|
||||
const PriceSource& buy,
|
||||
const PriceSource& sell) const;
|
||||
|
||||
ScannerConfig config_;
|
||||
std::unordered_map<std::string, std::vector<PriceSource>> prices_;
|
||||
std::unordered_map<std::string, CrossChainInfo> chains_;
|
||||
std::vector<OpportunityCallback> callbacks_;
|
||||
mutable std::mutex prices_mutex_;
|
||||
mutable std::mutex chains_mutex_;
|
||||
mutable std::mutex callbacks_mutex_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::unique_ptr<std::thread> scan_thread_;
|
||||
};
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,347 +0,0 @@
|
||||
// LX Trading SDK - Arbitrage Types
|
||||
// LX-First Strategy: LX DEX is the price oracle
|
||||
//
|
||||
// Key Insight: LX DEX is the FASTEST venue (nanosecond updates, 200ms blocks)
|
||||
// By the time other venues update, LX has already moved.
|
||||
// LX DEX price is the "TRUTH" - other venues are always STALE.
|
||||
// Arbitrage = correcting stale venues to match LX
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
/// Cross-chain transport protocol
|
||||
enum class CrossChainTransport : uint8_t {
|
||||
/// Warp - Lux native messaging between subnets (<500ms)
|
||||
Warp = 0,
|
||||
/// Teleport - EVM bridge for external chains (~30s)
|
||||
Teleport = 1,
|
||||
/// Direct - Same chain, no bridge needed
|
||||
Direct = 2,
|
||||
/// CEX API - API calls for centralized exchanges
|
||||
CexApi = 3
|
||||
};
|
||||
|
||||
inline constexpr const char* to_string(CrossChainTransport t) noexcept {
|
||||
switch (t) {
|
||||
case CrossChainTransport::Warp: return "warp";
|
||||
case CrossChainTransport::Teleport: return "teleport";
|
||||
case CrossChainTransport::Direct: return "direct";
|
||||
case CrossChainTransport::CexApi: return "cex_api";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/// Type of blockchain
|
||||
enum class ChainType : uint8_t {
|
||||
/// Lux subnet (Warp-enabled)
|
||||
LuxSubnet = 0,
|
||||
/// EVM-compatible chain
|
||||
Evm = 1,
|
||||
/// Centralized exchange
|
||||
Cex = 2
|
||||
};
|
||||
|
||||
inline constexpr const char* to_string(ChainType t) noexcept {
|
||||
switch (t) {
|
||||
case ChainType::LuxSubnet: return "lux_subnet";
|
||||
case ChainType::Evm: return "evm";
|
||||
case ChainType::Cex: return "cex";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/// Type of arbitrage
|
||||
enum class ArbType : uint8_t {
|
||||
/// Simple buy-low-sell-high
|
||||
Simple = 0,
|
||||
/// Triangular A->B->C->A
|
||||
Triangular = 1,
|
||||
/// Multi-hop complex routes
|
||||
MultiHop = 2,
|
||||
/// CEX-DEX arbitrage
|
||||
CexDex = 3,
|
||||
/// DEX flash swap
|
||||
FlashSwap = 4
|
||||
};
|
||||
|
||||
inline constexpr const char* to_string(ArbType t) noexcept {
|
||||
switch (t) {
|
||||
case ArbType::Simple: return "simple";
|
||||
case ArbType::Triangular: return "triangular";
|
||||
case ArbType::MultiHop: return "multi_hop";
|
||||
case ArbType::CexDex: return "cex_dex";
|
||||
case ArbType::FlashSwap: return "flash_swap";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/// Price feed from a specific venue/chain
|
||||
struct PriceSource {
|
||||
std::string chain_id;
|
||||
std::string venue;
|
||||
std::string symbol;
|
||||
Decimal bid;
|
||||
Decimal ask;
|
||||
Decimal liquidity;
|
||||
int64_t timestamp{0}; // Unix timestamp in milliseconds
|
||||
int64_t latency{0}; // Latency in milliseconds
|
||||
|
||||
[[nodiscard]] Decimal mid_price() const noexcept {
|
||||
return (bid + ask) / Decimal::from_double(2.0);
|
||||
}
|
||||
|
||||
[[nodiscard]] Decimal spread() const noexcept {
|
||||
return ask - bid;
|
||||
}
|
||||
|
||||
[[nodiscard]] Decimal spread_bps() const noexcept {
|
||||
if (bid.is_zero()) return Decimal::zero();
|
||||
return ((ask - bid) / bid) * Decimal::from_double(10000.0);
|
||||
}
|
||||
};
|
||||
|
||||
/// LX DEX price - the reference/oracle
|
||||
struct LxPrice {
|
||||
std::string symbol;
|
||||
Decimal bid;
|
||||
Decimal ask;
|
||||
Decimal mid;
|
||||
int64_t timestamp{0};
|
||||
uint64_t block_num{0};
|
||||
|
||||
static LxPrice create(const std::string& sym, Decimal b, Decimal a, int64_t ts, uint64_t block) {
|
||||
LxPrice price;
|
||||
price.symbol = sym;
|
||||
price.bid = b;
|
||||
price.ask = a;
|
||||
price.mid = (b + a) / Decimal::from_double(2.0);
|
||||
price.timestamp = ts;
|
||||
price.block_num = block;
|
||||
return price;
|
||||
}
|
||||
};
|
||||
|
||||
/// Price from a 'slow' venue
|
||||
struct VenuePrice {
|
||||
std::string venue;
|
||||
std::string symbol;
|
||||
Decimal bid;
|
||||
Decimal ask;
|
||||
int64_t timestamp{0};
|
||||
int64_t latency{0}; // How far behind LX this venue typically is (ms)
|
||||
bool stale{false}; // Is this price stale relative to LX?
|
||||
};
|
||||
|
||||
/// Single leg of an arbitrage
|
||||
struct Route {
|
||||
std::string chain_id;
|
||||
std::string venue;
|
||||
std::string action; // "buy" or "sell"
|
||||
std::string token_in;
|
||||
std::string token_out;
|
||||
Decimal amount_in;
|
||||
Decimal expected_out;
|
||||
Decimal min_amount_out;
|
||||
std::vector<uint8_t> swap_data;
|
||||
};
|
||||
|
||||
/// Detected arbitrage opportunity
|
||||
struct ArbitrageOpportunity {
|
||||
std::string id;
|
||||
ArbType arb_type{ArbType::Simple};
|
||||
std::vector<Route> routes;
|
||||
PriceSource buy_source;
|
||||
PriceSource sell_source;
|
||||
Decimal spread_bps; // Spread in basis points
|
||||
Decimal estimated_pnl;
|
||||
Decimal max_size; // Limited by liquidity
|
||||
Decimal gas_cost_usd;
|
||||
Decimal bridge_cost_usd;
|
||||
Decimal net_pnl;
|
||||
double confidence{0.0}; // 0-1, based on price freshness and liquidity
|
||||
int64_t expires_at{0};
|
||||
|
||||
[[nodiscard]] bool is_profitable() const noexcept {
|
||||
return net_pnl.is_positive();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_expired(int64_t now) const noexcept {
|
||||
return now > expires_at;
|
||||
}
|
||||
};
|
||||
|
||||
/// LX-first arbitrage opportunity
|
||||
struct LxFirstOpportunity {
|
||||
std::string id;
|
||||
std::string symbol;
|
||||
int64_t timestamp{0};
|
||||
LxPrice lx_price;
|
||||
std::string stale_venue;
|
||||
VenuePrice stale_price;
|
||||
int64_t staleness{0}; // Staleness in milliseconds
|
||||
std::string side; // "buy" or "sell"
|
||||
Decimal divergence;
|
||||
Decimal divergence_bps;
|
||||
Decimal expected_profit;
|
||||
Decimal max_size;
|
||||
double confidence{0.0};
|
||||
};
|
||||
|
||||
/// Unified arbitrage opportunity across venues
|
||||
struct UnifiedOpportunity {
|
||||
std::string id;
|
||||
std::string symbol;
|
||||
int64_t timestamp{0};
|
||||
int64_t expires_at{0};
|
||||
std::string buy_venue;
|
||||
Decimal buy_price;
|
||||
Decimal buy_size;
|
||||
std::string sell_venue;
|
||||
Decimal sell_price;
|
||||
Decimal sell_size;
|
||||
Decimal spread;
|
||||
Decimal spread_bps;
|
||||
Decimal max_size;
|
||||
Decimal gross_profit;
|
||||
Decimal est_fees;
|
||||
Decimal net_profit;
|
||||
double confidence{0.0};
|
||||
int64_t latency{0};
|
||||
};
|
||||
|
||||
/// Executed arbitrage result
|
||||
struct UnifiedExecution {
|
||||
std::string id;
|
||||
UnifiedOpportunity opportunity;
|
||||
int64_t start_time{0};
|
||||
int64_t end_time{0};
|
||||
std::string status; // "executing", "completed", "failed"
|
||||
std::optional<std::string> buy_order_id;
|
||||
std::optional<std::string> sell_order_id;
|
||||
Decimal actual_profit;
|
||||
Decimal fees;
|
||||
std::optional<std::string> error;
|
||||
};
|
||||
|
||||
/// Arbitrage statistics
|
||||
struct UnifiedArbStats {
|
||||
uint64_t total_executions{0};
|
||||
uint64_t successful_executions{0};
|
||||
Decimal total_pnl;
|
||||
double win_rate{0.0};
|
||||
};
|
||||
|
||||
/// Configuration for unified arbitrage system
|
||||
struct UnifiedArbConfig {
|
||||
Decimal min_spread_bps{Decimal::from_double(10.0)};
|
||||
Decimal min_profit{Decimal::from_double(5.0)};
|
||||
Decimal max_position_size{Decimal::from_double(10000.0)};
|
||||
Decimal max_total_exposure{Decimal::from_double(100000.0)};
|
||||
std::vector<std::string> symbols{"BTC-USDC", "ETH-USDC", "LUX-USDC"};
|
||||
std::vector<std::string> venue_priority{"lx_dex", "binance", "mexc", "lx_amm"};
|
||||
uint64_t scan_interval_ms{100};
|
||||
uint64_t execute_timeout_ms{5000};
|
||||
Decimal max_daily_loss{Decimal::from_double(1000.0)};
|
||||
uint32_t max_trades_per_day{100};
|
||||
|
||||
static UnifiedArbConfig defaults() {
|
||||
return UnifiedArbConfig{};
|
||||
}
|
||||
};
|
||||
|
||||
/// Configuration for LX-first strategy
|
||||
struct LxFirstConfig {
|
||||
int64_t max_staleness_ms{2000};
|
||||
Decimal min_divergence_bps{Decimal::from_double(10.0)};
|
||||
Decimal min_profit{Decimal::from_double(5.0)};
|
||||
Decimal max_position_size{Decimal::from_double(1000.0)};
|
||||
std::vector<std::string> symbols{"BTC-USDC", "ETH-USDC", "LUX-USDC"};
|
||||
std::map<std::string, int64_t> venue_latencies{
|
||||
{"binance", 50},
|
||||
{"mexc", 100},
|
||||
{"okx", 80},
|
||||
{"uniswap", 12000},
|
||||
{"pancakeswap", 3000}
|
||||
};
|
||||
|
||||
static LxFirstConfig defaults() {
|
||||
return LxFirstConfig{};
|
||||
}
|
||||
};
|
||||
|
||||
/// Configuration for arbitrage scanner
|
||||
struct ScannerConfig {
|
||||
Decimal min_spread_bps{Decimal::from_double(10.0)};
|
||||
Decimal min_profit_usd{Decimal::from_double(10.0)};
|
||||
int64_t max_price_age_ms{5000};
|
||||
std::vector<std::string> symbols{"BTC", "ETH", "LUX", "SOL", "AVAX"};
|
||||
std::vector<std::string> chain_ids{"lux", "ethereum", "bsc", "arbitrum", "polygon"};
|
||||
uint64_t scan_interval_ms{100};
|
||||
size_t max_concurrency{50};
|
||||
|
||||
static ScannerConfig defaults() {
|
||||
return ScannerConfig{};
|
||||
}
|
||||
};
|
||||
|
||||
/// Information about a chain
|
||||
struct CrossChainInfo {
|
||||
std::string chain_id;
|
||||
std::string name;
|
||||
ChainType chain_type{ChainType::LuxSubnet};
|
||||
uint64_t block_time_ms{0};
|
||||
uint64_t finality_ms{0};
|
||||
bool warp_supported{false};
|
||||
bool teleport_supported{false};
|
||||
std::vector<std::string> venues;
|
||||
};
|
||||
|
||||
/// Configuration for cross-chain routing
|
||||
struct CrossChainConfig {
|
||||
bool warp_enabled{true};
|
||||
std::optional<std::string> warp_endpoint;
|
||||
uint64_t warp_timeout_ms{5000};
|
||||
bool teleport_enabled{true};
|
||||
std::optional<std::string> teleport_endpoint;
|
||||
uint64_t teleport_timeout_ms{60000};
|
||||
std::map<std::string, CrossChainInfo> chains;
|
||||
|
||||
static CrossChainConfig defaults();
|
||||
};
|
||||
|
||||
/// Enhanced opportunity with routing information
|
||||
struct EnhancedOpportunity {
|
||||
UnifiedOpportunity base;
|
||||
CrossChainTransport transport{CrossChainTransport::Direct};
|
||||
int64_t estimated_latency{0};
|
||||
Decimal bridge_cost;
|
||||
Decimal adjusted_net_profit;
|
||||
};
|
||||
|
||||
/// Bridge transaction status
|
||||
struct BridgeStatus {
|
||||
std::string tx_id;
|
||||
std::string status; // pending, confirming, completed, failed
|
||||
std::string source_chain;
|
||||
std::string dest_chain;
|
||||
Decimal amount;
|
||||
Decimal fee;
|
||||
std::string source_tx;
|
||||
std::optional<std::string> dest_tx;
|
||||
int64_t timestamp{0};
|
||||
};
|
||||
|
||||
// Callback types
|
||||
using OpportunityCallback = std::function<void(const ArbitrageOpportunity&)>;
|
||||
using LxFirstCallback = std::function<void(const LxFirstOpportunity&)>;
|
||||
using UnifiedCallback = std::function<void(const UnifiedOpportunity&)>;
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,103 +0,0 @@
|
||||
// LX Trading SDK - Unified Liquidity Arbitrage
|
||||
//
|
||||
// Since LX DEX is the FASTEST venue (nanosecond updates, 200ms blocks),
|
||||
// it becomes the price ORACLE. Other venues are always stale by comparison.
|
||||
//
|
||||
// Architecture:
|
||||
// 1. LX DEX prices are the TRUTH (most current)
|
||||
// 2. Other venues (CEX, external DEX) are STALE
|
||||
// 3. Arbitrage = exploiting stale venues before they catch up
|
||||
// 4. LX always wins because it sees/moves prices first
|
||||
//
|
||||
// NO SMART CONTRACTS - just coordinated trades through unified SDK.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/arbitrage/types.hpp>
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
/// Aggregated orderbook level
|
||||
struct AggregatedLevel {
|
||||
Decimal price;
|
||||
Decimal quantity;
|
||||
std::string venue;
|
||||
int64_t timestamp{0};
|
||||
};
|
||||
|
||||
/// Aggregated orderbook from all venues
|
||||
struct AggregatedBook {
|
||||
std::string symbol;
|
||||
std::vector<AggregatedLevel> bids;
|
||||
std::vector<AggregatedLevel> asks;
|
||||
};
|
||||
|
||||
/// Trading client interface for arbitrage
|
||||
class TradingClient {
|
||||
public:
|
||||
virtual ~TradingClient() = default;
|
||||
|
||||
/// Get aggregated orderbook from all venues
|
||||
virtual AggregatedBook aggregated_orderbook(const std::string& symbol) = 0;
|
||||
|
||||
/// Place an order on a specific venue
|
||||
virtual Order place_order(const OrderRequest& request) = 0;
|
||||
};
|
||||
|
||||
/// Unified arbitrage across all SDK-connected venues
|
||||
class UnifiedArbitrage {
|
||||
public:
|
||||
UnifiedArbitrage(std::shared_ptr<TradingClient> client, UnifiedArbConfig config);
|
||||
~UnifiedArbitrage();
|
||||
|
||||
// Non-copyable
|
||||
UnifiedArbitrage(const UnifiedArbitrage&) = delete;
|
||||
UnifiedArbitrage& operator=(const UnifiedArbitrage&) = delete;
|
||||
|
||||
/// Start the arbitrage system
|
||||
void start();
|
||||
|
||||
/// Stop the arbitrage system
|
||||
void stop();
|
||||
|
||||
/// Subscribe to opportunity events
|
||||
void on_opportunity(UnifiedCallback callback);
|
||||
|
||||
/// Get arbitrage statistics
|
||||
[[nodiscard]] UnifiedArbStats get_stats() const;
|
||||
|
||||
/// Check if system is running
|
||||
[[nodiscard]] bool is_running() const noexcept { return running_.load(); }
|
||||
|
||||
/// Get current config
|
||||
[[nodiscard]] const UnifiedArbConfig& config() const noexcept { return config_; }
|
||||
|
||||
private:
|
||||
void scan_loop();
|
||||
void execute_loop();
|
||||
std::optional<UnifiedOpportunity> find_opportunity(const std::string& symbol);
|
||||
UnifiedExecution execute_opportunity(const UnifiedOpportunity& opp);
|
||||
|
||||
std::shared_ptr<TradingClient> client_;
|
||||
UnifiedArbConfig config_;
|
||||
Decimal total_pnl_;
|
||||
std::vector<UnifiedExecution> executions_;
|
||||
std::vector<UnifiedCallback> callbacks_;
|
||||
std::deque<UnifiedOpportunity> opportunity_queue_;
|
||||
mutable std::mutex pnl_mutex_;
|
||||
mutable std::mutex executions_mutex_;
|
||||
mutable std::mutex callbacks_mutex_;
|
||||
mutable std::mutex queue_mutex_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::unique_ptr<std::thread> scan_thread_;
|
||||
std::unique_ptr<std::thread> execute_thread_;
|
||||
};
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,206 +0,0 @@
|
||||
// LX Trading SDK - Unified Client
|
||||
// Smart routing across multiple venues
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/adapter.hpp>
|
||||
#include <lx/trading/config.hpp>
|
||||
#include <lx/trading/orderbook.hpp>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Forward declarations
|
||||
class RiskManager;
|
||||
|
||||
// Unified trading client
|
||||
class Client {
|
||||
public:
|
||||
explicit Client(const Config& config);
|
||||
~Client();
|
||||
|
||||
// Disallow copy
|
||||
Client(const Client&) = delete;
|
||||
Client& operator=(const Client&) = delete;
|
||||
|
||||
// Connection management
|
||||
std::future<void> connect();
|
||||
std::future<void> disconnect();
|
||||
|
||||
// Venue access
|
||||
[[nodiscard]] VenueAdapter* venue(std::string_view name);
|
||||
[[nodiscard]] const VenueAdapter* venue(std::string_view name) const;
|
||||
[[nodiscard]] std::vector<VenueInfo> venues() const;
|
||||
|
||||
// =========================================================================
|
||||
// Market Data
|
||||
// =========================================================================
|
||||
|
||||
// Get orderbook from specific venue
|
||||
std::future<std::unique_ptr<Orderbook>> orderbook(
|
||||
const std::string& symbol,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
// Get aggregated orderbook from all venues
|
||||
std::future<AggregatedOrderbook> aggregated_orderbook(const std::string& symbol);
|
||||
|
||||
// Get ticker from specific venue
|
||||
std::future<Ticker> ticker(
|
||||
const std::string& symbol,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
// Get tickers from all venues
|
||||
std::future<std::vector<Ticker>> tickers(const std::string& symbol);
|
||||
|
||||
// =========================================================================
|
||||
// Account
|
||||
// =========================================================================
|
||||
|
||||
// Get aggregated balances across all venues
|
||||
std::future<std::vector<AggregatedBalance>> balances();
|
||||
|
||||
// Get balance for specific asset from venue
|
||||
std::future<Balance> balance(
|
||||
const std::string& asset,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
// =========================================================================
|
||||
// Order Management
|
||||
// =========================================================================
|
||||
|
||||
// Place market buy order
|
||||
std::future<Order> buy(
|
||||
const std::string& symbol,
|
||||
Decimal quantity,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
// Place market sell order
|
||||
std::future<Order> sell(
|
||||
const std::string& symbol,
|
||||
Decimal quantity,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
// Place limit buy order
|
||||
std::future<Order> limit_buy(
|
||||
const std::string& symbol,
|
||||
Decimal quantity,
|
||||
Decimal price,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
// Place limit sell order
|
||||
std::future<Order> limit_sell(
|
||||
const std::string& symbol,
|
||||
Decimal quantity,
|
||||
Decimal price,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
// Place order with full control
|
||||
std::future<Order> place_order(const OrderRequest& request);
|
||||
|
||||
// Cancel order
|
||||
std::future<Order> cancel_order(
|
||||
const std::string& order_id,
|
||||
const std::string& symbol,
|
||||
const std::string& venue);
|
||||
|
||||
// Cancel all orders
|
||||
std::future<std::vector<Order>> cancel_all_orders(
|
||||
std::optional<std::string_view> symbol = std::nullopt,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
// Get all open orders
|
||||
std::future<std::vector<Order>> open_orders(
|
||||
std::optional<std::string_view> symbol = std::nullopt);
|
||||
|
||||
// =========================================================================
|
||||
// AMM Operations
|
||||
// =========================================================================
|
||||
|
||||
// Get swap quote
|
||||
std::future<SwapQuote> quote(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy,
|
||||
const std::string& venue);
|
||||
|
||||
// Execute swap
|
||||
std::future<Trade> swap(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy,
|
||||
double slippage,
|
||||
const std::string& venue);
|
||||
|
||||
// Get pool info
|
||||
std::future<PoolInfo> pool_info(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
const std::string& venue);
|
||||
|
||||
// Add liquidity
|
||||
std::future<LiquidityResult> add_liquidity(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal base_amount,
|
||||
Decimal quote_amount,
|
||||
double slippage,
|
||||
const std::string& venue);
|
||||
|
||||
// Remove liquidity
|
||||
std::future<LiquidityResult> remove_liquidity(
|
||||
const std::string& pool_address,
|
||||
Decimal liquidity_amount,
|
||||
double slippage,
|
||||
const std::string& venue);
|
||||
|
||||
// Get LP positions
|
||||
std::future<std::vector<LpPosition>> lp_positions(const std::string& venue);
|
||||
|
||||
// =========================================================================
|
||||
// Streaming
|
||||
// =========================================================================
|
||||
|
||||
void subscribe_ticker(
|
||||
const std::string& symbol,
|
||||
VenueAdapter::TickerCallback callback,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
void subscribe_trades(
|
||||
const std::string& symbol,
|
||||
VenueAdapter::TradeCallback callback,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
void subscribe_orderbook(
|
||||
const std::string& symbol,
|
||||
VenueAdapter::OrderbookCallback callback,
|
||||
std::optional<std::string_view> venue = std::nullopt);
|
||||
|
||||
void subscribe_orders(VenueAdapter::OrderCallback callback);
|
||||
|
||||
void unsubscribe_all();
|
||||
|
||||
// =========================================================================
|
||||
// Risk Management
|
||||
// =========================================================================
|
||||
|
||||
[[nodiscard]] RiskManager& risk_manager() { return *risk_manager_; }
|
||||
[[nodiscard]] const RiskManager& risk_manager() const { return *risk_manager_; }
|
||||
|
||||
private:
|
||||
Order place_order_impl(const OrderRequest& request);
|
||||
Order smart_route(const OrderRequest& request);
|
||||
VenueAdapter* get_venue(std::optional<std::string_view> name);
|
||||
|
||||
Config config_;
|
||||
std::unordered_map<std::string, std::unique_ptr<VenueAdapter>> venues_;
|
||||
std::optional<std::string> default_venue_;
|
||||
std::unique_ptr<RiskManager> risk_manager_;
|
||||
};
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,244 +0,0 @@
|
||||
// LX Trading SDK - Configuration
|
||||
// Builder pattern for fluent configuration
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// General SDK settings
|
||||
struct GeneralConfig {
|
||||
std::string log_level = "info";
|
||||
int timeout_ms = 30000;
|
||||
bool smart_routing = true;
|
||||
std::vector<std::string> venue_priority;
|
||||
int min_improvement_bps = 5;
|
||||
};
|
||||
|
||||
// Risk management settings
|
||||
struct RiskConfig {
|
||||
bool enabled = true;
|
||||
Decimal max_position_size;
|
||||
Decimal max_order_size;
|
||||
Decimal max_daily_loss;
|
||||
int max_open_orders = 100;
|
||||
bool kill_switch_enabled = false;
|
||||
std::unordered_map<std::string, Decimal> position_limits;
|
||||
};
|
||||
|
||||
// Native venue config (LX DEX or LX AMM)
|
||||
class NativeVenueConfig {
|
||||
public:
|
||||
std::string venue_type = "dex"; // "dex" or "amm"
|
||||
std::string api_url;
|
||||
std::optional<std::string> ws_url;
|
||||
std::optional<std::string> api_key;
|
||||
std::optional<std::string> api_secret;
|
||||
std::optional<std::string> wallet_address;
|
||||
std::optional<std::string> private_key;
|
||||
std::string network = "mainnet";
|
||||
int chain_id = 96369;
|
||||
bool streaming = true;
|
||||
std::optional<Decimal> maker_fee;
|
||||
std::optional<Decimal> taker_fee;
|
||||
|
||||
NativeVenueConfig() = default;
|
||||
|
||||
static NativeVenueConfig lx_dex(std::string_view api_url) {
|
||||
NativeVenueConfig cfg;
|
||||
cfg.venue_type = "dex";
|
||||
cfg.api_url = std::string(api_url);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static NativeVenueConfig lx_amm(std::string_view api_url) {
|
||||
NativeVenueConfig cfg;
|
||||
cfg.venue_type = "amm";
|
||||
cfg.api_url = std::string(api_url);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
NativeVenueConfig& with_credentials(std::string_view key, std::string_view secret) {
|
||||
api_key = std::string(key);
|
||||
api_secret = std::string(secret);
|
||||
return *this;
|
||||
}
|
||||
|
||||
NativeVenueConfig& with_wallet(std::string_view address, std::string_view priv_key) {
|
||||
wallet_address = std::string(address);
|
||||
private_key = std::string(priv_key);
|
||||
return *this;
|
||||
}
|
||||
|
||||
NativeVenueConfig& with_ws(std::string_view url) {
|
||||
ws_url = std::string(url);
|
||||
return *this;
|
||||
}
|
||||
|
||||
NativeVenueConfig& testnet() {
|
||||
network = "testnet";
|
||||
chain_id = 8888;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// CCXT exchange config
|
||||
class CcxtConfig {
|
||||
public:
|
||||
std::string exchange_id;
|
||||
std::optional<std::string> api_key;
|
||||
std::optional<std::string> api_secret;
|
||||
std::optional<std::string> password;
|
||||
bool sandbox = false;
|
||||
bool rate_limit = true;
|
||||
std::unordered_map<std::string, std::string> options;
|
||||
|
||||
CcxtConfig() = default;
|
||||
|
||||
static CcxtConfig create(std::string_view exchange) {
|
||||
CcxtConfig cfg;
|
||||
cfg.exchange_id = std::string(exchange);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
CcxtConfig& with_credentials(std::string_view key, std::string_view secret) {
|
||||
api_key = std::string(key);
|
||||
api_secret = std::string(secret);
|
||||
return *this;
|
||||
}
|
||||
|
||||
CcxtConfig& with_password(std::string_view pwd) {
|
||||
password = std::string(pwd);
|
||||
return *this;
|
||||
}
|
||||
|
||||
CcxtConfig& enable_sandbox() {
|
||||
sandbox = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
CcxtConfig& with_option(std::string_view key, std::string_view value) {
|
||||
options[std::string(key)] = std::string(value);
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// Hummingbot Gateway config
|
||||
class HummingbotConfig {
|
||||
public:
|
||||
std::string host = "localhost";
|
||||
int port = 15888;
|
||||
bool https = false;
|
||||
std::string connector;
|
||||
std::string chain = "lux";
|
||||
std::string network = "mainnet";
|
||||
std::optional<std::string> wallet_address;
|
||||
|
||||
HummingbotConfig() = default;
|
||||
|
||||
static HummingbotConfig create(std::string_view conn) {
|
||||
HummingbotConfig cfg;
|
||||
cfg.connector = std::string(conn);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
HummingbotConfig& with_wallet(std::string_view address) {
|
||||
wallet_address = std::string(address);
|
||||
return *this;
|
||||
}
|
||||
|
||||
HummingbotConfig& with_endpoint(std::string_view h, int p) {
|
||||
host = std::string(h);
|
||||
port = p;
|
||||
return *this;
|
||||
}
|
||||
|
||||
HummingbotConfig& enable_https() {
|
||||
https = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string base_url() const {
|
||||
return (https ? "https://" : "http://") + host + ":" + std::to_string(port);
|
||||
}
|
||||
};
|
||||
|
||||
// Main SDK configuration
|
||||
class Config {
|
||||
public:
|
||||
GeneralConfig general;
|
||||
RiskConfig risk;
|
||||
std::unordered_map<std::string, NativeVenueConfig> native;
|
||||
std::unordered_map<std::string, CcxtConfig> ccxt;
|
||||
std::unordered_map<std::string, HummingbotConfig> hummingbot;
|
||||
|
||||
Config() = default;
|
||||
|
||||
// Load from TOML file
|
||||
static Config from_file(std::string_view path);
|
||||
|
||||
// Load from TOML string
|
||||
static Config from_toml(std::string_view content);
|
||||
|
||||
// Builder methods
|
||||
Config& with_native(std::string_view name, NativeVenueConfig cfg) {
|
||||
native[std::string(name)] = std::move(cfg);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& with_ccxt(std::string_view name, CcxtConfig cfg) {
|
||||
ccxt[std::string(name)] = std::move(cfg);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& with_hummingbot(std::string_view name, HummingbotConfig cfg) {
|
||||
hummingbot[std::string(name)] = std::move(cfg);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& enable_smart_routing(bool enabled = true) {
|
||||
general.smart_routing = enabled;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& set_timeout(int ms) {
|
||||
general.timeout_ms = ms;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& set_venue_priority(std::vector<std::string> priority) {
|
||||
general.venue_priority = std::move(priority);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& enable_risk_management(bool enabled = true) {
|
||||
risk.enabled = enabled;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& set_max_order_size(Decimal size) {
|
||||
risk.max_order_size = size;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& set_max_position_size(Decimal size) {
|
||||
risk.max_position_size = size;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& set_max_daily_loss(Decimal loss) {
|
||||
risk.max_daily_loss = loss;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Config& set_position_limit(std::string_view asset, Decimal limit) {
|
||||
risk.position_limits[std::string(asset)] = limit;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,225 +0,0 @@
|
||||
// LX Trading SDK - Execution Algorithms
|
||||
// TWAP, VWAP, Iceberg, Sniper execution strategies
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Forward declaration
|
||||
class Client;
|
||||
|
||||
// Execution result
|
||||
struct ExecutionResult {
|
||||
std::vector<Order> orders;
|
||||
Decimal total_quantity;
|
||||
Decimal total_filled;
|
||||
std::optional<Decimal> average_price;
|
||||
int64_t start_time;
|
||||
int64_t end_time;
|
||||
bool completed;
|
||||
std::string error;
|
||||
|
||||
[[nodiscard]] Decimal fill_rate() const {
|
||||
if (total_quantity.is_zero()) return Decimal::zero();
|
||||
return total_filled / total_quantity;
|
||||
}
|
||||
|
||||
[[nodiscard]] int64_t duration_ms() const {
|
||||
return end_time - start_time;
|
||||
}
|
||||
};
|
||||
|
||||
// Progress callback
|
||||
using ExecutionCallback = std::function<void(const Order&, Decimal remaining)>;
|
||||
|
||||
// Base executor interface
|
||||
class Executor {
|
||||
public:
|
||||
virtual ~Executor() = default;
|
||||
virtual std::future<ExecutionResult> execute() = 0;
|
||||
virtual void cancel() = 0;
|
||||
virtual void set_callback(ExecutionCallback cb) { callback_ = std::move(cb); }
|
||||
|
||||
protected:
|
||||
ExecutionCallback callback_;
|
||||
};
|
||||
|
||||
// TWAP - Time-Weighted Average Price
|
||||
class TwapExecutor : public Executor {
|
||||
public:
|
||||
TwapExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
std::chrono::seconds duration,
|
||||
int num_slices);
|
||||
|
||||
std::future<ExecutionResult> execute() override;
|
||||
void cancel() override;
|
||||
|
||||
private:
|
||||
Client& client_;
|
||||
std::string symbol_;
|
||||
Side side_;
|
||||
Decimal total_quantity_;
|
||||
std::chrono::seconds duration_;
|
||||
int num_slices_;
|
||||
std::atomic<bool> cancelled_{false};
|
||||
};
|
||||
|
||||
// VWAP - Volume-Weighted Average Price
|
||||
class VwapExecutor : public Executor {
|
||||
public:
|
||||
VwapExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
Decimal participation_rate, // e.g., 0.1 = 10% of volume
|
||||
std::chrono::seconds max_duration);
|
||||
|
||||
std::future<ExecutionResult> execute() override;
|
||||
void cancel() override;
|
||||
|
||||
private:
|
||||
Client& client_;
|
||||
std::string symbol_;
|
||||
Side side_;
|
||||
Decimal total_quantity_;
|
||||
Decimal participation_rate_;
|
||||
std::chrono::seconds max_duration_;
|
||||
std::atomic<bool> cancelled_{false};
|
||||
};
|
||||
|
||||
// Iceberg - Hidden large order
|
||||
class IcebergExecutor : public Executor {
|
||||
public:
|
||||
IcebergExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
Decimal visible_quantity,
|
||||
Decimal price,
|
||||
std::optional<std::string> venue = std::nullopt);
|
||||
|
||||
std::future<ExecutionResult> execute() override;
|
||||
void cancel() override;
|
||||
|
||||
private:
|
||||
Client& client_;
|
||||
std::string symbol_;
|
||||
Side side_;
|
||||
Decimal total_quantity_;
|
||||
Decimal visible_quantity_;
|
||||
Decimal price_;
|
||||
std::optional<std::string> venue_;
|
||||
std::atomic<bool> cancelled_{false};
|
||||
};
|
||||
|
||||
// Sniper - Wait for price target then execute
|
||||
class SniperExecutor : public Executor {
|
||||
public:
|
||||
SniperExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal quantity,
|
||||
Decimal target_price,
|
||||
std::chrono::seconds timeout);
|
||||
|
||||
std::future<ExecutionResult> execute() override;
|
||||
void cancel() override;
|
||||
|
||||
private:
|
||||
Client& client_;
|
||||
std::string symbol_;
|
||||
Side side_;
|
||||
Decimal quantity_;
|
||||
Decimal target_price_;
|
||||
std::chrono::seconds timeout_;
|
||||
std::atomic<bool> cancelled_{false};
|
||||
};
|
||||
|
||||
// POV - Percentage of Volume
|
||||
class PovExecutor : public Executor {
|
||||
public:
|
||||
PovExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
Decimal target_participation, // e.g., 0.15 = 15% of volume
|
||||
std::chrono::seconds max_duration,
|
||||
std::optional<Decimal> price_limit = std::nullopt);
|
||||
|
||||
std::future<ExecutionResult> execute() override;
|
||||
void cancel() override;
|
||||
|
||||
private:
|
||||
Client& client_;
|
||||
std::string symbol_;
|
||||
Side side_;
|
||||
Decimal total_quantity_;
|
||||
Decimal target_participation_;
|
||||
std::chrono::seconds max_duration_;
|
||||
std::optional<Decimal> price_limit_;
|
||||
std::atomic<bool> cancelled_{false};
|
||||
};
|
||||
|
||||
// Factory functions for convenience
|
||||
inline std::unique_ptr<TwapExecutor> make_twap(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal quantity,
|
||||
std::chrono::seconds duration,
|
||||
int slices = 10) {
|
||||
return std::make_unique<TwapExecutor>(
|
||||
client, std::move(symbol), side, quantity, duration, slices);
|
||||
}
|
||||
|
||||
inline std::unique_ptr<VwapExecutor> make_vwap(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal quantity,
|
||||
Decimal participation = Decimal::from_double(0.1),
|
||||
std::chrono::seconds max_duration = std::chrono::seconds(3600)) {
|
||||
return std::make_unique<VwapExecutor>(
|
||||
client, std::move(symbol), side, quantity, participation, max_duration);
|
||||
}
|
||||
|
||||
inline std::unique_ptr<IcebergExecutor> make_iceberg(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
Decimal visible_quantity,
|
||||
Decimal price) {
|
||||
return std::make_unique<IcebergExecutor>(
|
||||
client, std::move(symbol), side, total_quantity, visible_quantity, price);
|
||||
}
|
||||
|
||||
inline std::unique_ptr<SniperExecutor> make_sniper(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal quantity,
|
||||
Decimal target_price,
|
||||
std::chrono::seconds timeout = std::chrono::seconds(60)) {
|
||||
return std::make_unique<SniperExecutor>(
|
||||
client, std::move(symbol), side, quantity, target_price, timeout);
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,226 +0,0 @@
|
||||
// LX Trading SDK - Financial Mathematics
|
||||
// SIMD-optimized options pricing, AMM math, and risk metrics
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <cmath>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
// SIMD support detection
|
||||
#if defined(__AVX2__)
|
||||
#include <immintrin.h>
|
||||
#define LX_SIMD_AVX2 1
|
||||
#elif defined(__SSE4_1__)
|
||||
#include <smmintrin.h>
|
||||
#define LX_SIMD_SSE4 1
|
||||
#endif
|
||||
|
||||
namespace lx::trading::math {
|
||||
|
||||
// =============================================================================
|
||||
// Constants
|
||||
// =============================================================================
|
||||
|
||||
constexpr double PI = 3.14159265358979323846;
|
||||
constexpr double SQRT_2PI = 2.506628274631000502;
|
||||
constexpr double SQRT_2 = 1.41421356237309504880;
|
||||
|
||||
// =============================================================================
|
||||
// Options Pricing - Black-Scholes
|
||||
// =============================================================================
|
||||
|
||||
// Greeks structure
|
||||
struct Greeks {
|
||||
double delta;
|
||||
double gamma;
|
||||
double theta;
|
||||
double vega;
|
||||
double rho;
|
||||
};
|
||||
|
||||
// Standard normal CDF (approximation - Abramowitz & Stegun)
|
||||
inline double norm_cdf(double x) noexcept {
|
||||
constexpr double a1 = 0.254829592;
|
||||
constexpr double a2 = -0.284496736;
|
||||
constexpr double a3 = 1.421413741;
|
||||
constexpr double a4 = -1.453152027;
|
||||
constexpr double a5 = 1.061405429;
|
||||
constexpr double p = 0.3275911;
|
||||
|
||||
int sign = (x < 0) ? -1 : 1;
|
||||
x = std::abs(x) / SQRT_2;
|
||||
|
||||
double t = 1.0 / (1.0 + p * x);
|
||||
double y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * std::exp(-x * x);
|
||||
|
||||
return 0.5 * (1.0 + sign * y);
|
||||
}
|
||||
|
||||
// Standard normal PDF
|
||||
inline double norm_pdf(double x) noexcept {
|
||||
return std::exp(-0.5 * x * x) / SQRT_2PI;
|
||||
}
|
||||
|
||||
// Black-Scholes option price
|
||||
// S: spot, K: strike, T: time (years), r: rate, sigma: vol
|
||||
double black_scholes(
|
||||
double S, double K, double T, double r, double sigma,
|
||||
bool is_call = true) noexcept;
|
||||
|
||||
// Implied volatility from option price (Newton-Raphson)
|
||||
double implied_volatility(
|
||||
double price, double S, double K, double T, double r,
|
||||
bool is_call = true, double tol = 1e-6, int max_iter = 100) noexcept;
|
||||
|
||||
// Calculate all Greeks
|
||||
Greeks greeks(
|
||||
double S, double K, double T, double r, double sigma,
|
||||
bool is_call = true) noexcept;
|
||||
|
||||
// =============================================================================
|
||||
// AMM Pricing
|
||||
// =============================================================================
|
||||
|
||||
// Constant product AMM (Uniswap V2 style)
|
||||
// Returns: (output_amount, effective_price)
|
||||
std::pair<double, double> constant_product_price(
|
||||
double reserve_x, double reserve_y,
|
||||
double amount_in, double fee_rate = 0.003,
|
||||
bool is_x_to_y = true) noexcept;
|
||||
|
||||
// Concentrated liquidity (Uniswap V3 style)
|
||||
// Returns: (output_amount, new_sqrt_price, price_impact)
|
||||
std::tuple<double, double, double> concentrated_liquidity_price(
|
||||
double liquidity,
|
||||
double sqrt_price_current,
|
||||
double sqrt_price_lower,
|
||||
double sqrt_price_upper,
|
||||
double amount_in,
|
||||
double fee_rate = 0.003,
|
||||
bool is_token0_in = true) noexcept;
|
||||
|
||||
// Calculate liquidity for concentrated position
|
||||
double calculate_liquidity(
|
||||
double amount_x, double amount_y,
|
||||
double sqrt_price_current,
|
||||
double sqrt_price_lower,
|
||||
double sqrt_price_upper) noexcept;
|
||||
|
||||
// Price conversions
|
||||
inline double price_to_sqrt_price(double price) noexcept {
|
||||
return std::sqrt(price);
|
||||
}
|
||||
|
||||
inline double sqrt_price_to_price(double sqrt_price) noexcept {
|
||||
return sqrt_price * sqrt_price;
|
||||
}
|
||||
|
||||
inline double tick_to_sqrt_price(int tick) noexcept {
|
||||
return std::pow(1.0001, tick / 2.0);
|
||||
}
|
||||
|
||||
inline int sqrt_price_to_tick(double sqrt_price, int tick_spacing = 60) noexcept {
|
||||
int tick = static_cast<int>(2.0 * std::log(sqrt_price) / std::log(1.0001));
|
||||
return (tick / tick_spacing) * tick_spacing;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Risk Metrics
|
||||
// =============================================================================
|
||||
|
||||
// Historical volatility
|
||||
double volatility(
|
||||
const std::vector<double>& returns,
|
||||
bool annualize = true,
|
||||
int periods_per_year = 252) noexcept;
|
||||
|
||||
// Sharpe ratio
|
||||
double sharpe_ratio(
|
||||
const std::vector<double>& returns,
|
||||
double risk_free_rate = 0.0,
|
||||
int periods_per_year = 252) noexcept;
|
||||
|
||||
// Sortino ratio (downside deviation)
|
||||
double sortino_ratio(
|
||||
const std::vector<double>& returns,
|
||||
double risk_free_rate = 0.0,
|
||||
double target_return = 0.0,
|
||||
int periods_per_year = 252) noexcept;
|
||||
|
||||
// Maximum drawdown
|
||||
// Returns: (max_drawdown, peak_index, trough_index)
|
||||
std::tuple<double, size_t, size_t> max_drawdown(
|
||||
const std::vector<double>& prices) noexcept;
|
||||
|
||||
// Value at Risk (Historical or Parametric)
|
||||
double var(
|
||||
const std::vector<double>& returns,
|
||||
double confidence = 0.95,
|
||||
bool parametric = false) noexcept;
|
||||
|
||||
// Conditional VaR (Expected Shortfall)
|
||||
double cvar(
|
||||
const std::vector<double>& returns,
|
||||
double confidence = 0.95) noexcept;
|
||||
|
||||
// =============================================================================
|
||||
// SIMD Optimized Operations
|
||||
// =============================================================================
|
||||
|
||||
#ifdef LX_SIMD_AVX2
|
||||
|
||||
// Batch Black-Scholes pricing (AVX2)
|
||||
void black_scholes_batch(
|
||||
const double* S, const double* K, const double* T,
|
||||
const double* r, const double* sigma,
|
||||
double* prices, size_t count, bool is_call = true) noexcept;
|
||||
|
||||
// Batch norm_cdf (AVX2)
|
||||
void norm_cdf_batch(
|
||||
const double* x, double* result, size_t count) noexcept;
|
||||
|
||||
// Vectorized sum (AVX2)
|
||||
double sum_avx2(const double* data, size_t count) noexcept;
|
||||
|
||||
// Vectorized mean (AVX2)
|
||||
double mean_avx2(const double* data, size_t count) noexcept;
|
||||
|
||||
// Vectorized variance (AVX2)
|
||||
double variance_avx2(const double* data, size_t count, double mean) noexcept;
|
||||
|
||||
#endif // LX_SIMD_AVX2
|
||||
|
||||
// =============================================================================
|
||||
// Statistical Utilities
|
||||
// =============================================================================
|
||||
|
||||
// Calculate returns from prices
|
||||
std::vector<double> calculate_returns(const std::vector<double>& prices);
|
||||
|
||||
// Rolling mean
|
||||
std::vector<double> rolling_mean(const std::vector<double>& data, size_t window);
|
||||
|
||||
// Rolling std deviation
|
||||
std::vector<double> rolling_std(const std::vector<double>& data, size_t window);
|
||||
|
||||
// Exponential moving average
|
||||
std::vector<double> ema(const std::vector<double>& data, double alpha);
|
||||
|
||||
// Correlation coefficient
|
||||
double correlation(
|
||||
const std::vector<double>& x,
|
||||
const std::vector<double>& y) noexcept;
|
||||
|
||||
// Covariance
|
||||
double covariance(
|
||||
const std::vector<double>& x,
|
||||
const std::vector<double>& y) noexcept;
|
||||
|
||||
// Beta coefficient
|
||||
double beta(
|
||||
const std::vector<double>& asset_returns,
|
||||
const std::vector<double>& market_returns) noexcept;
|
||||
|
||||
} // namespace lx::trading::math
|
||||
@@ -1,387 +0,0 @@
|
||||
// LX Trading SDK - Lock-Free Orderbook
|
||||
// High-performance orderbook with lock-free updates
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Single venue orderbook with lock-free reads
|
||||
class Orderbook {
|
||||
public:
|
||||
explicit Orderbook(std::string_view symbol, std::string_view venue = "")
|
||||
: symbol_(symbol), venue_(venue), timestamp_(now_ms()), sequence_(0) {}
|
||||
|
||||
Orderbook(const Orderbook&) = delete;
|
||||
Orderbook& operator=(const Orderbook&) = delete;
|
||||
Orderbook(Orderbook&&) = default;
|
||||
Orderbook& operator=(Orderbook&&) = default;
|
||||
|
||||
// Accessors
|
||||
[[nodiscard]] const std::string& symbol() const { return symbol_; }
|
||||
[[nodiscard]] const std::string& venue() const { return venue_; }
|
||||
[[nodiscard]] int64_t timestamp() const { return timestamp_.load(std::memory_order_acquire); }
|
||||
[[nodiscard]] uint64_t sequence() const { return sequence_.load(std::memory_order_acquire); }
|
||||
|
||||
// Mutators (thread-safe)
|
||||
void add_bid(Decimal price, Decimal quantity) {
|
||||
std::unique_lock lock(mutex_);
|
||||
bids_.push_back({price, quantity});
|
||||
}
|
||||
|
||||
void add_ask(Decimal price, Decimal quantity) {
|
||||
std::unique_lock lock(mutex_);
|
||||
asks_.push_back({price, quantity});
|
||||
}
|
||||
|
||||
void set_bid(Decimal price, Decimal quantity) {
|
||||
std::unique_lock lock(mutex_);
|
||||
for (auto& level : bids_) {
|
||||
if (level.price == price) {
|
||||
level.quantity = quantity;
|
||||
return;
|
||||
}
|
||||
}
|
||||
bids_.push_back({price, quantity});
|
||||
}
|
||||
|
||||
void set_ask(Decimal price, Decimal quantity) {
|
||||
std::unique_lock lock(mutex_);
|
||||
for (auto& level : asks_) {
|
||||
if (level.price == price) {
|
||||
level.quantity = quantity;
|
||||
return;
|
||||
}
|
||||
}
|
||||
asks_.push_back({price, quantity});
|
||||
}
|
||||
|
||||
void remove_bid(Decimal price) {
|
||||
std::unique_lock lock(mutex_);
|
||||
bids_.erase(std::remove_if(bids_.begin(), bids_.end(),
|
||||
[price](const PriceLevel& l) { return l.price == price; }), bids_.end());
|
||||
}
|
||||
|
||||
void remove_ask(Decimal price) {
|
||||
std::unique_lock lock(mutex_);
|
||||
asks_.erase(std::remove_if(asks_.begin(), asks_.end(),
|
||||
[price](const PriceLevel& l) { return l.price == price; }), asks_.end());
|
||||
}
|
||||
|
||||
void clear() {
|
||||
std::unique_lock lock(mutex_);
|
||||
bids_.clear();
|
||||
asks_.clear();
|
||||
}
|
||||
|
||||
void sort() {
|
||||
std::unique_lock lock(mutex_);
|
||||
std::sort(bids_.begin(), bids_.end(),
|
||||
[](const PriceLevel& a, const PriceLevel& b) { return a.price > b.price; });
|
||||
std::sort(asks_.begin(), asks_.end(),
|
||||
[](const PriceLevel& a, const PriceLevel& b) { return a.price < b.price; });
|
||||
sequence_.fetch_add(1, std::memory_order_release);
|
||||
timestamp_.store(now_ms(), std::memory_order_release);
|
||||
}
|
||||
|
||||
void set_timestamp(int64_t ts) { timestamp_.store(ts, std::memory_order_release); }
|
||||
void set_sequence(uint64_t seq) { sequence_.store(seq, std::memory_order_release); }
|
||||
|
||||
// Readers (lock-free via snapshot)
|
||||
[[nodiscard]] std::vector<PriceLevel> bids() const {
|
||||
std::shared_lock lock(mutex_);
|
||||
return bids_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<PriceLevel> asks() const {
|
||||
std::shared_lock lock(mutex_);
|
||||
return asks_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<Decimal> best_bid() const {
|
||||
std::shared_lock lock(mutex_);
|
||||
return bids_.empty() ? std::nullopt : std::optional<Decimal>(bids_[0].price);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<Decimal> best_ask() const {
|
||||
std::shared_lock lock(mutex_);
|
||||
return asks_.empty() ? std::nullopt : std::optional<Decimal>(asks_[0].price);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<Decimal> mid_price() const {
|
||||
std::shared_lock lock(mutex_);
|
||||
if (bids_.empty() || asks_.empty()) return std::nullopt;
|
||||
return (bids_[0].price + asks_[0].price) / Decimal::from_double(2.0);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<Decimal> spread() const {
|
||||
std::shared_lock lock(mutex_);
|
||||
if (bids_.empty() || asks_.empty()) return std::nullopt;
|
||||
return asks_[0].price - bids_[0].price;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<Decimal> spread_percent() const {
|
||||
auto s = spread();
|
||||
auto m = mid_price();
|
||||
if (!s || !m || m->is_zero()) return std::nullopt;
|
||||
return (*s / *m) * Decimal::from_double(100.0);
|
||||
}
|
||||
|
||||
[[nodiscard]] Decimal bid_liquidity() const {
|
||||
std::shared_lock lock(mutex_);
|
||||
Decimal total;
|
||||
for (const auto& level : bids_) {
|
||||
total = total + level.value();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
[[nodiscard]] Decimal ask_liquidity() const {
|
||||
std::shared_lock lock(mutex_);
|
||||
Decimal total;
|
||||
for (const auto& level : asks_) {
|
||||
total = total + level.value();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
[[nodiscard]] Decimal bid_depth(int levels) const {
|
||||
std::shared_lock lock(mutex_);
|
||||
Decimal total;
|
||||
int count = 0;
|
||||
for (const auto& level : bids_) {
|
||||
if (count >= levels) break;
|
||||
total = total + level.value();
|
||||
++count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
[[nodiscard]] Decimal ask_depth(int levels) const {
|
||||
std::shared_lock lock(mutex_);
|
||||
Decimal total;
|
||||
int count = 0;
|
||||
for (const auto& level : asks_) {
|
||||
if (count >= levels) break;
|
||||
total = total + level.value();
|
||||
++count;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// VWAP calculation for buying `amount`
|
||||
[[nodiscard]] std::optional<Decimal> vwap_buy(Decimal amount) const {
|
||||
std::shared_lock lock(mutex_);
|
||||
return calculate_vwap(asks_, amount);
|
||||
}
|
||||
|
||||
// VWAP calculation for selling `amount`
|
||||
[[nodiscard]] std::optional<Decimal> vwap_sell(Decimal amount) const {
|
||||
std::shared_lock lock(mutex_);
|
||||
return calculate_vwap(bids_, amount);
|
||||
}
|
||||
|
||||
// Check if sufficient liquidity exists
|
||||
[[nodiscard]] bool has_liquidity(Side side, Decimal amount) const {
|
||||
std::shared_lock lock(mutex_);
|
||||
const auto& levels = (side == Side::Buy) ? asks_ : bids_;
|
||||
Decimal total;
|
||||
for (const auto& level : levels) {
|
||||
total = total + level.quantity;
|
||||
}
|
||||
return total >= amount;
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] static std::optional<Decimal> calculate_vwap(
|
||||
const std::vector<PriceLevel>& levels, Decimal amount) {
|
||||
Decimal remaining = amount;
|
||||
Decimal total_value;
|
||||
Decimal total_qty;
|
||||
|
||||
for (const auto& level : levels) {
|
||||
if (remaining <= Decimal::zero()) break;
|
||||
|
||||
Decimal fill_qty = (remaining < level.quantity) ? remaining : level.quantity;
|
||||
total_value = total_value + (fill_qty * level.price);
|
||||
total_qty = total_qty + fill_qty;
|
||||
remaining = remaining - fill_qty;
|
||||
}
|
||||
|
||||
if (total_qty.is_zero()) return std::nullopt;
|
||||
return total_value / total_qty;
|
||||
}
|
||||
|
||||
std::string symbol_;
|
||||
std::string venue_;
|
||||
std::atomic<int64_t> timestamp_;
|
||||
std::atomic<uint64_t> sequence_;
|
||||
mutable std::shared_mutex mutex_;
|
||||
std::vector<PriceLevel> bids_;
|
||||
std::vector<PriceLevel> asks_;
|
||||
};
|
||||
|
||||
// Hash functor for Decimal
|
||||
struct DecimalHash {
|
||||
size_t operator()(const Decimal& d) const {
|
||||
return std::hash<int64_t>()(d.scaled_value());
|
||||
}
|
||||
};
|
||||
|
||||
// Type alias for price-level map
|
||||
using PriceLevelMap = std::unordered_map<Decimal, std::vector<std::pair<std::string, Decimal>>, DecimalHash>;
|
||||
|
||||
// Aggregated orderbook from multiple venues
|
||||
class AggregatedOrderbook {
|
||||
public:
|
||||
explicit AggregatedOrderbook(std::string_view symbol) : symbol_(symbol) {}
|
||||
|
||||
[[nodiscard]] const std::string& symbol() const { return symbol_; }
|
||||
[[nodiscard]] int64_t timestamp() const { return timestamp_; }
|
||||
|
||||
void add_orderbook(const Orderbook& book) {
|
||||
auto bids = book.bids();
|
||||
auto asks = book.asks();
|
||||
|
||||
for (const auto& level : bids) {
|
||||
bids_[level.price].emplace_back(book.venue(), level.quantity);
|
||||
}
|
||||
|
||||
for (const auto& level : asks) {
|
||||
asks_[level.price].emplace_back(book.venue(), level.quantity);
|
||||
}
|
||||
|
||||
timestamp_ = std::max(timestamp_, book.timestamp());
|
||||
}
|
||||
|
||||
// Best bid across all venues: (price, venue, qty)
|
||||
[[nodiscard]] std::optional<std::tuple<Decimal, std::string, Decimal>> best_bid() const {
|
||||
if (bids_.empty()) return std::nullopt;
|
||||
|
||||
auto it = std::max_element(bids_.begin(), bids_.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
|
||||
if (it == bids_.end() || it->second.empty()) return std::nullopt;
|
||||
|
||||
const auto& [venue, qty] = it->second[0];
|
||||
return std::make_tuple(it->first, venue, qty);
|
||||
}
|
||||
|
||||
// Best ask across all venues: (price, venue, qty)
|
||||
[[nodiscard]] std::optional<std::tuple<Decimal, std::string, Decimal>> best_ask() const {
|
||||
if (asks_.empty()) return std::nullopt;
|
||||
|
||||
auto it = std::min_element(asks_.begin(), asks_.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
|
||||
if (it == asks_.end() || it->second.empty()) return std::nullopt;
|
||||
|
||||
const auto& [venue, qty] = it->second[0];
|
||||
return std::make_tuple(it->first, venue, qty);
|
||||
}
|
||||
|
||||
// Get aggregated bid levels
|
||||
[[nodiscard]] std::vector<PriceLevel> aggregated_bids() const {
|
||||
std::vector<std::pair<Decimal, Decimal>> temp;
|
||||
for (const auto& [price, venues] : bids_) {
|
||||
Decimal total_qty;
|
||||
for (const auto& [v, qty] : venues) {
|
||||
total_qty = total_qty + qty;
|
||||
}
|
||||
temp.emplace_back(price, total_qty);
|
||||
}
|
||||
std::sort(temp.begin(), temp.end(),
|
||||
[](const auto& a, const auto& b) { return a.first > b.first; });
|
||||
|
||||
std::vector<PriceLevel> result;
|
||||
result.reserve(temp.size());
|
||||
for (const auto& [price, qty] : temp) {
|
||||
result.push_back({price, qty});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get aggregated ask levels
|
||||
[[nodiscard]] std::vector<PriceLevel> aggregated_asks() const {
|
||||
std::vector<std::pair<Decimal, Decimal>> temp;
|
||||
for (const auto& [price, venues] : asks_) {
|
||||
Decimal total_qty;
|
||||
for (const auto& [v, qty] : venues) {
|
||||
total_qty = total_qty + qty;
|
||||
}
|
||||
temp.emplace_back(price, total_qty);
|
||||
}
|
||||
std::sort(temp.begin(), temp.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
|
||||
std::vector<PriceLevel> result;
|
||||
result.reserve(temp.size());
|
||||
for (const auto& [price, qty] : temp) {
|
||||
result.push_back({price, qty});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Find best venue for buying amount: (venue, price)
|
||||
[[nodiscard]] std::optional<std::pair<std::string, Decimal>> best_venue_buy(Decimal amount) const {
|
||||
if (asks_.empty()) return std::nullopt;
|
||||
|
||||
// Use the same direct iteration approach as aggregated_asks
|
||||
// Find the lowest price ask level
|
||||
std::optional<Decimal> best_price;
|
||||
std::string best_venue;
|
||||
|
||||
for (const auto& [price, venues] : asks_) {
|
||||
if ((!best_price || price < *best_price) && !venues.empty()) {
|
||||
best_price = price;
|
||||
best_venue = venues[0].first;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_venue.empty() || !best_price) return std::nullopt;
|
||||
return std::make_pair(best_venue, *best_price);
|
||||
}
|
||||
|
||||
// Find best venue for selling amount: (venue, price)
|
||||
[[nodiscard]] std::optional<std::pair<std::string, Decimal>> best_venue_sell(Decimal amount) const {
|
||||
if (bids_.empty()) return std::nullopt;
|
||||
|
||||
// Find the highest price bid level
|
||||
std::optional<Decimal> best_price;
|
||||
std::string best_venue;
|
||||
|
||||
for (const auto& [price, venues] : bids_) {
|
||||
if ((!best_price || price > *best_price) && !venues.empty()) {
|
||||
best_price = price;
|
||||
best_venue = venues[0].first;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_venue.empty() || !best_price) return std::nullopt;
|
||||
return std::make_pair(best_venue, *best_price);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
bids_.clear();
|
||||
asks_.clear();
|
||||
timestamp_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string symbol_;
|
||||
int64_t timestamp_ = 0;
|
||||
|
||||
// price -> [(venue, qty), ...]
|
||||
PriceLevelMap bids_;
|
||||
PriceLevelMap asks_;
|
||||
};
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,97 +0,0 @@
|
||||
// LX Trading SDK - Risk Management
|
||||
// Thread-safe risk controls and position tracking
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <lx/trading/config.hpp>
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Risk limit violation exception
|
||||
class RiskError : public std::runtime_error {
|
||||
public:
|
||||
explicit RiskError(const std::string& msg) : std::runtime_error(msg) {}
|
||||
};
|
||||
|
||||
// Thread-safe risk manager
|
||||
class RiskManager {
|
||||
public:
|
||||
explicit RiskManager(const RiskConfig& config);
|
||||
|
||||
// Configuration
|
||||
[[nodiscard]] bool is_enabled() const noexcept { return config_.enabled; }
|
||||
[[nodiscard]] const RiskConfig& config() const noexcept { return config_; }
|
||||
|
||||
// Kill switch
|
||||
[[nodiscard]] bool is_killed() const noexcept { return kill_switch_.load(std::memory_order_acquire); }
|
||||
void kill() noexcept { kill_switch_.store(true, std::memory_order_release); }
|
||||
void reset() noexcept { kill_switch_.store(false, std::memory_order_release); }
|
||||
|
||||
// Order validation - throws RiskError if invalid
|
||||
void validate_order(const OrderRequest& request);
|
||||
|
||||
// Position tracking
|
||||
void update_position(const std::string& asset, Decimal quantity, Side side);
|
||||
[[nodiscard]] Decimal position(const std::string& asset) const;
|
||||
[[nodiscard]] std::unordered_map<std::string, Decimal> positions() const;
|
||||
|
||||
// PnL tracking
|
||||
void update_pnl(Decimal pnl);
|
||||
[[nodiscard]] Decimal daily_pnl() const;
|
||||
void reset_daily_pnl();
|
||||
|
||||
// Order tracking
|
||||
void order_opened(const std::string& symbol);
|
||||
void order_closed(const std::string& symbol);
|
||||
[[nodiscard]] int open_orders(const std::string& symbol) const;
|
||||
|
||||
// Pre-trade checks (return false instead of throwing)
|
||||
[[nodiscard]] bool check_order_size(Decimal quantity) const noexcept;
|
||||
[[nodiscard]] bool check_position_limit(const std::string& asset, Decimal new_position) const noexcept;
|
||||
[[nodiscard]] bool check_daily_loss() const noexcept;
|
||||
[[nodiscard]] bool check_open_orders(const std::string& symbol) const noexcept;
|
||||
|
||||
private:
|
||||
RiskConfig config_;
|
||||
std::atomic<bool> kill_switch_{false};
|
||||
|
||||
mutable std::shared_mutex position_mutex_;
|
||||
std::unordered_map<std::string, Decimal> positions_;
|
||||
|
||||
mutable std::shared_mutex pnl_mutex_;
|
||||
Decimal daily_pnl_;
|
||||
|
||||
mutable std::shared_mutex orders_mutex_;
|
||||
std::unordered_map<std::string, int> open_orders_;
|
||||
};
|
||||
|
||||
// RAII order tracker
|
||||
class OrderTracker {
|
||||
public:
|
||||
OrderTracker(RiskManager& rm, const std::string& symbol)
|
||||
: risk_manager_(rm), symbol_(symbol) {
|
||||
risk_manager_.order_opened(symbol_);
|
||||
}
|
||||
|
||||
~OrderTracker() {
|
||||
if (!released_) {
|
||||
risk_manager_.order_closed(symbol_);
|
||||
}
|
||||
}
|
||||
|
||||
void release() { released_ = true; }
|
||||
|
||||
private:
|
||||
RiskManager& risk_manager_;
|
||||
std::string symbol_;
|
||||
bool released_ = false;
|
||||
};
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,417 +0,0 @@
|
||||
// LX Trading SDK - Core Types
|
||||
// Zero-copy, cache-friendly structures for HFT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Fixed-point decimal for exact financial arithmetic
|
||||
// Stores value as integer * 10^(-precision)
|
||||
class Decimal {
|
||||
public:
|
||||
static constexpr int PRECISION = 8;
|
||||
static constexpr int64_t SCALE = 100000000LL;
|
||||
|
||||
constexpr Decimal() noexcept : value_(0) {}
|
||||
constexpr explicit Decimal(int64_t scaled) noexcept : value_(scaled) {}
|
||||
|
||||
static Decimal from_double(double d) noexcept {
|
||||
return Decimal(static_cast<int64_t>(d * SCALE));
|
||||
}
|
||||
|
||||
static Decimal from_string(std::string_view s);
|
||||
|
||||
[[nodiscard]] double to_double() const noexcept {
|
||||
return static_cast<double>(value_) / SCALE;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string to_string() const;
|
||||
|
||||
[[nodiscard]] int64_t scaled_value() const noexcept { return value_; }
|
||||
|
||||
constexpr Decimal operator+(Decimal rhs) const noexcept {
|
||||
return Decimal(value_ + rhs.value_);
|
||||
}
|
||||
constexpr Decimal operator-(Decimal rhs) const noexcept {
|
||||
return Decimal(value_ - rhs.value_);
|
||||
}
|
||||
constexpr Decimal operator*(Decimal rhs) const noexcept {
|
||||
return Decimal((value_ * rhs.value_) / SCALE);
|
||||
}
|
||||
constexpr Decimal operator/(Decimal rhs) const noexcept {
|
||||
return Decimal((value_ * SCALE) / rhs.value_);
|
||||
}
|
||||
|
||||
constexpr bool operator==(Decimal rhs) const noexcept { return value_ == rhs.value_; }
|
||||
constexpr bool operator!=(Decimal rhs) const noexcept { return value_ != rhs.value_; }
|
||||
constexpr bool operator<(Decimal rhs) const noexcept { return value_ < rhs.value_; }
|
||||
constexpr bool operator<=(Decimal rhs) const noexcept { return value_ <= rhs.value_; }
|
||||
constexpr bool operator>(Decimal rhs) const noexcept { return value_ > rhs.value_; }
|
||||
constexpr bool operator>=(Decimal rhs) const noexcept { return value_ >= rhs.value_; }
|
||||
|
||||
constexpr Decimal abs() const noexcept { return Decimal(value_ < 0 ? -value_ : value_); }
|
||||
constexpr bool is_zero() const noexcept { return value_ == 0; }
|
||||
constexpr bool is_positive() const noexcept { return value_ > 0; }
|
||||
constexpr bool is_negative() const noexcept { return value_ < 0; }
|
||||
|
||||
static constexpr Decimal zero() noexcept { return Decimal(0); }
|
||||
static constexpr Decimal one() noexcept { return Decimal(SCALE); }
|
||||
|
||||
private:
|
||||
int64_t value_;
|
||||
};
|
||||
|
||||
// Trading side
|
||||
enum class Side : uint8_t {
|
||||
Buy = 0,
|
||||
Sell = 1
|
||||
};
|
||||
|
||||
inline constexpr const char* to_string(Side s) noexcept {
|
||||
return s == Side::Buy ? "buy" : "sell";
|
||||
}
|
||||
|
||||
// Order types
|
||||
enum class OrderType : uint8_t {
|
||||
Market = 0,
|
||||
Limit = 1,
|
||||
LimitMaker = 2,
|
||||
StopLoss = 3,
|
||||
StopLossLimit = 4,
|
||||
TakeProfit = 5,
|
||||
TakeProfitLimit = 6
|
||||
};
|
||||
|
||||
inline constexpr const char* to_string(OrderType t) noexcept {
|
||||
switch (t) {
|
||||
case OrderType::Market: return "market";
|
||||
case OrderType::Limit: return "limit";
|
||||
case OrderType::LimitMaker: return "limit_maker";
|
||||
case OrderType::StopLoss: return "stop_loss";
|
||||
case OrderType::StopLossLimit: return "stop_loss_limit";
|
||||
case OrderType::TakeProfit: return "take_profit";
|
||||
case OrderType::TakeProfitLimit: return "take_profit_limit";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// Time in force
|
||||
enum class TimeInForce : uint8_t {
|
||||
GTC = 0, // Good till cancelled
|
||||
IOC = 1, // Immediate or cancel
|
||||
FOK = 2, // Fill or kill
|
||||
GTD = 3, // Good till date
|
||||
PostOnly = 4
|
||||
};
|
||||
|
||||
inline constexpr const char* to_string(TimeInForce tif) noexcept {
|
||||
switch (tif) {
|
||||
case TimeInForce::GTC: return "GTC";
|
||||
case TimeInForce::IOC: return "IOC";
|
||||
case TimeInForce::FOK: return "FOK";
|
||||
case TimeInForce::GTD: return "GTD";
|
||||
case TimeInForce::PostOnly: return "POST_ONLY";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// Order status
|
||||
enum class OrderStatus : uint8_t {
|
||||
Pending = 0,
|
||||
Open = 1,
|
||||
PartiallyFilled = 2,
|
||||
Filled = 3,
|
||||
Cancelled = 4,
|
||||
Rejected = 5,
|
||||
Expired = 6
|
||||
};
|
||||
|
||||
inline constexpr const char* to_string(OrderStatus s) noexcept {
|
||||
switch (s) {
|
||||
case OrderStatus::Pending: return "pending";
|
||||
case OrderStatus::Open: return "open";
|
||||
case OrderStatus::PartiallyFilled: return "partially_filled";
|
||||
case OrderStatus::Filled: return "filled";
|
||||
case OrderStatus::Cancelled: return "cancelled";
|
||||
case OrderStatus::Rejected: return "rejected";
|
||||
case OrderStatus::Expired: return "expired";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// Venue type
|
||||
enum class VenueType : uint8_t {
|
||||
Native = 0,
|
||||
Ccxt = 1,
|
||||
Hummingbot = 2,
|
||||
Custom = 3
|
||||
};
|
||||
|
||||
// Trading pair - inline storage for performance
|
||||
struct TradingPair {
|
||||
std::array<char, 16> base{};
|
||||
std::array<char, 16> quote{};
|
||||
|
||||
static std::optional<TradingPair> from_symbol(std::string_view symbol);
|
||||
std::string to_hummingbot() const;
|
||||
std::string to_ccxt() const;
|
||||
std::string to_string() const { return to_hummingbot(); }
|
||||
};
|
||||
|
||||
// Fee information
|
||||
struct Fee {
|
||||
std::string asset;
|
||||
Decimal amount;
|
||||
std::optional<Decimal> rate;
|
||||
};
|
||||
|
||||
// Balance
|
||||
struct Balance {
|
||||
std::string asset;
|
||||
std::string venue;
|
||||
Decimal free;
|
||||
Decimal locked;
|
||||
|
||||
[[nodiscard]] Decimal total() const noexcept { return free + locked; }
|
||||
};
|
||||
|
||||
// Aggregated balance across venues
|
||||
struct AggregatedBalance {
|
||||
std::string asset;
|
||||
Decimal total_free;
|
||||
Decimal total_locked;
|
||||
std::vector<Balance> by_venue;
|
||||
|
||||
[[nodiscard]] Decimal total() const noexcept { return total_free + total_locked; }
|
||||
};
|
||||
|
||||
// Price level in orderbook
|
||||
struct PriceLevel {
|
||||
Decimal price;
|
||||
Decimal quantity;
|
||||
|
||||
[[nodiscard]] Decimal value() const noexcept { return price * quantity; }
|
||||
};
|
||||
|
||||
// Order request - builder pattern
|
||||
class OrderRequest {
|
||||
public:
|
||||
std::string symbol;
|
||||
Side side = Side::Buy;
|
||||
OrderType order_type = OrderType::Market;
|
||||
Decimal quantity;
|
||||
std::optional<Decimal> price;
|
||||
std::optional<Decimal> stop_price;
|
||||
TimeInForce time_in_force = TimeInForce::GTC;
|
||||
bool reduce_only = false;
|
||||
bool post_only = false;
|
||||
std::optional<std::string> venue;
|
||||
std::string client_order_id;
|
||||
|
||||
OrderRequest() = default;
|
||||
|
||||
static OrderRequest market(std::string_view symbol, Side side, Decimal quantity);
|
||||
static OrderRequest limit(std::string_view symbol, Side side, Decimal quantity, Decimal price);
|
||||
|
||||
OrderRequest& with_venue(std::string_view v) {
|
||||
venue = std::string(v);
|
||||
return *this;
|
||||
}
|
||||
|
||||
OrderRequest& with_post_only() {
|
||||
post_only = true;
|
||||
time_in_force = TimeInForce::PostOnly;
|
||||
return *this;
|
||||
}
|
||||
|
||||
OrderRequest& with_client_id(std::string_view id) {
|
||||
client_order_id = std::string(id);
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
// Order
|
||||
struct Order {
|
||||
std::string order_id;
|
||||
std::string client_order_id;
|
||||
std::string symbol;
|
||||
std::string venue;
|
||||
Side side = Side::Buy;
|
||||
OrderType order_type = OrderType::Limit;
|
||||
OrderStatus status = OrderStatus::Pending;
|
||||
Decimal quantity;
|
||||
Decimal filled_quantity;
|
||||
Decimal remaining_quantity;
|
||||
std::optional<Decimal> price;
|
||||
std::optional<Decimal> average_price;
|
||||
int64_t created_at = 0;
|
||||
int64_t updated_at = 0;
|
||||
std::vector<Fee> fees;
|
||||
|
||||
[[nodiscard]] bool is_open() const noexcept {
|
||||
return status == OrderStatus::Open ||
|
||||
status == OrderStatus::PartiallyFilled ||
|
||||
status == OrderStatus::Pending;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_done() const noexcept {
|
||||
return status == OrderStatus::Filled ||
|
||||
status == OrderStatus::Cancelled ||
|
||||
status == OrderStatus::Rejected ||
|
||||
status == OrderStatus::Expired;
|
||||
}
|
||||
|
||||
[[nodiscard]] Decimal fill_percent() const noexcept {
|
||||
if (quantity.is_zero()) return Decimal::zero();
|
||||
return (filled_quantity / quantity) * Decimal::from_double(100.0);
|
||||
}
|
||||
};
|
||||
|
||||
// Trade/fill
|
||||
struct Trade {
|
||||
std::string trade_id;
|
||||
std::string order_id;
|
||||
std::string symbol;
|
||||
std::string venue;
|
||||
Side side = Side::Buy;
|
||||
Decimal price;
|
||||
Decimal quantity;
|
||||
Fee fee;
|
||||
int64_t timestamp = 0;
|
||||
bool is_maker = false;
|
||||
|
||||
[[nodiscard]] Decimal value() const noexcept { return price * quantity; }
|
||||
};
|
||||
|
||||
// Ticker
|
||||
struct Ticker {
|
||||
std::string symbol;
|
||||
std::string venue;
|
||||
std::optional<Decimal> bid;
|
||||
std::optional<Decimal> ask;
|
||||
std::optional<Decimal> last;
|
||||
std::optional<Decimal> volume_24h;
|
||||
std::optional<Decimal> high_24h;
|
||||
std::optional<Decimal> low_24h;
|
||||
std::optional<Decimal> change_24h;
|
||||
int64_t timestamp = 0;
|
||||
|
||||
[[nodiscard]] std::optional<Decimal> mid_price() const noexcept {
|
||||
if (bid && ask) {
|
||||
return (*bid + *ask) / Decimal::from_double(2.0);
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<Decimal> spread() const noexcept {
|
||||
if (bid && ask) {
|
||||
return *ask - *bid;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<Decimal> spread_percent() const noexcept {
|
||||
if (bid && ask && bid->is_positive()) {
|
||||
return ((*ask - *bid) / *bid) * Decimal::from_double(100.0);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
// AMM swap quote
|
||||
struct SwapQuote {
|
||||
std::string base_token;
|
||||
std::string quote_token;
|
||||
Decimal input_amount;
|
||||
Decimal output_amount;
|
||||
Decimal price;
|
||||
Decimal price_impact;
|
||||
Decimal fee;
|
||||
std::vector<std::string> route;
|
||||
int64_t expires_at = 0;
|
||||
};
|
||||
|
||||
// Pool information
|
||||
struct PoolInfo {
|
||||
std::string address;
|
||||
std::string base_token;
|
||||
std::string quote_token;
|
||||
Decimal base_reserve;
|
||||
Decimal quote_reserve;
|
||||
Decimal total_liquidity;
|
||||
Decimal fee_rate;
|
||||
std::optional<Decimal> apy;
|
||||
};
|
||||
|
||||
// LP position
|
||||
struct LpPosition {
|
||||
std::string pool_address;
|
||||
std::string base_token;
|
||||
std::string quote_token;
|
||||
Decimal lp_tokens;
|
||||
Decimal base_amount;
|
||||
Decimal quote_amount;
|
||||
Decimal share_percent;
|
||||
std::optional<Decimal> unrealized_pnl;
|
||||
};
|
||||
|
||||
// Liquidity operation result
|
||||
struct LiquidityResult {
|
||||
std::string tx_hash;
|
||||
std::string pool_address;
|
||||
Decimal base_amount;
|
||||
Decimal quote_amount;
|
||||
Decimal lp_tokens;
|
||||
Decimal share_percent;
|
||||
};
|
||||
|
||||
// Venue information
|
||||
struct VenueInfo {
|
||||
std::string name;
|
||||
VenueType venue_type = VenueType::Native;
|
||||
bool connected = false;
|
||||
std::optional<int> latency_ms;
|
||||
std::vector<std::string> supported_pairs;
|
||||
Decimal maker_fee;
|
||||
Decimal taker_fee;
|
||||
};
|
||||
|
||||
// Market information
|
||||
struct MarketInfo {
|
||||
std::string symbol;
|
||||
std::string base;
|
||||
std::string quote;
|
||||
int price_precision = 8;
|
||||
int quantity_precision = 8;
|
||||
Decimal min_quantity;
|
||||
std::optional<Decimal> max_quantity;
|
||||
std::optional<Decimal> min_notional;
|
||||
Decimal tick_size;
|
||||
Decimal lot_size;
|
||||
};
|
||||
|
||||
// Timestamp utilities
|
||||
inline int64_t now_ms() noexcept {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
inline int64_t now_us() noexcept {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<microseconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
inline int64_t now_ns() noexcept {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<nanoseconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,10 +0,0 @@
|
||||
// LX Trading SDK - Adapter Base Implementation
|
||||
|
||||
#include <lx/trading/adapter.hpp>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Base adapter has no implementation - pure interface
|
||||
// This file exists for potential future common functionality
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,160 +0,0 @@
|
||||
// LX Trading SDK - Cross-Chain Router Implementation
|
||||
|
||||
#include <lx/trading/arbitrage/cross_chain.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
CrossChainRouter::CrossChainRouter(CrossChainConfig config)
|
||||
: config_(std::move(config)) {}
|
||||
|
||||
void CrossChainRouter::set_warp_client(std::shared_ptr<WarpClient> client) {
|
||||
std::lock_guard<std::mutex> lock(warp_mutex_);
|
||||
warp_client_ = std::move(client);
|
||||
}
|
||||
|
||||
void CrossChainRouter::set_teleport_client(std::shared_ptr<TeleportClient> client) {
|
||||
std::lock_guard<std::mutex> lock(teleport_mutex_);
|
||||
teleport_client_ = std::move(client);
|
||||
}
|
||||
|
||||
std::shared_ptr<WarpClient> CrossChainRouter::warp() const {
|
||||
std::lock_guard<std::mutex> lock(warp_mutex_);
|
||||
return warp_client_;
|
||||
}
|
||||
|
||||
std::shared_ptr<TeleportClient> CrossChainRouter::teleport() const {
|
||||
std::lock_guard<std::mutex> lock(teleport_mutex_);
|
||||
return teleport_client_;
|
||||
}
|
||||
|
||||
CrossChainTransport CrossChainRouter::determine_transport(
|
||||
const std::string& source_chain,
|
||||
const std::string& dest_chain) const {
|
||||
|
||||
auto src_it = config_.chains.find(source_chain);
|
||||
auto dst_it = config_.chains.find(dest_chain);
|
||||
|
||||
// Same chain = direct
|
||||
if (source_chain == dest_chain) {
|
||||
return CrossChainTransport::Direct;
|
||||
}
|
||||
|
||||
// CEX = API
|
||||
if (src_it != config_.chains.end() && src_it->second.chain_type == ChainType::Cex) {
|
||||
return CrossChainTransport::CexApi;
|
||||
}
|
||||
if (dst_it != config_.chains.end() && dst_it->second.chain_type == ChainType::Cex) {
|
||||
return CrossChainTransport::CexApi;
|
||||
}
|
||||
|
||||
// Both Lux subnets = Warp (fastest)
|
||||
if (src_it != config_.chains.end() && dst_it != config_.chains.end()) {
|
||||
const auto& src = src_it->second;
|
||||
const auto& dst = dst_it->second;
|
||||
|
||||
if (src.chain_type == ChainType::LuxSubnet &&
|
||||
dst.chain_type == ChainType::LuxSubnet &&
|
||||
src.warp_supported && dst.warp_supported &&
|
||||
config_.warp_enabled) {
|
||||
return CrossChainTransport::Warp;
|
||||
}
|
||||
}
|
||||
|
||||
// Both EVM or mixed = Teleport
|
||||
if (src_it != config_.chains.end() && dst_it != config_.chains.end()) {
|
||||
const auto& src = src_it->second;
|
||||
const auto& dst = dst_it->second;
|
||||
|
||||
if (src.teleport_supported && dst.teleport_supported && config_.teleport_enabled) {
|
||||
return CrossChainTransport::Teleport;
|
||||
}
|
||||
}
|
||||
|
||||
// No viable transport - return Direct as fallback
|
||||
return CrossChainTransport::Direct;
|
||||
}
|
||||
|
||||
int64_t CrossChainRouter::estimate_latency(
|
||||
const std::string& source_chain,
|
||||
const std::string& dest_chain) const {
|
||||
|
||||
CrossChainTransport transport = determine_transport(source_chain, dest_chain);
|
||||
|
||||
switch (transport) {
|
||||
case CrossChainTransport::Direct:
|
||||
return 0;
|
||||
case CrossChainTransport::Warp:
|
||||
return 500; // Sub-second
|
||||
case CrossChainTransport::CexApi:
|
||||
return 100; // API call
|
||||
case CrossChainTransport::Teleport: {
|
||||
auto src_it = config_.chains.find(source_chain);
|
||||
if (src_it != config_.chains.end()) {
|
||||
return static_cast<int64_t>(src_it->second.finality_ms) + 10000; // Finality + processing
|
||||
}
|
||||
return 3600000; // 1 hour fallback
|
||||
}
|
||||
}
|
||||
return 3600000;
|
||||
}
|
||||
|
||||
Decimal CrossChainRouter::estimate_cost(
|
||||
const std::string& source_chain,
|
||||
const std::string& dest_chain,
|
||||
const std::string& token,
|
||||
Decimal amount) const {
|
||||
|
||||
CrossChainTransport transport = determine_transport(source_chain, dest_chain);
|
||||
|
||||
switch (transport) {
|
||||
case CrossChainTransport::Direct:
|
||||
return Decimal::zero();
|
||||
case CrossChainTransport::Warp:
|
||||
return Decimal::from_double(0.001); // Nearly free
|
||||
case CrossChainTransport::CexApi:
|
||||
return Decimal::zero(); // No bridge cost
|
||||
case CrossChainTransport::Teleport: {
|
||||
std::lock_guard<std::mutex> lock(teleport_mutex_);
|
||||
if (teleport_client_) {
|
||||
try {
|
||||
return teleport_client_->estimate_bridge_fee(dest_chain, token, amount);
|
||||
} catch (...) {
|
||||
return Decimal::from_double(1.0); // Fallback estimate
|
||||
}
|
||||
}
|
||||
return Decimal::from_double(1.0); // Estimate $1
|
||||
}
|
||||
}
|
||||
return Decimal::zero();
|
||||
}
|
||||
|
||||
std::string CrossChainRouter::venue_to_chain(const std::string& venue) const {
|
||||
for (const auto& [chain_id, info] : config_.chains) {
|
||||
for (const auto& v : info.venues) {
|
||||
if (v == venue) {
|
||||
return chain_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return venue; // Fallback to venue name
|
||||
}
|
||||
|
||||
EnhancedOpportunity CrossChainRouter::enhance_opportunity(const UnifiedOpportunity& opp) const {
|
||||
std::string buy_chain = venue_to_chain(opp.buy_venue);
|
||||
std::string sell_chain = venue_to_chain(opp.sell_venue);
|
||||
|
||||
CrossChainTransport transport = determine_transport(buy_chain, sell_chain);
|
||||
int64_t estimated_latency = estimate_latency(buy_chain, sell_chain);
|
||||
Decimal bridge_cost = estimate_cost(buy_chain, sell_chain, opp.symbol, opp.max_size);
|
||||
|
||||
return EnhancedOpportunity{
|
||||
.base = opp,
|
||||
.transport = transport,
|
||||
.estimated_latency = estimated_latency,
|
||||
.bridge_cost = bridge_cost,
|
||||
.adjusted_net_profit = opp.net_profit - bridge_cost
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,172 +0,0 @@
|
||||
// LX Trading SDK - LX-First Arbitrage Implementation
|
||||
|
||||
#include <lx/trading/arbitrage/lx_first.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
LxFirstArbitrage::LxFirstArbitrage(LxFirstConfig config)
|
||||
: config_(std::move(config)) {}
|
||||
|
||||
void LxFirstArbitrage::update_lx_price(const LxPrice& price) {
|
||||
std::string symbol = price.symbol;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lx_prices_mutex_);
|
||||
lx_prices_[symbol] = price;
|
||||
}
|
||||
|
||||
// Immediately check for opportunities against stale venues
|
||||
check_opportunities(symbol);
|
||||
}
|
||||
|
||||
void LxFirstArbitrage::update_venue_price(const VenuePrice& price) {
|
||||
std::lock_guard<std::mutex> lock(venue_prices_mutex_);
|
||||
auto& prices = venue_prices_[price.symbol];
|
||||
|
||||
// Update or append
|
||||
bool found = false;
|
||||
for (auto& p : prices) {
|
||||
if (p.venue == price.venue) {
|
||||
p = price;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
prices.push_back(price);
|
||||
}
|
||||
}
|
||||
|
||||
void LxFirstArbitrage::on_opportunity(LxFirstCallback callback) {
|
||||
std::lock_guard<std::mutex> lock(callbacks_mutex_);
|
||||
callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void LxFirstArbitrage::start() {
|
||||
running_.store(true);
|
||||
}
|
||||
|
||||
void LxFirstArbitrage::stop() {
|
||||
running_.store(false);
|
||||
}
|
||||
|
||||
void LxFirstArbitrage::check_opportunities(const std::string& symbol) {
|
||||
if (!running_.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LxPrice lx_price;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lx_prices_mutex_);
|
||||
auto it = lx_prices_.find(symbol);
|
||||
if (it == lx_prices_.end()) {
|
||||
return;
|
||||
}
|
||||
lx_price = it->second;
|
||||
}
|
||||
|
||||
std::vector<VenuePrice> vps;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(venue_prices_mutex_);
|
||||
auto it = venue_prices_.find(symbol);
|
||||
if (it == venue_prices_.end()) {
|
||||
return;
|
||||
}
|
||||
vps = it->second;
|
||||
}
|
||||
|
||||
int64_t now = now_ms();
|
||||
|
||||
for (const auto& vp : vps) {
|
||||
// Calculate how stale the venue is
|
||||
int64_t staleness = now - vp.timestamp;
|
||||
if (staleness > config_.max_staleness_ms) {
|
||||
continue; // Too stale, might have updated by now
|
||||
}
|
||||
|
||||
// Check for BUY opportunity (venue ask < LX mid)
|
||||
// The slow venue hasn't caught up to LX's higher price
|
||||
if (vp.ask < lx_price.mid) {
|
||||
Decimal divergence = lx_price.mid - vp.ask;
|
||||
Decimal divergence_bps = (divergence / lx_price.mid) * Decimal::from_double(10000.0);
|
||||
|
||||
if (divergence_bps >= config_.min_divergence_bps) {
|
||||
auto opp = create_opportunity(
|
||||
symbol, lx_price, vp, staleness,
|
||||
"buy", divergence, divergence_bps
|
||||
);
|
||||
if (opp.expected_profit >= config_.min_profit) {
|
||||
std::lock_guard<std::mutex> lock(callbacks_mutex_);
|
||||
for (const auto& callback : callbacks_) {
|
||||
callback(opp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for SELL opportunity (venue bid > LX mid)
|
||||
// The slow venue hasn't caught up to LX's lower price
|
||||
if (vp.bid > lx_price.mid) {
|
||||
Decimal divergence = vp.bid - lx_price.mid;
|
||||
Decimal divergence_bps = (divergence / lx_price.mid) * Decimal::from_double(10000.0);
|
||||
|
||||
if (divergence_bps >= config_.min_divergence_bps) {
|
||||
auto opp = create_opportunity(
|
||||
symbol, lx_price, vp, staleness,
|
||||
"sell", divergence, divergence_bps
|
||||
);
|
||||
if (opp.expected_profit >= config_.min_profit) {
|
||||
std::lock_guard<std::mutex> lock(callbacks_mutex_);
|
||||
for (const auto& callback : callbacks_) {
|
||||
callback(opp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LxFirstOpportunity LxFirstArbitrage::create_opportunity(
|
||||
const std::string& symbol,
|
||||
const LxPrice& lx_price,
|
||||
const VenuePrice& vp,
|
||||
int64_t staleness,
|
||||
const std::string& side,
|
||||
Decimal divergence,
|
||||
Decimal divergence_bps) const {
|
||||
|
||||
int64_t now = now_ms();
|
||||
Decimal expected_profit = divergence * config_.max_position_size;
|
||||
double confidence = calculate_confidence(staleness, divergence_bps);
|
||||
|
||||
LxFirstOpportunity opp;
|
||||
opp.id = symbol + "-" + vp.venue + "-" + side + "-" + std::to_string(now);
|
||||
opp.symbol = symbol;
|
||||
opp.timestamp = now;
|
||||
opp.lx_price = lx_price;
|
||||
opp.stale_venue = vp.venue;
|
||||
opp.stale_price = vp;
|
||||
opp.staleness = staleness;
|
||||
opp.side = side;
|
||||
opp.divergence = divergence;
|
||||
opp.divergence_bps = divergence_bps;
|
||||
opp.expected_profit = expected_profit;
|
||||
opp.max_size = config_.max_position_size;
|
||||
opp.confidence = confidence;
|
||||
|
||||
return opp;
|
||||
}
|
||||
|
||||
double LxFirstArbitrage::calculate_confidence(int64_t staleness, Decimal divergence_bps) const {
|
||||
// Higher confidence when:
|
||||
// 1. Venue is more stale (hasn't had time to update)
|
||||
// 2. Divergence is larger (more room for profit)
|
||||
double staleness_score = std::max(0.0, 1.0 - static_cast<double>(staleness) / 5000.0); // 5s max
|
||||
double divergence_score = std::min(1.0, divergence_bps.to_double() / 100.0); // 100bps = 1.0
|
||||
|
||||
return 0.5 * staleness_score + 0.5 * divergence_score;
|
||||
}
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,366 +0,0 @@
|
||||
// LX Trading SDK - Arbitrage Scanner Implementation
|
||||
|
||||
#include <lx/trading/arbitrage/scanner.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
Scanner::Scanner(ScannerConfig config)
|
||||
: config_(std::move(config)) {}
|
||||
|
||||
Scanner::~Scanner() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void Scanner::add_chain(const CrossChainInfo& info) {
|
||||
std::lock_guard<std::mutex> lock(chains_mutex_);
|
||||
chains_[info.chain_id] = info;
|
||||
}
|
||||
|
||||
void Scanner::update_price(const PriceSource& source) {
|
||||
std::lock_guard<std::mutex> lock(prices_mutex_);
|
||||
auto& sources = prices_[source.symbol];
|
||||
|
||||
// Update existing or append new
|
||||
bool found = false;
|
||||
for (auto& s : sources) {
|
||||
if (s.chain_id == source.chain_id && s.venue == source.venue) {
|
||||
s = source;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
sources.push_back(source);
|
||||
}
|
||||
}
|
||||
|
||||
void Scanner::on_opportunity(OpportunityCallback callback) {
|
||||
std::lock_guard<std::mutex> lock(callbacks_mutex_);
|
||||
callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void Scanner::start() {
|
||||
if (running_.exchange(true)) {
|
||||
return; // Already running
|
||||
}
|
||||
|
||||
scan_thread_ = std::make_unique<std::thread>(&Scanner::scan_loop, this);
|
||||
}
|
||||
|
||||
void Scanner::stop() {
|
||||
running_.store(false);
|
||||
|
||||
if (scan_thread_ && scan_thread_->joinable()) {
|
||||
scan_thread_->join();
|
||||
}
|
||||
scan_thread_.reset();
|
||||
}
|
||||
|
||||
void Scanner::scan_loop() {
|
||||
while (running_.load()) {
|
||||
auto opportunities = scan();
|
||||
|
||||
// Emit opportunities
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callbacks_mutex_);
|
||||
for (const auto& opp : opportunities) {
|
||||
for (const auto& callback : callbacks_) {
|
||||
callback(opp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(config_.scan_interval_ms));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ArbitrageOpportunity> Scanner::scan() {
|
||||
std::vector<ArbitrageOpportunity> opportunities;
|
||||
|
||||
std::lock_guard<std::mutex> lock(prices_mutex_);
|
||||
|
||||
for (const auto& [symbol, sources] : prices_) {
|
||||
if (sources.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int64_t now = now_ms();
|
||||
|
||||
// Filter stale prices
|
||||
std::vector<PriceSource> valid_sources;
|
||||
for (const auto& s : sources) {
|
||||
if (now - s.timestamp < config_.max_price_age_ms) {
|
||||
valid_sources.push_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (valid_sources.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Simple arbitrage
|
||||
auto simple_opps = find_simple_arb(symbol, valid_sources);
|
||||
opportunities.insert(opportunities.end(), simple_opps.begin(), simple_opps.end());
|
||||
|
||||
// CEX-DEX arbitrage
|
||||
auto cex_dex_opps = find_cex_dex_arb(symbol, valid_sources);
|
||||
opportunities.insert(opportunities.end(), cex_dex_opps.begin(), cex_dex_opps.end());
|
||||
}
|
||||
|
||||
return opportunities;
|
||||
}
|
||||
|
||||
std::vector<ArbitrageOpportunity> Scanner::find_simple_arb(
|
||||
const std::string& symbol,
|
||||
const std::vector<PriceSource>& sources) const {
|
||||
|
||||
std::vector<ArbitrageOpportunity> opportunities;
|
||||
|
||||
// Sort by ask (lowest first for buying)
|
||||
std::vector<PriceSource> buy_order = sources;
|
||||
std::sort(buy_order.begin(), buy_order.end(),
|
||||
[](const auto& a, const auto& b) { return a.ask < b.ask; });
|
||||
|
||||
// Sort by bid (highest first for selling)
|
||||
std::vector<PriceSource> sell_order = sources;
|
||||
std::sort(sell_order.begin(), sell_order.end(),
|
||||
[](const auto& a, const auto& b) { return a.bid > b.bid; });
|
||||
|
||||
for (const auto& buy_src : buy_order) {
|
||||
for (const auto& sell_src : sell_order) {
|
||||
// Skip same venue/chain
|
||||
if (buy_src.chain_id == sell_src.chain_id && buy_src.venue == sell_src.venue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate spread
|
||||
Decimal spread = sell_src.bid - buy_src.ask;
|
||||
if (spread <= Decimal::zero()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Decimal spread_bps = (spread / buy_src.ask) * Decimal::from_double(10000.0);
|
||||
if (spread_bps < config_.min_spread_bps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate costs
|
||||
auto [gas_cost, bridge_cost] = calculate_costs(buy_src.chain_id, sell_src.chain_id);
|
||||
|
||||
// Maximum size limited by liquidity
|
||||
Decimal max_size = std::min(buy_src.liquidity, sell_src.liquidity);
|
||||
|
||||
// Calculate PnL
|
||||
Decimal gross_pnl = spread * max_size;
|
||||
Decimal net_pnl = gross_pnl - gas_cost - bridge_cost;
|
||||
|
||||
if (net_pnl < config_.min_profit_usd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate confidence
|
||||
double confidence = calculate_confidence(buy_src, sell_src);
|
||||
|
||||
int64_t now = now_ms();
|
||||
ArbitrageOpportunity opp;
|
||||
opp.id = "simple-" + symbol + "-" + buy_src.venue + "-" + sell_src.venue + "-" + std::to_string(now);
|
||||
opp.arb_type = ArbType::Simple;
|
||||
opp.buy_source = buy_src;
|
||||
opp.sell_source = sell_src;
|
||||
opp.spread_bps = spread_bps;
|
||||
opp.estimated_pnl = gross_pnl;
|
||||
opp.max_size = max_size;
|
||||
opp.gas_cost_usd = gas_cost;
|
||||
opp.bridge_cost_usd = bridge_cost;
|
||||
opp.net_pnl = net_pnl;
|
||||
opp.confidence = confidence;
|
||||
opp.expires_at = now + 5000;
|
||||
|
||||
// Build routes
|
||||
opp.routes.push_back(Route{
|
||||
.chain_id = buy_src.chain_id,
|
||||
.venue = buy_src.venue,
|
||||
.action = "buy",
|
||||
.token_in = "USDC",
|
||||
.token_out = symbol,
|
||||
.amount_in = max_size * buy_src.ask,
|
||||
.expected_out = max_size,
|
||||
.min_amount_out = max_size * Decimal::from_double(0.99)
|
||||
});
|
||||
|
||||
opp.routes.push_back(Route{
|
||||
.chain_id = sell_src.chain_id,
|
||||
.venue = sell_src.venue,
|
||||
.action = "sell",
|
||||
.token_in = symbol,
|
||||
.token_out = "USDC",
|
||||
.amount_in = max_size,
|
||||
.expected_out = max_size * sell_src.bid,
|
||||
.min_amount_out = max_size * sell_src.bid * Decimal::from_double(0.99)
|
||||
});
|
||||
|
||||
opportunities.push_back(std::move(opp));
|
||||
}
|
||||
}
|
||||
|
||||
return opportunities;
|
||||
}
|
||||
|
||||
std::vector<ArbitrageOpportunity> Scanner::find_cex_dex_arb(
|
||||
const std::string& symbol,
|
||||
const std::vector<PriceSource>& sources) const {
|
||||
|
||||
std::vector<ArbitrageOpportunity> opportunities;
|
||||
|
||||
// Separate CEX and DEX sources
|
||||
std::vector<PriceSource> cex_sources;
|
||||
std::vector<PriceSource> dex_sources;
|
||||
|
||||
for (const auto& s : sources) {
|
||||
if (CEX_VENUES.count(s.venue)) {
|
||||
cex_sources.push_back(s);
|
||||
} else {
|
||||
dex_sources.push_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
// CEX buy -> DEX sell
|
||||
for (const auto& cex : cex_sources) {
|
||||
for (const auto& dex : dex_sources) {
|
||||
Decimal spread = dex.bid - cex.ask;
|
||||
if (spread <= Decimal::zero()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Decimal spread_bps = (spread / cex.ask) * Decimal::from_double(10000.0);
|
||||
if (spread_bps < config_.min_spread_bps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Decimal max_size = std::min(cex.liquidity, dex.liquidity);
|
||||
Decimal gross_pnl = spread * max_size;
|
||||
|
||||
int64_t now = now_ms();
|
||||
ArbitrageOpportunity opp;
|
||||
opp.id = "cexdex-" + symbol + "-" + cex.venue + "-" + dex.venue + "-" + std::to_string(now);
|
||||
opp.arb_type = ArbType::CexDex;
|
||||
opp.buy_source = cex;
|
||||
opp.sell_source = dex;
|
||||
opp.spread_bps = spread_bps;
|
||||
opp.estimated_pnl = gross_pnl;
|
||||
opp.max_size = max_size;
|
||||
opp.gas_cost_usd = Decimal::from_double(0.5);
|
||||
opp.bridge_cost_usd = Decimal::zero();
|
||||
opp.net_pnl = gross_pnl - Decimal::from_double(0.5);
|
||||
opp.confidence = 0.7;
|
||||
opp.expires_at = now + 3000;
|
||||
|
||||
opportunities.push_back(std::move(opp));
|
||||
}
|
||||
}
|
||||
|
||||
// DEX buy -> CEX sell
|
||||
for (const auto& dex : dex_sources) {
|
||||
for (const auto& cex : cex_sources) {
|
||||
Decimal spread = cex.bid - dex.ask;
|
||||
if (spread <= Decimal::zero()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Decimal spread_bps = (spread / dex.ask) * Decimal::from_double(10000.0);
|
||||
if (spread_bps < config_.min_spread_bps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Decimal max_size = std::min(dex.liquidity, cex.liquidity);
|
||||
Decimal gross_pnl = spread * max_size;
|
||||
|
||||
int64_t now = now_ms();
|
||||
ArbitrageOpportunity opp;
|
||||
opp.id = "cexdex-" + symbol + "-" + dex.venue + "-" + cex.venue + "-" + std::to_string(now);
|
||||
opp.arb_type = ArbType::CexDex;
|
||||
opp.buy_source = dex;
|
||||
opp.sell_source = cex;
|
||||
opp.spread_bps = spread_bps;
|
||||
opp.estimated_pnl = gross_pnl;
|
||||
opp.max_size = max_size;
|
||||
opp.gas_cost_usd = Decimal::from_double(0.5);
|
||||
opp.bridge_cost_usd = Decimal::zero();
|
||||
opp.net_pnl = gross_pnl - Decimal::from_double(0.5);
|
||||
opp.confidence = 0.7;
|
||||
opp.expires_at = now + 3000;
|
||||
|
||||
opportunities.push_back(std::move(opp));
|
||||
}
|
||||
}
|
||||
|
||||
return opportunities;
|
||||
}
|
||||
|
||||
std::pair<Decimal, Decimal> Scanner::calculate_costs(
|
||||
const std::string& source_chain,
|
||||
const std::string& dest_chain) const {
|
||||
|
||||
std::lock_guard<std::mutex> lock(chains_mutex_);
|
||||
|
||||
auto src_it = chains_.find(source_chain);
|
||||
auto dst_it = chains_.find(dest_chain);
|
||||
|
||||
// Estimate gas cost
|
||||
Decimal gas_cost = src_it != chains_.end()
|
||||
? Decimal::from_double(0.05)
|
||||
: Decimal::from_double(0.1);
|
||||
|
||||
// Bridge cost if crossing chains
|
||||
Decimal bridge_cost = Decimal::zero();
|
||||
if (source_chain != dest_chain && src_it != chains_.end() && dst_it != chains_.end()) {
|
||||
const auto& src = src_it->second;
|
||||
const auto& dst = dst_it->second;
|
||||
|
||||
if (src.warp_supported && dst.warp_supported) {
|
||||
bridge_cost = Decimal::from_double(0.01); // Warp is nearly free
|
||||
} else if (src.teleport_supported && dst.teleport_supported) {
|
||||
bridge_cost = Decimal::from_double(0.10); // Teleport for EVM
|
||||
} else {
|
||||
bridge_cost = Decimal::from_double(1.0); // Generic bridge
|
||||
}
|
||||
}
|
||||
|
||||
return {gas_cost, bridge_cost};
|
||||
}
|
||||
|
||||
double Scanner::calculate_confidence(
|
||||
const PriceSource& buy,
|
||||
const PriceSource& sell) const {
|
||||
|
||||
int64_t now = now_ms();
|
||||
double max_age = static_cast<double>(config_.max_price_age_ms) / 1000.0;
|
||||
|
||||
// Freshness score
|
||||
double buy_age = static_cast<double>(now - buy.timestamp) / 1000.0;
|
||||
double sell_age = static_cast<double>(now - sell.timestamp) / 1000.0;
|
||||
double freshness_score = std::max(0.0, 1.0 - (buy_age + sell_age) / (2.0 * max_age));
|
||||
|
||||
// Liquidity score
|
||||
Decimal min_liq = std::min(buy.liquidity, sell.liquidity);
|
||||
double liquidity_score;
|
||||
if (min_liq > Decimal::from_double(100000.0)) {
|
||||
liquidity_score = 1.0;
|
||||
} else if (min_liq > Decimal::from_double(10000.0)) {
|
||||
liquidity_score = 0.8;
|
||||
} else {
|
||||
liquidity_score = 0.5;
|
||||
}
|
||||
|
||||
// Latency score
|
||||
double avg_latency = static_cast<double>(buy.latency + sell.latency) / 2.0;
|
||||
double latency_score = std::max(0.0, 1.0 - avg_latency / 1000.0);
|
||||
|
||||
// Weighted average
|
||||
return 0.4 * freshness_score + 0.4 * liquidity_score + 0.2 * latency_score;
|
||||
}
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,93 +0,0 @@
|
||||
// LX Trading SDK - Arbitrage Types Implementation
|
||||
|
||||
#include <lx/trading/arbitrage/types.hpp>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
CrossChainConfig CrossChainConfig::defaults() {
|
||||
CrossChainConfig config;
|
||||
|
||||
// Lux ecosystem (Warp enabled)
|
||||
config.chains["lux_mainnet"] = CrossChainInfo{
|
||||
.chain_id = "lux_mainnet",
|
||||
.name = "Lux Mainnet",
|
||||
.chain_type = ChainType::LuxSubnet,
|
||||
.block_time_ms = 400,
|
||||
.finality_ms = 400,
|
||||
.warp_supported = true,
|
||||
.teleport_supported = true,
|
||||
.venues = {"lx_dex", "lx_amm"}
|
||||
};
|
||||
|
||||
config.chains["lx_dex_subnet"] = CrossChainInfo{
|
||||
.chain_id = "lx_dex_subnet",
|
||||
.name = "LX DEX Subnet",
|
||||
.chain_type = ChainType::LuxSubnet,
|
||||
.block_time_ms = 200,
|
||||
.finality_ms = 200,
|
||||
.warp_supported = true,
|
||||
.teleport_supported = false,
|
||||
.venues = {"lx_dex"}
|
||||
};
|
||||
|
||||
// EVM chains (Teleport enabled)
|
||||
config.chains["ethereum"] = CrossChainInfo{
|
||||
.chain_id = "1",
|
||||
.name = "Ethereum",
|
||||
.chain_type = ChainType::Evm,
|
||||
.block_time_ms = 12000,
|
||||
.finality_ms = 15 * 60 * 1000, // 15 minutes
|
||||
.warp_supported = false,
|
||||
.teleport_supported = true,
|
||||
.venues = {"uniswap", "sushiswap"}
|
||||
};
|
||||
|
||||
config.chains["bsc"] = CrossChainInfo{
|
||||
.chain_id = "56",
|
||||
.name = "BNB Smart Chain",
|
||||
.chain_type = ChainType::Evm,
|
||||
.block_time_ms = 3000,
|
||||
.finality_ms = 45000,
|
||||
.warp_supported = false,
|
||||
.teleport_supported = true,
|
||||
.venues = {"pancakeswap"}
|
||||
};
|
||||
|
||||
config.chains["arbitrum"] = CrossChainInfo{
|
||||
.chain_id = "42161",
|
||||
.name = "Arbitrum One",
|
||||
.chain_type = ChainType::Evm,
|
||||
.block_time_ms = 250,
|
||||
.finality_ms = 15 * 60 * 1000,
|
||||
.warp_supported = false,
|
||||
.teleport_supported = true,
|
||||
.venues = {"uniswap", "camelot"}
|
||||
};
|
||||
|
||||
// CEX (API only)
|
||||
config.chains["binance"] = CrossChainInfo{
|
||||
.chain_id = "binance",
|
||||
.name = "Binance",
|
||||
.chain_type = ChainType::Cex,
|
||||
.block_time_ms = 0,
|
||||
.finality_ms = 0,
|
||||
.warp_supported = false,
|
||||
.teleport_supported = false,
|
||||
.venues = {"binance"}
|
||||
};
|
||||
|
||||
config.chains["mexc"] = CrossChainInfo{
|
||||
.chain_id = "mexc",
|
||||
.name = "MEXC",
|
||||
.chain_type = ChainType::Cex,
|
||||
.block_time_ms = 0,
|
||||
.finality_ms = 0,
|
||||
.warp_supported = false,
|
||||
.teleport_supported = false,
|
||||
.venues = {"mexc"}
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,263 +0,0 @@
|
||||
// LX Trading SDK - Unified Arbitrage Implementation
|
||||
|
||||
#include <lx/trading/arbitrage/unified.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
|
||||
namespace lx::trading::arbitrage {
|
||||
|
||||
UnifiedArbitrage::UnifiedArbitrage(std::shared_ptr<TradingClient> client, UnifiedArbConfig config)
|
||||
: client_(std::move(client)),
|
||||
config_(std::move(config)),
|
||||
total_pnl_(Decimal::zero()) {}
|
||||
|
||||
UnifiedArbitrage::~UnifiedArbitrage() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void UnifiedArbitrage::start() {
|
||||
if (running_.exchange(true)) {
|
||||
return; // Already running
|
||||
}
|
||||
|
||||
scan_thread_ = std::make_unique<std::thread>(&UnifiedArbitrage::scan_loop, this);
|
||||
execute_thread_ = std::make_unique<std::thread>(&UnifiedArbitrage::execute_loop, this);
|
||||
}
|
||||
|
||||
void UnifiedArbitrage::stop() {
|
||||
running_.store(false);
|
||||
|
||||
if (scan_thread_ && scan_thread_->joinable()) {
|
||||
scan_thread_->join();
|
||||
}
|
||||
scan_thread_.reset();
|
||||
|
||||
if (execute_thread_ && execute_thread_->joinable()) {
|
||||
execute_thread_->join();
|
||||
}
|
||||
execute_thread_.reset();
|
||||
}
|
||||
|
||||
void UnifiedArbitrage::on_opportunity(UnifiedCallback callback) {
|
||||
std::lock_guard<std::mutex> lock(callbacks_mutex_);
|
||||
callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
UnifiedArbStats UnifiedArbitrage::get_stats() const {
|
||||
std::lock_guard<std::mutex> pnl_lock(pnl_mutex_);
|
||||
std::lock_guard<std::mutex> exec_lock(executions_mutex_);
|
||||
|
||||
uint64_t successful = 0;
|
||||
for (const auto& exec : executions_) {
|
||||
if (exec.status == "completed" && exec.actual_profit.is_positive()) {
|
||||
++successful;
|
||||
}
|
||||
}
|
||||
|
||||
double win_rate = executions_.empty()
|
||||
? 0.0
|
||||
: static_cast<double>(successful) / static_cast<double>(executions_.size());
|
||||
|
||||
return UnifiedArbStats{
|
||||
.total_executions = executions_.size(),
|
||||
.successful_executions = successful,
|
||||
.total_pnl = total_pnl_,
|
||||
.win_rate = win_rate
|
||||
};
|
||||
}
|
||||
|
||||
void UnifiedArbitrage::scan_loop() {
|
||||
while (running_.load()) {
|
||||
// Scan each symbol
|
||||
for (const auto& symbol : config_.symbols) {
|
||||
auto opp = find_opportunity(symbol);
|
||||
if (opp && opp->net_profit > config_.min_profit) {
|
||||
// Add to queue
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queue_mutex_);
|
||||
if (opportunity_queue_.size() < 1000) {
|
||||
opportunity_queue_.push_back(*opp);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit to callbacks
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callbacks_mutex_);
|
||||
for (const auto& callback : callbacks_) {
|
||||
callback(*opp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(config_.scan_interval_ms));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<UnifiedOpportunity> UnifiedArbitrage::find_opportunity(const std::string& symbol) {
|
||||
AggregatedBook book;
|
||||
try {
|
||||
book = client_->aggregated_orderbook(symbol);
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (book.bids.empty() || book.asks.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto& best_bid = book.bids[0];
|
||||
const auto& best_ask = book.asks[0];
|
||||
|
||||
// Cross-venue arbitrage: bid on one venue > ask on another
|
||||
if (best_bid.price <= best_ask.price) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Decimal spread = best_bid.price - best_ask.price;
|
||||
Decimal spread_bps = (spread / best_ask.price) * Decimal::from_double(10000.0);
|
||||
|
||||
if (spread_bps < config_.min_spread_bps) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Decimal max_size = std::min({
|
||||
best_bid.quantity,
|
||||
best_ask.quantity,
|
||||
config_.max_position_size
|
||||
});
|
||||
|
||||
Decimal gross_profit = spread * max_size;
|
||||
Decimal total_fees = best_ask.price * max_size * Decimal::from_double(0.002); // ~0.2% total fees
|
||||
Decimal net_profit = gross_profit - total_fees;
|
||||
|
||||
int64_t now = now_ms();
|
||||
|
||||
UnifiedOpportunity opp;
|
||||
opp.id = "arb-" + symbol + "-" + std::to_string(now);
|
||||
opp.symbol = symbol;
|
||||
opp.timestamp = now;
|
||||
opp.expires_at = now + 5000;
|
||||
opp.buy_venue = best_ask.venue;
|
||||
opp.buy_price = best_ask.price;
|
||||
opp.buy_size = best_ask.quantity;
|
||||
opp.sell_venue = best_bid.venue;
|
||||
opp.sell_price = best_bid.price;
|
||||
opp.sell_size = best_bid.quantity;
|
||||
opp.spread = spread;
|
||||
opp.spread_bps = spread_bps;
|
||||
opp.max_size = max_size;
|
||||
opp.gross_profit = gross_profit;
|
||||
opp.est_fees = total_fees;
|
||||
opp.net_profit = net_profit;
|
||||
opp.confidence = 0.8;
|
||||
opp.latency = now - best_ask.timestamp;
|
||||
|
||||
return opp;
|
||||
}
|
||||
|
||||
void UnifiedArbitrage::execute_loop() {
|
||||
while (running_.load()) {
|
||||
UnifiedOpportunity opp;
|
||||
bool has_opp = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(queue_mutex_);
|
||||
if (!opportunity_queue_.empty()) {
|
||||
opp = opportunity_queue_.front();
|
||||
opportunity_queue_.pop_front();
|
||||
has_opp = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_opp) {
|
||||
auto result = execute_opportunity(opp);
|
||||
|
||||
// Update stats
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pnl_mutex_);
|
||||
total_pnl_ = total_pnl_ + result.actual_profit;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(executions_mutex_);
|
||||
executions_.push_back(std::move(result));
|
||||
}
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnifiedExecution UnifiedArbitrage::execute_opportunity(const UnifiedOpportunity& opp) {
|
||||
int64_t now = now_ms();
|
||||
|
||||
// Check if expired
|
||||
if (now > opp.expires_at) {
|
||||
return UnifiedExecution{
|
||||
.id = opp.id,
|
||||
.opportunity = opp,
|
||||
.start_time = now,
|
||||
.end_time = now,
|
||||
.status = "expired",
|
||||
.buy_order_id = std::nullopt,
|
||||
.sell_order_id = std::nullopt,
|
||||
.actual_profit = Decimal::zero(),
|
||||
.fees = Decimal::zero(),
|
||||
.error = "Opportunity expired"
|
||||
};
|
||||
}
|
||||
|
||||
UnifiedExecution exec_result;
|
||||
exec_result.id = opp.id;
|
||||
exec_result.opportunity = opp;
|
||||
exec_result.start_time = now;
|
||||
exec_result.status = "executing";
|
||||
exec_result.actual_profit = Decimal::zero();
|
||||
exec_result.fees = Decimal::zero();
|
||||
|
||||
try {
|
||||
// Execute both legs
|
||||
OrderRequest buy_request = OrderRequest::limit(opp.symbol, Side::Buy, opp.max_size, opp.buy_price)
|
||||
.with_venue(opp.buy_venue);
|
||||
|
||||
OrderRequest sell_request = OrderRequest::limit(opp.symbol, Side::Sell, opp.max_size, opp.sell_price)
|
||||
.with_venue(opp.sell_venue);
|
||||
|
||||
// Execute orders (in production, these would be async/parallel)
|
||||
Order buy_order = client_->place_order(buy_request);
|
||||
Order sell_order = client_->place_order(sell_request);
|
||||
|
||||
exec_result.end_time = now_ms();
|
||||
exec_result.buy_order_id = buy_order.order_id;
|
||||
exec_result.sell_order_id = sell_order.order_id;
|
||||
|
||||
// Calculate actual profit
|
||||
if (buy_order.average_price && sell_order.average_price) {
|
||||
Decimal buy_value = *buy_order.average_price * buy_order.filled_quantity;
|
||||
Decimal sell_value = *sell_order.average_price * sell_order.filled_quantity;
|
||||
exec_result.actual_profit = sell_value - buy_value;
|
||||
|
||||
// Subtract fees
|
||||
Decimal buy_fees = Decimal::zero();
|
||||
for (const auto& f : buy_order.fees) {
|
||||
buy_fees = buy_fees + f.amount;
|
||||
}
|
||||
Decimal sell_fees = Decimal::zero();
|
||||
for (const auto& f : sell_order.fees) {
|
||||
sell_fees = sell_fees + f.amount;
|
||||
}
|
||||
exec_result.fees = buy_fees + sell_fees;
|
||||
exec_result.actual_profit = exec_result.actual_profit - exec_result.fees;
|
||||
}
|
||||
|
||||
exec_result.status = "completed";
|
||||
} catch (const std::exception& e) {
|
||||
exec_result.end_time = now_ms();
|
||||
exec_result.status = "failed";
|
||||
exec_result.error = e.what();
|
||||
}
|
||||
|
||||
return exec_result;
|
||||
}
|
||||
|
||||
} // namespace lx::trading::arbitrage
|
||||
@@ -1,432 +0,0 @@
|
||||
// LX Trading SDK - CCXT Adapter Implementation
|
||||
|
||||
#include <lx/trading/adapters/ccxt.hpp>
|
||||
#include <lx/trading/orderbook.hpp>
|
||||
#include <cpr/cpr.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
CcxtAdapter::CcxtAdapter(std::string_view name, const CcxtConfig& config)
|
||||
: name_(name), config_(config), capabilities_(VenueCapabilities::clob()) {
|
||||
capabilities_.batch_orders = false; // CCXT REST doesn't have unified batch
|
||||
}
|
||||
|
||||
CcxtAdapter::~CcxtAdapter() = default;
|
||||
|
||||
void CcxtAdapter::set_service_url(std::string_view url) {
|
||||
service_url_ = std::string(url);
|
||||
}
|
||||
|
||||
void CcxtAdapter::update_latency(int64_t start_ns) {
|
||||
int64_t elapsed = now_ns() - start_ns;
|
||||
latency_.store(static_cast<int>(elapsed / 1000000), std::memory_order_release);
|
||||
}
|
||||
|
||||
std::future<void> CcxtAdapter::connect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"exchange", config_.exchange_id},
|
||||
{"apiKey", config_.api_key.value_or("")},
|
||||
{"secret", config_.api_secret.value_or("")},
|
||||
{"sandbox", config_.sandbox}
|
||||
};
|
||||
|
||||
if (config_.password) {
|
||||
body["password"] = *config_.password;
|
||||
}
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{service_url_ + "/connect"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("CCXT connect failed: " + response.text);
|
||||
}
|
||||
|
||||
update_latency(start);
|
||||
connected_.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<void> CcxtAdapter::disconnect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
connected_.store(false, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<MarketInfo>> CcxtAdapter::get_markets() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
|
||||
auto response = cpr::Get(
|
||||
cpr::Url{service_url_ + "/markets/" + config_.exchange_id});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get markets: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
std::vector<MarketInfo> markets;
|
||||
|
||||
for (const auto& m : data) {
|
||||
MarketInfo info;
|
||||
info.symbol = m["symbol"];
|
||||
info.base = m.value("base", "");
|
||||
info.quote = m.value("quote", "");
|
||||
info.price_precision = m.value("precision", json{}).value("price", 8);
|
||||
info.quantity_precision = m.value("precision", json{}).value("amount", 8);
|
||||
|
||||
auto limits = m.value("limits", json{});
|
||||
auto amount_limits = limits.value("amount", json{});
|
||||
if (amount_limits.contains("min") && !amount_limits["min"].is_null()) {
|
||||
info.min_quantity = Decimal::from_double(amount_limits["min"].get<double>());
|
||||
}
|
||||
if (amount_limits.contains("max") && !amount_limits["max"].is_null()) {
|
||||
info.max_quantity = Decimal::from_double(amount_limits["max"].get<double>());
|
||||
}
|
||||
|
||||
info.tick_size = Decimal::from_double(0.00000001);
|
||||
info.lot_size = Decimal::from_double(0.00000001);
|
||||
markets.push_back(std::move(info));
|
||||
}
|
||||
|
||||
return markets;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Ticker> CcxtAdapter::get_ticker(const std::string& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
auto start = now_ns();
|
||||
|
||||
auto response = cpr::Get(
|
||||
cpr::Url{service_url_ + "/ticker/" + config_.exchange_id + "/" + symbol});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get ticker: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
|
||||
Ticker ticker;
|
||||
ticker.symbol = data.value("symbol", symbol);
|
||||
ticker.venue = name_;
|
||||
|
||||
if (data.contains("bid") && !data["bid"].is_null())
|
||||
ticker.bid = Decimal::from_double(data["bid"].get<double>());
|
||||
if (data.contains("ask") && !data["ask"].is_null())
|
||||
ticker.ask = Decimal::from_double(data["ask"].get<double>());
|
||||
if (data.contains("last") && !data["last"].is_null())
|
||||
ticker.last = Decimal::from_double(data["last"].get<double>());
|
||||
if (data.contains("baseVolume") && !data["baseVolume"].is_null())
|
||||
ticker.volume_24h = Decimal::from_double(data["baseVolume"].get<double>());
|
||||
if (data.contains("high") && !data["high"].is_null())
|
||||
ticker.high_24h = Decimal::from_double(data["high"].get<double>());
|
||||
if (data.contains("low") && !data["low"].is_null())
|
||||
ticker.low_24h = Decimal::from_double(data["low"].get<double>());
|
||||
if (data.contains("percentage") && !data["percentage"].is_null())
|
||||
ticker.change_24h = Decimal::from_double(data["percentage"].get<double>());
|
||||
|
||||
ticker.timestamp = data.value("timestamp", now_ms());
|
||||
|
||||
return ticker;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::unique_ptr<Orderbook>> CcxtAdapter::get_orderbook(
|
||||
const std::string& symbol, std::optional<int> depth) {
|
||||
return std::async(std::launch::async, [this, symbol, depth]() {
|
||||
auto start = now_ns();
|
||||
|
||||
std::string url = service_url_ + "/orderbook/" + config_.exchange_id + "/" + symbol;
|
||||
if (depth) url += "?limit=" + std::to_string(*depth);
|
||||
|
||||
auto response = cpr::Get(cpr::Url{url});
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get orderbook: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
auto book = std::make_unique<Orderbook>(symbol, name_);
|
||||
|
||||
for (const auto& bid : data["bids"]) {
|
||||
book->add_bid(
|
||||
Decimal::from_double(bid[0].get<double>()),
|
||||
Decimal::from_double(bid[1].get<double>()));
|
||||
}
|
||||
|
||||
for (const auto& ask : data["asks"]) {
|
||||
book->add_ask(
|
||||
Decimal::from_double(ask[0].get<double>()),
|
||||
Decimal::from_double(ask[1].get<double>()));
|
||||
}
|
||||
|
||||
book->sort();
|
||||
return book;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Trade>> CcxtAdapter::get_trades(
|
||||
const std::string& symbol, std::optional<int> limit) {
|
||||
return std::async(std::launch::async, [this, symbol, limit]() {
|
||||
auto start = now_ns();
|
||||
|
||||
std::string url = service_url_ + "/trades/" + config_.exchange_id + "/" + symbol;
|
||||
if (limit) url += "?limit=" + std::to_string(*limit);
|
||||
|
||||
auto response = cpr::Get(cpr::Url{url});
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get trades: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
std::vector<Trade> trades;
|
||||
|
||||
for (const auto& t : data) {
|
||||
Trade trade;
|
||||
trade.trade_id = t.value("id", "");
|
||||
trade.order_id = t.value("order", "");
|
||||
trade.symbol = t.value("symbol", symbol);
|
||||
trade.venue = name_;
|
||||
trade.side = (t.value("side", "buy") == "buy") ? Side::Buy : Side::Sell;
|
||||
trade.price = Decimal::from_double(t.value("price", 0.0));
|
||||
trade.quantity = Decimal::from_double(t.value("amount", 0.0));
|
||||
|
||||
auto fee = t.value("fee", json{});
|
||||
trade.fee.asset = fee.value("currency", "");
|
||||
trade.fee.amount = Decimal::from_double(fee.value("cost", 0.0));
|
||||
|
||||
trade.timestamp = t.value("timestamp", int64_t(0));
|
||||
trade.is_maker = (t.value("takerOrMaker", "taker") == "maker");
|
||||
trades.push_back(std::move(trade));
|
||||
}
|
||||
|
||||
return trades;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Balance>> CcxtAdapter::get_balances() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"exchange", config_.exchange_id},
|
||||
{"apiKey", config_.api_key.value_or("")},
|
||||
{"secret", config_.api_secret.value_or("")}
|
||||
};
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{service_url_ + "/balance"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get balances: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
std::vector<Balance> balances;
|
||||
|
||||
auto total = data.value("total", json{});
|
||||
auto free = data.value("free", json{});
|
||||
auto used = data.value("used", json{});
|
||||
|
||||
for (auto& [asset, amount] : total.items()) {
|
||||
if (amount.is_null() || amount.get<double>() <= 0) continue;
|
||||
|
||||
Balance bal;
|
||||
bal.asset = asset;
|
||||
bal.venue = name_;
|
||||
bal.free = Decimal::from_double(free.value(asset, 0.0));
|
||||
bal.locked = Decimal::from_double(used.value(asset, 0.0));
|
||||
balances.push_back(std::move(bal));
|
||||
}
|
||||
|
||||
return balances;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Balance> CcxtAdapter::get_balance(const std::string& asset) {
|
||||
return std::async(std::launch::async, [this, asset]() {
|
||||
auto balances = get_balances().get();
|
||||
for (const auto& b : balances) {
|
||||
if (b.asset == asset) return b;
|
||||
}
|
||||
return Balance{asset, name_, Decimal::zero(), Decimal::zero()};
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> CcxtAdapter::get_open_orders(
|
||||
const std::optional<std::string>& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"exchange", config_.exchange_id},
|
||||
{"apiKey", config_.api_key.value_or("")},
|
||||
{"secret", config_.api_secret.value_or("")}
|
||||
};
|
||||
if (symbol) body["symbol"] = *symbol;
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{service_url_ + "/openOrders"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get open orders: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
std::vector<Order> orders;
|
||||
|
||||
for (const auto& o : data) {
|
||||
orders.push_back(convert_order(o));
|
||||
}
|
||||
|
||||
return orders;
|
||||
});
|
||||
}
|
||||
|
||||
Order CcxtAdapter::convert_order(const json& o) {
|
||||
Order order;
|
||||
order.order_id = o.value("id", "");
|
||||
order.client_order_id = o.value("clientOrderId", "");
|
||||
order.symbol = o.value("symbol", "");
|
||||
order.venue = name_;
|
||||
order.side = (o.value("side", "buy") == "buy") ? Side::Buy : Side::Sell;
|
||||
|
||||
std::string type_str = o.value("type", "limit");
|
||||
if (type_str == "market") order.order_type = OrderType::Market;
|
||||
else if (type_str == "limit") order.order_type = OrderType::Limit;
|
||||
else if (type_str == "stop") order.order_type = OrderType::StopLoss;
|
||||
else if (type_str == "stop_limit") order.order_type = OrderType::StopLossLimit;
|
||||
|
||||
std::string status_str = o.value("status", "open");
|
||||
if (status_str == "open") order.status = OrderStatus::Open;
|
||||
else if (status_str == "closed") order.status = OrderStatus::Filled;
|
||||
else if (status_str == "canceled" || status_str == "cancelled")
|
||||
order.status = OrderStatus::Cancelled;
|
||||
else if (status_str == "expired") order.status = OrderStatus::Expired;
|
||||
else if (status_str == "rejected") order.status = OrderStatus::Rejected;
|
||||
|
||||
order.quantity = Decimal::from_double(o.value("amount", 0.0));
|
||||
order.filled_quantity = Decimal::from_double(o.value("filled", 0.0));
|
||||
order.remaining_quantity = order.quantity - order.filled_quantity;
|
||||
|
||||
if (o.contains("price") && !o["price"].is_null())
|
||||
order.price = Decimal::from_double(o["price"].get<double>());
|
||||
if (o.contains("average") && !o["average"].is_null())
|
||||
order.average_price = Decimal::from_double(o["average"].get<double>());
|
||||
|
||||
order.created_at = o.value("timestamp", int64_t(0));
|
||||
order.updated_at = o.value("lastTradeTimestamp", order.created_at);
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
std::future<Order> CcxtAdapter::place_order(const OrderRequest& request) {
|
||||
return std::async(std::launch::async, [this, request]() -> Order {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"exchange", config_.exchange_id},
|
||||
{"apiKey", config_.api_key.value_or("")},
|
||||
{"secret", config_.api_secret.value_or("")},
|
||||
{"symbol", request.symbol},
|
||||
{"side", request.side == Side::Buy ? "buy" : "sell"},
|
||||
{"type", request.order_type == OrderType::Market ? "market" : "limit"},
|
||||
{"amount", request.quantity.to_double()}
|
||||
};
|
||||
|
||||
if (request.price) {
|
||||
body["price"] = request.price->to_double();
|
||||
}
|
||||
|
||||
if (!request.client_order_id.empty()) {
|
||||
body["clientOrderId"] = request.client_order_id;
|
||||
}
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{service_url_ + "/order"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200 && response.status_code != 201) {
|
||||
throw AdapterError("Failed to place order: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
return convert_order(data);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Order> CcxtAdapter::cancel_order(
|
||||
const std::string& order_id, const std::string& symbol) {
|
||||
return std::async(std::launch::async, [this, order_id, symbol]() -> Order {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"exchange", config_.exchange_id},
|
||||
{"apiKey", config_.api_key.value_or("")},
|
||||
{"secret", config_.api_secret.value_or("")},
|
||||
{"orderId", order_id},
|
||||
{"symbol", symbol}
|
||||
};
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{service_url_ + "/cancelOrder"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to cancel order: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
return convert_order(data);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> CcxtAdapter::cancel_all_orders(
|
||||
const std::optional<std::string>& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
// Get open orders then cancel each
|
||||
auto open = get_open_orders(symbol).get();
|
||||
std::vector<Order> cancelled;
|
||||
|
||||
for (const auto& order : open) {
|
||||
try {
|
||||
auto result = cancel_order(order.order_id, order.symbol).get();
|
||||
cancelled.push_back(std::move(result));
|
||||
} catch (...) {
|
||||
// Continue with other orders
|
||||
}
|
||||
}
|
||||
|
||||
return cancelled;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,401 +0,0 @@
|
||||
// LX Trading SDK - Unified Client Implementation
|
||||
|
||||
#include <lx/trading/client.hpp>
|
||||
#include <lx/trading/adapters/native.hpp>
|
||||
#include <lx/trading/adapters/ccxt.hpp>
|
||||
#include <lx/trading/adapters/hummingbot.hpp>
|
||||
#include <lx/trading/risk.hpp>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
Client::Client(const Config& config)
|
||||
: config_(config),
|
||||
risk_manager_(std::make_unique<RiskManager>(config.risk)) {}
|
||||
|
||||
Client::~Client() = default;
|
||||
|
||||
std::future<void> Client::connect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
// Connect native venues
|
||||
for (const auto& [name, cfg] : config_.native) {
|
||||
std::unique_ptr<VenueAdapter> adapter;
|
||||
if (cfg.venue_type == "dex") {
|
||||
adapter = std::make_unique<LxDexAdapter>(name, cfg);
|
||||
} else {
|
||||
adapter = std::make_unique<LxAmmAdapter>(name, cfg);
|
||||
}
|
||||
adapter->connect().get();
|
||||
venues_[name] = std::move(adapter);
|
||||
}
|
||||
|
||||
// Connect CCXT exchanges
|
||||
for (const auto& [name, cfg] : config_.ccxt) {
|
||||
auto adapter = std::make_unique<CcxtAdapter>(name, cfg);
|
||||
adapter->connect().get();
|
||||
venues_[name] = std::move(adapter);
|
||||
}
|
||||
|
||||
// Connect Hummingbot gateways
|
||||
for (const auto& [name, cfg] : config_.hummingbot) {
|
||||
auto adapter = std::make_unique<HummingbotAdapter>(name, cfg);
|
||||
adapter->connect().get();
|
||||
venues_[name] = std::move(adapter);
|
||||
}
|
||||
|
||||
// Set default venue
|
||||
if (!config_.general.venue_priority.empty()) {
|
||||
default_venue_ = config_.general.venue_priority[0];
|
||||
} else if (!venues_.empty()) {
|
||||
default_venue_ = venues_.begin()->first;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::future<void> Client::disconnect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
for (auto& [name, adapter] : venues_) {
|
||||
adapter->disconnect().get();
|
||||
}
|
||||
venues_.clear();
|
||||
default_venue_.reset();
|
||||
});
|
||||
}
|
||||
|
||||
VenueAdapter* Client::venue(std::string_view name) {
|
||||
auto it = venues_.find(std::string(name));
|
||||
return it != venues_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
const VenueAdapter* Client::venue(std::string_view name) const {
|
||||
auto it = venues_.find(std::string(name));
|
||||
return it != venues_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
std::vector<VenueInfo> Client::venues() const {
|
||||
std::vector<VenueInfo> result;
|
||||
for (const auto& [name, adapter] : venues_) {
|
||||
result.push_back(adapter->info());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
VenueAdapter* Client::get_venue(std::optional<std::string_view> name) {
|
||||
std::string venue_name;
|
||||
if (name) {
|
||||
venue_name = std::string(*name);
|
||||
} else if (default_venue_) {
|
||||
venue_name = *default_venue_;
|
||||
} else {
|
||||
throw AdapterError("No venue specified and no default venue");
|
||||
}
|
||||
|
||||
auto it = venues_.find(venue_name);
|
||||
if (it == venues_.end()) {
|
||||
throw AdapterError("Venue not found: " + venue_name);
|
||||
}
|
||||
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
// Market Data
|
||||
std::future<std::unique_ptr<Orderbook>> Client::orderbook(
|
||||
const std::string& symbol, std::optional<std::string_view> venue) {
|
||||
return get_venue(venue)->get_orderbook(symbol);
|
||||
}
|
||||
|
||||
std::future<AggregatedOrderbook> Client::aggregated_orderbook(const std::string& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
AggregatedOrderbook agg(symbol);
|
||||
|
||||
for (auto& [name, adapter] : venues_) {
|
||||
if (adapter->capabilities().orderbook) {
|
||||
try {
|
||||
auto book = adapter->get_orderbook(symbol, 20).get();
|
||||
agg.add_orderbook(*book);
|
||||
} catch (...) {
|
||||
// Skip venues that don't support this pair
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return agg;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Ticker> Client::ticker(
|
||||
const std::string& symbol, std::optional<std::string_view> venue) {
|
||||
return get_venue(venue)->get_ticker(symbol);
|
||||
}
|
||||
|
||||
std::future<std::vector<Ticker>> Client::tickers(const std::string& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
std::vector<Ticker> result;
|
||||
|
||||
for (auto& [name, adapter] : venues_) {
|
||||
try {
|
||||
auto t = adapter->get_ticker(symbol).get();
|
||||
result.push_back(std::move(t));
|
||||
} catch (...) {
|
||||
// Skip
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
// Account
|
||||
std::future<std::vector<AggregatedBalance>> Client::balances() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
std::unordered_map<std::string, std::vector<Balance>> by_asset;
|
||||
|
||||
for (auto& [name, adapter] : venues_) {
|
||||
try {
|
||||
for (auto& bal : adapter->get_balances().get()) {
|
||||
by_asset[bal.asset].push_back(std::move(bal));
|
||||
}
|
||||
} catch (...) {
|
||||
// Skip
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AggregatedBalance> result;
|
||||
for (auto& [asset, bals] : by_asset) {
|
||||
AggregatedBalance agg;
|
||||
agg.asset = asset;
|
||||
agg.by_venue = std::move(bals);
|
||||
|
||||
for (const auto& b : agg.by_venue) {
|
||||
agg.total_free = agg.total_free + b.free;
|
||||
agg.total_locked = agg.total_locked + b.locked;
|
||||
}
|
||||
|
||||
result.push_back(std::move(agg));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Balance> Client::balance(
|
||||
const std::string& asset, std::optional<std::string_view> venue) {
|
||||
return get_venue(venue)->get_balance(asset);
|
||||
}
|
||||
|
||||
// Orders
|
||||
std::future<Order> Client::buy(
|
||||
const std::string& symbol, Decimal quantity, std::optional<std::string_view> venue) {
|
||||
OrderRequest req = OrderRequest::market(symbol, Side::Buy, quantity);
|
||||
if (venue) req.venue = std::string(*venue);
|
||||
|
||||
return place_order(req);
|
||||
}
|
||||
|
||||
std::future<Order> Client::sell(
|
||||
const std::string& symbol, Decimal quantity, std::optional<std::string_view> venue) {
|
||||
OrderRequest req = OrderRequest::market(symbol, Side::Sell, quantity);
|
||||
if (venue) req.venue = std::string(*venue);
|
||||
|
||||
return place_order(req);
|
||||
}
|
||||
|
||||
std::future<Order> Client::limit_buy(
|
||||
const std::string& symbol, Decimal quantity, Decimal price,
|
||||
std::optional<std::string_view> venue) {
|
||||
OrderRequest req = OrderRequest::limit(symbol, Side::Buy, quantity, price);
|
||||
if (venue) req.venue = std::string(*venue);
|
||||
|
||||
return place_order(req);
|
||||
}
|
||||
|
||||
std::future<Order> Client::limit_sell(
|
||||
const std::string& symbol, Decimal quantity, Decimal price,
|
||||
std::optional<std::string_view> venue) {
|
||||
OrderRequest req = OrderRequest::limit(symbol, Side::Sell, quantity, price);
|
||||
if (venue) req.venue = std::string(*venue);
|
||||
|
||||
return place_order(req);
|
||||
}
|
||||
|
||||
std::future<Order> Client::place_order(const OrderRequest& request) {
|
||||
return std::async(std::launch::async, [this, request]() {
|
||||
// Validate with risk manager
|
||||
risk_manager_->validate_order(request);
|
||||
|
||||
// Route order
|
||||
if (request.venue) {
|
||||
auto adapter = get_venue(request.venue);
|
||||
auto order = adapter->place_order(request).get();
|
||||
risk_manager_->order_opened(request.symbol);
|
||||
return order;
|
||||
}
|
||||
|
||||
if (config_.general.smart_routing &&
|
||||
request.order_type == OrderType::Market) {
|
||||
return smart_route(request);
|
||||
}
|
||||
|
||||
auto adapter = get_venue(std::nullopt);
|
||||
auto order = adapter->place_order(request).get();
|
||||
risk_manager_->order_opened(request.symbol);
|
||||
return order;
|
||||
});
|
||||
}
|
||||
|
||||
Order Client::smart_route(const OrderRequest& request) {
|
||||
auto agg_book = aggregated_orderbook(request.symbol).get();
|
||||
|
||||
std::optional<std::pair<std::string, Decimal>> best;
|
||||
if (request.side == Side::Buy) {
|
||||
best = agg_book.best_venue_buy(request.quantity);
|
||||
} else {
|
||||
best = agg_book.best_venue_sell(request.quantity);
|
||||
}
|
||||
|
||||
if (best) {
|
||||
OrderRequest routed = request;
|
||||
routed.venue = best->first;
|
||||
auto adapter = get_venue(routed.venue);
|
||||
auto order = adapter->place_order(routed).get();
|
||||
risk_manager_->order_opened(request.symbol);
|
||||
return order;
|
||||
}
|
||||
|
||||
// Fallback to default
|
||||
auto adapter = get_venue(std::nullopt);
|
||||
auto order = adapter->place_order(request).get();
|
||||
risk_manager_->order_opened(request.symbol);
|
||||
return order;
|
||||
}
|
||||
|
||||
std::future<Order> Client::cancel_order(
|
||||
const std::string& order_id, const std::string& symbol, const std::string& venue) {
|
||||
return std::async(std::launch::async, [this, order_id, symbol, venue]() {
|
||||
auto adapter = get_venue(venue);
|
||||
auto order = adapter->cancel_order(order_id, symbol).get();
|
||||
risk_manager_->order_closed(symbol);
|
||||
return order;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> Client::cancel_all_orders(
|
||||
std::optional<std::string_view> symbol, std::optional<std::string_view> venue) {
|
||||
return std::async(std::launch::async, [this, symbol, venue]() {
|
||||
std::optional<std::string> sym;
|
||||
if (symbol) sym = std::string(*symbol);
|
||||
|
||||
if (venue) {
|
||||
auto adapter = get_venue(venue);
|
||||
auto orders = adapter->cancel_all_orders(sym).get();
|
||||
for (const auto& o : orders) {
|
||||
risk_manager_->order_closed(o.symbol);
|
||||
}
|
||||
return orders;
|
||||
}
|
||||
|
||||
std::vector<Order> all_orders;
|
||||
for (auto& [name, adapter] : venues_) {
|
||||
try {
|
||||
auto orders = adapter->cancel_all_orders(sym).get();
|
||||
for (const auto& o : orders) {
|
||||
risk_manager_->order_closed(o.symbol);
|
||||
}
|
||||
all_orders.insert(all_orders.end(), orders.begin(), orders.end());
|
||||
} catch (...) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
return all_orders;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> Client::open_orders(std::optional<std::string_view> symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
std::optional<std::string> sym;
|
||||
if (symbol) sym = std::string(*symbol);
|
||||
|
||||
std::vector<Order> all_orders;
|
||||
for (auto& [name, adapter] : venues_) {
|
||||
try {
|
||||
auto orders = adapter->get_open_orders(sym).get();
|
||||
all_orders.insert(all_orders.end(), orders.begin(), orders.end());
|
||||
} catch (...) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
return all_orders;
|
||||
});
|
||||
}
|
||||
|
||||
// AMM Operations
|
||||
std::future<SwapQuote> Client::quote(
|
||||
const std::string& base_token, const std::string& quote_token,
|
||||
Decimal amount, bool is_buy, const std::string& venue) {
|
||||
return get_venue(venue)->get_swap_quote(base_token, quote_token, amount, is_buy);
|
||||
}
|
||||
|
||||
std::future<Trade> Client::swap(
|
||||
const std::string& base_token, const std::string& quote_token,
|
||||
Decimal amount, bool is_buy, double slippage, const std::string& venue) {
|
||||
return get_venue(venue)->execute_swap(
|
||||
base_token, quote_token, amount, is_buy, Decimal::from_double(slippage));
|
||||
}
|
||||
|
||||
std::future<PoolInfo> Client::pool_info(
|
||||
const std::string& base_token, const std::string& quote_token,
|
||||
const std::string& venue) {
|
||||
return get_venue(venue)->get_pool_info(base_token, quote_token);
|
||||
}
|
||||
|
||||
std::future<LiquidityResult> Client::add_liquidity(
|
||||
const std::string& base_token, const std::string& quote_token,
|
||||
Decimal base_amount, Decimal quote_amount,
|
||||
double slippage, const std::string& venue) {
|
||||
return get_venue(venue)->add_liquidity(
|
||||
base_token, quote_token, base_amount, quote_amount,
|
||||
Decimal::from_double(slippage));
|
||||
}
|
||||
|
||||
std::future<LiquidityResult> Client::remove_liquidity(
|
||||
const std::string& pool_address, Decimal liquidity_amount,
|
||||
double slippage, const std::string& venue) {
|
||||
return get_venue(venue)->remove_liquidity(
|
||||
pool_address, liquidity_amount, Decimal::from_double(slippage));
|
||||
}
|
||||
|
||||
std::future<std::vector<LpPosition>> Client::lp_positions(const std::string& venue) {
|
||||
return get_venue(venue)->get_lp_positions();
|
||||
}
|
||||
|
||||
// Streaming
|
||||
void Client::subscribe_ticker(
|
||||
const std::string& symbol, VenueAdapter::TickerCallback callback,
|
||||
std::optional<std::string_view> venue) {
|
||||
get_venue(venue)->subscribe_ticker(symbol, std::move(callback));
|
||||
}
|
||||
|
||||
void Client::subscribe_trades(
|
||||
const std::string& symbol, VenueAdapter::TradeCallback callback,
|
||||
std::optional<std::string_view> venue) {
|
||||
get_venue(venue)->subscribe_trades(symbol, std::move(callback));
|
||||
}
|
||||
|
||||
void Client::subscribe_orderbook(
|
||||
const std::string& symbol, VenueAdapter::OrderbookCallback callback,
|
||||
std::optional<std::string_view> venue) {
|
||||
get_venue(venue)->subscribe_orderbook(symbol, std::move(callback));
|
||||
}
|
||||
|
||||
void Client::subscribe_orders(VenueAdapter::OrderCallback callback) {
|
||||
for (auto& [name, adapter] : venues_) {
|
||||
adapter->subscribe_orders(callback);
|
||||
}
|
||||
}
|
||||
|
||||
void Client::unsubscribe_all() {
|
||||
for (auto& [name, adapter] : venues_) {
|
||||
adapter->unsubscribe_all();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,135 +0,0 @@
|
||||
// LX Trading SDK - Configuration Implementation
|
||||
|
||||
#include <lx/trading/config.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Simple TOML parser (handles basic cases)
|
||||
namespace {
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t start = s.find_first_not_of(" \t\r\n");
|
||||
if (start == std::string::npos) return "";
|
||||
size_t end = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
std::string unquote(const std::string& s) {
|
||||
if (s.size() >= 2 && s[0] == '"' && s.back() == '"') {
|
||||
return s.substr(1, s.size() - 2);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Config Config::from_file(std::string_view path) {
|
||||
std::string path_str{path};
|
||||
std::ifstream file{path_str};
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Cannot open config file: " + path_str);
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
return from_toml(buffer.str());
|
||||
}
|
||||
|
||||
Config Config::from_toml(std::string_view content) {
|
||||
Config config;
|
||||
std::string current_section;
|
||||
std::string current_subsection;
|
||||
|
||||
std::string content_str{content};
|
||||
std::istringstream stream{content_str};
|
||||
std::string line;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
line = trim(line);
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
// Section header
|
||||
if (line[0] == '[') {
|
||||
size_t end = line.find(']');
|
||||
if (end != std::string::npos) {
|
||||
std::string section = line.substr(1, end - 1);
|
||||
|
||||
// Check for subsection [section.name]
|
||||
auto dot = section.find('.');
|
||||
if (dot != std::string::npos) {
|
||||
current_section = section.substr(0, dot);
|
||||
current_subsection = section.substr(dot + 1);
|
||||
} else {
|
||||
current_section = section;
|
||||
current_subsection.clear();
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key-value pair
|
||||
auto eq = line.find('=');
|
||||
if (eq != std::string::npos) {
|
||||
std::string key = trim(line.substr(0, eq));
|
||||
std::string value = trim(line.substr(eq + 1));
|
||||
value = unquote(value);
|
||||
|
||||
// Parse based on section
|
||||
if (current_section == "general") {
|
||||
if (key == "log_level") config.general.log_level = value;
|
||||
else if (key == "timeout_ms") config.general.timeout_ms = std::stoi(value);
|
||||
else if (key == "smart_routing") config.general.smart_routing = (value == "true");
|
||||
else if (key == "min_improvement_bps") config.general.min_improvement_bps = std::stoi(value);
|
||||
}
|
||||
else if (current_section == "risk") {
|
||||
if (key == "enabled") config.risk.enabled = (value == "true");
|
||||
else if (key == "max_position_size") config.risk.max_position_size = Decimal::from_string(value);
|
||||
else if (key == "max_order_size") config.risk.max_order_size = Decimal::from_string(value);
|
||||
else if (key == "max_daily_loss") config.risk.max_daily_loss = Decimal::from_string(value);
|
||||
else if (key == "max_open_orders") config.risk.max_open_orders = std::stoi(value);
|
||||
else if (key == "kill_switch_enabled") config.risk.kill_switch_enabled = (value == "true");
|
||||
}
|
||||
else if (current_section == "native" && !current_subsection.empty()) {
|
||||
auto& native_cfg = config.native[current_subsection];
|
||||
if (key == "venue_type") native_cfg.venue_type = value;
|
||||
else if (key == "api_url") native_cfg.api_url = value;
|
||||
else if (key == "ws_url") native_cfg.ws_url = value;
|
||||
else if (key == "api_key") native_cfg.api_key = value;
|
||||
else if (key == "api_secret") native_cfg.api_secret = value;
|
||||
else if (key == "wallet_address") native_cfg.wallet_address = value;
|
||||
else if (key == "private_key") native_cfg.private_key = value;
|
||||
else if (key == "network") native_cfg.network = value;
|
||||
else if (key == "chain_id") native_cfg.chain_id = std::stoi(value);
|
||||
else if (key == "streaming") native_cfg.streaming = (value == "true");
|
||||
}
|
||||
else if (current_section == "ccxt" && !current_subsection.empty()) {
|
||||
auto& ccxt_cfg = config.ccxt[current_subsection];
|
||||
if (key == "exchange_id") ccxt_cfg.exchange_id = value;
|
||||
else if (key == "api_key") ccxt_cfg.api_key = value;
|
||||
else if (key == "api_secret") ccxt_cfg.api_secret = value;
|
||||
else if (key == "password") ccxt_cfg.password = value;
|
||||
else if (key == "sandbox") ccxt_cfg.sandbox = (value == "true");
|
||||
else if (key == "rate_limit") ccxt_cfg.rate_limit = (value == "true");
|
||||
}
|
||||
else if (current_section == "hummingbot" && !current_subsection.empty()) {
|
||||
auto& hb_cfg = config.hummingbot[current_subsection];
|
||||
if (key == "host") hb_cfg.host = value;
|
||||
else if (key == "port") hb_cfg.port = std::stoi(value);
|
||||
else if (key == "https") hb_cfg.https = (value == "true");
|
||||
else if (key == "connector") hb_cfg.connector = value;
|
||||
else if (key == "chain") hb_cfg.chain = value;
|
||||
else if (key == "network") hb_cfg.network = value;
|
||||
else if (key == "wallet_address") hb_cfg.wallet_address = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,469 +0,0 @@
|
||||
// LX Trading SDK - Execution Algorithms Implementation
|
||||
|
||||
#include <lx/trading/execution.hpp>
|
||||
#include <lx/trading/client.hpp>
|
||||
#include <thread>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// =============================================================================
|
||||
// TWAP Executor
|
||||
// =============================================================================
|
||||
|
||||
TwapExecutor::TwapExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
std::chrono::seconds duration,
|
||||
int num_slices)
|
||||
: client_(client),
|
||||
symbol_(std::move(symbol)),
|
||||
side_(side),
|
||||
total_quantity_(total_quantity),
|
||||
duration_(duration),
|
||||
num_slices_(num_slices) {}
|
||||
|
||||
std::future<ExecutionResult> TwapExecutor::execute() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
ExecutionResult result;
|
||||
result.start_time = now_ms();
|
||||
result.total_quantity = total_quantity_;
|
||||
result.completed = false;
|
||||
|
||||
Decimal slice_qty = total_quantity_ / Decimal::from_double(num_slices_);
|
||||
auto interval = std::chrono::milliseconds(
|
||||
duration_.count() * 1000 / num_slices_);
|
||||
|
||||
Decimal total_value;
|
||||
|
||||
for (int i = 0; i < num_slices_; ++i) {
|
||||
if (cancelled_.load(std::memory_order_acquire)) {
|
||||
result.error = "Cancelled";
|
||||
break;
|
||||
}
|
||||
|
||||
Decimal remaining = total_quantity_ - (slice_qty * Decimal::from_double(i));
|
||||
Decimal qty = (remaining < slice_qty) ? remaining : slice_qty;
|
||||
|
||||
if (qty <= Decimal::zero()) break;
|
||||
|
||||
try {
|
||||
Order order;
|
||||
if (side_ == Side::Buy) {
|
||||
order = client_.buy(symbol_, qty).get();
|
||||
} else {
|
||||
order = client_.sell(symbol_, qty).get();
|
||||
}
|
||||
|
||||
result.orders.push_back(order);
|
||||
result.total_filled = result.total_filled + order.filled_quantity;
|
||||
|
||||
if (order.average_price) {
|
||||
total_value = total_value +
|
||||
(order.filled_quantity * *order.average_price);
|
||||
}
|
||||
|
||||
if (callback_) {
|
||||
callback_(order, remaining - qty);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
result.error = e.what();
|
||||
break;
|
||||
}
|
||||
|
||||
if (i < num_slices_ - 1) {
|
||||
std::this_thread::sleep_for(interval);
|
||||
}
|
||||
}
|
||||
|
||||
result.end_time = now_ms();
|
||||
result.completed = result.error.empty();
|
||||
|
||||
if (result.total_filled.is_positive()) {
|
||||
result.average_price = total_value / result.total_filled;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
void TwapExecutor::cancel() {
|
||||
cancelled_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VWAP Executor
|
||||
// =============================================================================
|
||||
|
||||
VwapExecutor::VwapExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
Decimal participation_rate,
|
||||
std::chrono::seconds max_duration)
|
||||
: client_(client),
|
||||
symbol_(std::move(symbol)),
|
||||
side_(side),
|
||||
total_quantity_(total_quantity),
|
||||
participation_rate_(participation_rate),
|
||||
max_duration_(max_duration) {}
|
||||
|
||||
std::future<ExecutionResult> VwapExecutor::execute() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
ExecutionResult result;
|
||||
result.start_time = now_ms();
|
||||
result.total_quantity = total_quantity_;
|
||||
result.completed = false;
|
||||
|
||||
Decimal remaining = total_quantity_;
|
||||
const int check_interval_ms = 5000;
|
||||
int elapsed_ms = 0;
|
||||
Decimal total_value;
|
||||
|
||||
while (remaining > Decimal::zero() &&
|
||||
elapsed_ms < max_duration_.count() * 1000) {
|
||||
if (cancelled_.load(std::memory_order_acquire)) {
|
||||
result.error = "Cancelled";
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
auto ticker = client_.ticker(symbol_).get();
|
||||
Decimal volume = ticker.volume_24h.value_or(Decimal::from_double(1000));
|
||||
|
||||
// Calculate slice based on participation
|
||||
Decimal hourly_volume = volume / Decimal::from_double(24);
|
||||
Decimal slice_volume = hourly_volume * participation_rate_ /
|
||||
Decimal::from_double(3600000.0 / check_interval_ms);
|
||||
Decimal qty = (remaining < slice_volume) ? remaining : slice_volume;
|
||||
|
||||
if (qty > Decimal::zero()) {
|
||||
Order order;
|
||||
if (side_ == Side::Buy) {
|
||||
order = client_.buy(symbol_, qty).get();
|
||||
} else {
|
||||
order = client_.sell(symbol_, qty).get();
|
||||
}
|
||||
|
||||
result.orders.push_back(order);
|
||||
result.total_filled = result.total_filled + order.filled_quantity;
|
||||
remaining = remaining - order.filled_quantity;
|
||||
|
||||
if (order.average_price) {
|
||||
total_value = total_value +
|
||||
(order.filled_quantity * *order.average_price);
|
||||
}
|
||||
|
||||
if (callback_) {
|
||||
callback_(order, remaining);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
// Log but continue
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(check_interval_ms));
|
||||
elapsed_ms += check_interval_ms;
|
||||
}
|
||||
|
||||
result.end_time = now_ms();
|
||||
result.completed = result.error.empty() && remaining <= Decimal::zero();
|
||||
|
||||
if (result.total_filled.is_positive()) {
|
||||
result.average_price = total_value / result.total_filled;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
void VwapExecutor::cancel() {
|
||||
cancelled_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Iceberg Executor
|
||||
// =============================================================================
|
||||
|
||||
IcebergExecutor::IcebergExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
Decimal visible_quantity,
|
||||
Decimal price,
|
||||
std::optional<std::string> venue)
|
||||
: client_(client),
|
||||
symbol_(std::move(symbol)),
|
||||
side_(side),
|
||||
total_quantity_(total_quantity),
|
||||
visible_quantity_(visible_quantity),
|
||||
price_(price),
|
||||
venue_(std::move(venue)) {}
|
||||
|
||||
std::future<ExecutionResult> IcebergExecutor::execute() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
ExecutionResult result;
|
||||
result.start_time = now_ms();
|
||||
result.total_quantity = total_quantity_;
|
||||
result.completed = false;
|
||||
|
||||
Decimal remaining = total_quantity_;
|
||||
Decimal total_value;
|
||||
|
||||
while (remaining > Decimal::zero()) {
|
||||
if (cancelled_.load(std::memory_order_acquire)) {
|
||||
result.error = "Cancelled";
|
||||
break;
|
||||
}
|
||||
|
||||
Decimal qty = (remaining < visible_quantity_) ? remaining : visible_quantity_;
|
||||
|
||||
try {
|
||||
Order order;
|
||||
if (side_ == Side::Buy) {
|
||||
order = client_.limit_buy(
|
||||
symbol_, qty, price_,
|
||||
venue_ ? std::optional<std::string_view>(*venue_)
|
||||
: std::nullopt).get();
|
||||
} else {
|
||||
order = client_.limit_sell(
|
||||
symbol_, qty, price_,
|
||||
venue_ ? std::optional<std::string_view>(*venue_)
|
||||
: std::nullopt).get();
|
||||
}
|
||||
|
||||
// Wait for fill (simplified - in production would poll/stream)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
result.orders.push_back(order);
|
||||
result.total_filled = result.total_filled + order.filled_quantity;
|
||||
remaining = remaining - order.filled_quantity;
|
||||
|
||||
if (order.average_price) {
|
||||
total_value = total_value +
|
||||
(order.filled_quantity * *order.average_price);
|
||||
}
|
||||
|
||||
if (callback_) {
|
||||
callback_(order, remaining);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
result.error = e.what();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.end_time = now_ms();
|
||||
result.completed = result.error.empty() && remaining <= Decimal::zero();
|
||||
|
||||
if (result.total_filled.is_positive()) {
|
||||
result.average_price = total_value / result.total_filled;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
void IcebergExecutor::cancel() {
|
||||
cancelled_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Sniper Executor
|
||||
// =============================================================================
|
||||
|
||||
SniperExecutor::SniperExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal quantity,
|
||||
Decimal target_price,
|
||||
std::chrono::seconds timeout)
|
||||
: client_(client),
|
||||
symbol_(std::move(symbol)),
|
||||
side_(side),
|
||||
quantity_(quantity),
|
||||
target_price_(target_price),
|
||||
timeout_(timeout) {}
|
||||
|
||||
std::future<ExecutionResult> SniperExecutor::execute() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
ExecutionResult result;
|
||||
result.start_time = now_ms();
|
||||
result.total_quantity = quantity_;
|
||||
result.completed = false;
|
||||
|
||||
const int check_interval_ms = 100;
|
||||
int elapsed_ms = 0;
|
||||
|
||||
while (elapsed_ms < timeout_.count() * 1000) {
|
||||
if (cancelled_.load(std::memory_order_acquire)) {
|
||||
result.error = "Cancelled";
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
auto ticker = client_.ticker(symbol_).get();
|
||||
|
||||
bool should_execute = false;
|
||||
if (side_ == Side::Buy) {
|
||||
if (ticker.ask && *ticker.ask <= target_price_) {
|
||||
should_execute = true;
|
||||
}
|
||||
} else {
|
||||
if (ticker.bid && *ticker.bid >= target_price_) {
|
||||
should_execute = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (should_execute) {
|
||||
Order order;
|
||||
if (side_ == Side::Buy) {
|
||||
order = client_.buy(symbol_, quantity_).get();
|
||||
} else {
|
||||
order = client_.sell(symbol_, quantity_).get();
|
||||
}
|
||||
|
||||
result.orders.push_back(order);
|
||||
result.total_filled = order.filled_quantity;
|
||||
result.average_price = order.average_price;
|
||||
result.completed = true;
|
||||
result.end_time = now_ms();
|
||||
|
||||
if (callback_) {
|
||||
callback_(order, Decimal::zero());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
// Continue waiting
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(check_interval_ms));
|
||||
elapsed_ms += check_interval_ms;
|
||||
}
|
||||
|
||||
result.end_time = now_ms();
|
||||
if (result.error.empty() && !result.completed) {
|
||||
result.error = "Timeout";
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
void SniperExecutor::cancel() {
|
||||
cancelled_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// POV Executor
|
||||
// =============================================================================
|
||||
|
||||
PovExecutor::PovExecutor(
|
||||
Client& client,
|
||||
std::string symbol,
|
||||
Side side,
|
||||
Decimal total_quantity,
|
||||
Decimal target_participation,
|
||||
std::chrono::seconds max_duration,
|
||||
std::optional<Decimal> price_limit)
|
||||
: client_(client),
|
||||
symbol_(std::move(symbol)),
|
||||
side_(side),
|
||||
total_quantity_(total_quantity),
|
||||
target_participation_(target_participation),
|
||||
max_duration_(max_duration),
|
||||
price_limit_(price_limit) {}
|
||||
|
||||
std::future<ExecutionResult> PovExecutor::execute() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
ExecutionResult result;
|
||||
result.start_time = now_ms();
|
||||
result.total_quantity = total_quantity_;
|
||||
result.completed = false;
|
||||
|
||||
// Similar to VWAP but with stricter participation targeting
|
||||
Decimal remaining = total_quantity_;
|
||||
const int check_interval_ms = 5000;
|
||||
int elapsed_ms = 0;
|
||||
Decimal total_value;
|
||||
|
||||
while (remaining > Decimal::zero() &&
|
||||
elapsed_ms < max_duration_.count() * 1000) {
|
||||
if (cancelled_.load(std::memory_order_acquire)) {
|
||||
result.error = "Cancelled";
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
auto ticker = client_.ticker(symbol_).get();
|
||||
|
||||
// Check price limit
|
||||
if (price_limit_) {
|
||||
if (side_ == Side::Buy && ticker.ask && *ticker.ask > *price_limit_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(check_interval_ms));
|
||||
elapsed_ms += check_interval_ms;
|
||||
continue;
|
||||
}
|
||||
if (side_ == Side::Sell && ticker.bid && *ticker.bid < *price_limit_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(check_interval_ms));
|
||||
elapsed_ms += check_interval_ms;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Decimal volume = ticker.volume_24h.value_or(Decimal::from_double(1000));
|
||||
Decimal interval_volume = volume / Decimal::from_double(24 * 3600000.0 / check_interval_ms);
|
||||
Decimal target_qty = interval_volume * target_participation_;
|
||||
Decimal qty = (remaining < target_qty) ? remaining : target_qty;
|
||||
|
||||
if (qty > Decimal::zero()) {
|
||||
Order order;
|
||||
if (side_ == Side::Buy) {
|
||||
order = client_.buy(symbol_, qty).get();
|
||||
} else {
|
||||
order = client_.sell(symbol_, qty).get();
|
||||
}
|
||||
|
||||
result.orders.push_back(order);
|
||||
result.total_filled = result.total_filled + order.filled_quantity;
|
||||
remaining = remaining - order.filled_quantity;
|
||||
|
||||
if (order.average_price) {
|
||||
total_value = total_value +
|
||||
(order.filled_quantity * *order.average_price);
|
||||
}
|
||||
|
||||
if (callback_) {
|
||||
callback_(order, remaining);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
// Continue
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(check_interval_ms));
|
||||
elapsed_ms += check_interval_ms;
|
||||
}
|
||||
|
||||
result.end_time = now_ms();
|
||||
result.completed = result.error.empty() && remaining <= Decimal::zero();
|
||||
|
||||
if (result.total_filled.is_positive()) {
|
||||
result.average_price = total_value / result.total_filled;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
void PovExecutor::cancel() {
|
||||
cancelled_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,504 +0,0 @@
|
||||
// LX Trading SDK - Hummingbot Adapter Implementation
|
||||
|
||||
#include <lx/trading/adapters/hummingbot.hpp>
|
||||
#include <lx/trading/orderbook.hpp>
|
||||
#include <cpr/cpr.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
HummingbotAdapter::HummingbotAdapter(std::string_view name, const HummingbotConfig& config)
|
||||
: name_(name), config_(config), capabilities_(VenueCapabilities::amm()) {}
|
||||
|
||||
HummingbotAdapter::~HummingbotAdapter() = default;
|
||||
|
||||
void HummingbotAdapter::update_latency(int64_t start_ns) {
|
||||
int64_t elapsed = now_ns() - start_ns;
|
||||
latency_.store(static_cast<int>(elapsed / 1000000), std::memory_order_release);
|
||||
}
|
||||
|
||||
json HummingbotAdapter::build_request_body() {
|
||||
json body = {
|
||||
{"chain", config_.chain},
|
||||
{"network", config_.network},
|
||||
{"connector", config_.connector}
|
||||
};
|
||||
|
||||
if (config_.wallet_address) {
|
||||
body["address"] = *config_.wallet_address;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
std::future<void> HummingbotAdapter::connect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
|
||||
auto response = cpr::Get(cpr::Url{config_.base_url()});
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Gateway not ready: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
if (data.value("status", "") != "ok") {
|
||||
throw AdapterError("Gateway not ready");
|
||||
}
|
||||
|
||||
update_latency(start);
|
||||
connected_.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<void> HummingbotAdapter::disconnect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
connected_.store(false, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<MarketInfo>> HummingbotAdapter::get_markets() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/amm/tokens"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{build_request_body().dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get markets: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
std::vector<MarketInfo> markets;
|
||||
|
||||
auto tokens = data.value("tokens", json::array());
|
||||
for (size_t i = 0; i < tokens.size(); ++i) {
|
||||
for (size_t j = i + 1; j < tokens.size(); ++j) {
|
||||
std::string sym1 = tokens[i].value("symbol", "");
|
||||
std::string sym2 = tokens[j].value("symbol", "");
|
||||
if (sym1.empty() || sym2.empty()) continue;
|
||||
|
||||
MarketInfo info;
|
||||
info.symbol = sym1 + "-" + sym2;
|
||||
info.base = sym1;
|
||||
info.quote = sym2;
|
||||
info.price_precision = 8;
|
||||
info.quantity_precision = 8;
|
||||
info.tick_size = Decimal::from_double(0.00000001);
|
||||
info.lot_size = Decimal::from_double(0.00000001);
|
||||
markets.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
return markets;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Ticker> HummingbotAdapter::get_ticker(const std::string& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
auto pair = TradingPair::from_symbol(symbol);
|
||||
if (!pair) throw AdapterError("Invalid symbol: " + symbol);
|
||||
|
||||
std::string base(pair->base.data());
|
||||
std::string quote(pair->quote.data());
|
||||
|
||||
auto start = now_ns();
|
||||
|
||||
json body = build_request_body();
|
||||
body["base"] = base;
|
||||
body["quote"] = quote;
|
||||
body["amount"] = "1";
|
||||
body["side"] = "BUY";
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/amm/price"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get ticker: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
|
||||
Ticker ticker;
|
||||
ticker.symbol = symbol;
|
||||
ticker.venue = name_;
|
||||
|
||||
if (data.contains("price") && !data["price"].is_null()) {
|
||||
auto price = Decimal::from_string(data["price"].get<std::string>());
|
||||
ticker.bid = price;
|
||||
ticker.ask = price;
|
||||
ticker.last = price;
|
||||
}
|
||||
|
||||
ticker.timestamp = now_ms();
|
||||
return ticker;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::unique_ptr<Orderbook>> HummingbotAdapter::get_orderbook(
|
||||
const std::string& symbol, std::optional<int> depth) {
|
||||
(void)symbol; (void)depth;
|
||||
return std::async(std::launch::deferred, []() -> std::unique_ptr<Orderbook> {
|
||||
throw AdapterError("Gateway AMM does not have orderbook");
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Trade>> HummingbotAdapter::get_trades(
|
||||
const std::string& symbol, std::optional<int> limit) {
|
||||
(void)symbol; (void)limit;
|
||||
return std::async(std::launch::deferred, []() {
|
||||
return std::vector<Trade>{}; // Gateway doesn't provide trade history
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Balance>> HummingbotAdapter::get_balances() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/chain/balances"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{build_request_body().dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get balances: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
std::vector<Balance> balances;
|
||||
|
||||
auto bals = data.value("balances", json::object());
|
||||
for (auto& [asset, amount] : bals.items()) {
|
||||
Balance bal;
|
||||
bal.asset = asset;
|
||||
bal.venue = name_;
|
||||
bal.free = Decimal::from_string(amount.get<std::string>());
|
||||
bal.locked = Decimal::zero();
|
||||
balances.push_back(std::move(bal));
|
||||
}
|
||||
|
||||
return balances;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Balance> HummingbotAdapter::get_balance(const std::string& asset) {
|
||||
return std::async(std::launch::async, [this, asset]() {
|
||||
auto balances = get_balances().get();
|
||||
for (const auto& b : balances) {
|
||||
if (b.asset == asset) return b;
|
||||
}
|
||||
return Balance{asset, name_, Decimal::zero(), Decimal::zero()};
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> HummingbotAdapter::get_open_orders(
|
||||
const std::optional<std::string>& symbol) {
|
||||
(void)symbol;
|
||||
return std::async(std::launch::deferred, []() {
|
||||
return std::vector<Order>{}; // AMM doesn't have orders
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Order> HummingbotAdapter::place_order(const OrderRequest& request) {
|
||||
return std::async(std::launch::async, [this, request]() {
|
||||
auto pair = TradingPair::from_symbol(request.symbol);
|
||||
if (!pair) throw AdapterError("Invalid symbol: " + request.symbol);
|
||||
|
||||
std::string base(pair->base.data());
|
||||
std::string quote(pair->quote.data());
|
||||
|
||||
auto trade = execute_swap(
|
||||
base, quote, request.quantity,
|
||||
request.side == Side::Buy,
|
||||
Decimal::from_double(0.01)).get();
|
||||
|
||||
Order order;
|
||||
order.order_id = trade.trade_id;
|
||||
order.client_order_id = request.client_order_id;
|
||||
order.symbol = request.symbol;
|
||||
order.venue = name_;
|
||||
order.side = request.side;
|
||||
order.order_type = OrderType::Market;
|
||||
order.status = OrderStatus::Filled;
|
||||
order.quantity = request.quantity;
|
||||
order.filled_quantity = trade.quantity;
|
||||
order.remaining_quantity = Decimal::zero();
|
||||
order.price = trade.price;
|
||||
order.average_price = trade.price;
|
||||
order.created_at = trade.timestamp;
|
||||
order.updated_at = trade.timestamp;
|
||||
order.fees.push_back(trade.fee);
|
||||
return order;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Order> HummingbotAdapter::cancel_order(
|
||||
const std::string& order_id, const std::string& symbol) {
|
||||
(void)order_id; (void)symbol;
|
||||
return std::async(std::launch::deferred, []() -> Order {
|
||||
throw AdapterError("Gateway AMM swaps cannot be cancelled");
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> HummingbotAdapter::cancel_all_orders(
|
||||
const std::optional<std::string>& symbol) {
|
||||
(void)symbol;
|
||||
return std::async(std::launch::deferred, []() {
|
||||
return std::vector<Order>{};
|
||||
});
|
||||
}
|
||||
|
||||
std::future<SwapQuote> HummingbotAdapter::get_swap_quote(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy) {
|
||||
return std::async(std::launch::async,
|
||||
[this, base_token, quote_token, amount, is_buy]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = build_request_body();
|
||||
body["base"] = base_token;
|
||||
body["quote"] = quote_token;
|
||||
body["amount"] = amount.to_string();
|
||||
body["side"] = is_buy ? "BUY" : "SELL";
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/amm/price"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get quote: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
|
||||
SwapQuote quote;
|
||||
quote.base_token = base_token;
|
||||
quote.quote_token = quote_token;
|
||||
quote.input_amount = amount;
|
||||
quote.output_amount = Decimal::from_string(data.value("expectedAmount", "0"));
|
||||
quote.price = Decimal::from_string(data.value("price", "0"));
|
||||
quote.price_impact = Decimal::zero();
|
||||
quote.fee = Decimal::zero();
|
||||
quote.expires_at = now_ms() + 60000;
|
||||
return quote;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Trade> HummingbotAdapter::execute_swap(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::async,
|
||||
[this, base_token, quote_token, amount, is_buy, slippage]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = build_request_body();
|
||||
body["base"] = base_token;
|
||||
body["quote"] = quote_token;
|
||||
body["amount"] = amount.to_string();
|
||||
body["side"] = is_buy ? "BUY" : "SELL";
|
||||
body["limitPrice"] = "";
|
||||
body["allowedSlippage"] = slippage.to_string() + "/100";
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/amm/trade"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to execute swap: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
|
||||
Trade trade;
|
||||
trade.trade_id = data.value("txHash", "");
|
||||
trade.order_id = trade.trade_id;
|
||||
trade.symbol = base_token + "-" + quote_token;
|
||||
trade.venue = name_;
|
||||
trade.side = is_buy ? Side::Buy : Side::Sell;
|
||||
trade.price = Decimal::from_string(data.value("price", "0"));
|
||||
trade.quantity = amount;
|
||||
trade.fee.asset = "GAS";
|
||||
trade.fee.amount = Decimal::from_string(data.value("gasPrice", "0"));
|
||||
trade.timestamp = now_ms();
|
||||
trade.is_maker = false;
|
||||
return trade;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<PoolInfo> HummingbotAdapter::get_pool_info(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token) {
|
||||
return std::async(std::launch::async, [this, base_token, quote_token]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = build_request_body();
|
||||
body["token0"] = base_token;
|
||||
body["token1"] = quote_token;
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/amm/poolPrice"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get pool info: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
|
||||
PoolInfo info;
|
||||
info.address = data.value("token0Address", "");
|
||||
info.base_token = base_token;
|
||||
info.quote_token = quote_token;
|
||||
info.base_reserve = Decimal::from_string(data.value("token0Balance", "0"));
|
||||
info.quote_reserve = Decimal::from_string(data.value("token1Balance", "0"));
|
||||
info.total_liquidity = Decimal::zero();
|
||||
info.fee_rate = Decimal::from_double(0.003);
|
||||
return info;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<LiquidityResult> HummingbotAdapter::add_liquidity(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal base_amount,
|
||||
Decimal quote_amount,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::async,
|
||||
[this, base_token, quote_token, base_amount, quote_amount, slippage]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = build_request_body();
|
||||
body["token0"] = base_token;
|
||||
body["token1"] = quote_token;
|
||||
body["amount0"] = base_amount.to_string();
|
||||
body["amount1"] = quote_amount.to_string();
|
||||
body["allowedSlippage"] = slippage.to_string() + "/100";
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/amm/liquidity/add"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to add liquidity: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
|
||||
LiquidityResult result;
|
||||
result.tx_hash = data.value("txHash", "");
|
||||
result.pool_address = data.value("poolAddress", "");
|
||||
result.base_amount = base_amount;
|
||||
result.quote_amount = quote_amount;
|
||||
result.lp_tokens = Decimal::zero();
|
||||
result.share_percent = Decimal::zero();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<LiquidityResult> HummingbotAdapter::remove_liquidity(
|
||||
const std::string& pool_address,
|
||||
Decimal liquidity_amount,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::async,
|
||||
[this, pool_address, liquidity_amount, slippage]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = build_request_body();
|
||||
body["tokenId"] = pool_address;
|
||||
body["decreasePercent"] = "100";
|
||||
body["allowedSlippage"] = slippage.to_string() + "/100";
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/amm/liquidity/remove"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to remove liquidity: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
|
||||
LiquidityResult result;
|
||||
result.tx_hash = data.value("txHash", "");
|
||||
result.pool_address = pool_address;
|
||||
result.base_amount = Decimal::zero();
|
||||
result.quote_amount = Decimal::zero();
|
||||
result.lp_tokens = liquidity_amount;
|
||||
result.share_percent = Decimal::zero();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<LpPosition>> HummingbotAdapter::get_lp_positions() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{config_.base_url() + "/amm/position"},
|
||||
cpr::Header{{"Content-Type", "application/json"}},
|
||||
cpr::Body{build_request_body().dump()});
|
||||
|
||||
update_latency(start);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("Failed to get LP positions: " + response.text);
|
||||
}
|
||||
|
||||
auto data = json::parse(response.text);
|
||||
std::vector<LpPosition> positions;
|
||||
|
||||
if (data.is_array()) {
|
||||
for (const auto& p : data) {
|
||||
LpPosition pos;
|
||||
pos.pool_address = p.value("tokenId", "");
|
||||
pos.base_token = p.value("token0", "");
|
||||
pos.quote_token = p.value("token1", "");
|
||||
pos.lp_tokens = Decimal::zero();
|
||||
pos.base_amount = Decimal::from_string(p.value("amount0", "0"));
|
||||
pos.quote_amount = Decimal::from_string(p.value("amount1", "0"));
|
||||
pos.share_percent = Decimal::zero();
|
||||
if (p.contains("unclaimedToken0") && !p["unclaimedToken0"].is_null()) {
|
||||
pos.unrealized_pnl = Decimal::from_string(
|
||||
p["unclaimedToken0"].get<std::string>());
|
||||
}
|
||||
positions.push_back(std::move(pos));
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,546 +0,0 @@
|
||||
// LX Trading SDK - Financial Mathematics Implementation
|
||||
// SIMD-optimized where beneficial
|
||||
|
||||
#include <lx/trading/math.hpp>
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
namespace lx::trading::math {
|
||||
|
||||
// =============================================================================
|
||||
// Black-Scholes Implementation
|
||||
// =============================================================================
|
||||
|
||||
double black_scholes(
|
||||
double S, double K, double T, double r, double sigma, bool is_call) noexcept {
|
||||
if (T <= 0) {
|
||||
return is_call ? std::max(S - K, 0.0) : std::max(K - S, 0.0);
|
||||
}
|
||||
|
||||
double sqrt_T = std::sqrt(T);
|
||||
double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrt_T);
|
||||
double d2 = d1 - sigma * sqrt_T;
|
||||
|
||||
if (is_call) {
|
||||
return S * norm_cdf(d1) - K * std::exp(-r * T) * norm_cdf(d2);
|
||||
}
|
||||
return K * std::exp(-r * T) * norm_cdf(-d2) - S * norm_cdf(-d1);
|
||||
}
|
||||
|
||||
double implied_volatility(
|
||||
double price, double S, double K, double T, double r,
|
||||
bool is_call, double tol, int max_iter) noexcept {
|
||||
// Newton-Raphson
|
||||
double sigma = 0.2;
|
||||
|
||||
for (int i = 0; i < max_iter; ++i) {
|
||||
double bs_price = black_scholes(S, K, T, r, sigma, is_call);
|
||||
|
||||
// Vega
|
||||
double sqrt_T = std::sqrt(T);
|
||||
double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrt_T);
|
||||
double vega = S * norm_pdf(d1) * sqrt_T;
|
||||
|
||||
if (std::abs(vega) < 1e-10) break;
|
||||
|
||||
double diff = bs_price - price;
|
||||
if (std::abs(diff) < tol) return sigma;
|
||||
|
||||
sigma -= diff / vega;
|
||||
sigma = std::clamp(sigma, 0.001, 5.0);
|
||||
}
|
||||
|
||||
return sigma;
|
||||
}
|
||||
|
||||
Greeks greeks(
|
||||
double S, double K, double T, double r, double sigma, bool is_call) noexcept {
|
||||
Greeks g{0, 0, 0, 0, 0};
|
||||
|
||||
if (T <= 0) return g;
|
||||
|
||||
double sqrt_T = std::sqrt(T);
|
||||
double d1 = (std::log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrt_T);
|
||||
double d2 = d1 - sigma * sqrt_T;
|
||||
|
||||
double pdf_d1 = norm_pdf(d1);
|
||||
double cdf_d1 = norm_cdf(d1);
|
||||
double cdf_d2 = norm_cdf(d2);
|
||||
double cdf_neg_d1 = norm_cdf(-d1);
|
||||
double cdf_neg_d2 = norm_cdf(-d2);
|
||||
double exp_neg_rT = std::exp(-r * T);
|
||||
|
||||
if (is_call) {
|
||||
g.delta = cdf_d1;
|
||||
g.theta = (-S * pdf_d1 * sigma / (2 * sqrt_T) -
|
||||
r * K * exp_neg_rT * cdf_d2);
|
||||
g.rho = K * T * exp_neg_rT * cdf_d2;
|
||||
} else {
|
||||
g.delta = cdf_d1 - 1;
|
||||
g.theta = (-S * pdf_d1 * sigma / (2 * sqrt_T) +
|
||||
r * K * exp_neg_rT * cdf_neg_d2);
|
||||
g.rho = -K * T * exp_neg_rT * cdf_neg_d2;
|
||||
}
|
||||
|
||||
g.gamma = pdf_d1 / (S * sigma * sqrt_T);
|
||||
g.vega = S * pdf_d1 * sqrt_T / 100; // Per 1% vol change
|
||||
g.theta /= 365; // Daily theta
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AMM Pricing
|
||||
// =============================================================================
|
||||
|
||||
std::pair<double, double> constant_product_price(
|
||||
double reserve_x, double reserve_y,
|
||||
double amount_in, double fee_rate, bool is_x_to_y) noexcept {
|
||||
double amount_in_with_fee = amount_in * (1.0 - fee_rate);
|
||||
|
||||
double amount_out;
|
||||
if (is_x_to_y) {
|
||||
amount_out = (reserve_y * amount_in_with_fee) /
|
||||
(reserve_x + amount_in_with_fee);
|
||||
} else {
|
||||
amount_out = (reserve_x * amount_in_with_fee) /
|
||||
(reserve_y + amount_in_with_fee);
|
||||
}
|
||||
|
||||
double effective_price = (amount_in > 0) ? amount_out / amount_in : 0;
|
||||
return {amount_out, effective_price};
|
||||
}
|
||||
|
||||
std::tuple<double, double, double> concentrated_liquidity_price(
|
||||
double liquidity, double sqrt_price_current,
|
||||
double sqrt_price_lower, double sqrt_price_upper,
|
||||
double amount_in, double fee_rate, bool is_token0_in) noexcept {
|
||||
double amount_in_with_fee = amount_in * (1.0 - fee_rate);
|
||||
double new_sqrt_p;
|
||||
double amount_out;
|
||||
|
||||
if (is_token0_in) {
|
||||
// Swapping X for Y (price goes up)
|
||||
double delta_inv_sqrt_p = amount_in_with_fee / liquidity;
|
||||
double new_inv_sqrt_p = 1.0 / sqrt_price_current - delta_inv_sqrt_p;
|
||||
|
||||
if (new_inv_sqrt_p <= 0) {
|
||||
new_sqrt_p = sqrt_price_upper;
|
||||
} else {
|
||||
new_sqrt_p = 1.0 / new_inv_sqrt_p;
|
||||
}
|
||||
|
||||
new_sqrt_p = std::min(new_sqrt_p, sqrt_price_upper);
|
||||
amount_out = liquidity * (new_sqrt_p - sqrt_price_current);
|
||||
} else {
|
||||
// Swapping Y for X (price goes down)
|
||||
double delta_sqrt_p = amount_in_with_fee / liquidity;
|
||||
new_sqrt_p = sqrt_price_current - delta_sqrt_p;
|
||||
|
||||
new_sqrt_p = std::max(new_sqrt_p, sqrt_price_lower);
|
||||
amount_out = liquidity * (1.0 / new_sqrt_p - 1.0 / sqrt_price_current);
|
||||
}
|
||||
|
||||
double old_price = sqrt_price_current * sqrt_price_current;
|
||||
double new_price = new_sqrt_p * new_sqrt_p;
|
||||
double price_impact = std::abs(new_price - old_price) / old_price;
|
||||
|
||||
return {std::max(amount_out, 0.0), new_sqrt_p, price_impact};
|
||||
}
|
||||
|
||||
double calculate_liquidity(
|
||||
double amount_x, double amount_y,
|
||||
double sqrt_price_current,
|
||||
double sqrt_price_lower, double sqrt_price_upper) noexcept {
|
||||
if (sqrt_price_current <= sqrt_price_lower) {
|
||||
// Only token X
|
||||
return amount_x * sqrt_price_lower * sqrt_price_upper /
|
||||
(sqrt_price_upper - sqrt_price_lower);
|
||||
} else if (sqrt_price_current >= sqrt_price_upper) {
|
||||
// Only token Y
|
||||
return amount_y / (sqrt_price_upper - sqrt_price_lower);
|
||||
} else {
|
||||
// Both tokens
|
||||
double l_x = amount_x * sqrt_price_current * sqrt_price_upper /
|
||||
(sqrt_price_upper - sqrt_price_current);
|
||||
double l_y = amount_y / (sqrt_price_current - sqrt_price_lower);
|
||||
return std::min(l_x, l_y);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Risk Metrics
|
||||
// =============================================================================
|
||||
|
||||
double volatility(
|
||||
const std::vector<double>& returns, bool annualize, int periods_per_year) noexcept {
|
||||
if (returns.size() < 2) return 0.0;
|
||||
|
||||
double mean = std::accumulate(returns.begin(), returns.end(), 0.0) / returns.size();
|
||||
|
||||
double variance = 0.0;
|
||||
for (double r : returns) {
|
||||
double diff = r - mean;
|
||||
variance += diff * diff;
|
||||
}
|
||||
variance /= (returns.size() - 1);
|
||||
|
||||
double std_dev = std::sqrt(variance);
|
||||
|
||||
if (annualize) {
|
||||
std_dev *= std::sqrt(periods_per_year);
|
||||
}
|
||||
|
||||
return std_dev;
|
||||
}
|
||||
|
||||
double sharpe_ratio(
|
||||
const std::vector<double>& returns, double risk_free_rate, int periods_per_year) noexcept {
|
||||
if (returns.size() < 2) return 0.0;
|
||||
|
||||
double mean = std::accumulate(returns.begin(), returns.end(), 0.0) / returns.size();
|
||||
|
||||
double variance = 0.0;
|
||||
for (double r : returns) {
|
||||
double diff = r - mean;
|
||||
variance += diff * diff;
|
||||
}
|
||||
variance /= (returns.size() - 1);
|
||||
|
||||
double std_dev = std::sqrt(variance);
|
||||
if (std_dev == 0.0) return 0.0;
|
||||
|
||||
double period_rf = risk_free_rate / periods_per_year;
|
||||
double excess_return = mean - period_rf;
|
||||
|
||||
return (excess_return * periods_per_year) / (std_dev * std::sqrt(periods_per_year));
|
||||
}
|
||||
|
||||
double sortino_ratio(
|
||||
const std::vector<double>& returns,
|
||||
double risk_free_rate, double target_return, int periods_per_year) noexcept {
|
||||
if (returns.size() < 2) return 0.0;
|
||||
|
||||
double mean = std::accumulate(returns.begin(), returns.end(), 0.0) / returns.size();
|
||||
|
||||
// Downside deviation
|
||||
double downside_sum = 0.0;
|
||||
for (double r : returns) {
|
||||
double diff = std::min(r - target_return, 0.0);
|
||||
downside_sum += diff * diff;
|
||||
}
|
||||
double downside_std = std::sqrt(downside_sum / returns.size());
|
||||
|
||||
if (downside_std == 0.0) {
|
||||
return (mean > risk_free_rate / periods_per_year) ? INFINITY : 0.0;
|
||||
}
|
||||
|
||||
double period_rf = risk_free_rate / periods_per_year;
|
||||
double excess_return = mean - period_rf;
|
||||
|
||||
return (excess_return * periods_per_year) / (downside_std * std::sqrt(periods_per_year));
|
||||
}
|
||||
|
||||
std::tuple<double, size_t, size_t> max_drawdown(
|
||||
const std::vector<double>& prices) noexcept {
|
||||
if (prices.size() < 2) return {0.0, 0, 0};
|
||||
|
||||
double peak = prices[0];
|
||||
size_t peak_idx = 0;
|
||||
double max_dd = 0.0;
|
||||
size_t max_dd_peak = 0;
|
||||
size_t max_dd_trough = 0;
|
||||
|
||||
for (size_t i = 0; i < prices.size(); ++i) {
|
||||
if (prices[i] > peak) {
|
||||
peak = prices[i];
|
||||
peak_idx = i;
|
||||
}
|
||||
|
||||
double dd = (peak > 0) ? (peak - prices[i]) / peak : 0.0;
|
||||
|
||||
if (dd > max_dd) {
|
||||
max_dd = dd;
|
||||
max_dd_peak = peak_idx;
|
||||
max_dd_trough = i;
|
||||
}
|
||||
}
|
||||
|
||||
return {max_dd, max_dd_peak, max_dd_trough};
|
||||
}
|
||||
|
||||
double var(
|
||||
const std::vector<double>& returns, double confidence, bool parametric) noexcept {
|
||||
if (returns.size() < 10) return 0.0;
|
||||
|
||||
if (!parametric) {
|
||||
// Historical VaR
|
||||
std::vector<double> sorted = returns;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
size_t idx = static_cast<size_t>(sorted.size() * (1.0 - confidence));
|
||||
return -sorted[idx];
|
||||
}
|
||||
|
||||
// Parametric VaR (assumes normal)
|
||||
double mean = std::accumulate(returns.begin(), returns.end(), 0.0) / returns.size();
|
||||
|
||||
double variance = 0.0;
|
||||
for (double r : returns) {
|
||||
double diff = r - mean;
|
||||
variance += diff * diff;
|
||||
}
|
||||
variance /= (returns.size() - 1);
|
||||
double std_dev = std::sqrt(variance);
|
||||
|
||||
// Z-score approximation
|
||||
double z = (confidence == 0.95) ? -1.645 : -2.326;
|
||||
|
||||
return -(mean + z * std_dev);
|
||||
}
|
||||
|
||||
double cvar(
|
||||
const std::vector<double>& returns, double confidence) noexcept {
|
||||
if (returns.size() < 10) return 0.0;
|
||||
|
||||
double var_value = var(returns, confidence, false);
|
||||
|
||||
// Average of returns worse than VaR
|
||||
std::vector<double> tail;
|
||||
for (double r : returns) {
|
||||
if (r <= -var_value) {
|
||||
tail.push_back(r);
|
||||
}
|
||||
}
|
||||
|
||||
if (tail.empty()) return var_value;
|
||||
|
||||
return -std::accumulate(tail.begin(), tail.end(), 0.0) / tail.size();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SIMD Operations (AVX2)
|
||||
// =============================================================================
|
||||
|
||||
#ifdef LX_SIMD_AVX2
|
||||
|
||||
void black_scholes_batch(
|
||||
const double* S, const double* K, const double* T,
|
||||
const double* r, const double* sigma,
|
||||
double* prices, size_t count, bool is_call) noexcept {
|
||||
// Process 4 at a time with AVX2
|
||||
size_t i = 0;
|
||||
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
__m256d vS = _mm256_loadu_pd(&S[i]);
|
||||
__m256d vK = _mm256_loadu_pd(&K[i]);
|
||||
__m256d vT = _mm256_loadu_pd(&T[i]);
|
||||
__m256d vr = _mm256_loadu_pd(&r[i]);
|
||||
__m256d vsigma = _mm256_loadu_pd(&sigma[i]);
|
||||
|
||||
// sqrt(T)
|
||||
__m256d sqrt_T = _mm256_sqrt_pd(vT);
|
||||
|
||||
// d1 = (log(S/K) + (r + 0.5*sigma^2)*T) / (sigma*sqrt(T))
|
||||
__m256d log_SK = _mm256_div_pd(vS, vK);
|
||||
// Note: Would need log intrinsics or loop here
|
||||
|
||||
// Simplified: fall back to scalar for now
|
||||
for (size_t j = 0; j < 4; ++j) {
|
||||
prices[i + j] = black_scholes(S[i+j], K[i+j], T[i+j], r[i+j], sigma[i+j], is_call);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remainder
|
||||
for (; i < count; ++i) {
|
||||
prices[i] = black_scholes(S[i], K[i], T[i], r[i], sigma[i], is_call);
|
||||
}
|
||||
}
|
||||
|
||||
double sum_avx2(const double* data, size_t count) noexcept {
|
||||
__m256d sum = _mm256_setzero_pd();
|
||||
size_t i = 0;
|
||||
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
__m256d v = _mm256_loadu_pd(&data[i]);
|
||||
sum = _mm256_add_pd(sum, v);
|
||||
}
|
||||
|
||||
// Horizontal sum
|
||||
double result[4];
|
||||
_mm256_storeu_pd(result, sum);
|
||||
double total = result[0] + result[1] + result[2] + result[3];
|
||||
|
||||
// Remainder
|
||||
for (; i < count; ++i) {
|
||||
total += data[i];
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
double mean_avx2(const double* data, size_t count) noexcept {
|
||||
if (count == 0) return 0.0;
|
||||
return sum_avx2(data, count) / count;
|
||||
}
|
||||
|
||||
double variance_avx2(const double* data, size_t count, double mean) noexcept {
|
||||
if (count < 2) return 0.0;
|
||||
|
||||
__m256d vmean = _mm256_set1_pd(mean);
|
||||
__m256d sum_sq = _mm256_setzero_pd();
|
||||
size_t i = 0;
|
||||
|
||||
for (; i + 4 <= count; i += 4) {
|
||||
__m256d v = _mm256_loadu_pd(&data[i]);
|
||||
__m256d diff = _mm256_sub_pd(v, vmean);
|
||||
sum_sq = _mm256_fmadd_pd(diff, diff, sum_sq);
|
||||
}
|
||||
|
||||
double result[4];
|
||||
_mm256_storeu_pd(result, sum_sq);
|
||||
double total = result[0] + result[1] + result[2] + result[3];
|
||||
|
||||
// Remainder
|
||||
for (; i < count; ++i) {
|
||||
double diff = data[i] - mean;
|
||||
total += diff * diff;
|
||||
}
|
||||
|
||||
return total / (count - 1);
|
||||
}
|
||||
|
||||
#endif // LX_SIMD_AVX2
|
||||
|
||||
// =============================================================================
|
||||
// Statistical Utilities
|
||||
// =============================================================================
|
||||
|
||||
std::vector<double> calculate_returns(const std::vector<double>& prices) {
|
||||
if (prices.size() < 2) return {};
|
||||
|
||||
std::vector<double> returns;
|
||||
returns.reserve(prices.size() - 1);
|
||||
|
||||
for (size_t i = 1; i < prices.size(); ++i) {
|
||||
if (prices[i-1] > 0) {
|
||||
returns.push_back((prices[i] - prices[i-1]) / prices[i-1]);
|
||||
}
|
||||
}
|
||||
|
||||
return returns;
|
||||
}
|
||||
|
||||
std::vector<double> rolling_mean(const std::vector<double>& data, size_t window) {
|
||||
if (data.size() < window) return {};
|
||||
|
||||
std::vector<double> result;
|
||||
result.reserve(data.size() - window + 1);
|
||||
|
||||
double sum = std::accumulate(data.begin(), data.begin() + window, 0.0);
|
||||
result.push_back(sum / window);
|
||||
|
||||
for (size_t i = window; i < data.size(); ++i) {
|
||||
sum += data[i] - data[i - window];
|
||||
result.push_back(sum / window);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<double> rolling_std(const std::vector<double>& data, size_t window) {
|
||||
if (data.size() < window) return {};
|
||||
|
||||
std::vector<double> result;
|
||||
result.reserve(data.size() - window + 1);
|
||||
|
||||
for (size_t i = 0; i <= data.size() - window; ++i) {
|
||||
double mean = 0.0;
|
||||
for (size_t j = 0; j < window; ++j) {
|
||||
mean += data[i + j];
|
||||
}
|
||||
mean /= window;
|
||||
|
||||
double variance = 0.0;
|
||||
for (size_t j = 0; j < window; ++j) {
|
||||
double diff = data[i + j] - mean;
|
||||
variance += diff * diff;
|
||||
}
|
||||
variance /= (window - 1);
|
||||
|
||||
result.push_back(std::sqrt(variance));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<double> ema(const std::vector<double>& data, double alpha) {
|
||||
if (data.empty()) return {};
|
||||
|
||||
std::vector<double> result;
|
||||
result.reserve(data.size());
|
||||
result.push_back(data[0]);
|
||||
|
||||
for (size_t i = 1; i < data.size(); ++i) {
|
||||
result.push_back(alpha * data[i] + (1.0 - alpha) * result.back());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
double correlation(const std::vector<double>& x, const std::vector<double>& y) noexcept {
|
||||
if (x.size() != y.size() || x.size() < 2) return 0.0;
|
||||
|
||||
size_t n = x.size();
|
||||
|
||||
double mean_x = std::accumulate(x.begin(), x.end(), 0.0) / n;
|
||||
double mean_y = std::accumulate(y.begin(), y.end(), 0.0) / n;
|
||||
|
||||
double cov = 0.0, var_x = 0.0, var_y = 0.0;
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double dx = x[i] - mean_x;
|
||||
double dy = y[i] - mean_y;
|
||||
cov += dx * dy;
|
||||
var_x += dx * dx;
|
||||
var_y += dy * dy;
|
||||
}
|
||||
|
||||
double denom = std::sqrt(var_x * var_y);
|
||||
return (denom > 0) ? cov / denom : 0.0;
|
||||
}
|
||||
|
||||
double covariance(const std::vector<double>& x, const std::vector<double>& y) noexcept {
|
||||
if (x.size() != y.size() || x.size() < 2) return 0.0;
|
||||
|
||||
size_t n = x.size();
|
||||
|
||||
double mean_x = std::accumulate(x.begin(), x.end(), 0.0) / n;
|
||||
double mean_y = std::accumulate(y.begin(), y.end(), 0.0) / n;
|
||||
|
||||
double cov = 0.0;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
cov += (x[i] - mean_x) * (y[i] - mean_y);
|
||||
}
|
||||
|
||||
return cov / (n - 1);
|
||||
}
|
||||
|
||||
double beta(
|
||||
const std::vector<double>& asset_returns,
|
||||
const std::vector<double>& market_returns) noexcept {
|
||||
double cov = covariance(asset_returns, market_returns);
|
||||
|
||||
double mean_m = std::accumulate(market_returns.begin(), market_returns.end(), 0.0) /
|
||||
market_returns.size();
|
||||
|
||||
double var_m = 0.0;
|
||||
for (double r : market_returns) {
|
||||
double diff = r - mean_m;
|
||||
var_m += diff * diff;
|
||||
}
|
||||
var_m /= (market_returns.size() - 1);
|
||||
|
||||
return (var_m > 0) ? cov / var_m : 0.0;
|
||||
}
|
||||
|
||||
} // namespace lx::trading::math
|
||||
@@ -1,750 +0,0 @@
|
||||
// LX Trading SDK - Native Adapters Implementation
|
||||
|
||||
#include <lx/trading/adapters/native.hpp>
|
||||
#include <lx/trading/orderbook.hpp>
|
||||
#include <cpr/cpr.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// HTTP client wrapper
|
||||
class HttpClient {
|
||||
public:
|
||||
explicit HttpClient(const std::string& base_url) : base_url_(base_url) {}
|
||||
|
||||
json get(const std::string& path,
|
||||
const std::optional<std::string>& api_key = std::nullopt) {
|
||||
cpr::Header headers;
|
||||
if (api_key) {
|
||||
headers["X-API-KEY"] = *api_key;
|
||||
headers["X-TIMESTAMP"] = std::to_string(now_ms());
|
||||
}
|
||||
|
||||
auto response = cpr::Get(
|
||||
cpr::Url{base_url_ + path},
|
||||
headers);
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("HTTP " + std::to_string(response.status_code) +
|
||||
": " + response.text);
|
||||
}
|
||||
|
||||
return json::parse(response.text);
|
||||
}
|
||||
|
||||
json post(const std::string& path,
|
||||
const json& body,
|
||||
const std::optional<std::string>& api_key = std::nullopt) {
|
||||
cpr::Header headers{{"Content-Type", "application/json"}};
|
||||
if (api_key) {
|
||||
headers["X-API-KEY"] = *api_key;
|
||||
headers["X-TIMESTAMP"] = std::to_string(now_ms());
|
||||
}
|
||||
|
||||
auto response = cpr::Post(
|
||||
cpr::Url{base_url_ + path},
|
||||
headers,
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
if (response.status_code != 200 && response.status_code != 201) {
|
||||
throw AdapterError("HTTP " + std::to_string(response.status_code) +
|
||||
": " + response.text);
|
||||
}
|
||||
|
||||
return json::parse(response.text);
|
||||
}
|
||||
|
||||
json del(const std::string& path,
|
||||
const json& body = {},
|
||||
const std::optional<std::string>& api_key = std::nullopt) {
|
||||
cpr::Header headers{{"Content-Type", "application/json"}};
|
||||
if (api_key) {
|
||||
headers["X-API-KEY"] = *api_key;
|
||||
headers["X-TIMESTAMP"] = std::to_string(now_ms());
|
||||
}
|
||||
|
||||
auto response = cpr::Delete(
|
||||
cpr::Url{base_url_ + path},
|
||||
headers,
|
||||
cpr::Body{body.dump()});
|
||||
|
||||
if (response.status_code != 200) {
|
||||
throw AdapterError("HTTP " + std::to_string(response.status_code) +
|
||||
": " + response.text);
|
||||
}
|
||||
|
||||
return json::parse(response.text);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string base_url_;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// LxDexAdapter Implementation
|
||||
// =============================================================================
|
||||
|
||||
LxDexAdapter::LxDexAdapter(std::string_view name, const NativeVenueConfig& config)
|
||||
: name_(name), config_(config), capabilities_(VenueCapabilities::clob()) {
|
||||
http_ = std::make_unique<HttpClient>(config.api_url);
|
||||
}
|
||||
|
||||
LxDexAdapter::~LxDexAdapter() = default;
|
||||
|
||||
void LxDexAdapter::update_latency(int64_t start_ns) {
|
||||
int64_t elapsed = now_ns() - start_ns;
|
||||
latency_.store(static_cast<int>(elapsed / 1000000), std::memory_order_release);
|
||||
}
|
||||
|
||||
std::future<void> LxDexAdapter::connect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
http_->get("/api/v1/health");
|
||||
update_latency(start);
|
||||
connected_.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<void> LxDexAdapter::disconnect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
connected_.store(false, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<MarketInfo>> LxDexAdapter::get_markets() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/markets", config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
std::vector<MarketInfo> markets;
|
||||
for (const auto& m : data) {
|
||||
MarketInfo info;
|
||||
info.symbol = m["symbol"];
|
||||
info.base = m["base"];
|
||||
info.quote = m["quote"];
|
||||
info.price_precision = m.value("pricePrecision", 8);
|
||||
info.quantity_precision = m.value("quantityPrecision", 8);
|
||||
info.min_quantity = Decimal::from_string(m.value("minQuantity", "0"));
|
||||
if (m.contains("maxQuantity") && !m["maxQuantity"].is_null()) {
|
||||
info.max_quantity = Decimal::from_string(m["maxQuantity"].get<std::string>());
|
||||
}
|
||||
if (m.contains("minNotional") && !m["minNotional"].is_null()) {
|
||||
info.min_notional = Decimal::from_string(m["minNotional"].get<std::string>());
|
||||
}
|
||||
info.tick_size = Decimal::from_string(m.value("tickSize", "0.00000001"));
|
||||
info.lot_size = Decimal::from_string(m.value("lotSize", "0.00000001"));
|
||||
markets.push_back(std::move(info));
|
||||
}
|
||||
return markets;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Ticker> LxDexAdapter::get_ticker(const std::string& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/ticker/" + symbol, config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
Ticker ticker;
|
||||
ticker.symbol = data.value("symbol", symbol);
|
||||
ticker.venue = name_;
|
||||
if (data.contains("bid") && !data["bid"].is_null())
|
||||
ticker.bid = Decimal::from_string(data["bid"].get<std::string>());
|
||||
if (data.contains("ask") && !data["ask"].is_null())
|
||||
ticker.ask = Decimal::from_string(data["ask"].get<std::string>());
|
||||
if (data.contains("last") && !data["last"].is_null())
|
||||
ticker.last = Decimal::from_string(data["last"].get<std::string>());
|
||||
if (data.contains("volume24h") && !data["volume24h"].is_null())
|
||||
ticker.volume_24h = Decimal::from_string(data["volume24h"].get<std::string>());
|
||||
ticker.timestamp = data.value("timestamp", now_ms());
|
||||
return ticker;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::unique_ptr<Orderbook>> LxDexAdapter::get_orderbook(
|
||||
const std::string& symbol, std::optional<int> depth) {
|
||||
return std::async(std::launch::async, [this, symbol, depth]() {
|
||||
auto start = now_ns();
|
||||
std::string path = "/api/v1/orderbook/" + symbol;
|
||||
if (depth) path += "?depth=" + std::to_string(*depth);
|
||||
|
||||
auto data = http_->get(path, config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
auto book = std::make_unique<Orderbook>(symbol, name_);
|
||||
|
||||
for (const auto& bid : data["bids"]) {
|
||||
book->add_bid(
|
||||
Decimal::from_string(bid[0].get<std::string>()),
|
||||
Decimal::from_string(bid[1].get<std::string>()));
|
||||
}
|
||||
|
||||
for (const auto& ask : data["asks"]) {
|
||||
book->add_ask(
|
||||
Decimal::from_string(ask[0].get<std::string>()),
|
||||
Decimal::from_string(ask[1].get<std::string>()));
|
||||
}
|
||||
|
||||
book->sort();
|
||||
return book;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Trade>> LxDexAdapter::get_trades(
|
||||
const std::string& symbol, std::optional<int> limit) {
|
||||
return std::async(std::launch::async, [this, symbol, limit]() {
|
||||
auto start = now_ns();
|
||||
std::string path = "/api/v1/trades/" + symbol;
|
||||
if (limit) path += "?limit=" + std::to_string(*limit);
|
||||
|
||||
auto data = http_->get(path, config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
std::vector<Trade> trades;
|
||||
for (const auto& t : data) {
|
||||
Trade trade;
|
||||
trade.trade_id = t["id"];
|
||||
trade.order_id = t.value("orderId", "");
|
||||
trade.symbol = symbol;
|
||||
trade.venue = name_;
|
||||
trade.side = (t["side"] == "buy") ? Side::Buy : Side::Sell;
|
||||
trade.price = Decimal::from_string(t["price"].get<std::string>());
|
||||
trade.quantity = Decimal::from_string(t["quantity"].get<std::string>());
|
||||
trade.fee.asset = t.value("feeAsset", "");
|
||||
trade.fee.amount = Decimal::from_string(t.value("feeAmount", "0"));
|
||||
trade.timestamp = t["timestamp"];
|
||||
trade.is_maker = t.value("isMaker", false);
|
||||
trades.push_back(std::move(trade));
|
||||
}
|
||||
return trades;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Balance>> LxDexAdapter::get_balances() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/account/balances", config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
std::vector<Balance> balances;
|
||||
for (const auto& b : data) {
|
||||
Balance bal;
|
||||
bal.asset = b["asset"];
|
||||
bal.venue = name_;
|
||||
bal.free = Decimal::from_string(b["free"].get<std::string>());
|
||||
bal.locked = Decimal::from_string(b["locked"].get<std::string>());
|
||||
balances.push_back(std::move(bal));
|
||||
}
|
||||
return balances;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Balance> LxDexAdapter::get_balance(const std::string& asset) {
|
||||
return std::async(std::launch::async, [this, asset]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/account/balance/" + asset, config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
Balance bal;
|
||||
bal.asset = data["asset"];
|
||||
bal.venue = name_;
|
||||
bal.free = Decimal::from_string(data["free"].get<std::string>());
|
||||
bal.locked = Decimal::from_string(data["locked"].get<std::string>());
|
||||
return bal;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> LxDexAdapter::get_open_orders(
|
||||
const std::optional<std::string>& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
auto start = now_ns();
|
||||
std::string path = "/api/v1/orders?status=open";
|
||||
if (symbol) path += "&symbol=" + *symbol;
|
||||
|
||||
auto data = http_->get(path, config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
std::vector<Order> orders;
|
||||
for (const auto& o : data) {
|
||||
orders.push_back(convert_order(o));
|
||||
}
|
||||
return orders;
|
||||
});
|
||||
}
|
||||
|
||||
Order LxDexAdapter::convert_order(const json& o) {
|
||||
Order order;
|
||||
order.order_id = o["orderId"];
|
||||
order.client_order_id = o.value("clientOrderId", "");
|
||||
order.symbol = o["symbol"];
|
||||
order.venue = name_;
|
||||
order.side = (o["side"] == "buy") ? Side::Buy : Side::Sell;
|
||||
|
||||
std::string type_str = o.value("type", "limit");
|
||||
if (type_str == "market") order.order_type = OrderType::Market;
|
||||
else if (type_str == "limit") order.order_type = OrderType::Limit;
|
||||
else if (type_str == "stop_loss") order.order_type = OrderType::StopLoss;
|
||||
else if (type_str == "stop_loss_limit") order.order_type = OrderType::StopLossLimit;
|
||||
|
||||
std::string status_str = o.value("status", "open");
|
||||
if (status_str == "pending") order.status = OrderStatus::Pending;
|
||||
else if (status_str == "open") order.status = OrderStatus::Open;
|
||||
else if (status_str == "partially_filled") order.status = OrderStatus::PartiallyFilled;
|
||||
else if (status_str == "filled") order.status = OrderStatus::Filled;
|
||||
else if (status_str == "cancelled") order.status = OrderStatus::Cancelled;
|
||||
else if (status_str == "rejected") order.status = OrderStatus::Rejected;
|
||||
else if (status_str == "expired") order.status = OrderStatus::Expired;
|
||||
|
||||
order.quantity = Decimal::from_string(o.value("quantity", "0"));
|
||||
order.filled_quantity = Decimal::from_string(o.value("filledQuantity", "0"));
|
||||
order.remaining_quantity = order.quantity - order.filled_quantity;
|
||||
|
||||
if (o.contains("price") && !o["price"].is_null())
|
||||
order.price = Decimal::from_string(o["price"].get<std::string>());
|
||||
if (o.contains("averagePrice") && !o["averagePrice"].is_null())
|
||||
order.average_price = Decimal::from_string(o["averagePrice"].get<std::string>());
|
||||
|
||||
order.created_at = o.value("createdAt", int64_t(0));
|
||||
order.updated_at = o.value("updatedAt", int64_t(0));
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
std::future<Order> LxDexAdapter::place_order(const OrderRequest& request) {
|
||||
return std::async(std::launch::async, [this, request]() -> Order {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"clientOrderId", request.client_order_id},
|
||||
{"symbol", request.symbol},
|
||||
{"side", to_string(request.side)},
|
||||
{"type", to_string(request.order_type)},
|
||||
{"quantity", request.quantity.to_string()},
|
||||
{"timeInForce", to_string(request.time_in_force)}
|
||||
};
|
||||
|
||||
if (request.price) {
|
||||
body["price"] = request.price->to_string();
|
||||
}
|
||||
|
||||
auto data = http_->post("/api/v1/orders", body, config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
return convert_order(data);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Order> LxDexAdapter::cancel_order(
|
||||
const std::string& order_id, const std::string& symbol) {
|
||||
return std::async(std::launch::async, [this, order_id, symbol]() -> Order {
|
||||
auto start = now_ns();
|
||||
json body = {{"symbol", symbol}};
|
||||
auto data = http_->del("/api/v1/orders/" + order_id, body, config_.api_key);
|
||||
update_latency(start);
|
||||
return convert_order(data);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> LxDexAdapter::cancel_all_orders(
|
||||
const std::optional<std::string>& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
auto start = now_ns();
|
||||
json body = {};
|
||||
if (symbol) body["symbol"] = *symbol;
|
||||
|
||||
auto data = http_->del("/api/v1/orders/all", body, config_.api_key);
|
||||
update_latency(start);
|
||||
|
||||
std::vector<Order> orders;
|
||||
for (const auto& o : data) {
|
||||
orders.push_back(convert_order(o));
|
||||
}
|
||||
return orders;
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LxAmmAdapter Implementation
|
||||
// =============================================================================
|
||||
|
||||
LxAmmAdapter::LxAmmAdapter(std::string_view name, const NativeVenueConfig& config)
|
||||
: name_(name), config_(config), capabilities_(VenueCapabilities::amm()) {
|
||||
http_ = std::make_unique<HttpClient>(config.api_url);
|
||||
}
|
||||
|
||||
LxAmmAdapter::~LxAmmAdapter() = default;
|
||||
|
||||
void LxAmmAdapter::update_latency(int64_t start_ns) {
|
||||
int64_t elapsed = now_ns() - start_ns;
|
||||
latency_.store(static_cast<int>(elapsed / 1000000), std::memory_order_release);
|
||||
}
|
||||
|
||||
std::future<void> LxAmmAdapter::connect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
connected_.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<void> LxAmmAdapter::disconnect() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
connected_.store(false, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<MarketInfo>> LxAmmAdapter::get_markets() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/amm/pools");
|
||||
update_latency(start);
|
||||
|
||||
std::vector<MarketInfo> markets;
|
||||
for (const auto& p : data) {
|
||||
MarketInfo info;
|
||||
std::string base = p["baseToken"];
|
||||
std::string quote = p["quoteToken"];
|
||||
info.symbol = base + "-" + quote;
|
||||
info.base = base;
|
||||
info.quote = quote;
|
||||
info.price_precision = 8;
|
||||
info.quantity_precision = 8;
|
||||
info.tick_size = Decimal::from_double(0.00000001);
|
||||
info.lot_size = Decimal::from_double(0.00000001);
|
||||
markets.push_back(std::move(info));
|
||||
}
|
||||
return markets;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Ticker> LxAmmAdapter::get_ticker(const std::string& symbol) {
|
||||
return std::async(std::launch::async, [this, symbol]() {
|
||||
auto pair = TradingPair::from_symbol(symbol);
|
||||
if (!pair) throw AdapterError("Invalid symbol: " + symbol);
|
||||
|
||||
std::string base(pair->base.data());
|
||||
std::string quote(pair->quote.data());
|
||||
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/amm/price/" + base + "/" + quote);
|
||||
update_latency(start);
|
||||
|
||||
Decimal price = Decimal::from_string(data.value("price", "0"));
|
||||
|
||||
Ticker ticker;
|
||||
ticker.symbol = symbol;
|
||||
ticker.venue = name_;
|
||||
ticker.bid = price;
|
||||
ticker.ask = price;
|
||||
ticker.last = price;
|
||||
ticker.timestamp = now_ms();
|
||||
return ticker;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::unique_ptr<Orderbook>> LxAmmAdapter::get_orderbook(
|
||||
const std::string& symbol, std::optional<int> depth) {
|
||||
(void)symbol; (void)depth;
|
||||
return std::async(std::launch::deferred, []() -> std::unique_ptr<Orderbook> {
|
||||
throw AdapterError("AMM does not have orderbook");
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Trade>> LxAmmAdapter::get_trades(
|
||||
const std::string& symbol, std::optional<int> limit) {
|
||||
return std::async(std::launch::async, [this, symbol, limit]() {
|
||||
auto pair = TradingPair::from_symbol(symbol);
|
||||
if (!pair) return std::vector<Trade>{};
|
||||
|
||||
std::string base(pair->base.data());
|
||||
std::string quote(pair->quote.data());
|
||||
|
||||
std::string path = "/api/v1/amm/swaps/" + base + "/" + quote;
|
||||
if (limit) path += "?limit=" + std::to_string(*limit);
|
||||
|
||||
auto start = now_ns();
|
||||
auto data = http_->get(path);
|
||||
update_latency(start);
|
||||
|
||||
std::vector<Trade> trades;
|
||||
for (const auto& t : data) {
|
||||
Trade trade;
|
||||
trade.trade_id = t["txHash"];
|
||||
trade.order_id = t["txHash"];
|
||||
trade.symbol = symbol;
|
||||
trade.venue = name_;
|
||||
trade.side = (t.value("side", "buy") == "buy") ? Side::Buy : Side::Sell;
|
||||
trade.price = Decimal::from_string(t["price"].get<std::string>());
|
||||
trade.quantity = Decimal::from_string(t["amount"].get<std::string>());
|
||||
trade.fee.amount = Decimal::from_string(t.value("fee", "0"));
|
||||
trade.timestamp = t["timestamp"];
|
||||
trade.is_maker = false;
|
||||
trades.push_back(std::move(trade));
|
||||
}
|
||||
return trades;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Balance>> LxAmmAdapter::get_balances() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/account/balances");
|
||||
update_latency(start);
|
||||
|
||||
std::vector<Balance> balances;
|
||||
for (const auto& b : data) {
|
||||
Balance bal;
|
||||
bal.asset = b["asset"];
|
||||
bal.venue = name_;
|
||||
bal.free = Decimal::from_string(b["free"].get<std::string>());
|
||||
bal.locked = Decimal::from_string(b.value("locked", "0"));
|
||||
balances.push_back(std::move(bal));
|
||||
}
|
||||
return balances;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Balance> LxAmmAdapter::get_balance(const std::string& asset) {
|
||||
return std::async(std::launch::async, [this, asset]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/account/balance/" + asset);
|
||||
update_latency(start);
|
||||
|
||||
Balance bal;
|
||||
bal.asset = data["asset"];
|
||||
bal.venue = name_;
|
||||
bal.free = Decimal::from_string(data["free"].get<std::string>());
|
||||
bal.locked = Decimal::from_string(data.value("locked", "0"));
|
||||
return bal;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> LxAmmAdapter::get_open_orders(
|
||||
const std::optional<std::string>& symbol) {
|
||||
(void)symbol;
|
||||
return std::async(std::launch::deferred, []() { return std::vector<Order>{}; });
|
||||
}
|
||||
|
||||
std::future<Order> LxAmmAdapter::place_order(const OrderRequest& request) {
|
||||
return std::async(std::launch::async, [this, request]() {
|
||||
auto pair = TradingPair::from_symbol(request.symbol);
|
||||
if (!pair) throw AdapterError("Invalid symbol: " + request.symbol);
|
||||
|
||||
std::string base(pair->base.data());
|
||||
std::string quote(pair->quote.data());
|
||||
|
||||
auto trade = execute_swap(
|
||||
base, quote, request.quantity,
|
||||
request.side == Side::Buy,
|
||||
Decimal::from_double(0.01)).get();
|
||||
|
||||
Order order;
|
||||
order.order_id = trade.trade_id;
|
||||
order.client_order_id = request.client_order_id;
|
||||
order.symbol = request.symbol;
|
||||
order.venue = name_;
|
||||
order.side = request.side;
|
||||
order.order_type = OrderType::Market;
|
||||
order.status = OrderStatus::Filled;
|
||||
order.quantity = request.quantity;
|
||||
order.filled_quantity = trade.quantity;
|
||||
order.remaining_quantity = Decimal::zero();
|
||||
order.price = trade.price;
|
||||
order.average_price = trade.price;
|
||||
order.created_at = trade.timestamp;
|
||||
order.updated_at = trade.timestamp;
|
||||
order.fees.push_back(trade.fee);
|
||||
return order;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Order> LxAmmAdapter::cancel_order(
|
||||
const std::string& order_id, const std::string& symbol) {
|
||||
(void)order_id; (void)symbol;
|
||||
return std::async(std::launch::deferred, []() -> Order {
|
||||
throw AdapterError("AMM swaps cannot be cancelled");
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<Order>> LxAmmAdapter::cancel_all_orders(
|
||||
const std::optional<std::string>& symbol) {
|
||||
(void)symbol;
|
||||
return std::async(std::launch::deferred, []() { return std::vector<Order>{}; });
|
||||
}
|
||||
|
||||
std::future<SwapQuote> LxAmmAdapter::get_swap_quote(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy) {
|
||||
return std::async(std::launch::async, [this, base_token, quote_token, amount, is_buy]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"baseToken", base_token},
|
||||
{"quoteToken", quote_token},
|
||||
{"amount", amount.to_string()},
|
||||
{"side", is_buy ? "buy" : "sell"}
|
||||
};
|
||||
|
||||
auto data = http_->post("/api/v1/amm/quote", body);
|
||||
update_latency(start);
|
||||
|
||||
SwapQuote quote;
|
||||
quote.base_token = base_token;
|
||||
quote.quote_token = quote_token;
|
||||
quote.input_amount = amount;
|
||||
quote.output_amount = Decimal::from_string(data["outputAmount"].get<std::string>());
|
||||
quote.price = Decimal::from_string(data["price"].get<std::string>());
|
||||
quote.price_impact = Decimal::from_string(data.value("priceImpact", "0"));
|
||||
quote.fee = Decimal::from_string(data.value("fee", "0"));
|
||||
quote.expires_at = now_ms() + 60000;
|
||||
return quote;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<Trade> LxAmmAdapter::execute_swap(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal amount,
|
||||
bool is_buy,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::async,
|
||||
[this, base_token, quote_token, amount, is_buy, slippage]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"baseToken", base_token},
|
||||
{"quoteToken", quote_token},
|
||||
{"amount", amount.to_string()},
|
||||
{"side", is_buy ? "buy" : "sell"},
|
||||
{"slippage", slippage.to_string()}
|
||||
};
|
||||
|
||||
auto data = http_->post("/api/v1/amm/swap", body);
|
||||
update_latency(start);
|
||||
|
||||
Trade trade;
|
||||
trade.trade_id = data["txHash"];
|
||||
trade.order_id = data["txHash"];
|
||||
trade.symbol = base_token + "-" + quote_token;
|
||||
trade.venue = name_;
|
||||
trade.side = is_buy ? Side::Buy : Side::Sell;
|
||||
trade.price = Decimal::from_string(data["price"].get<std::string>());
|
||||
trade.quantity = amount;
|
||||
trade.fee.amount = Decimal::from_string(data.value("fee", "0"));
|
||||
trade.timestamp = now_ms();
|
||||
trade.is_maker = false;
|
||||
return trade;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<PoolInfo> LxAmmAdapter::get_pool_info(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token) {
|
||||
return std::async(std::launch::async, [this, base_token, quote_token]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/amm/pool/" + base_token + "/" + quote_token);
|
||||
update_latency(start);
|
||||
|
||||
PoolInfo info;
|
||||
info.address = data["address"];
|
||||
info.base_token = base_token;
|
||||
info.quote_token = quote_token;
|
||||
info.base_reserve = Decimal::from_string(data["baseReserve"].get<std::string>());
|
||||
info.quote_reserve = Decimal::from_string(data["quoteReserve"].get<std::string>());
|
||||
info.total_liquidity = Decimal::from_string(data.value("totalLiquidity", "0"));
|
||||
info.fee_rate = Decimal::from_string(data.value("feeRate", "0.003"));
|
||||
if (data.contains("apy") && !data["apy"].is_null()) {
|
||||
info.apy = Decimal::from_string(data["apy"].get<std::string>());
|
||||
}
|
||||
return info;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<LiquidityResult> LxAmmAdapter::add_liquidity(
|
||||
const std::string& base_token,
|
||||
const std::string& quote_token,
|
||||
Decimal base_amount,
|
||||
Decimal quote_amount,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::async,
|
||||
[this, base_token, quote_token, base_amount, quote_amount, slippage]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"baseToken", base_token},
|
||||
{"quoteToken", quote_token},
|
||||
{"baseAmount", base_amount.to_string()},
|
||||
{"quoteAmount", quote_amount.to_string()},
|
||||
{"slippage", slippage.to_string()}
|
||||
};
|
||||
|
||||
auto data = http_->post("/api/v1/amm/liquidity/add", body);
|
||||
update_latency(start);
|
||||
|
||||
LiquidityResult result;
|
||||
result.tx_hash = data["txHash"];
|
||||
result.pool_address = data.value("poolAddress", "");
|
||||
result.base_amount = base_amount;
|
||||
result.quote_amount = quote_amount;
|
||||
result.lp_tokens = Decimal::from_string(data.value("lpTokens", "0"));
|
||||
result.share_percent = Decimal::from_string(data.value("sharePercent", "0"));
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<LiquidityResult> LxAmmAdapter::remove_liquidity(
|
||||
const std::string& pool_address,
|
||||
Decimal liquidity_amount,
|
||||
Decimal slippage) {
|
||||
return std::async(std::launch::async,
|
||||
[this, pool_address, liquidity_amount, slippage]() {
|
||||
auto start = now_ns();
|
||||
|
||||
json body = {
|
||||
{"poolAddress", pool_address},
|
||||
{"liquidity", liquidity_amount.to_string()},
|
||||
{"slippage", slippage.to_string()}
|
||||
};
|
||||
|
||||
auto data = http_->post("/api/v1/amm/liquidity/remove", body);
|
||||
update_latency(start);
|
||||
|
||||
LiquidityResult result;
|
||||
result.tx_hash = data["txHash"];
|
||||
result.pool_address = pool_address;
|
||||
result.base_amount = Decimal::from_string(data.value("baseAmount", "0"));
|
||||
result.quote_amount = Decimal::from_string(data.value("quoteAmount", "0"));
|
||||
result.lp_tokens = liquidity_amount;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
std::future<std::vector<LpPosition>> LxAmmAdapter::get_lp_positions() {
|
||||
return std::async(std::launch::async, [this]() {
|
||||
auto start = now_ns();
|
||||
auto data = http_->get("/api/v1/amm/positions");
|
||||
update_latency(start);
|
||||
|
||||
std::vector<LpPosition> positions;
|
||||
for (const auto& p : data) {
|
||||
LpPosition pos;
|
||||
pos.pool_address = p["poolAddress"];
|
||||
pos.base_token = p["baseToken"];
|
||||
pos.quote_token = p["quoteToken"];
|
||||
pos.lp_tokens = Decimal::from_string(p.value("lpTokens", "0"));
|
||||
pos.base_amount = Decimal::from_string(p["baseAmount"].get<std::string>());
|
||||
pos.quote_amount = Decimal::from_string(p["quoteAmount"].get<std::string>());
|
||||
pos.share_percent = Decimal::from_string(p.value("sharePercent", "0"));
|
||||
if (p.contains("unrealizedPnl") && !p["unrealizedPnl"].is_null()) {
|
||||
pos.unrealized_pnl = Decimal::from_string(p["unrealizedPnl"].get<std::string>());
|
||||
}
|
||||
positions.push_back(std::move(pos));
|
||||
}
|
||||
return positions;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,10 +0,0 @@
|
||||
// LX Trading SDK - Orderbook Implementation
|
||||
|
||||
#include <lx/trading/orderbook.hpp>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Orderbook and AggregatedOrderbook implementations are header-only
|
||||
// This file exists for potential future optimizations
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,181 +0,0 @@
|
||||
// LX Trading SDK - Risk Manager Implementation
|
||||
|
||||
#include <lx/trading/risk.hpp>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
RiskManager::RiskManager(const RiskConfig& config) : config_(config) {}
|
||||
|
||||
void RiskManager::validate_order(const OrderRequest& request) {
|
||||
if (!config_.enabled) return;
|
||||
|
||||
if (kill_switch_.load(std::memory_order_acquire)) {
|
||||
throw RiskError("Kill switch is active");
|
||||
}
|
||||
|
||||
// Check order size
|
||||
if (config_.max_order_size.is_positive() &&
|
||||
request.quantity > config_.max_order_size) {
|
||||
throw RiskError(
|
||||
"Order size " + request.quantity.to_string() +
|
||||
" exceeds max " + config_.max_order_size.to_string());
|
||||
}
|
||||
|
||||
// Check position limit
|
||||
auto pair = TradingPair::from_symbol(request.symbol);
|
||||
if (pair) {
|
||||
std::string base(pair->base.data());
|
||||
|
||||
Decimal current;
|
||||
{
|
||||
std::shared_lock lock(position_mutex_);
|
||||
auto it = positions_.find(base);
|
||||
if (it != positions_.end()) {
|
||||
current = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
Decimal new_position = (request.side == Side::Buy)
|
||||
? current + request.quantity
|
||||
: current - request.quantity;
|
||||
|
||||
// Asset-specific limit
|
||||
auto limit_it = config_.position_limits.find(base);
|
||||
if (limit_it != config_.position_limits.end()) {
|
||||
if (new_position.abs() > limit_it->second) {
|
||||
throw RiskError(
|
||||
"Position limit exceeded for " + base + ": " +
|
||||
current.to_string() + " + " + request.quantity.to_string() +
|
||||
" > " + limit_it->second.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Global position limit
|
||||
if (config_.max_position_size.is_positive() &&
|
||||
new_position.abs() > config_.max_position_size) {
|
||||
throw RiskError(
|
||||
"Max position size exceeded: " +
|
||||
new_position.abs().to_string() + " > " +
|
||||
config_.max_position_size.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Check open orders count
|
||||
{
|
||||
std::shared_lock lock(orders_mutex_);
|
||||
auto it = open_orders_.find(request.symbol);
|
||||
int count = (it != open_orders_.end()) ? it->second : 0;
|
||||
if (count >= config_.max_open_orders) {
|
||||
throw RiskError(
|
||||
"Max open orders (" + std::to_string(config_.max_open_orders) +
|
||||
") reached for " + request.symbol);
|
||||
}
|
||||
}
|
||||
|
||||
// Check daily loss
|
||||
if (config_.max_daily_loss.is_positive()) {
|
||||
std::shared_lock lock(pnl_mutex_);
|
||||
if (daily_pnl_.is_negative() &&
|
||||
daily_pnl_.abs() > config_.max_daily_loss) {
|
||||
throw RiskError(
|
||||
"Daily loss limit exceeded: " +
|
||||
daily_pnl_.abs().to_string() + " > " +
|
||||
config_.max_daily_loss.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RiskManager::update_position(const std::string& asset, Decimal quantity, Side side) {
|
||||
std::unique_lock lock(position_mutex_);
|
||||
auto& pos = positions_[asset];
|
||||
pos = (side == Side::Buy) ? pos + quantity : pos - quantity;
|
||||
}
|
||||
|
||||
Decimal RiskManager::position(const std::string& asset) const {
|
||||
std::shared_lock lock(position_mutex_);
|
||||
auto it = positions_.find(asset);
|
||||
return (it != positions_.end()) ? it->second : Decimal::zero();
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, Decimal> RiskManager::positions() const {
|
||||
std::shared_lock lock(position_mutex_);
|
||||
return positions_;
|
||||
}
|
||||
|
||||
void RiskManager::update_pnl(Decimal pnl) {
|
||||
std::unique_lock lock(pnl_mutex_);
|
||||
daily_pnl_ = daily_pnl_ + pnl;
|
||||
|
||||
// Auto kill switch
|
||||
if (config_.kill_switch_enabled &&
|
||||
config_.max_daily_loss.is_positive() &&
|
||||
daily_pnl_.is_negative() &&
|
||||
daily_pnl_.abs() > config_.max_daily_loss) {
|
||||
kill_switch_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
Decimal RiskManager::daily_pnl() const {
|
||||
std::shared_lock lock(pnl_mutex_);
|
||||
return daily_pnl_;
|
||||
}
|
||||
|
||||
void RiskManager::reset_daily_pnl() {
|
||||
std::unique_lock lock(pnl_mutex_);
|
||||
daily_pnl_ = Decimal::zero();
|
||||
}
|
||||
|
||||
void RiskManager::order_opened(const std::string& symbol) {
|
||||
std::unique_lock lock(orders_mutex_);
|
||||
open_orders_[symbol]++;
|
||||
}
|
||||
|
||||
void RiskManager::order_closed(const std::string& symbol) {
|
||||
std::unique_lock lock(orders_mutex_);
|
||||
auto it = open_orders_.find(symbol);
|
||||
if (it != open_orders_.end() && it->second > 0) {
|
||||
it->second--;
|
||||
}
|
||||
}
|
||||
|
||||
int RiskManager::open_orders(const std::string& symbol) const {
|
||||
std::shared_lock lock(orders_mutex_);
|
||||
auto it = open_orders_.find(symbol);
|
||||
return (it != open_orders_.end()) ? it->second : 0;
|
||||
}
|
||||
|
||||
bool RiskManager::check_order_size(Decimal quantity) const noexcept {
|
||||
if (!config_.max_order_size.is_positive()) return true;
|
||||
return quantity <= config_.max_order_size;
|
||||
}
|
||||
|
||||
bool RiskManager::check_position_limit(
|
||||
const std::string& asset, Decimal new_position) const noexcept {
|
||||
auto it = config_.position_limits.find(asset);
|
||||
if (it != config_.position_limits.end()) {
|
||||
if (new_position.abs() > it->second) return false;
|
||||
}
|
||||
|
||||
if (config_.max_position_size.is_positive()) {
|
||||
if (new_position.abs() > config_.max_position_size) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RiskManager::check_daily_loss() const noexcept {
|
||||
if (!config_.max_daily_loss.is_positive()) return true;
|
||||
|
||||
std::shared_lock lock(pnl_mutex_);
|
||||
return !daily_pnl_.is_negative() ||
|
||||
daily_pnl_.abs() <= config_.max_daily_loss;
|
||||
}
|
||||
|
||||
bool RiskManager::check_open_orders(const std::string& symbol) const noexcept {
|
||||
std::shared_lock lock(orders_mutex_);
|
||||
auto it = open_orders_.find(symbol);
|
||||
int count = (it != open_orders_.end()) ? it->second : 0;
|
||||
return count < config_.max_open_orders;
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,149 +0,0 @@
|
||||
// LX Trading SDK - Types Implementation
|
||||
|
||||
#include <lx/trading/types.hpp>
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <sstream>
|
||||
|
||||
namespace lx::trading {
|
||||
|
||||
// Decimal implementation
|
||||
Decimal Decimal::from_string(std::string_view s) {
|
||||
// Find decimal point
|
||||
auto dot = s.find('.');
|
||||
if (dot == std::string_view::npos) {
|
||||
// Integer
|
||||
int64_t val = 0;
|
||||
std::from_chars(s.data(), s.data() + s.size(), val);
|
||||
return Decimal(val * SCALE);
|
||||
}
|
||||
|
||||
// Parse integer and fractional parts
|
||||
std::string_view int_part = s.substr(0, dot);
|
||||
std::string_view frac_part = s.substr(dot + 1);
|
||||
|
||||
int64_t int_val = 0;
|
||||
if (!int_part.empty()) {
|
||||
std::from_chars(int_part.data(), int_part.data() + int_part.size(), int_val);
|
||||
}
|
||||
|
||||
int64_t frac_val = 0;
|
||||
if (!frac_part.empty()) {
|
||||
// Pad or truncate to PRECISION digits
|
||||
std::string frac_str(frac_part);
|
||||
if (frac_str.size() < PRECISION) {
|
||||
frac_str.append(PRECISION - frac_str.size(), '0');
|
||||
} else if (frac_str.size() > PRECISION) {
|
||||
frac_str = frac_str.substr(0, PRECISION);
|
||||
}
|
||||
std::from_chars(frac_str.data(), frac_str.data() + frac_str.size(), frac_val);
|
||||
}
|
||||
|
||||
bool negative = (s[0] == '-');
|
||||
int64_t result = int_val * SCALE + (negative ? -frac_val : frac_val);
|
||||
return Decimal(result);
|
||||
}
|
||||
|
||||
std::string Decimal::to_string() const {
|
||||
int64_t abs_val = value_ < 0 ? -value_ : value_;
|
||||
int64_t int_part = abs_val / SCALE;
|
||||
int64_t frac_part = abs_val % SCALE;
|
||||
|
||||
std::ostringstream oss;
|
||||
if (value_ < 0) oss << '-';
|
||||
oss << int_part << '.';
|
||||
|
||||
// Format fractional part with leading zeros
|
||||
std::string frac_str = std::to_string(frac_part);
|
||||
oss << std::string(PRECISION - frac_str.size(), '0') << frac_str;
|
||||
|
||||
std::string result = oss.str();
|
||||
|
||||
// Trim trailing zeros after decimal point
|
||||
size_t last_non_zero = result.find_last_not_of('0');
|
||||
if (last_non_zero != std::string::npos && result[last_non_zero] == '.') {
|
||||
last_non_zero--;
|
||||
}
|
||||
result = result.substr(0, last_non_zero + 1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TradingPair implementation
|
||||
std::optional<TradingPair> TradingPair::from_symbol(std::string_view symbol) {
|
||||
const char separators[] = {'-', '/', '_'};
|
||||
|
||||
for (char sep : separators) {
|
||||
auto pos = symbol.find(sep);
|
||||
if (pos != std::string_view::npos) {
|
||||
TradingPair pair;
|
||||
auto base = symbol.substr(0, pos);
|
||||
auto quote = symbol.substr(pos + 1);
|
||||
|
||||
if (base.size() > 15 || quote.size() > 15) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::copy(base.begin(), base.end(), pair.base.begin());
|
||||
std::copy(quote.begin(), quote.end(), pair.quote.begin());
|
||||
|
||||
return pair;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string TradingPair::to_hummingbot() const {
|
||||
std::string result;
|
||||
result.reserve(33);
|
||||
for (char c : base) {
|
||||
if (c == 0) break;
|
||||
result += c;
|
||||
}
|
||||
result += '-';
|
||||
for (char c : quote) {
|
||||
if (c == 0) break;
|
||||
result += c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string TradingPair::to_ccxt() const {
|
||||
std::string result;
|
||||
result.reserve(33);
|
||||
for (char c : base) {
|
||||
if (c == 0) break;
|
||||
result += c;
|
||||
}
|
||||
result += '/';
|
||||
for (char c : quote) {
|
||||
if (c == 0) break;
|
||||
result += c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// OrderRequest factory methods
|
||||
OrderRequest OrderRequest::market(std::string_view symbol, Side side, Decimal quantity) {
|
||||
OrderRequest req;
|
||||
req.symbol = std::string(symbol);
|
||||
req.side = side;
|
||||
req.order_type = OrderType::Market;
|
||||
req.quantity = quantity;
|
||||
req.time_in_force = TimeInForce::IOC;
|
||||
return req;
|
||||
}
|
||||
|
||||
OrderRequest OrderRequest::limit(std::string_view symbol, Side side, Decimal quantity, Decimal price) {
|
||||
OrderRequest req;
|
||||
req.symbol = std::string(symbol);
|
||||
req.side = side;
|
||||
req.order_type = OrderType::Limit;
|
||||
req.quantity = quantity;
|
||||
req.price = price;
|
||||
req.time_in_force = TimeInForce::GTC;
|
||||
return req;
|
||||
}
|
||||
|
||||
} // namespace lx::trading
|
||||
@@ -1,238 +0,0 @@
|
||||
// LX Trading SDK - Math Tests
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <lx/trading/math.hpp>
|
||||
|
||||
using namespace lx::trading::math;
|
||||
using Catch::Approx;
|
||||
|
||||
TEST_CASE("Black-Scholes pricing", "[math]") {
|
||||
SECTION("At-the-money call") {
|
||||
double price = black_scholes(100, 100, 1.0, 0.05, 0.2, true);
|
||||
REQUIRE(price == Approx(10.45).margin(0.1));
|
||||
}
|
||||
|
||||
SECTION("In-the-money call") {
|
||||
double price = black_scholes(110, 100, 1.0, 0.05, 0.2, true);
|
||||
REQUIRE(price == Approx(17.68).margin(0.1));
|
||||
}
|
||||
|
||||
SECTION("Out-of-the-money put") {
|
||||
double price = black_scholes(110, 100, 1.0, 0.05, 0.2, false);
|
||||
REQUIRE(price == Approx(2.80).margin(0.1));
|
||||
}
|
||||
|
||||
SECTION("Zero time to expiry") {
|
||||
double call_price = black_scholes(110, 100, 0, 0.05, 0.2, true);
|
||||
REQUIRE(call_price == Approx(10.0));
|
||||
|
||||
double put_price = black_scholes(90, 100, 0, 0.05, 0.2, false);
|
||||
REQUIRE(put_price == Approx(10.0));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Greeks calculation", "[math]") {
|
||||
Greeks g = greeks(100, 100, 1.0, 0.05, 0.2, true);
|
||||
|
||||
SECTION("Delta") {
|
||||
REQUIRE(g.delta == Approx(0.64).margin(0.02));
|
||||
}
|
||||
|
||||
SECTION("Gamma") {
|
||||
REQUIRE(g.gamma == Approx(0.019).margin(0.002));
|
||||
}
|
||||
|
||||
SECTION("Vega") {
|
||||
REQUIRE(g.vega == Approx(0.38).margin(0.02));
|
||||
}
|
||||
|
||||
SECTION("Theta (daily)") {
|
||||
REQUIRE(g.theta < 0); // Time decay
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Implied volatility", "[math]") {
|
||||
double true_vol = 0.25;
|
||||
double price = black_scholes(100, 100, 0.5, 0.05, true_vol, true);
|
||||
|
||||
double iv = implied_volatility(price, 100, 100, 0.5, 0.05, true);
|
||||
REQUIRE(iv == Approx(true_vol).margin(0.01));
|
||||
}
|
||||
|
||||
TEST_CASE("Constant product AMM", "[math]") {
|
||||
SECTION("Basic swap") {
|
||||
auto [out, price] = constant_product_price(1000, 1000, 10, 0.003, true);
|
||||
REQUIRE(out == Approx(9.88).margin(0.02));
|
||||
REQUIRE(price == Approx(0.988).margin(0.002));
|
||||
}
|
||||
|
||||
SECTION("Large swap with slippage") {
|
||||
auto [out, price] = constant_product_price(1000, 1000, 100, 0.003, true);
|
||||
// Larger trade = more slippage
|
||||
REQUIRE(out < 100);
|
||||
REQUIRE(price < 1.0);
|
||||
}
|
||||
|
||||
SECTION("Symmetric reserves") {
|
||||
auto [out1, p1] = constant_product_price(1000, 1000, 50, 0.003, true);
|
||||
auto [out2, p2] = constant_product_price(1000, 1000, 50, 0.003, false);
|
||||
REQUIRE(out1 == Approx(out2).margin(0.01));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Concentrated liquidity", "[math]") {
|
||||
// Price range 90-110, current sqrt price = 10 (price = 100)
|
||||
double sqrt_price = 10.0;
|
||||
double sqrt_lower = std::sqrt(90.0);
|
||||
double sqrt_upper = std::sqrt(110.0);
|
||||
double liquidity = 1000.0;
|
||||
|
||||
SECTION("Swap within range") {
|
||||
auto [out, new_sqrt_p, impact] = concentrated_liquidity_price(
|
||||
liquidity, sqrt_price, sqrt_lower, sqrt_upper, 10, 0.003, true);
|
||||
|
||||
REQUIRE(out > 0);
|
||||
REQUIRE(new_sqrt_p > sqrt_price); // Price increased
|
||||
REQUIRE(impact >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Volatility calculation", "[math]") {
|
||||
std::vector<double> returns = {0.01, -0.02, 0.015, -0.01, 0.02, 0.005};
|
||||
|
||||
SECTION("Non-annualized") {
|
||||
double vol = volatility(returns, false);
|
||||
REQUIRE(vol > 0);
|
||||
REQUIRE(vol < 0.1); // Daily returns typically small
|
||||
}
|
||||
|
||||
SECTION("Annualized") {
|
||||
double vol_daily = volatility(returns, false);
|
||||
double vol_annual = volatility(returns, true, 252);
|
||||
REQUIRE(vol_annual == Approx(vol_daily * std::sqrt(252)).margin(0.001));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Sharpe ratio", "[math]") {
|
||||
std::vector<double> positive_returns = {0.01, 0.02, 0.015, 0.01, 0.02};
|
||||
std::vector<double> mixed_returns = {0.01, -0.02, 0.015, -0.01, 0.02};
|
||||
|
||||
SECTION("Positive returns") {
|
||||
double sharpe = sharpe_ratio(positive_returns, 0.0);
|
||||
REQUIRE(sharpe > 0);
|
||||
}
|
||||
|
||||
SECTION("Mixed returns") {
|
||||
double sharpe = sharpe_ratio(mixed_returns, 0.0);
|
||||
// Lower Sharpe due to more volatility
|
||||
REQUIRE(sharpe < sharpe_ratio(positive_returns, 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Maximum drawdown", "[math]") {
|
||||
std::vector<double> prices = {100, 110, 105, 95, 90, 100, 85};
|
||||
|
||||
auto [dd, peak_idx, trough_idx] = max_drawdown(prices);
|
||||
|
||||
// Max DD from 110 to 85 = 22.7%
|
||||
REQUIRE(dd == Approx(0.227).margin(0.01));
|
||||
REQUIRE(peak_idx == 1); // 110
|
||||
REQUIRE(trough_idx == 6); // 85
|
||||
}
|
||||
|
||||
TEST_CASE("Value at Risk", "[math]") {
|
||||
std::vector<double> returns = {
|
||||
-0.05, -0.03, -0.02, -0.01, 0.0, 0.01, 0.02, 0.03, 0.04, 0.05,
|
||||
-0.04, -0.02, -0.01, 0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06
|
||||
};
|
||||
|
||||
SECTION("Historical VaR 95%") {
|
||||
double var95 = var(returns, 0.95, false);
|
||||
REQUIRE(var95 > 0);
|
||||
REQUIRE(var95 < 0.1);
|
||||
}
|
||||
|
||||
SECTION("Parametric VaR 95%") {
|
||||
double var95 = var(returns, 0.95, true);
|
||||
REQUIRE(var95 > 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Conditional VaR", "[math]") {
|
||||
std::vector<double> returns = {
|
||||
-0.08, -0.06, -0.05, -0.04, -0.03, -0.02, -0.01, 0.0, 0.01, 0.02,
|
||||
0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12
|
||||
};
|
||||
|
||||
double cvar95 = cvar(returns, 0.95);
|
||||
double var95 = var(returns, 0.95, false);
|
||||
|
||||
// CVaR should be >= VaR
|
||||
REQUIRE(cvar95 >= var95);
|
||||
}
|
||||
|
||||
TEST_CASE("Statistical utilities", "[math]") {
|
||||
SECTION("Calculate returns") {
|
||||
std::vector<double> prices = {100, 105, 102, 110};
|
||||
auto returns = calculate_returns(prices);
|
||||
|
||||
REQUIRE(returns.size() == 3);
|
||||
REQUIRE(returns[0] == Approx(0.05)); // 5%
|
||||
REQUIRE(returns[1] == Approx(-0.0286).margin(0.001)); // -2.86%
|
||||
REQUIRE(returns[2] == Approx(0.0784).margin(0.001)); // 7.84%
|
||||
}
|
||||
|
||||
SECTION("Rolling mean") {
|
||||
std::vector<double> data = {1, 2, 3, 4, 5, 6, 7};
|
||||
auto rm = rolling_mean(data, 3);
|
||||
|
||||
REQUIRE(rm.size() == 5);
|
||||
REQUIRE(rm[0] == Approx(2.0)); // (1+2+3)/3
|
||||
REQUIRE(rm[1] == Approx(3.0)); // (2+3+4)/3
|
||||
}
|
||||
|
||||
SECTION("EMA") {
|
||||
std::vector<double> data = {10, 12, 11, 13, 12, 14};
|
||||
auto e = ema(data, 0.3);
|
||||
|
||||
REQUIRE(e.size() == 6);
|
||||
REQUIRE(e[0] == Approx(10.0));
|
||||
// Each subsequent value is weighted average
|
||||
}
|
||||
|
||||
SECTION("Correlation") {
|
||||
std::vector<double> x = {1, 2, 3, 4, 5};
|
||||
std::vector<double> y = {2, 4, 6, 8, 10};
|
||||
|
||||
double corr = correlation(x, y);
|
||||
REQUIRE(corr == Approx(1.0)); // Perfect positive
|
||||
}
|
||||
|
||||
SECTION("Beta") {
|
||||
std::vector<double> asset = {0.02, 0.03, -0.01, 0.02, 0.01};
|
||||
std::vector<double> market = {0.01, 0.015, -0.005, 0.01, 0.005};
|
||||
|
||||
double b = beta(asset, market);
|
||||
REQUIRE(b > 1.0); // Asset more volatile than market
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Price conversions", "[math]") {
|
||||
SECTION("Price to sqrt price") {
|
||||
double sqrt_p = price_to_sqrt_price(100);
|
||||
REQUIRE(sqrt_p == Approx(10.0));
|
||||
}
|
||||
|
||||
SECTION("Sqrt price to price") {
|
||||
double p = sqrt_price_to_price(10.0);
|
||||
REQUIRE(p == Approx(100.0));
|
||||
}
|
||||
|
||||
SECTION("Round trip") {
|
||||
double original = 12345.67;
|
||||
double sqrt_p = price_to_sqrt_price(original);
|
||||
double back = sqrt_price_to_price(sqrt_p);
|
||||
REQUIRE(back == Approx(original));
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
// LX Trading SDK - Orderbook Tests
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <lx/trading/orderbook.hpp>
|
||||
|
||||
using namespace lx::trading;
|
||||
using Catch::Approx;
|
||||
|
||||
TEST_CASE("Orderbook basic operations", "[orderbook]") {
|
||||
Orderbook book("BTC-USDC", "test_venue");
|
||||
|
||||
SECTION("Add and retrieve levels") {
|
||||
book.add_bid(Decimal::from_double(100.0), Decimal::from_double(1.0));
|
||||
book.add_bid(Decimal::from_double(99.0), Decimal::from_double(2.0));
|
||||
book.add_ask(Decimal::from_double(101.0), Decimal::from_double(1.5));
|
||||
book.add_ask(Decimal::from_double(102.0), Decimal::from_double(2.5));
|
||||
book.sort();
|
||||
|
||||
auto bids = book.bids();
|
||||
auto asks = book.asks();
|
||||
|
||||
REQUIRE(bids.size() == 2);
|
||||
REQUIRE(asks.size() == 2);
|
||||
|
||||
// Bids sorted descending
|
||||
REQUIRE(bids[0].price.to_double() == Approx(100.0));
|
||||
REQUIRE(bids[1].price.to_double() == Approx(99.0));
|
||||
|
||||
// Asks sorted ascending
|
||||
REQUIRE(asks[0].price.to_double() == Approx(101.0));
|
||||
REQUIRE(asks[1].price.to_double() == Approx(102.0));
|
||||
}
|
||||
|
||||
SECTION("Best bid/ask") {
|
||||
book.add_bid(Decimal::from_double(100.0), Decimal::from_double(1.0));
|
||||
book.add_ask(Decimal::from_double(101.0), Decimal::from_double(1.0));
|
||||
book.sort();
|
||||
|
||||
REQUIRE(book.best_bid().value().to_double() == Approx(100.0));
|
||||
REQUIRE(book.best_ask().value().to_double() == Approx(101.0));
|
||||
}
|
||||
|
||||
SECTION("Mid price and spread") {
|
||||
book.add_bid(Decimal::from_double(100.0), Decimal::from_double(1.0));
|
||||
book.add_ask(Decimal::from_double(102.0), Decimal::from_double(1.0));
|
||||
book.sort();
|
||||
|
||||
REQUIRE(book.mid_price().value().to_double() == Approx(101.0));
|
||||
REQUIRE(book.spread().value().to_double() == Approx(2.0));
|
||||
REQUIRE(book.spread_percent().value().to_double() == Approx(1.98).margin(0.01));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Orderbook VWAP", "[orderbook]") {
|
||||
Orderbook book("BTC-USDC", "test_venue");
|
||||
|
||||
book.add_ask(Decimal::from_double(100.0), Decimal::from_double(1.0));
|
||||
book.add_ask(Decimal::from_double(101.0), Decimal::from_double(2.0));
|
||||
book.add_ask(Decimal::from_double(102.0), Decimal::from_double(3.0));
|
||||
book.sort();
|
||||
|
||||
SECTION("VWAP for small amount") {
|
||||
auto vwap = book.vwap_buy(Decimal::from_double(0.5));
|
||||
REQUIRE(vwap.has_value());
|
||||
REQUIRE(vwap->to_double() == Approx(100.0));
|
||||
}
|
||||
|
||||
SECTION("VWAP across multiple levels") {
|
||||
auto vwap = book.vwap_buy(Decimal::from_double(2.5));
|
||||
REQUIRE(vwap.has_value());
|
||||
// (1.0 * 100 + 1.5 * 101) / 2.5 = 100.6
|
||||
REQUIRE(vwap->to_double() == Approx(100.6));
|
||||
}
|
||||
|
||||
SECTION("VWAP for full book") {
|
||||
auto vwap = book.vwap_buy(Decimal::from_double(6.0));
|
||||
REQUIRE(vwap.has_value());
|
||||
// (1*100 + 2*101 + 3*102) / 6 = 101.333...
|
||||
REQUIRE(vwap->to_double() == Approx(101.333).margin(0.01));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Orderbook liquidity", "[orderbook]") {
|
||||
Orderbook book("BTC-USDC", "test_venue");
|
||||
|
||||
book.add_bid(Decimal::from_double(100.0), Decimal::from_double(1.0));
|
||||
book.add_bid(Decimal::from_double(99.0), Decimal::from_double(2.0));
|
||||
book.add_ask(Decimal::from_double(101.0), Decimal::from_double(1.5));
|
||||
book.add_ask(Decimal::from_double(102.0), Decimal::from_double(2.5));
|
||||
book.sort();
|
||||
|
||||
SECTION("Bid liquidity") {
|
||||
// 1.0 * 100 + 2.0 * 99 = 298
|
||||
REQUIRE(book.bid_liquidity().to_double() == Approx(298.0));
|
||||
}
|
||||
|
||||
SECTION("Ask liquidity") {
|
||||
// 1.5 * 101 + 2.5 * 102 = 406.5
|
||||
REQUIRE(book.ask_liquidity().to_double() == Approx(406.5));
|
||||
}
|
||||
|
||||
SECTION("Depth for specific levels") {
|
||||
REQUIRE(book.bid_depth(1).to_double() == Approx(100.0));
|
||||
REQUIRE(book.ask_depth(1).to_double() == Approx(151.5));
|
||||
}
|
||||
|
||||
SECTION("Has liquidity") {
|
||||
REQUIRE(book.has_liquidity(Side::Buy, Decimal::from_double(3.0)));
|
||||
REQUIRE_FALSE(book.has_liquidity(Side::Buy, Decimal::from_double(10.0)));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("AggregatedOrderbook", "[orderbook]") {
|
||||
AggregatedOrderbook agg("BTC-USDC");
|
||||
|
||||
// Create first venue orderbook
|
||||
Orderbook book1("BTC-USDC", "venue1");
|
||||
book1.add_bid(Decimal::from_double(100.0), Decimal::from_double(1.0));
|
||||
book1.add_ask(Decimal::from_double(102.0), Decimal::from_double(1.0));
|
||||
book1.sort();
|
||||
|
||||
// Create second venue orderbook
|
||||
Orderbook book2("BTC-USDC", "venue2");
|
||||
book2.add_bid(Decimal::from_double(99.0), Decimal::from_double(2.0));
|
||||
book2.add_ask(Decimal::from_double(101.0), Decimal::from_double(1.5));
|
||||
book2.sort();
|
||||
|
||||
agg.add_orderbook(book1);
|
||||
agg.add_orderbook(book2);
|
||||
|
||||
SECTION("Best bid across venues") {
|
||||
auto best = agg.best_bid();
|
||||
REQUIRE(best.has_value());
|
||||
auto [price, venue, qty] = *best;
|
||||
REQUIRE(price.to_double() == Approx(100.0));
|
||||
REQUIRE(venue == "venue1");
|
||||
}
|
||||
|
||||
SECTION("Best ask across venues") {
|
||||
auto best = agg.best_ask();
|
||||
REQUIRE(best.has_value());
|
||||
auto [price, venue, qty] = *best;
|
||||
REQUIRE(price.to_double() == Approx(101.0));
|
||||
REQUIRE(venue == "venue2");
|
||||
}
|
||||
|
||||
SECTION("Aggregated levels") {
|
||||
auto asks = agg.aggregated_asks();
|
||||
REQUIRE(asks.size() == 2);
|
||||
// Sorted by price ascending
|
||||
REQUIRE(asks[0].price.to_double() == Approx(101.0));
|
||||
REQUIRE(asks[1].price.to_double() == Approx(102.0));
|
||||
}
|
||||
|
||||
SECTION("Best venue for buying") {
|
||||
auto best = agg.best_venue_buy(Decimal::from_double(1.0));
|
||||
REQUIRE(best.has_value());
|
||||
auto [venue, price] = *best;
|
||||
REQUIRE(venue == "venue2"); // Lower ask price
|
||||
REQUIRE(price.to_double() == Approx(101.0));
|
||||
}
|
||||
|
||||
SECTION("Best venue for selling") {
|
||||
auto best = agg.best_venue_sell(Decimal::from_double(0.5));
|
||||
REQUIRE(best.has_value());
|
||||
auto [venue, price] = *best;
|
||||
REQUIRE(venue == "venue1"); // Higher bid price
|
||||
REQUIRE(price.to_double() == Approx(100.0));
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
// LX Trading SDK - Risk Manager Tests
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <lx/trading/risk.hpp>
|
||||
|
||||
using namespace lx::trading;
|
||||
using Catch::Approx;
|
||||
|
||||
TEST_CASE("RiskManager basic validation", "[risk]") {
|
||||
RiskConfig config;
|
||||
config.enabled = true;
|
||||
config.max_order_size = Decimal::from_double(100.0);
|
||||
config.max_position_size = Decimal::from_double(1000.0);
|
||||
config.max_open_orders = 10;
|
||||
|
||||
RiskManager rm(config);
|
||||
|
||||
SECTION("Valid order passes") {
|
||||
OrderRequest req = OrderRequest::market("BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(50.0));
|
||||
|
||||
REQUIRE_NOTHROW(rm.validate_order(req));
|
||||
}
|
||||
|
||||
SECTION("Order size exceeded") {
|
||||
OrderRequest req = OrderRequest::market("BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(150.0));
|
||||
|
||||
REQUIRE_THROWS_AS(rm.validate_order(req), RiskError);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RiskManager position limits", "[risk]") {
|
||||
RiskConfig config;
|
||||
config.enabled = true;
|
||||
config.max_position_size = Decimal::from_double(100.0);
|
||||
config.position_limits["BTC"] = Decimal::from_double(50.0);
|
||||
|
||||
RiskManager rm(config);
|
||||
|
||||
SECTION("Within global limit") {
|
||||
rm.update_position("BTC", Decimal::from_double(30.0), Side::Buy);
|
||||
REQUIRE(rm.position("BTC").to_double() == Approx(30.0));
|
||||
|
||||
OrderRequest req = OrderRequest::market("BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(10.0));
|
||||
REQUIRE_NOTHROW(rm.validate_order(req));
|
||||
}
|
||||
|
||||
SECTION("Exceeds asset-specific limit") {
|
||||
rm.update_position("BTC", Decimal::from_double(45.0), Side::Buy);
|
||||
|
||||
OrderRequest req = OrderRequest::market("BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(10.0));
|
||||
REQUIRE_THROWS_AS(rm.validate_order(req), RiskError);
|
||||
}
|
||||
|
||||
SECTION("Exceeds global limit") {
|
||||
rm.update_position("ETH", Decimal::from_double(90.0), Side::Buy);
|
||||
|
||||
OrderRequest req = OrderRequest::market("ETH-USDC", Side::Buy,
|
||||
Decimal::from_double(20.0));
|
||||
REQUIRE_THROWS_AS(rm.validate_order(req), RiskError);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RiskManager PnL tracking", "[risk]") {
|
||||
RiskConfig config;
|
||||
config.enabled = true;
|
||||
config.max_daily_loss = Decimal::from_double(1000.0);
|
||||
config.kill_switch_enabled = true;
|
||||
|
||||
RiskManager rm(config);
|
||||
|
||||
SECTION("Track PnL") {
|
||||
rm.update_pnl(Decimal::from_double(100.0));
|
||||
rm.update_pnl(Decimal::from_double(-50.0));
|
||||
REQUIRE(rm.daily_pnl().to_double() == Approx(50.0));
|
||||
}
|
||||
|
||||
SECTION("Daily loss limit triggers rejection") {
|
||||
rm.update_pnl(Decimal::from_double(-1001.0));
|
||||
|
||||
OrderRequest req = OrderRequest::market("BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(1.0));
|
||||
REQUIRE_THROWS_AS(rm.validate_order(req), RiskError);
|
||||
}
|
||||
|
||||
SECTION("Kill switch activation") {
|
||||
REQUIRE_FALSE(rm.is_killed());
|
||||
|
||||
rm.update_pnl(Decimal::from_double(-1001.0));
|
||||
REQUIRE(rm.is_killed());
|
||||
}
|
||||
|
||||
SECTION("Reset PnL") {
|
||||
rm.update_pnl(Decimal::from_double(-500.0));
|
||||
rm.reset_daily_pnl();
|
||||
REQUIRE(rm.daily_pnl().is_zero());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RiskManager order tracking", "[risk]") {
|
||||
RiskConfig config;
|
||||
config.enabled = true;
|
||||
config.max_open_orders = 5;
|
||||
|
||||
RiskManager rm(config);
|
||||
|
||||
SECTION("Track open orders") {
|
||||
rm.order_opened("BTC-USDC");
|
||||
rm.order_opened("BTC-USDC");
|
||||
REQUIRE(rm.open_orders("BTC-USDC") == 2);
|
||||
|
||||
rm.order_closed("BTC-USDC");
|
||||
REQUIRE(rm.open_orders("BTC-USDC") == 1);
|
||||
}
|
||||
|
||||
SECTION("Max open orders limit") {
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
rm.order_opened("ETH-USDC");
|
||||
}
|
||||
|
||||
OrderRequest req = OrderRequest::market("ETH-USDC", Side::Buy,
|
||||
Decimal::from_double(1.0));
|
||||
REQUIRE_THROWS_AS(rm.validate_order(req), RiskError);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RiskManager kill switch", "[risk]") {
|
||||
RiskConfig config;
|
||||
config.enabled = true;
|
||||
|
||||
RiskManager rm(config);
|
||||
|
||||
SECTION("Manual kill") {
|
||||
REQUIRE_FALSE(rm.is_killed());
|
||||
|
||||
rm.kill();
|
||||
REQUIRE(rm.is_killed());
|
||||
|
||||
OrderRequest req = OrderRequest::market("BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(1.0));
|
||||
REQUIRE_THROWS_AS(rm.validate_order(req), RiskError);
|
||||
}
|
||||
|
||||
SECTION("Reset kill switch") {
|
||||
rm.kill();
|
||||
rm.reset();
|
||||
REQUIRE_FALSE(rm.is_killed());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RiskManager disabled", "[risk]") {
|
||||
RiskConfig config;
|
||||
config.enabled = false;
|
||||
config.max_order_size = Decimal::from_double(10.0);
|
||||
|
||||
RiskManager rm(config);
|
||||
|
||||
SECTION("Validation passes when disabled") {
|
||||
OrderRequest req = OrderRequest::market("BTC-USDC", Side::Buy,
|
||||
Decimal::from_double(1000.0)); // Way over limit
|
||||
|
||||
REQUIRE_NOTHROW(rm.validate_order(req));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RiskManager pre-trade checks", "[risk]") {
|
||||
RiskConfig config;
|
||||
config.enabled = true;
|
||||
config.max_order_size = Decimal::from_double(100.0);
|
||||
config.max_position_size = Decimal::from_double(500.0);
|
||||
config.max_daily_loss = Decimal::from_double(1000.0);
|
||||
config.max_open_orders = 5;
|
||||
|
||||
RiskManager rm(config);
|
||||
|
||||
SECTION("Check order size") {
|
||||
REQUIRE(rm.check_order_size(Decimal::from_double(50.0)));
|
||||
REQUIRE_FALSE(rm.check_order_size(Decimal::from_double(150.0)));
|
||||
}
|
||||
|
||||
SECTION("Check position limit") {
|
||||
REQUIRE(rm.check_position_limit("BTC", Decimal::from_double(100.0)));
|
||||
REQUIRE_FALSE(rm.check_position_limit("BTC", Decimal::from_double(600.0)));
|
||||
}
|
||||
|
||||
SECTION("Check daily loss") {
|
||||
REQUIRE(rm.check_daily_loss());
|
||||
rm.update_pnl(Decimal::from_double(-1001.0));
|
||||
REQUIRE_FALSE(rm.check_daily_loss());
|
||||
}
|
||||
|
||||
SECTION("Check open orders") {
|
||||
REQUIRE(rm.check_open_orders("BTC-USDC"));
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
rm.order_opened("BTC-USDC");
|
||||
}
|
||||
REQUIRE_FALSE(rm.check_open_orders("BTC-USDC"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("OrderTracker RAII", "[risk]") {
|
||||
RiskConfig config;
|
||||
config.enabled = true;
|
||||
config.max_open_orders = 10;
|
||||
|
||||
RiskManager rm(config);
|
||||
|
||||
SECTION("Auto decrement on destruction") {
|
||||
{
|
||||
OrderTracker tracker(rm, "BTC-USDC");
|
||||
REQUIRE(rm.open_orders("BTC-USDC") == 1);
|
||||
}
|
||||
REQUIRE(rm.open_orders("BTC-USDC") == 0);
|
||||
}
|
||||
|
||||
SECTION("Release prevents decrement") {
|
||||
{
|
||||
OrderTracker tracker(rm, "BTC-USDC");
|
||||
tracker.release();
|
||||
}
|
||||
REQUIRE(rm.open_orders("BTC-USDC") == 1);
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
// LX Trading SDK - Types Tests
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <lx/trading/types.hpp>
|
||||
|
||||
using namespace lx::trading;
|
||||
using Catch::Approx;
|
||||
|
||||
TEST_CASE("Decimal arithmetic", "[types]") {
|
||||
SECTION("Basic operations") {
|
||||
Decimal a = Decimal::from_double(100.5);
|
||||
Decimal b = Decimal::from_double(50.25);
|
||||
|
||||
REQUIRE((a + b).to_double() == Approx(150.75));
|
||||
REQUIRE((a - b).to_double() == Approx(50.25));
|
||||
REQUIRE((a * Decimal::from_double(2.0)).to_double() == Approx(201.0));
|
||||
REQUIRE((a / Decimal::from_double(2.0)).to_double() == Approx(50.25));
|
||||
}
|
||||
|
||||
SECTION("String conversion") {
|
||||
auto d = Decimal::from_string("123.456");
|
||||
REQUIRE(d.to_double() == Approx(123.456));
|
||||
|
||||
auto d2 = Decimal::from_string("-99.99");
|
||||
REQUIRE(d2.to_double() == Approx(-99.99));
|
||||
REQUIRE(d2.is_negative());
|
||||
}
|
||||
|
||||
SECTION("Comparison") {
|
||||
Decimal a = Decimal::from_double(10.0);
|
||||
Decimal b = Decimal::from_double(20.0);
|
||||
|
||||
REQUIRE(a < b);
|
||||
REQUIRE(b > a);
|
||||
REQUIRE(a <= a);
|
||||
REQUIRE(a == a);
|
||||
REQUIRE(a != b);
|
||||
}
|
||||
|
||||
SECTION("Zero and one") {
|
||||
REQUIRE(Decimal::zero().is_zero());
|
||||
REQUIRE(Decimal::one().to_double() == Approx(1.0));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("TradingPair parsing", "[types]") {
|
||||
SECTION("Hyphen separator") {
|
||||
auto pair = TradingPair::from_symbol("BTC-USDC");
|
||||
REQUIRE(pair.has_value());
|
||||
REQUIRE(std::string(pair->base.data()) == "BTC");
|
||||
REQUIRE(std::string(pair->quote.data()) == "USDC");
|
||||
}
|
||||
|
||||
SECTION("Slash separator") {
|
||||
auto pair = TradingPair::from_symbol("ETH/USD");
|
||||
REQUIRE(pair.has_value());
|
||||
REQUIRE(std::string(pair->base.data()) == "ETH");
|
||||
REQUIRE(std::string(pair->quote.data()) == "USD");
|
||||
}
|
||||
|
||||
SECTION("Underscore separator") {
|
||||
auto pair = TradingPair::from_symbol("LUX_USDT");
|
||||
REQUIRE(pair.has_value());
|
||||
REQUIRE(std::string(pair->base.data()) == "LUX");
|
||||
REQUIRE(std::string(pair->quote.data()) == "USDT");
|
||||
}
|
||||
|
||||
SECTION("Invalid symbol") {
|
||||
auto pair = TradingPair::from_symbol("INVALID");
|
||||
REQUIRE_FALSE(pair.has_value());
|
||||
}
|
||||
|
||||
SECTION("Format conversions") {
|
||||
auto pair = TradingPair::from_symbol("BTC-USDC");
|
||||
REQUIRE(pair->to_hummingbot() == "BTC-USDC");
|
||||
REQUIRE(pair->to_ccxt() == "BTC/USDC");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("OrderRequest factory methods", "[types]") {
|
||||
SECTION("Market order") {
|
||||
auto req = OrderRequest::market("BTC-USDC", Side::Buy, Decimal::from_double(1.5));
|
||||
REQUIRE(req.symbol == "BTC-USDC");
|
||||
REQUIRE(req.side == Side::Buy);
|
||||
REQUIRE(req.order_type == OrderType::Market);
|
||||
REQUIRE(req.quantity.to_double() == Approx(1.5));
|
||||
REQUIRE(req.time_in_force == TimeInForce::IOC);
|
||||
}
|
||||
|
||||
SECTION("Limit order") {
|
||||
auto req = OrderRequest::limit("ETH-USDC", Side::Sell,
|
||||
Decimal::from_double(10.0), Decimal::from_double(2000.0));
|
||||
REQUIRE(req.symbol == "ETH-USDC");
|
||||
REQUIRE(req.side == Side::Sell);
|
||||
REQUIRE(req.order_type == OrderType::Limit);
|
||||
REQUIRE(req.price.value().to_double() == Approx(2000.0));
|
||||
REQUIRE(req.time_in_force == TimeInForce::GTC);
|
||||
}
|
||||
|
||||
SECTION("Builder methods") {
|
||||
auto req = OrderRequest::market("BTC-USDC", Side::Buy, Decimal::from_double(1.0))
|
||||
.with_venue("lx_dex")
|
||||
.with_post_only()
|
||||
.with_client_id("my-order-123");
|
||||
|
||||
REQUIRE(req.venue.value() == "lx_dex");
|
||||
REQUIRE(req.post_only == true);
|
||||
REQUIRE(req.time_in_force == TimeInForce::PostOnly);
|
||||
REQUIRE(req.client_order_id == "my-order-123");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Order status checks", "[types]") {
|
||||
Order order;
|
||||
order.quantity = Decimal::from_double(100.0);
|
||||
order.filled_quantity = Decimal::from_double(50.0);
|
||||
order.remaining_quantity = Decimal::from_double(50.0);
|
||||
|
||||
SECTION("Partially filled") {
|
||||
order.status = OrderStatus::PartiallyFilled;
|
||||
REQUIRE(order.is_open());
|
||||
REQUIRE_FALSE(order.is_done());
|
||||
REQUIRE(order.fill_percent().to_double() == Approx(50.0));
|
||||
}
|
||||
|
||||
SECTION("Filled") {
|
||||
order.status = OrderStatus::Filled;
|
||||
order.filled_quantity = order.quantity;
|
||||
REQUIRE_FALSE(order.is_open());
|
||||
REQUIRE(order.is_done());
|
||||
}
|
||||
|
||||
SECTION("Cancelled") {
|
||||
order.status = OrderStatus::Cancelled;
|
||||
REQUIRE_FALSE(order.is_open());
|
||||
REQUIRE(order.is_done());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Ticker calculations", "[types]") {
|
||||
Ticker ticker;
|
||||
ticker.bid = Decimal::from_double(100.0);
|
||||
ticker.ask = Decimal::from_double(101.0);
|
||||
|
||||
SECTION("Mid price") {
|
||||
auto mid = ticker.mid_price();
|
||||
REQUIRE(mid.has_value());
|
||||
REQUIRE(mid->to_double() == Approx(100.5));
|
||||
}
|
||||
|
||||
SECTION("Spread") {
|
||||
auto spread = ticker.spread();
|
||||
REQUIRE(spread.has_value());
|
||||
REQUIRE(spread->to_double() == Approx(1.0));
|
||||
}
|
||||
|
||||
SECTION("Spread percent") {
|
||||
auto spread_pct = ticker.spread_percent();
|
||||
REQUIRE(spread_pct.has_value());
|
||||
REQUIRE(spread_pct->to_double() == Approx(1.0));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Enum to_string", "[types]") {
|
||||
REQUIRE(std::string(to_string(Side::Buy)) == "buy");
|
||||
REQUIRE(std::string(to_string(Side::Sell)) == "sell");
|
||||
REQUIRE(std::string(to_string(OrderType::Market)) == "market");
|
||||
REQUIRE(std::string(to_string(OrderType::Limit)) == "limit");
|
||||
REQUIRE(std::string(to_string(TimeInForce::GTC)) == "GTC");
|
||||
REQUIRE(std::string(to_string(OrderStatus::Filled)) == "filled");
|
||||
}
|
||||
Reference in New Issue
Block a user