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