Skip to content

Instantly share code, notes, and snippets.

@jeremytregunna
Created August 2, 2019 16:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeremytregunna/ffe28ec22154fbf93e0684ddc6718a15 to your computer and use it in GitHub Desktop.
Save jeremytregunna/ffe28ec22154fbf93e0684ddc6718a15 to your computer and use it in GitHub Desktop.
defmodule Hodler.Signal.DailyVolume do
@moduledoc """
Produce a list of the top traded (by volume) crypto currencies on an exchange
as a standardized ratio of weighted sums relative to its initial value.
This is accomplished by taking the top `count` of assets by daily volume,
calculate the market share, then truncate it by the `max_percent`. Then we
rescale them, so the weights all add up to 1. Finally calculated the weighted
average.
This module is intended to run on one exchange, on a static set of the top
coins by volume on the day it starts. Rebalancing efforts are outside the
scope of this module.
"""
use GenServer
require Logger
def start_link(exchange, max_percent, count, opts \\ []) do
GenServer.start_link(__MODULE__, {exchange, max_percent, count}, opts)
end
def child_spec(name, exchange, max_percent, count, opts \\ []) when is_atom(name) do
%{
id: name,
start: {__MODULE__, :start_link, [exchange, max_percent, count, Keyword.merge(opts, name: name)]}
}
end
def init({exchange, max_percent, count}) do
{:ok, %{exchange: exchange, max_percent: max_percent, count: count, initial: nil}}
end
def handle_call(:latest, _from, state) do
case calculate(state) do
nil ->
{:reply, {:error, "Unable to calculate index"}, state}
{assets, new_state} ->
{:reply, assets, new_state}
end
end
def handle_call({:given, symbol, amount}, _from, state) do
case {state.exchange.asset_for_symbol(symbol), calculate(state)} do
{_, {:error, reason}} ->
Logger.error(fn -> "Unable to calculate index given base currency #{symbol}: #{inspect(reason)}" end)
{:reply, {:error, reason}, state}
{nil, _} ->
{:reply, {:error, "Invalid base coin symbol #{inspect(symbol)}"}, state}
{_base, {assets, _}} ->
adjusted =
Enum.reduce(assets, [], fn %{id: id, symbol: receive_symbol, weighted_average: points}, acc ->
{amount_in_base, ""} = Float.parse(amount)
send_amount = to_string(amount_in_base * points)
[%{id: id, receive_symbol: receive_symbol, points: points, send: send_amount, send_symbol: symbol} | acc]
end)
{:reply, adjusted, state}
end
end
def calculate(state) do
case state.exchange.all_assets() do
{:ok, assets} ->
assets
|> exclude_stablecoins()
|> exclude_blacklisted_coins()
|> select_top_assets(state.count)
|> calculate_market_share()
|> truncate_market_share(state.max_percent)
|> rescale(state)
|> weighted_average()
{:error, reason} ->
Logger.error(fn -> "Unable to calculate index: #{inspect(reason)}" end)
nil
end
end
def exclude_stablecoins(assets) do
stablecoins = ["tether", "dai", "trueusd", "usd-coin", "paxos-standard-token"]
Enum.filter(assets, fn k ->
!Enum.member?(stablecoins, k["id"])
end)
end
def exclude_blacklisted_coins(assets) do
blacklist = ["bitcoin-sv"]
Enum.filter(assets, fn k ->
!Enum.member?(blacklist, k["id"])
end)
end
def select_top_assets(assets, count) do
assets
|> Enum.sort_by(fn k ->
String.to_float(k["volumeUsd24Hr"])
end, &>=/2)
|> Enum.take(count)
end
def calculate_market_share(assets) do
whole_market = Enum.reduce(assets, 0, &(&2 + String.to_float(Map.get(&1, "volumeUsd24Hr"))))
Logger.info(fn -> "Daily volume for all included coins: #{inspect(whole_market)}" end)
Enum.map(assets, fn k ->
%{
id: k["id"],
symbol: k["symbol"],
market_share: String.to_float(k["volumeUsd24Hr"]) / whole_market
}
end)
end
def truncate_market_share(market_shares, max_percent) do
Enum.map(market_shares, fn k ->
%{
id: k.id,
symbol: k.symbol,
market_share: min(k.market_share, max_percent)
}
end)
end
def rescale(truncated_market_shares, %{initial: nil} = state) do
{a, b} = {minimum(truncated_market_shares), maximum(truncated_market_shares)}
assets =
Enum.map(truncated_market_shares, fn asset ->
rescaled = (0.2 - 0.02) * (asset.market_share - a) / (b - a) + 0.03
%{id: asset.id, symbol: asset.symbol, rescaled_market_share: rescaled}
end)
{assets, %{state | initial: assets}}
end
def rescale(truncated_market_shares, state) do
{a, b} = {minimum(truncated_market_shares), maximum(truncated_market_shares)}
assets =
Enum.map(truncated_market_shares, fn asset ->
rescaled = (0.2 - 0.02) * (asset.market_share - a) / (b - a) + 0.03
%{id: asset.id, symbol: asset.symbol, rescaled_market_share: rescaled}
end)
{assets, state}
end
def weighted_average({rescaled_assets, state}) do
sum = Enum.reduce(rescaled_assets, 0, &(&1.rescaled_market_share + &2))
weighted_average =
Enum.map(rescaled_assets, fn asset ->
%{id: asset.id, symbol: asset.symbol, weighted_average: asset.rescaled_market_share / sum}
end)
{weighted_average, state}
end
defp minimum(assets) do
Enum.reduce(assets, 1, fn asset, acc ->
if asset.market_share < acc do
asset.market_share
else
acc
end
end)
end
defp maximum(assets) do
Enum.reduce(assets, 0, fn asset, acc ->
if asset.market_share > acc do
asset.market_share
else
acc
end
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment