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> conv = conv_a_star(crypto, "ADA", "BTC");
julia> conv_value(conv)
0.000009582698067
julia> conv_chain(conv)
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
end
In 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.price
Now 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> conv = conv_a_star(crypto, "ADA", "BTC");
julia> conv_value(conv)
0.000009582698067
julia> conv_chain(conv)
2-element Vector{CcyConv.AbstractPrice}:
MyPrice("Binance", "ADA", "USDT", 0.4037234)
MyPrice("Huobi", "USDT", "BTC", 0.0000237)
Using context
Finally, we can go further and implement a context into our workspace. This will allow us to request and cache data from different sources without crossing them with each other.
First, let's define a new context MyCtx
that can store the previously requested data.
using CcyConv
using CryptoExchangeAPIs.Binance
struct MyCtx <: CcyConv.AbstractCtx
prices::Dict{String,Float64}
MyCtx() = new(Dict{String,Float64}())
end
Also, now the currency pair ExSymbol
will not store a specific price value, but instead will contain only the corresponding symbol required for the API request. This will be achieved by further overloading the price
method.
struct ExSymbol <: CcyConv.AbstractPrice
base_asset::String
quote_asset::String
symbol::String
end
As before, new getter methods must be defined to conform to the AbstractPrice
interface. Particular attention should be paid to the price
method, which now first tries to find the desired price in the context cache MyCtx
and only if this price has not been previously requested - makes a request to the exchange API.
function CcyConv.from_asset(x::ExSymbol)::String
return x.base_asset
end
function CcyConv.to_asset(x::ExSymbol)::String
return x.quote_asset
end
function CcyConv.price(ctx::MyCtx, x::ExSymbol)::Float64
return get!(ctx.prices, x.symbol) do
try
Binance.Spot.avg_price(; symbol = x.symbol).result.price
catch
NaN
end
end
end
Finally, you need to create a new graph, a custom context and add custom currency pairs to the graph:
my_graph = FXGraph()
my_ctx = MyCtx()
my_conv = (to, from) -> conv_value(my_graph(my_ctx, CcyConv.a_star_alg, to, from))
push!(my_graph, ExSymbol("ADA", "BTC", "ADABTC"))
push!(my_graph, ExSymbol("BTC", "USDT", "BTCUSDT"))
push!(my_graph, ExSymbol("PEPE", "USDT", "PEPEUSDT"))
push!(my_graph, ExSymbol("EOS", "USDT", "EOSUSDT"))
To set the context my_ctx
, you must use a lower-level method
with an explicit specification of the used context, as well as the path search algorithm.
# "long" request for prices from the exchange
julia> @time my_conv("ADA", "EOS")
4.740000 seconds (1.80 M allocations: 120.606 MiB, 0.52% gc time, 14.55% compilation time)
0.6004274502578457
# "quick" request for prices from cache
julia> @time my_conv("ADA", "EOS")
0.000130 seconds (46 allocations: 2.312 KiB)
0.6004274502578457
With this approach, due to the context, only the first data request will take a long time. Subsequent requests will take much less time.
You can go further and add a refresh rate for updating the data in the cache.
Pathfinding algorithm
If you are planning to implement your own graph pathfinding method, you should use the following function signature:
custom_alg(fx::FXGraph, from_id::UInt64, to_id::UInt64) -> Vector{Pair{Integer, Integer}}
Which returns a vector with pairs of indices corresponding to the fx
graph currencies.
The vector of such pairs should form a chain of conversions of the following form:
2-element Vector{Pair{Int64, Int64}}:
1 => 2
2 => 3
Then you can use a low-level method
to apply your pathfinding algorithm.
See previous section
to add your own context or just use a dummy one:
julia> my_graph = FXGraph();
julia> dummy_ctx = CcyConv.MyCtx();
julia> my_graph(dummy_ctx, custom_alg, "ADA", "USDT")
[...]