Skip to content
Snippets Groups Projects
Commit 1ce248a7 authored by David Hintermann's avatar David Hintermann
Browse files

wip: rebalance pool, rebalance reserves

parent c1e9f941
1 merge request!10feat(rebalancer): rebalance automatically
API_URL=https://sepolia.infura.io/v3/{yourApiKey} API_URL=https://sepolia.infura.io/v3/{yourApiKey}
ACCOUNT_PRIVATE_KEY= ACCOUNT_PRIVATE_KEY=
\ No newline at end of file POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=roesticoin
POSTGRES_USER=roesticoin
PROMETHEUS_BASE_URL=http://prometheus:9000
\ No newline at end of file
...@@ -9,8 +9,9 @@ namespace Frontend.Marketplaces.Interfaces ...@@ -9,8 +9,9 @@ namespace Frontend.Marketplaces.Interfaces
public Task<double> EthPriceInChf { get; } public Task<double> EthPriceInChf { get; }
public Task<double> PotatoKgPriceInChf { get; } public Task<double> PotatoKgPriceInChf { get; }
public Task<double> PotatoKgPriceInEth { get; } public Task<double> PotatoKgPriceInEth { get; }
public Task<bool> Approve(Web3 approverClient, BigInteger amount);
public Task<PotatoTransaction?> Buy(string ownerAddress); public Task<PotatoTransaction?> Buy(string ownerAddress);
public Task<bool> Sell(PotatoTransaction transaction, string receiverAddress); public Task<bool> Sell(PotatoTransaction transaction, string receiverAddress);
string Address { get; }
} }
} }
...@@ -24,18 +24,7 @@ public class PotatoMarketplaceMock( ...@@ -24,18 +24,7 @@ public class PotatoMarketplaceMock(
public Task<double> PotatoKgPriceInChf => Task.FromResult(potatoKgChfPrice); public Task<double> PotatoKgPriceInChf => Task.FromResult(potatoKgChfPrice);
public Task<double> PotatoKgPriceInEth => PotatoKgEthPrice(); public Task<double> PotatoKgPriceInEth => PotatoKgEthPrice();
public async Task<bool> Approve(Web3 approverClient, BigInteger amountInWei) public string Address => account.Address;
{
var approveHandler = approverClient.Eth.GetContractTransactionHandler<ApproveFunction>();
var approveFunction = new ApproveFunction
{
Spender = account.Address,
Value = amountInWei,
};
var receipt = await approveHandler.SendRequestAndWaitForReceiptAsync(wethAddress, approveFunction);
return receipt.Status.Value == 1;
}
public async Task<PotatoTransaction?> Buy(string ownerAddress) public async Task<PotatoTransaction?> Buy(string ownerAddress)
{ {
......
...@@ -26,6 +26,7 @@ var postgresHost = Environment.GetEnvironmentVariable("POSTGRES_HOST")!; ...@@ -26,6 +26,7 @@ var postgresHost = Environment.GetEnvironmentVariable("POSTGRES_HOST")!;
var postgresUser = Environment.GetEnvironmentVariable("POSTGRES_USER")!; var postgresUser = Environment.GetEnvironmentVariable("POSTGRES_USER")!;
var postgresPassword = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")!; var postgresPassword = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")!;
var postgresDb = Environment.GetEnvironmentVariable("POSTGRES_DB")!; var postgresDb = Environment.GetEnvironmentVariable("POSTGRES_DB")!;
var prometheusBaseUrl =Environment.GetEnvironmentVariable("PROMETHEUS_BASE_URL")!;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
...@@ -61,6 +62,8 @@ builder.Services.AddTransient<Web3>(sp => ...@@ -61,6 +62,8 @@ builder.Services.AddTransient<Web3>(sp =>
return new Web3(account, chainApiUrl); return new Web3(account, chainApiUrl);
}); });
builder.Services.AddTransient<PrometheusClient>(sp => new PrometheusClient(prometheusBaseUrl));
builder.Services.AddSingleton<PoolMetrics>(); builder.Services.AddSingleton<PoolMetrics>();
builder.Services.AddSingleton<IPotatoMarketPlace>(sp => builder.Services.AddSingleton<IPotatoMarketPlace>(sp =>
{ {
......
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace Frontend;
public class PrometheusClient
{
private readonly HttpClient _httpClient;
private readonly string _prometheusBaseUrl;
public PrometheusClient(string prometheusBaseUrl)
{
_httpClient = new HttpClient();
_prometheusBaseUrl = prometheusBaseUrl;
}
public async Task<double> QueryMetricAsync(string metricName)
{
var queryUrl = $"{_prometheusBaseUrl}/api/v1/query?query={metricName}";
var response = await _httpClient.GetAsync(queryUrl);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Error querying Prometheus: {response.ReasonPhrase}");
}
var content = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(content);
if (json["status"]!.ToString() != "success")
{
throw new Exception($"Error querying Prometheus: {json["error"]}");
}
var result = json["data"]!["result"]!;
if (result.HasValues)
{
var value = result[0]!["value"]![1]!.ToString();
return double.Parse(value);
}
throw new Exception("Metric not found");
}
}
\ No newline at end of file
using System.Numerics;
using System.Reflection;
using Frontend.Assets;
using Frontend.AssetStores;
using Frontend.Configuration;
using Frontend.Functions;
using Frontend.Marketplaces;
using Frontend.Marketplaces.Interfaces;
using Nethereum.Contracts;
using Nethereum.Contracts.Standards.ERC20.ContractDefinition;
using Nethereum.Web3;
using Nethereum.Web3.Accounts;
using Org.BouncyCastle.Crypto.Tls;
namespace Frontend;
public class Rebalancer
{
const decimal EPSIOLON = 0.001m;
private readonly Web3 _web3;
private readonly Account _account;
private readonly string _contractAddress;
private readonly PrometheusClient _prometheusClient;
private decimal _threshold = 0.01m;
private PoolV3Client _pool;
private readonly IPotatoMarketPlace _potatoMarket;
private readonly ILogger<Rebalancer> _logger;
public uint RebalanceTradeExecutedCount { get; private set; }
private readonly TokenClient _token0Client;
private readonly TokenClient _token1Client;
private readonly PotatoStorage _potatoStorage;
public Rebalancer(
Web3 web3,
PrometheusClient prometheusClient,
ChainSettings chainSettings,
Account account,
IPotatoMarketPlace potatoMarket,
ILogger<Rebalancer> logger,
PotatoStorage potatoStorage)
{
_web3 = web3;
_prometheusClient = prometheusClient;
_pool = new(web3, chainSettings, account);
_potatoMarket = potatoMarket;
_logger = logger;
RebalanceTradeExecutedCount = 0;
_token0Client = new TokenClient(_web3, _pool.Token0Address);
_token1Client = new TokenClient(_web3, _pool.Token1Address);
_potatoStorage = potatoStorage;
}
public async Task<decimal> GetPoolPriceAsync()
{
return (decimal)await _prometheusClient.QueryMetricAsync("Uniswap_ROCWETH_price_WETH_per_ROC");
}
public async Task<decimal> GetDesiredPoolPriceAsync()
{
return (decimal)await _prometheusClient.QueryMetricAsync("avg_over_time(Marketplace_Potato_Price_ETH[2m])");
}
public async Task MaintainPoolAsync()
{
var currentPrice=0.0m;
var desiredPrice=0.0m;
try{
currentPrice = await GetPoolPriceAsync();
desiredPrice = await GetDesiredPoolPriceAsync();
}catch(Exception e){
_logger.LogError("Error while getting prices: {}", e.Message);
return;
}
if (Math.Abs(1 - currentPrice / desiredPrice) > _threshold)
{
_logger.LogDebug("Must rebalance: Mean price {}, desired price {}", currentPrice, desiredPrice);
var quoteAfterSwap = await _pool.PerformSwapTowardsDesiredQuote(desiredPrice);
_logger.LogDebug("Quote afert swap: {}", quoteAfterSwap);
RebalanceTradeExecutedCount++;
}
}
public async Task MaintainLiquidReservesAsync()
{
var token0WeiReserve = await _token0Client.BalanceOf(_account.Address);
var token1WeiReserve = await _token1Client.BalanceOf(_account.Address);
var meanPoolPrice = await GetPoolPriceAsync();
var token0ReservesInToken1 = Web3.Convert.FromWei(token0WeiReserve) * meanPoolPrice;
var token1Reserve = Web3.Convert.FromWei(token1WeiReserve);
if (token0ReservesInToken1 / token1Reserve > 2) // token0 is ROC , token1 is WETH, our reserves in ROC are twice as big as in WETH
{
_logger.LogDebug("Must rebalance reserves: Token0 reserves {}(expressed in token1), Token1 reserves {}(expressed in token1)", token0ReservesInToken1, token1Reserve);
var amountToAdjustInToken1 = (token0ReservesInToken1 - token1Reserve) / 2;
var amountToAdjustInToken0 = amountToAdjustInToken1 / meanPoolPrice;
_logger.LogDebug("Half of the difference is {}(expressed in token0) or {}(expressed in token1). Sell it", amountToAdjustInToken0,amountToAdjustInToken1);
var transactionToSell= await _potatoStorage.RemovePotatoes((double)amountToAdjustInToken0);
var result= await _potatoMarket.Sell(transactionToSell, _account.Address);
if(result){
_logger.LogDebug("Potatoes sold successfully, burn the torken representation");
await _token0Client.Burn(Web3.Convert.ToWei(transactionToSell.Weight), new CancellationToken());
}else{
_logger.LogError("Potatoes not sold successfully, keep the token representation");
}
}else if(token1Reserve/token0ReservesInToken1 >2){
_logger.LogDebug("Must rebalance reserves: Token0 reserves {}(expressed in token1), Token1 reserves {}(expressed in token1)", token0ReservesInToken1, token1Reserve);
var amountToAdjustInToken1 = (token1Reserve - token0ReservesInToken1) / 2;
var amountToAdjustInToken0 = amountToAdjustInToken1 / meanPoolPrice;
_logger.LogDebug("Half of the difference is {}(expressed in token0) or {}(expressed in token1). Buy it", amountToAdjustInToken0,amountToAdjustInToken1);
await _token1Client.ApproveAsync(_potatoMarket.Address,Web3.Convert.ToWei(amountToAdjustInToken1));
var buyTransaction= await _potatoMarket.Buy(_account.Address);
if(buyTransaction!=null){
_logger.LogDebug("Potatoes bought successfully, mint the token representation");
await _potatoStorage.AddPotatoes(buyTransaction);
await _token0Client.Mint(_account.Address,Web3.Convert.ToWei(buyTransaction.Weight),new CancellationToken());
}else{
_logger.LogDebug("Potatoes bought successfully, mint the token representation");
}
}else{
_logger.LogDebug("Reserves are balanced enough: Token0 reserves {}[token1], Token1 reserves {}[token1]", token0ReservesInToken1, token1Reserve);
}
}
public async Task EnsureCoinSupplyMatchesRwaSupply()
{
var rwaSupply = (decimal)_potatoStorage.SupplyWeight();
var tokenSupply = await _token0Client.TotalSupply();
var tokenReserve = Web3.Convert.FromWei(await _token0Client.BalanceOf(_account.Address));
if(Math.Abs(rwaSupply-tokenSupply) < EPSIOLON){
_logger.LogDebug("Token supply matches RWA supply");
return;
}
if (rwaSupply < tokenSupply)
{
var shouldBurn = tokenSupply - rwaSupply;
var burnable = Math.Min(shouldBurn,tokenReserve); // we cannot burn mor then we control
_logger.LogDebug("Should burn {} tokens, currently controlling {} tokens",shouldBurn,tokenReserve);
await _token0Client.Burn(Web3.Convert.ToWei(burnable), new CancellationToken());
_logger.LogDebug("Burned {} tokens",burnable);
if(burnable < shouldBurn){
_logger.LogError("Trigger rebalancing reserves and try again");
await MaintainLiquidReservesAsync();
await EnsureCoinSupplyMatchesRwaSupply();
}
}
else if (rwaSupply > tokenSupply)
{
_logger.LogDebug("Must mint {} tokens", rwaSupply - tokenSupply);
await _token0Client.Mint(_account.Address, Web3.Convert.ToWei(rwaSupply - tokenSupply), new CancellationToken());
}
}
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment