Added Evelyn to the hub temporarily
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package com.fanitas.evelyn.math;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
|
||||
import static java.math.BigDecimal.ZERO;
|
||||
|
||||
public class BigDecimalUtils {
|
||||
public static BigDecimal min(BigDecimal a, BigDecimal b) {
|
||||
return a.compareTo(b) <= 0 ? a : b;
|
||||
}
|
||||
|
||||
public static BigDecimal max(BigDecimal a, BigDecimal b) {
|
||||
return a.compareTo(b) >= 0 ? a : b;
|
||||
}
|
||||
|
||||
public static BigDecimal sqrt(BigDecimal value, MathContext mc) {
|
||||
if (value.compareTo(ZERO) < 0) {
|
||||
throw new IllegalArgumentException("sqrt af negativ værdi");
|
||||
}
|
||||
if (value.compareTo(ZERO) == 0) {
|
||||
return ZERO;
|
||||
}
|
||||
|
||||
BigDecimal x = new BigDecimal(Math.sqrt(value.doubleValue()), mc);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
x = x.add(value.divide(x, mc), mc).divide(TWO, mc);
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
public static double erf(BigDecimal x) {
|
||||
double value = x.doubleValue();
|
||||
double sign = value < 0 ? -1.0 : 1.0;
|
||||
value = Math.abs(value);
|
||||
|
||||
double t = 1.0 / (1.0 + 0.5 * value);
|
||||
|
||||
double tau = t * Math.exp(
|
||||
-value * value
|
||||
- 1.26551223
|
||||
+ t * (1.00002368
|
||||
+ t * (0.37409196
|
||||
+ t * (0.09678418
|
||||
+ t * (-0.18628806
|
||||
+ t * (0.27886807
|
||||
+ t * (-1.13520398
|
||||
+ t * (1.48851587
|
||||
+ t * (-0.82215223
|
||||
+ t * 0.17087277))))))))
|
||||
);
|
||||
|
||||
double result = sign * (1.0 - tau);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static final BigDecimal TWO = new BigDecimal("2");
|
||||
public static final BigDecimal THREE = new BigDecimal("3");
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.fanitas.evelyn.core;
|
||||
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
|
||||
public interface DesiredPositionCalculator {
|
||||
|
||||
HashMap<ΩRaydiumLiquidityPoolPositionNftIdΩ, MoneyAmount> calculateRebalancingProposal(
|
||||
ΩPriceΩ currentPrice,
|
||||
MoneyAmount totalReadyAmountMintA,
|
||||
MoneyAmount totalReadyAmountMintB
|
||||
);
|
||||
|
||||
Pair calculateTotalDistributedSums();
|
||||
|
||||
Pair calculateLockedSums(ΩPriceΩ currentPrice);
|
||||
|
||||
Pair calculateRedistributableSums(
|
||||
ΩPriceΩ currentPrice,
|
||||
MoneyAmount inactiveInAccountMintA,
|
||||
MoneyAmount inactiveInAccountMintB,
|
||||
MoneyAmount reservedForBurnMintA,
|
||||
MoneyAmount reservedForBurnMintB
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.fanitas.evelyn.core;
|
||||
|
||||
public interface Evelyn {
|
||||
void executeService() throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.fanitas.evelyn.core;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record Pair(
|
||||
ΩAmountΩ amountA,
|
||||
ΩAmountΩ amountB
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.fanitas.evelyn.core;
|
||||
|
||||
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
public interface State {
|
||||
void update() throws IOException, InterruptedException;
|
||||
ΩmilliSecondsΩ getIterationInterval();
|
||||
ΩSolanaAmountΩ getSyrupOwnedByEvelyn();
|
||||
ΩSolanaAddressΩ getSolanaAddressForEvelynIOU();
|
||||
ΩSolanaAddressΩ getSolanaAddressForEvelynIOUBurner();
|
||||
ΩRaydiumLiquidityPoolIdΩ getRaydiumPoolId();
|
||||
ΩCurveWidthΩ getCurveWidth();
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> getLiquidityPositions();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.fanitas.evelyn.core;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record Triblet(
|
||||
ΩAmountΩ currentAmount,
|
||||
ΩAmountΩ suggestedAmount,
|
||||
ΩAmountΩ diff
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
package com.fanitas.evelyn.core.impl.ref;
|
||||
|
||||
import com.fanitas.evelyn.core.Pair;
|
||||
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
|
||||
import com.fanitas.evelyn.core.DesiredPositionCalculator;
|
||||
|
||||
import com.r35157.libs.valuetypes.basic.CurrencyType;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
|
||||
import static com.fanitas.evelyn.math.BigDecimalUtils.*;
|
||||
import static java.math.BigDecimal.ONE;
|
||||
import static java.math.BigDecimal.ZERO;
|
||||
|
||||
public class DesiredPositionCalculatorImpl implements DesiredPositionCalculator {
|
||||
|
||||
public DesiredPositionCalculatorImpl(
|
||||
ΩCurveWidthΩ curveWidth,
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityProviderPositions
|
||||
) {
|
||||
this.curveWidth = curveWidth;
|
||||
this.liquidityProviderPositions = liquidityProviderPositions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<ΩRaydiumLiquidityPoolPositionNftIdΩ, MoneyAmount> calculateRebalancingProposal(
|
||||
ΩPriceΩ currentPrice,
|
||||
MoneyAmount totalReadyAmountMintA,
|
||||
MoneyAmount totalReadyAmountMintB
|
||||
) {
|
||||
List<ΩPriceΩ> intervalStarts = getSortedPositionIntervalFromValues(liquidityProviderPositions);
|
||||
List<ΩPriceΩ> intervalEnds = getSortedPositionIntervalToValues(liquidityProviderPositions);
|
||||
|
||||
int indexOfPositionWithAnEndPriceBeforePositionWithCurrentPrice = lookupIndexOfLastLessThanOrEqual(
|
||||
intervalStarts,
|
||||
currentPrice
|
||||
);
|
||||
|
||||
ΩPriceΩ lastLessThanOrEqual = intervalEnds.get(indexOfPositionWithAnEndPriceBeforePositionWithCurrentPrice);
|
||||
HashMap<ΩRaydiumLiquidityPoolPositionIdΩ, MoneyAmount> desiredPositions = new HashMap<>();
|
||||
|
||||
boolean beforeNonZeroPos = true;
|
||||
|
||||
for (int i = 0; i < intervalStarts.size(); i++) {
|
||||
ΩPriceΩ iStart = intervalStarts.get(i);
|
||||
ΩPriceΩ iEnd = intervalEnds.get(i);
|
||||
|
||||
ΩAmountΩ dp = calculateDesiredPositionSizeForInterval(
|
||||
currentPrice,
|
||||
totalReadyAmountMintB.amount(),
|
||||
iStart,
|
||||
iEnd,
|
||||
lastLessThanOrEqual
|
||||
);
|
||||
|
||||
int desiredPositionSign = dp.compareTo(ZERO);
|
||||
|
||||
if (desiredPositionSign > 0) {
|
||||
beforeNonZeroPos = false;
|
||||
} else if (!beforeNonZeroPos && desiredPositionSign == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
MoneyAmount desiredPositionSize = new MoneyAmount(
|
||||
dp,
|
||||
totalReadyAmountMintB.currencyType()
|
||||
);
|
||||
|
||||
RaydiumLiquidityPoolPositionConcentrated position = getPositionByStartPriceA(iStart);
|
||||
ΩRaydiumLiquidityPoolPositionNftIdΩ poolPositionId = position.nftId();
|
||||
desiredPositions.put(poolPositionId, desiredPositionSize);
|
||||
}
|
||||
|
||||
return desiredPositions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair calculateTotalDistributedSums() {
|
||||
Collection c = liquidityProviderPositions.values();
|
||||
|
||||
if(c.isEmpty()) {
|
||||
// TODO: I do not like this - I prefer 0 - but without any positions, do I then know the currencyType?
|
||||
throw new IllegalStateException("No positions in pool!");
|
||||
}
|
||||
|
||||
// TODO: This smells - setting to null. Oh dear!
|
||||
CurrencyType ct = null;
|
||||
ΩAmountΩ amountB = ZERO;
|
||||
for (RaydiumLiquidityPoolPositionConcentrated position : liquidityProviderPositions.values()) {
|
||||
ct = position.amountMintB().currencyType(); // TODO: Too redundant - please rethink
|
||||
amountB = amountB.add(position.accountingInfo().addedMintB());
|
||||
}
|
||||
|
||||
MoneyAmount ma = new MoneyAmount(amountB, ct);
|
||||
return new Pair(ZERO, ma);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair calculateLockedSums(ΩPriceΩ currentPrice) {
|
||||
List<ΩPriceΩ> ascendingPrices = getSortedPositionIntervalFromValues(liquidityProviderPositions);
|
||||
ΩAmountΩ sum = ZERO;
|
||||
|
||||
// TODO: This smells - setting to null. Oh dear!
|
||||
CurrencyType ct = null;
|
||||
|
||||
int index = lookupIndexOfFirstGreaterThanOrEqual(ascendingPrices, currentPrice);
|
||||
if(index >= 0) {
|
||||
ΩPriceΩ startPriceOfStartPos = ascendingPrices.get(index);
|
||||
|
||||
for (RaydiumLiquidityPoolPositionConcentrated position : liquidityProviderPositions.values()) {
|
||||
if (position.priceRange().from().compareTo(startPriceOfStartPos) >= 0) {
|
||||
ct = position.amountMintA().currencyType();
|
||||
sum = sum.add(position.amountMintB().amount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MoneyAmount ma = new MoneyAmount(sum, ct);
|
||||
return new Pair(ZERO, ma);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair calculateRedistributableSums(
|
||||
ΩPriceΩ currentPrice,
|
||||
MoneyAmount inactiveInAccountMintA,
|
||||
MoneyAmount inactiveInAccountMintB,
|
||||
MoneyAmount reservedForBurnMintA,
|
||||
MoneyAmount reservedForBurnMintB
|
||||
) {
|
||||
List<ΩPriceΩ> ascendingValues = getSortedPositionIntervalFromValues(liquidityProviderPositions);
|
||||
ΩAmountΩ redistSum = ZERO;
|
||||
CurrencyType ct = null;
|
||||
|
||||
int index = lookupIndexOfLastLessThan(ascendingValues, currentPrice);
|
||||
if(index >= 0) {
|
||||
ΩPriceΩ startPriceOfStartPos = ascendingValues.get(index);
|
||||
|
||||
for (RaydiumLiquidityPoolPositionConcentrated position : liquidityProviderPositions.values()) {
|
||||
if (position.priceRange().from().compareTo(startPriceOfStartPos) <= 0) {
|
||||
ct = position.amountMintB().currencyType();
|
||||
redistSum = redistSum.add(position.amountMintB().amount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redistSum = add(inactiveInAccountMintB, redistSum);
|
||||
redistSum = subtract(redistSum, reservedForBurnMintB);
|
||||
MoneyAmount ma = new MoneyAmount(redistSum, ct);
|
||||
return new Pair(ZERO, ma);
|
||||
}
|
||||
|
||||
|
||||
// TODO: Can this be deleted?
|
||||
private MoneyAmount add(ΩAmountΩ a, ΩAmountΩ b, CurrencyType ct) {
|
||||
MoneyAmount ma = new MoneyAmount(a.add(b), ct);
|
||||
return ma;
|
||||
}
|
||||
|
||||
private MoneyAmount add(MoneyAmount a, MoneyAmount b) {
|
||||
MoneyAmount ma = new MoneyAmount(a.amount().add(b.amount()), a.currencyType());
|
||||
return ma;
|
||||
}
|
||||
|
||||
private ΩAmountΩ add(MoneyAmount a, ΩAmountΩ b) {
|
||||
return a.amount().add(b);
|
||||
}
|
||||
|
||||
private ΩAmountΩ subtract(ΩAmountΩ a, MoneyAmount b) {
|
||||
return a.subtract(b.amount());
|
||||
}
|
||||
|
||||
private RaydiumLiquidityPoolPositionConcentrated getPositionByStartPriceA(ΩPriceΩ startPriceA) {
|
||||
for (RaydiumLiquidityPoolPositionConcentrated candidate : liquidityProviderPositions.values()) {
|
||||
if (candidate.priceRange().from().compareTo(startPriceA) == 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ΩAmountΩ calculateDesiredPositionSizeForInterval(
|
||||
ΩPriceΩ currentPrice,
|
||||
ΩAmountΩ totalSyrupToDistribution,
|
||||
ΩPriceΩ intervalPriceFrom,
|
||||
ΩPriceΩ intervalPriceTo,
|
||||
ΩPriceΩ lookupPrice
|
||||
) {
|
||||
ΩPriceΩ curveStartPrice = currentPrice.multiply(
|
||||
ONE.subtract(curveWidth, MC),
|
||||
MC
|
||||
);
|
||||
|
||||
boolean intervalIsCompletelyBeforeCurve =
|
||||
intervalPriceTo.compareTo(curveStartPrice) <= 0;
|
||||
|
||||
boolean intervalIsCompletelyAfterLookupLimit =
|
||||
intervalPriceFrom.compareTo(lookupPrice) >= 0;
|
||||
|
||||
if (intervalIsCompletelyBeforeCurve || intervalIsCompletelyAfterLookupLimit) {
|
||||
return ZERO;
|
||||
}
|
||||
|
||||
ΩPriceΩ effectiveIntervalFrom = max(intervalPriceFrom, curveStartPrice);
|
||||
ΩPriceΩ effectiveIntervalTo = min(intervalPriceTo, lookupPrice);
|
||||
|
||||
BigDecimal sigma = currentPrice.multiply(curveWidth, MC)
|
||||
.divide(THREE, MC);
|
||||
|
||||
BigDecimal sqrtTwo = sqrt(TWO, MC);
|
||||
BigDecimal denominator = sigma.multiply(sqrtTwo, MC);
|
||||
|
||||
ΩPriceΩ normalizedEffectiveTo = effectiveIntervalTo.subtract(currentPrice, MC)
|
||||
.divide(denominator, MC);
|
||||
|
||||
ΩPriceΩ normalizedEffectiveFrom = effectiveIntervalFrom.subtract(currentPrice, MC)
|
||||
.divide(denominator, MC);
|
||||
|
||||
ΩPriceΩ normalizedLookupPrice = lookupPrice.subtract(currentPrice, MC)
|
||||
.divide(denominator, MC);
|
||||
|
||||
ΩPriceΩ normalizedCurveStart = curveStartPrice.subtract(currentPrice, MC)
|
||||
.divide(denominator, MC);
|
||||
|
||||
BigDecimal intervalWeight = new BigDecimal(erf(normalizedEffectiveTo) - erf(normalizedEffectiveFrom));
|
||||
BigDecimal totalWeightInActiveCurve = new BigDecimal(erf(normalizedLookupPrice) - erf(normalizedCurveStart));
|
||||
|
||||
ΩAmountΩ dps = totalSyrupToDistribution.multiply(intervalWeight, MC)
|
||||
.divide(totalWeightInActiveCurve, MC);
|
||||
|
||||
return dps;
|
||||
}
|
||||
|
||||
private static int lookupIndexOfFirstGreaterThan(List<ΩPriceΩ> ascendingPrices, ΩPriceΩ searchPrice) {
|
||||
int index = -1;
|
||||
|
||||
for (int i = ascendingPrices.size() - 1; i >= 0; i--) {
|
||||
BigDecimal p = ascendingPrices.get(i);
|
||||
if (p.compareTo(searchPrice) <= 0) {
|
||||
break;
|
||||
} else {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException(
|
||||
"No position has a greater start price than a position containing '" + searchPrice + "'"
|
||||
);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static int lookupIndexOfFirstGreaterThanOrEqual(List<ΩPriceΩ> ascendingPrices, ΩPriceΩ searchPrice) {
|
||||
int index = -1;
|
||||
|
||||
for (int i = ascendingPrices.size() - 1; i >= 0; i--) {
|
||||
BigDecimal p = ascendingPrices.get(i);
|
||||
if (p.compareTo(searchPrice) <= 0) {
|
||||
break;
|
||||
} else {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException(
|
||||
"No position has a greater start price than a position containing '" + searchPrice + "'"
|
||||
);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static int lookupIndexOfLastLessThanOrEqual(List<ΩPriceΩ> ascendingPrices, ΩPriceΩ searchPrice) {
|
||||
int index = -1;
|
||||
|
||||
for (int i = 0; i < ascendingPrices.size() - 1; i++) {
|
||||
ΩPriceΩ p = ascendingPrices.get(i);
|
||||
if (p.compareTo(searchPrice) < 0) {
|
||||
index = i - 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static int lookupIndexOfLastLessThan(List<ΩPriceΩ> ascendingPrices, ΩPriceΩ searchPrice) {
|
||||
int index = -1;
|
||||
|
||||
for (int i = 0; i < ascendingPrices.size() - 1; i++) {
|
||||
ΩPriceΩ p = ascendingPrices.get(i);
|
||||
if (p.compareTo(searchPrice) < 0) {
|
||||
index = i - 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static List<ΩPriceΩ> getSortedPositionIntervalFromValues(
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityPositions
|
||||
) {
|
||||
List<ΩPriceΩ> result = new ArrayList<>(liquidityPositions.size());
|
||||
for (RaydiumLiquidityPoolPositionConcentrated position : liquidityPositions.values()) {
|
||||
result.add(position.priceRange().from());
|
||||
}
|
||||
result.sort(ΩPriceΩ::compareTo);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<ΩPriceΩ> getSortedPositionIntervalToValues(
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityPositions
|
||||
) {
|
||||
List<ΩPriceΩ> result = new ArrayList<>(liquidityPositions.size());
|
||||
for (RaydiumLiquidityPoolPositionConcentrated position : liquidityPositions.values()) {
|
||||
result.add(position.priceRange().to());
|
||||
}
|
||||
result.sort(ΩPriceΩ::compareTo);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final MathContext MC = new MathContext(20, RoundingMode.HALF_UP);
|
||||
|
||||
private final ΩCurveWidthΩ curveWidth;
|
||||
private final Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityProviderPositions;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.fanitas.evelyn.core.impl.ref;
|
||||
|
||||
import com.fanitas.evelyn.core.*;
|
||||
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
|
||||
|
||||
import com.r35157.libs.raydium.Raydium;
|
||||
import com.r35157.libs.raydium.RaydiumLiquidityPoolPrice;
|
||||
import com.r35157.libs.solana.SPLTokenHolding;
|
||||
import com.r35157.libs.solana.SolanaBlockChain;
|
||||
import com.r35157.libs.solana.SolanaConstants;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM;
|
||||
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM;
|
||||
|
||||
public class EvelynImpl implements Evelyn {
|
||||
|
||||
public EvelynImpl(
|
||||
State state,
|
||||
DesiredPositionCalculator desiredPositionCalculator,
|
||||
Raydium raydium,
|
||||
SolanaBlockChain solanaChain
|
||||
) {
|
||||
this.state = state;
|
||||
this.desiredPositionCalculator = desiredPositionCalculator;
|
||||
this.raydium = raydium;
|
||||
this.solanaChain = solanaChain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeService() throws Exception {
|
||||
handleRebalancingProposal();
|
||||
}
|
||||
|
||||
private SPLTokenHolding getSPLHolding(ΩSolanaAddressΩ ownerAddress, ΩSPLMintAddressΩ splMintAddress) throws Exception {
|
||||
Map<ΩSPLMintAddressΩ, SPLTokenHolding> holdings = new HashMap<>();
|
||||
|
||||
holdings.putAll(solanaChain.getSPLTokenHoldings(ownerAddress, SPL_TOKEN_PROGRAM));
|
||||
holdings.putAll(solanaChain.getSPLTokenHoldings(ownerAddress, TOKEN_2022_PROGRAM));
|
||||
|
||||
SPLTokenHolding result = holdings.get(splMintAddress);
|
||||
return result;
|
||||
}
|
||||
|
||||
private MoneyAmount add(MoneyAmount a, ΩAmountΩ b) {
|
||||
MoneyAmount ma = new MoneyAmount(a.amount().add(b), a.currencyType());
|
||||
return ma;
|
||||
}
|
||||
|
||||
private MoneyAmount subtract(MoneyAmount a, MoneyAmount b) {
|
||||
if(a.currencyType() != b.currencyType()) {
|
||||
String errTxt = "Cannot subtract " + b.currencyType().name()
|
||||
+ " from " + a.currencyType().name();
|
||||
throw new IllegalArgumentException(errTxt);
|
||||
}
|
||||
|
||||
MoneyAmount ma = new MoneyAmount(a.amount().subtract(b.amount()), a.currencyType());
|
||||
return ma;
|
||||
}
|
||||
|
||||
private MoneyAmount subtract(MoneyAmount a, ΩAmountΩ b) {
|
||||
MoneyAmount ma = new MoneyAmount(a.amount().subtract(b), a.currencyType());
|
||||
return ma;
|
||||
}
|
||||
|
||||
private ΩAmountΩ subtract(ΩAmountΩ a, ΩAmountΩ b) {
|
||||
return a.subtract(b);
|
||||
}
|
||||
|
||||
private ΩAmountΩ subtract(ΩAmountΩ a, MoneyAmount b) {
|
||||
return a.subtract(b.amount());
|
||||
}
|
||||
|
||||
private void handleRebalancingProposal() {
|
||||
HashMap<ΩRaydiumLiquidityPoolPositionNftIdΩ, MoneyAmount> rebalancingProposal;
|
||||
|
||||
try {
|
||||
ΩSolanaAddressΩ solanaAddressForEvelynIOU = state.getSolanaAddressForEvelynIOU();
|
||||
ΩSPLMintAddressΩ syrupUSDCMintAddr = SolanaConstants.SPL_TOKEN_SYRUPUSDC;
|
||||
|
||||
SPLTokenHolding syrupHolding = getSPLHolding(solanaAddressForEvelynIOU, syrupUSDCMintAddr);
|
||||
ΩAmountΩ inactiveInAccountSyrup = syrupHolding.uiAmount();
|
||||
ΩPriceΩ currentPriceFromRaydium;
|
||||
MoneyAmount currentPriceFromChain;
|
||||
|
||||
while(true) {
|
||||
state.update();
|
||||
ΩmilliSecondsΩ iterationStartTime = System.currentTimeMillis();
|
||||
|
||||
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
RaydiumLiquidityPoolPrice concentratedResult;
|
||||
Future<RaydiumLiquidityPoolPrice> cFuture = executor.submit(
|
||||
() -> new RaydiumLiquidityPoolPrice(
|
||||
state.getRaydiumPoolId(),
|
||||
raydium.fetchPoolPrice(state.getRaydiumPoolId())
|
||||
)
|
||||
);
|
||||
|
||||
concentratedResult = cFuture.get();
|
||||
currentPriceFromRaydium = concentratedResult.amount().amount();
|
||||
}
|
||||
currentPriceFromChain = raydium.fetchPoolPrice(state.getRaydiumPoolId());
|
||||
|
||||
System.out.println("Iteration interval: " + (state.getIterationInterval() / 1000) + " secs");
|
||||
|
||||
System.out.println("Solana balances:");
|
||||
ΩSolanaAmountΩ solBalanceEvelyn = solanaChain.getBalanceInSolana(state.getSolanaAddressForEvelynIOU());
|
||||
System.out.println(" Evelyn: " + solBalanceEvelyn.amount());
|
||||
ΩSolanaAmountΩ solBalanceBurn = solanaChain.getBalanceInSolana(state.getSolanaAddressForEvelynIOUBurner());
|
||||
System.out.println(" Burn: " + solBalanceBurn.amount());
|
||||
System.out.println("SYRUP owned by Evelyn: " + state.getSyrupOwnedByEvelyn().amount());
|
||||
System.out.println("SYRUP inactive on Evelyn account: " + inactiveInAccountSyrup);
|
||||
|
||||
System.out.println("Pool price: " + currentPriceFromRaydium + "/" + currentPriceFromChain.amount());
|
||||
|
||||
Pair totalDistributedSums = desiredPositionCalculator.calculateTotalDistributedSums();
|
||||
ΩSolanaAmountΩ totalDistributedSumSolana = totalDistributedSums.amountA();
|
||||
ΩSyrupAmountΩ totalDistributedSumSyrup = totalDistributedSums.amountB();
|
||||
System.out.println("Total amount currently distributed: " + totalDistributedSumSolana
|
||||
+ " / " + totalDistributedSumSyrup);
|
||||
totalDistributedSum
|
||||
Pair amountsLocked = desiredPositionCalculator.calculateLockedSums(currentPriceFromChain.amount());
|
||||
ΩSolanaAmountΩ amountLockedSolana = amountsLocked.amountA();
|
||||
ΩSyrupAmountΩ amountLockedSyrup = amountsLocked.amountB();
|
||||
System.out.println("Total amount locked due to HIGH price: " + amountLockedSolana);
|
||||
System.out.println("Total amount locked due to LOW price: " + amountLockedSyrup);
|
||||
|
||||
ΩSyrupAmountΩ totalAmountSyrup = add(totalDistributedSumSyrup, inactiveInAccountSyrup);
|
||||
ΩSyrupAmountΩ reservedForBurnSyrup = subtract(totalAmountSyrup, state.getSyrupOwnedByEvelyn());
|
||||
ΩSolanaAmountΩ readyForBurnSolana = subtract(solBalanceEvelyn, SOFT_LOW_LIMIT_SOLANA_BALANCE);
|
||||
System.out.println("Amount reserved for burn: Syrup:" + reservedForBurnSyrup.amount()
|
||||
+ " + Solana: " + readyForBurnSolana.amount());
|
||||
|
||||
Pair/*ΩSyrupAmountΩ*/ syrupTotalReadyAmount = desiredPositionCalculator.calculateRedistributableSums(
|
||||
currentPriceFromChain.amount(),
|
||||
new ΩSyrupAmountΩ(inactiveInAccountSyrup, readyForBurnSolana.currencyType()), // TODO: Wow! This is not pretty! Stealing cyrrenctType from another object. Oh dear!
|
||||
reservedForBurnSyrup
|
||||
);
|
||||
System.out.println("Total amount of Syrup ready for distribution: " + syrupTotalReadyAmount.amount());
|
||||
|
||||
rebalancingProposal = desiredPositionCalculator.calculateRebalancingProposal(
|
||||
currentPriceFromChain.amount(),
|
||||
syrupTotalReadyAmount
|
||||
);
|
||||
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityPositions = state.getLiquidityPositions();
|
||||
|
||||
Map<String, Triblet> diffs = calculateDiffs(
|
||||
rebalancingProposal,
|
||||
liquidityPositions
|
||||
);
|
||||
|
||||
String minKey = diffs.entrySet().stream()
|
||||
.min(Comparator.comparing(entry -> entry.getValue().diff()))
|
||||
.map(Map.Entry::getKey)
|
||||
.orElseThrow();
|
||||
|
||||
String maxKey = diffs.entrySet().stream()
|
||||
.max(Comparator.comparing(entry -> entry.getValue().diff()))
|
||||
.map(Map.Entry::getKey)
|
||||
.orElseThrow();
|
||||
|
||||
System.out.println("Move value from (" + maxKey + "): ");
|
||||
|
||||
RaydiumLiquidityPoolPositionConcentrated maxPos = liquidityPositions.get(maxKey);
|
||||
Triblet maxDiff = diffs.get(maxKey);
|
||||
System.out.println(" " + maxPos.priceRange().from() + ".."
|
||||
+ maxPos.priceRange().to() + " - Current: "
|
||||
+ maxDiff.currentAmount() + ", Suggested: " + maxDiff.suggestedAmount()
|
||||
+ ", Diff: " + maxDiff.diff());
|
||||
|
||||
System.out.println(" --> ");
|
||||
|
||||
RaydiumLiquidityPoolPositionConcentrated minPos = liquidityPositions.get(minKey);
|
||||
Triblet minDiff = diffs.get(minKey);
|
||||
System.out.println(" " + minPos.priceRange().from() + ".."
|
||||
+ minPos.priceRange().to() + " - Current: "
|
||||
+ minDiff.currentAmount() + ", Suggested: " + minDiff.suggestedAmount()
|
||||
+ ", Diff: " + minDiff.diff());
|
||||
|
||||
ΩmilliSecondsΩ iterationExecutionTime = (System.currentTimeMillis() - iterationStartTime);
|
||||
|
||||
for(ΩmilliSecondsΩ sleepTime = state.getIterationInterval() - iterationExecutionTime; sleepTime > 0; sleepTime -= 1000) {
|
||||
System.out.print("\rSleeping for " + (sleepTime / 1000) + " secs... ");
|
||||
System.out.flush();
|
||||
Thread.sleep(1000L);
|
||||
}
|
||||
System.out.println();
|
||||
System.out.println("-------------------------------------------------------------");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, Triblet> calculateDiffs(
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, ΩSyrupAmountΩ> rebalancingProposal,
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityProviderPositionMap
|
||||
) {
|
||||
HashMap<String, Triblet> diffs = new HashMap<>(rebalancingProposal.size());
|
||||
|
||||
for(String nftId : rebalancingProposal.keySet()) {
|
||||
RaydiumLiquidityPoolPositionConcentrated pos = liquidityProviderPositionMap.get(nftId);
|
||||
ΩAmountΩ currentlyAmountAdded = pos.amountMintB().amount();
|
||||
ΩSyrupAmountΩ suggestedAmount = rebalancingProposal.get(nftId);
|
||||
ΩAmountΩ diff = subtract(currentlyAmountAdded, suggestedAmount);
|
||||
Triblet t = new Triblet(currentlyAmountAdded, suggestedAmount.amount(), diff);
|
||||
diffs.put(nftId, t);
|
||||
}
|
||||
|
||||
return diffs;
|
||||
}
|
||||
|
||||
private final ΩAmountΩ SOFT_LOW_LIMIT_SOLANA_BALANCE = new BigDecimal("0.1");
|
||||
|
||||
private final State state;
|
||||
private final DesiredPositionCalculator desiredPositionCalculator;
|
||||
private final Raydium raydium;
|
||||
private final SolanaBlockChain solanaChain;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.fanitas.evelyn.core.impl.ref;
|
||||
|
||||
import com.fanitas.evelyn.core.Evelyn;
|
||||
import com.fanitas.evelyn.core.DesiredPositionCalculator;
|
||||
import com.fanitas.evelyn.core.State;
|
||||
import com.r35157.libs.raydium.*;
|
||||
import com.r35157.libs.raydium.impl.ref.RaydiumImpl;
|
||||
import com.r35157.libs.solana.SPLTokenHolding;
|
||||
import com.r35157.libs.solana.SPLTokenSupply;
|
||||
import com.r35157.libs.solana.SolanaAccountInfo;
|
||||
import com.r35157.libs.solana.SolanaBlockChain;
|
||||
import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl;
|
||||
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
|
||||
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
||||
import com.r35157.libs.valuetypes.basic.Range;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class Main {
|
||||
|
||||
static void main() throws Exception {
|
||||
log.info("Initializing Evelyn...");
|
||||
SolanaBlockChain solanaChain = new SolanaBlockChainImpl();
|
||||
Raydium raydium = new RaydiumImpl(solanaChain);
|
||||
System.out.println(" Done.");
|
||||
|
||||
State state = new StateImpl(raydium);
|
||||
DesiredPositionCalculator desiredPositionCalculator = new DesiredPositionCalculatorImpl(
|
||||
state.getCurveWidth(),
|
||||
state.getLiquidityPositions()
|
||||
);
|
||||
|
||||
Evelyn evelyn = new EvelynImpl(
|
||||
state,
|
||||
desiredPositionCalculator,
|
||||
raydium,
|
||||
solanaChain
|
||||
);
|
||||
|
||||
evelyn.executeService();
|
||||
}
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Main.class);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package com.fanitas.evelyn.core.impl.ref;
|
||||
|
||||
import com.fanitas.evelyn.core.State;
|
||||
import com.fanitas.evelyn.raydium.PriceRange;
|
||||
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionAccounting;
|
||||
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
|
||||
import com.r35157.libs.raydium.*;
|
||||
import com.r35157.libs.solana.valuetypes.WellKnownCurrencyTypes;
|
||||
import com.r35157.libs.valuetypes.basic.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class StateImpl implements State {
|
||||
|
||||
public StateImpl(Raydium raydium) throws IOException, InterruptedException {
|
||||
this.raydium = raydium;
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ΩmilliSecondsΩ getIterationInterval() {
|
||||
return iterationInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ΩSyrupAmountΩ getSyrupOwnedByEvelyn() {
|
||||
return syrupOwnedByEvelyn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ΩSolanaAddressΩ getSolanaAddressForEvelynIOU() {
|
||||
return solanaAddressForEvelynIOU;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ΩSolanaAddressΩ getSolanaAddressForEvelynIOUBurner() {
|
||||
return solanaAddressForEvelynIOUBurner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ΩRaydiumLiquidityPoolIdΩ getRaydiumPoolId() {
|
||||
return raydiumPoolId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getCurveWidth() {
|
||||
return curveWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> getLiquidityPositions() {
|
||||
return liquidityPositions;
|
||||
}
|
||||
|
||||
public void update() throws IOException, InterruptedException {
|
||||
System.out.print("Updating from config file...");
|
||||
updateStateFromPersistence();
|
||||
System.out.println(" Done.");
|
||||
System.out.print("Updating from Raydium...");
|
||||
updateStateFromRaydium();
|
||||
System.out.println(" Done.");
|
||||
System.out.print("Updating from positions file...");
|
||||
updateStateFromPositionsFile();
|
||||
System.out.println(" Done.");
|
||||
}
|
||||
|
||||
private void updateStateFromPersistence() throws IOException {
|
||||
try (BufferedReader reader = Files.newBufferedReader(PATH_EVELYN_STATE, StandardCharsets.UTF_8)) {
|
||||
iterationInterval = readIterationInterval(reader);
|
||||
syrupOwnedByEvelyn = readSyrupOwnedByEvelyn(reader);
|
||||
solanaAddressForEvelynIOU = readSolanaAddress(reader);
|
||||
solanaAddressForEvelynIOUBurner = readSolanaAddress(reader);
|
||||
raydiumPoolId = readRaydiumPoolId(reader);
|
||||
curveWidth = readCurveWidth(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStateFromRaydium() throws IOException, InterruptedException {
|
||||
Set<ΩRaydiumLiquidityPoolPositionNftIdΩ> positionNftIds =
|
||||
raydium.fetchConcentratedPositionNftIds(solanaAddressForEvelynIOU);
|
||||
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> positions =
|
||||
new HashMap<>();
|
||||
|
||||
System.out.println("Detected " + positionNftIds.size() + " position(s)...");
|
||||
for (ΩRaydiumLiquidityPoolPositionNftIdΩ positionNftId : positionNftIds) {
|
||||
RaydiumConcentratedPositionState positionState =
|
||||
raydium.fetchConcentratedPositionState(positionNftId);
|
||||
|
||||
RaydiumConcentratedPoolInfo poolInfo =
|
||||
raydium.fetchConcentratedPoolInfo(positionState.poolId());
|
||||
|
||||
RaydiumConcentratedPoolState poolState =
|
||||
raydium.fetchConcentratedPoolState(positionState.poolId());
|
||||
|
||||
CurrencyType ctSol = WellKnownCurrencyTypes.SOLANA.getCurrencyType();
|
||||
CurrencyType ctSyrup = WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType();
|
||||
|
||||
int mintBDecimals = poolInfo.mintBDecimals();
|
||||
Range<ΩPriceΩ> raydiumPriceRange = raydium.calculateConcentratedPositionPriceRange(
|
||||
positionState,
|
||||
poolInfo,
|
||||
mintBDecimals
|
||||
);
|
||||
|
||||
PriceRange priceRange = new PriceRange(
|
||||
ctSyrup,
|
||||
raydiumPriceRange
|
||||
);
|
||||
|
||||
RaydiumLiquidityPoolTokenAmounts tokenAmounts =
|
||||
raydium.calculateConcentratedPositionTokenAmounts(
|
||||
positionState,
|
||||
poolInfo,
|
||||
poolState
|
||||
);
|
||||
|
||||
MoneyAmount solAmount = new MoneyAmount(tokenAmounts.amountA(), ctSol);
|
||||
MoneyAmount syrupAmount = new MoneyAmount(tokenAmounts.amountB(), ctSyrup);
|
||||
RaydiumLiquidityPoolPositionConcentrated position =
|
||||
new RaydiumLiquidityPoolPositionConcentrated(
|
||||
positionState.poolId(),
|
||||
positionNftId,
|
||||
priceRange,
|
||||
solAmount,
|
||||
syrupAmount,
|
||||
null // The accounting info will be added from 'conf/positions.conf' later.
|
||||
);
|
||||
|
||||
positions.put(positionNftId, position);
|
||||
System.out.println(" Added '" + position.nftId() + "': "
|
||||
+ "Range:" + position.priceRange()
|
||||
+ ", Liquidity:" + solAmount + "," + syrupAmount
|
||||
);
|
||||
}
|
||||
|
||||
liquidityPositions = Map.copyOf(positions);
|
||||
}
|
||||
|
||||
private void updateStateFromPositionsFile() throws IOException {
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> accountingEntries =
|
||||
readPositionAccounting();
|
||||
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> updatedEntries =
|
||||
new HashMap<>(liquidityPositions);
|
||||
|
||||
for (RaydiumLiquidityPoolPositionAccounting accountingEntry : accountingEntries.values()) {
|
||||
String nftId = accountingEntry.nftId();
|
||||
RaydiumLiquidityPoolPositionConcentrated position = updatedEntries.get(nftId);
|
||||
|
||||
if (position == null) {
|
||||
System.out.println(
|
||||
"WARNING: File 'conf/positions.conf' contains accounting for an NFT '" + nftId
|
||||
+ "' that was not discovered in the wallet!");
|
||||
continue;
|
||||
}
|
||||
|
||||
RaydiumLiquidityPoolPositionConcentrated updatedPosition =
|
||||
new RaydiumLiquidityPoolPositionConcentrated(
|
||||
position.poolId(),
|
||||
position.nftId(),
|
||||
position.priceRange(),
|
||||
position.amountMintA(),
|
||||
position.amountMintB(),
|
||||
accountingEntry
|
||||
);
|
||||
|
||||
updatedEntries.put(nftId, updatedPosition);
|
||||
}
|
||||
|
||||
liquidityPositions = Map.copyOf(updatedEntries);
|
||||
}
|
||||
|
||||
private Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> readPositionAccounting()
|
||||
throws IOException {
|
||||
Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionAccounting> result = new HashMap<>();
|
||||
|
||||
try (BufferedReader reader = Files.newBufferedReader(PATH_POSITIONS, StandardCharsets.UTF_8)) {
|
||||
String line;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
|
||||
if (line.isEmpty() || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove comments at end of line
|
||||
int index = line.indexOf('#');
|
||||
if(index > -1) {
|
||||
line = line.substring(0, index).trim();
|
||||
}
|
||||
|
||||
RaydiumLiquidityPoolPositionAccounting accounting = mapPositionAccounting(line);
|
||||
|
||||
RaydiumLiquidityPoolPositionAccounting previous = result.put(
|
||||
accounting.nftId(),
|
||||
accounting
|
||||
);
|
||||
|
||||
if (previous != null) {
|
||||
throw new IOException("Duplicate position accounting for NFT id: " + accounting.nftId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
private RaydiumLiquidityPoolPositionAccounting mapPositionAccounting(String line) throws IOException {
|
||||
String[] values = line.split(";");
|
||||
|
||||
if (values.length != 5) {
|
||||
String errormsg = "Expected position accounting line with 5 fields: "
|
||||
+ "'NFTId ; AddedMintA ; AddedMintB ; BorrowedMintA ; BorrowedMintB'. "
|
||||
+ "Line was: '" + line + "'";
|
||||
throw new IOException(errormsg);
|
||||
}
|
||||
|
||||
ΩRaydiumLiquidityPoolPositionNftIdΩ nftId = values[0].trim();
|
||||
ΩAmountΩ addedMintA = new BigDecimal(values[1].trim());
|
||||
ΩAmountΩ addedMintB = new BigDecimal(values[2].trim());
|
||||
ΩAmountΩ borrowedMintA = new BigDecimal(values[3].trim());
|
||||
ΩAmountΩ borrowedMintB = new BigDecimal(values[4].trim());
|
||||
|
||||
return new RaydiumLiquidityPoolPositionAccounting(
|
||||
nftId,
|
||||
addedMintA,
|
||||
addedMintB,
|
||||
borrowedMintA,
|
||||
borrowedMintB
|
||||
);
|
||||
}
|
||||
|
||||
private ΩSolanaAddressΩ readSolanaAddress(BufferedReader reader) throws IOException {
|
||||
ΩSolanaAddressΩ sa = readLineFromFile(reader);
|
||||
return sa;
|
||||
}
|
||||
|
||||
private ΩRaydiumLiquidityPoolIdΩ readRaydiumPoolId(BufferedReader reader) throws IOException {
|
||||
ΩRaydiumLiquidityPoolIdΩ rlpid = readLineFromFile(reader);
|
||||
return rlpid;
|
||||
}
|
||||
|
||||
private ΩCurveWidthΩ readCurveWidth(BufferedReader reader) throws IOException {
|
||||
String cwStr = readLineFromFile(reader);
|
||||
ΩCurveWidthΩ cw = new ΩCurveWidthΩ(cwStr);
|
||||
return cw;
|
||||
}
|
||||
|
||||
private ΩmilliSecondsΩ readIterationInterval(BufferedReader reader) throws IOException {
|
||||
String iterationIntervalStr = readLineFromFile(reader);
|
||||
ΩmilliSecondsΩ millis = Long.parseLong(iterationIntervalStr) * 1000;
|
||||
return millis;
|
||||
}
|
||||
|
||||
private ΩSyrupAmountΩ readSyrupOwnedByEvelyn(BufferedReader reader) throws IOException {
|
||||
String syrupOwnedByEvelynStr = readLineFromFile(reader);
|
||||
CurrencyType ct = WellKnownCurrencyTypes.SYRUPUSDC.getCurrencyType();
|
||||
ΩSyrupAmountΩ ma = stringToMoneyAmount(syrupOwnedByEvelynStr, ct);
|
||||
return ma;
|
||||
}
|
||||
|
||||
private static String readLineFromFile(BufferedReader reader) throws IOException {
|
||||
String line;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
|
||||
if (line.isEmpty() || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private MoneyAmount bigDecimalToMoneyAmount(BigDecimal bd, CurrencyType ct) {
|
||||
MoneyAmount ma = new MoneyAmount(bd, ct);
|
||||
return ma;
|
||||
}
|
||||
|
||||
private MoneyAmount stringToMoneyAmount(String str, CurrencyType ct) {
|
||||
ΩAmountΩ a = new ΩAmountΩ(str);
|
||||
MoneyAmount ma = new MoneyAmount(a, ct);
|
||||
return ma;
|
||||
}
|
||||
|
||||
private static final Path PATH_EVELYN_STATE = Path.of("conf/evelyn.conf");
|
||||
private static final Path PATH_POSITIONS = Path.of("conf/positions.conf");
|
||||
|
||||
private final Raydium raydium;
|
||||
|
||||
private ΩmilliSecondsΩ iterationInterval;
|
||||
private ΩSyrupAmountΩ syrupOwnedByEvelyn;
|
||||
private ΩSolanaAddressΩ solanaAddressForEvelynIOU;
|
||||
private ΩSolanaAddressΩ solanaAddressForEvelynIOUBurner;
|
||||
private ΩRaydiumLiquidityPoolIdΩ raydiumPoolId;
|
||||
private ΩCurveWidthΩ curveWidth;
|
||||
private Map<ΩRaydiumLiquidityPoolPositionNftIdΩ, RaydiumLiquidityPoolPositionConcentrated> liquidityPositions;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.fanitas.evelyn.raydium;
|
||||
|
||||
import com.r35157.libs.valuetypes.basic.CurrencyType;
|
||||
import com.r35157.libs.valuetypes.basic.Range;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record PriceRange(
|
||||
CurrencyType currencyType,
|
||||
Range<ΩPriceΩ> range
|
||||
) {
|
||||
public ΩPriceΩ from() {
|
||||
return range.from();
|
||||
}
|
||||
|
||||
public boolean fromIncluding() {
|
||||
return range.fromIncluding();
|
||||
}
|
||||
|
||||
public ΩPriceΩ to() {
|
||||
return range.to();
|
||||
}
|
||||
|
||||
public boolean toIncluding() {
|
||||
return range.toIncluding();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return range + " " + currencyType().name();
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.fanitas.evelyn.raydium;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record RaydiumLiquidityPoolPositionAccounting(
|
||||
ΩRaydiumLiquidityPoolPositionNftIdΩ nftId,
|
||||
ΩAmountΩ addedMintA,
|
||||
ΩAmountΩ addedMintB,
|
||||
ΩAmountΩ borrowedMintA,
|
||||
ΩAmountΩ borrowedMintB
|
||||
) {
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.fanitas.evelyn.raydium;
|
||||
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Represents a concentrated liquidity position in a Raydium liquidity pool.
|
||||
*
|
||||
* <p>A concentrated liquidity position is represented on Solana by a position NFT.
|
||||
* The NFT identifies the individual position, while the pool id identifies the
|
||||
* Raydium concentrated liquidity pool that the position belongs to.</p>
|
||||
*
|
||||
* <p>The position is only active within its configured price range. The two mint
|
||||
* amounts represent the token amounts associated with the position at the time
|
||||
* the position data was fetched or calculated.</p>
|
||||
*
|
||||
* @param poolId the Raydium concentrated liquidity pool id
|
||||
* @param nftId the Raydium liquidity pool position NFT id identifying this position
|
||||
* @param priceRange the price range where this concentrated liquidity position is active
|
||||
* @param accountingInfo the amount added to and borrowed from the position
|
||||
*/
|
||||
public record RaydiumLiquidityPoolPositionConcentrated(
|
||||
ΩRaydiumLiquidityPoolConcentratedIdΩ poolId,
|
||||
ΩRaydiumLiquidityPoolPositionNftIdΩ nftId,
|
||||
PriceRange priceRange,
|
||||
MoneyAmount amountMintA,
|
||||
MoneyAmount amountMintB,
|
||||
RaydiumLiquidityPoolPositionAccounting accountingInfo
|
||||
) {
|
||||
}
|
||||
Reference in New Issue
Block a user