Manual
Let's take a closer look at a few examples of how this package can be used in practice.
Basic usage
In the simplest case, you need to create a new FXGraph graph object:
using CcyConv
# Create a new graph
crypto = FXGraph()Then add information about currency pairs to it as Price objects:
# Add exchange rates
push!(crypto, Price("ADA", "USDT", 0.4037234))
push!(crypto, Price("USDT", "BTC", 0.0000237))
push!(crypto, Price("BTC", "ETH", 18.808910))
push!(crypto, Price("ETH", "ALGO", 14735.460))
# Or use 'append!':
append!(
crypto,
[
Price("ADA", "USDT", 0.4037234),
Price("USDT", "BTC", 0.0000237),
Price("BTC", "ETH", 18.808910),
Price("ETH", "ALGO", 14735.460),
],
);Finally, you can use one of the algorithms to find a path between required currency pairs:
# Convert ADA to BTC
julia> result = conv(crypto, "ADA", "BTC");
julia> conv_value(result)
0.000009582698067
julia> conv_chain(result)
2-element Vector{CcyConv.AbstractPrice}:
Price("ADA", "USDT", 0.4037234)
Price("USDT", "BTC", 0.0000237)Custom price
You can also define a new custom subtype of an abstract type AbstractPrice representing the price of a currency pair and, for example, having information about the exchange to which it belongs. In this case, there may be several edges between two currencies with different prices of the corresponding exchanges.
using CcyConv
# Create a new graph
crypto = FXGraph()
# Custom Price
struct MyPrice <: CcyConv.AbstractPrice
exchange::String
from_asset::String
to_asset::String
price::Float64
endIn this case, it is important to override the following getter methods for the new custom type MyPrice because they will be used during pathfinding:
CcyConv.from_asset(x::MyPrice) = x.from_asset
CcyConv.to_asset(x::MyPrice) = x.to_asset
CcyConv.price(x::MyPrice) = x.priceNow we can add objects of our new custom type to the graph and find the conversion path using the available algorithms:
# Add exchange rates
push!(crypto, MyPrice("Binance", "ADA", "USDT", 0.4037234))
push!(crypto, MyPrice("Huobi", "USDT", "BTC", 0.0000237))
push!(crypto, MyPrice("Okex", "BTC", "ETH", 18.808910))
push!(crypto, MyPrice("Gateio", "ETH", "ALGO", 14735.460))# Convert ADA to BTC
julia> result = conv(crypto, "ADA", "BTC");
julia> conv_value(result)
0.000009582698067
julia> conv_chain(result)
2-element Vector{CcyConv.AbstractPrice}:
MyPrice("Binance", "ADA", "USDT", 0.4037234)
MyPrice("Huobi", "USDT", "BTC", 0.0000237)Using context
The graph topology can be built upfront — without actual prices — and the rates resolved lazily at query time through a custom context. This lets you fetch and cache live data from any source on the fly.
First, let's define a new context BinanceCtx that can store the previously requested data, and a custom price type BinancePair that holds only the symbol name (no price value).
using CcyConv
using EasyCurl
using Serde
struct BinanceCtx <: CcyConv.AbstractCtx
prices::Dict{String,Float64}
BinanceCtx() = new(Dict{String,Float64}())
end
struct BinancePair <: CcyConv.AbstractPrice
base_asset::String
quote_asset::String
symbol::String
endNew getter methods must be defined to conform to the AbstractPrice interface. The price method first checks the context cache and only fetches from the exchange API on a cache miss.
CcyConv.from_asset(x::BinancePair) = x.base_asset
CcyConv.to_asset(x::BinancePair) = x.quote_asset
function CcyConv.price(ctx::BinanceCtx, x::BinancePair)::Float64
return get!(ctx.prices, x.symbol) do
try
resp = http_get("https://api.binance.com/api/v3/avgPrice?symbol=$(x.symbol)")
data = Serde.parse_json(http_body(resp))
parse(Float64, data["price"])
catch
NaN
end
end
endCreate a graph, a context, and add custom currency pairs:
fx = FXGraph()
ctx = BinanceCtx()
push!(fx, BinancePair("ADA", "BTC", "ADABTC"))
push!(fx, BinancePair("BTC", "USDT", "BTCUSDT"))
push!(fx, BinancePair("PEPE", "USDT", "PEPEUSDT"))
push!(fx, BinancePair("EOS", "USDT", "EOSUSDT"))Pass the context as a keyword argument to conv:
# First call fetches prices from the exchange
julia> @time conv(fx, "ADA", "EOS"; ctx = ctx) |> conv_value
0.350000 seconds (...)
0.6004274502578457
# Subsequent calls use cached prices
julia> @time conv(fx, "ADA", "EOS"; ctx = ctx) |> conv_value
0.000130 seconds (46 allocations: 2.312 KiB)
0.6004274502578457Only the first request is slow (fetches from the exchange). Subsequent calls hit the cache.
Min rate pathfinding
By default, conv uses state-space A* (AStar) to find the path whose product of exchange rates is minimum, running over edges weighted with log(rate). Negative log-weights — produced whenever a rate is below 1 — would normally break a Dijkstra-style search, so the algorithm lifts the search onto a graph whose vertices are (currency, visited_set) pairs. Every walk in the lifted graph is a simple path in the original by construction, which means the algorithm returns the same minimum-rate answer as DFS on every graph (including those with arbitrage cycles). A min-edge-weight admissible heuristic prunes branches that cannot improve the best path found so far. For graphs with more than 64 currencies the implementation falls back to an exhaustive DFS.
DFS finds the path whose product of exchange rates is minimum, using an exhaustive DFS that enumerates all simple paths between the source and target currencies. This guarantees the optimal result but has exponential worst-case complexity, so prefer these functions for small and medium-sized graphs.
using CcyConv
fx = FXGraph()
append!(
fx,
[
Price("A", "B", 2.0),
Price("B", "D", 3.0),
Price("D", "F", 0.5),
Price("A", "C", 1.5),
Price("C", "E", 2.0),
Price("E", "F", 3.0),
],
)julia> conv(fx, "A", "F", DFS()) |> conv_value
3.0Custom pathfinding algorithm
You can define a custom algorithm type and extend conv:
struct MyAlg end
function CcyConv.conv(fx::FXGraph, from::String, to::String, ::MyAlg; ctx::CcyConv.AbstractCtx = CcyConv.MyCtx())
# custom pathfinding logic
endThen use it like any built-in algorithm:
julia> fx = FXGraph();
julia> conv(fx, "ADA", "USDT", MyAlg())
[...]