Created
August 2, 2019 16:19
-
-
Save jeremytregunna/ffe28ec22154fbf93e0684ddc6718a15 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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