39: Add configurable Jupiter Perps position decrease alarm action
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# Wallet and signer used for position-decrease transactions
|
||||
################################################################
|
||||
WALLET_ID {{JUPITER_PERPS_WALLET}}
|
||||
SIGNER_KEY_NAME <SIGNER>
|
||||
|
||||
|
||||
# Id Asset Direction ReceiveToken SizeDeltaUSD MaxSlippageBPS
|
||||
########################################################################
|
||||
# Values may reference {{VARIABLES}} defined in alarms.conf.
|
||||
# ReceiveToken must be USDC or match Asset.
|
||||
#3 SOL SHORT USDC 5.00 200
|
||||
#6 SOL LONG USDC 5.00 200
|
||||
@@ -92,6 +92,12 @@ public final class JupiterPerpsAlarmImpl {
|
||||
jupiterPerpsService,
|
||||
variableResolver
|
||||
));
|
||||
actions.add(new JupiterPerpsPositionDecreaseAlarmAction(
|
||||
alarmConfigurationDirectory,
|
||||
solanaBlockChain,
|
||||
jupiterPerpsService,
|
||||
variableResolver
|
||||
));
|
||||
actions.add(new PushoverAlarmAction(
|
||||
alarmConfigurationDirectory,
|
||||
variableResolver
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||
import com.r35157.cryptowallet.solana.impl.ref.SolanaWalletImpl;
|
||||
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
|
||||
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
|
||||
import com.r35157.libs.solana.SolanaBlockChain;
|
||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class JupiterPerpsPositionDecreaseAlarmAction
|
||||
implements ConfiguredAlarmAction {
|
||||
public JupiterPerpsPositionDecreaseAlarmAction(
|
||||
Path configurationDirectory,
|
||||
SolanaBlockChain solanaBlockChain,
|
||||
JupiterPerpsService jupiter,
|
||||
AlarmVariableResolver variableResolver
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(
|
||||
configurationDirectory,
|
||||
"configurationDirectory"
|
||||
);
|
||||
Objects.requireNonNull(solanaBlockChain, "solanaBlockChain");
|
||||
this.jupiter = Objects.requireNonNull(jupiter, "jupiter");
|
||||
this.variableResolver = Objects.requireNonNull(
|
||||
variableResolver,
|
||||
"variableResolver"
|
||||
);
|
||||
|
||||
configuration =
|
||||
JupiterPerpsPositionDecreaseAlarmActionConfigurationParser
|
||||
.parse(
|
||||
configurationDirectory.resolve(
|
||||
getConfigurationFileName()
|
||||
),
|
||||
variableResolver
|
||||
);
|
||||
wallet = new SolanaWalletImpl(
|
||||
configuration.walletId(),
|
||||
configuration.signerKeyName(),
|
||||
solanaBlockChain
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigurationFileName() {
|
||||
return "alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
|
||||
JupiterPerpsPositionDecreaseAlarmActionConfiguration.PositionDecrease
|
||||
definition =
|
||||
configuration.positionDecreases().get(alarm.id());
|
||||
if (definition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JupiterPerpsPositionDecreaseAlarmActionConfiguration
|
||||
.ResolvedPositionDecrease positionDecrease =
|
||||
definition.resolve(variableResolver);
|
||||
|
||||
JupiterPerpsPosition position =
|
||||
findOpenPosition(positionDecrease);
|
||||
|
||||
SolanaUnsignedTransaction utrans =
|
||||
jupiter.buildPositionDecreaseTransaction(
|
||||
position.positionAccount(),
|
||||
positionDecrease.receiveToken()
|
||||
.receiveTokenMint(),
|
||||
positionDecrease.sizeUsdDelta(),
|
||||
positionDecrease.maxSlippageBps()
|
||||
);
|
||||
SolanaSignedTransaction strans =
|
||||
wallet.signTransaction(utrans);
|
||||
ΩSolanaTransactionSignatureΩ signature =
|
||||
jupiter.executePositionDecreaseTransaction(strans);
|
||||
|
||||
System.out.println(
|
||||
"Position Decrease Signature: " + signature
|
||||
);
|
||||
} catch(Exception e) {
|
||||
System.err.println("EXCEPTION: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private JupiterPerpsPosition findOpenPosition(
|
||||
JupiterPerpsPositionDecreaseAlarmActionConfiguration
|
||||
.ResolvedPositionDecrease positionDecrease
|
||||
) throws IOException, InterruptedException {
|
||||
JupiterPerpsPosition matchingPosition = null;
|
||||
|
||||
for (JupiterPerpsPosition position
|
||||
: jupiter.getOpenPositions(wallet.getAddress())) {
|
||||
if (!position.tradedTokenMint().equals(
|
||||
positionDecrease.asset().tradedTokenMint()
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (position.direction() != positionDecrease.direction()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchingPosition != null) {
|
||||
throw new IllegalStateException(
|
||||
"Found multiple open "
|
||||
+ positionDecrease.asset()
|
||||
+ " "
|
||||
+ positionDecrease.direction()
|
||||
+ " positions for wallet "
|
||||
+ wallet.getAddress()
|
||||
);
|
||||
}
|
||||
|
||||
matchingPosition = position;
|
||||
}
|
||||
|
||||
if (matchingPosition == null) {
|
||||
throw new IllegalStateException(
|
||||
"No open "
|
||||
+ positionDecrease.asset()
|
||||
+ " "
|
||||
+ positionDecrease.direction()
|
||||
+ " position found for wallet "
|
||||
+ wallet.getAddress()
|
||||
);
|
||||
}
|
||||
|
||||
return matchingPosition;
|
||||
}
|
||||
|
||||
private final JupiterPerpsPositionDecreaseAlarmActionConfiguration
|
||||
configuration;
|
||||
private final SolanaWallet wallet;
|
||||
private final JupiterPerpsService jupiter;
|
||||
private final AlarmVariableResolver variableResolver;
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public record JupiterPerpsPositionDecreaseAlarmActionConfiguration(
|
||||
ΩSolanaWalletIdΩ walletId,
|
||||
String signerKeyName,
|
||||
Map<Integer, PositionDecrease> positionDecreases
|
||||
) {
|
||||
public JupiterPerpsPositionDecreaseAlarmActionConfiguration {
|
||||
Objects.requireNonNull(walletId, "walletId");
|
||||
Objects.requireNonNull(signerKeyName, "signerKeyName");
|
||||
positionDecreases = Map.copyOf(positionDecreases);
|
||||
|
||||
if (walletId.isBlank()) {
|
||||
throw new IllegalArgumentException("Wallet id cannot be blank");
|
||||
}
|
||||
|
||||
if (signerKeyName.isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Signer key name cannot be blank"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record PositionDecrease(
|
||||
String assetExpression,
|
||||
String directionExpression,
|
||||
String receiveTokenExpression,
|
||||
String sizeUsdDeltaExpression,
|
||||
String maxSlippageBpsExpression
|
||||
) {
|
||||
public PositionDecrease {
|
||||
requireNonBlank(assetExpression, "asset");
|
||||
requireNonBlank(directionExpression, "direction");
|
||||
requireNonBlank(receiveTokenExpression, "receive token");
|
||||
requireNonBlank(
|
||||
sizeUsdDeltaExpression,
|
||||
"position size delta"
|
||||
);
|
||||
requireNonBlank(
|
||||
maxSlippageBpsExpression,
|
||||
"maximum slippage"
|
||||
);
|
||||
}
|
||||
|
||||
public ResolvedPositionDecrease resolve(
|
||||
AlarmVariableResolver variableResolver
|
||||
) {
|
||||
Objects.requireNonNull(
|
||||
variableResolver,
|
||||
"variableResolver"
|
||||
);
|
||||
|
||||
return new ResolvedPositionDecrease(
|
||||
JupiterPerpsAsset.valueOf(
|
||||
variableResolver.resolve(assetExpression)
|
||||
.toUpperCase(Locale.ROOT)
|
||||
),
|
||||
JupiterPerpsPositionDirection.valueOf(
|
||||
variableResolver.resolve(directionExpression)
|
||||
.toUpperCase(Locale.ROOT)
|
||||
),
|
||||
JupiterPerpsReceiveToken.valueOf(
|
||||
variableResolver.resolve(receiveTokenExpression)
|
||||
.toUpperCase(Locale.ROOT)
|
||||
),
|
||||
new ΩUSDCAmountΩ(
|
||||
variableResolver.resolve(
|
||||
sizeUsdDeltaExpression
|
||||
)
|
||||
),
|
||||
Integer.parseInt(
|
||||
variableResolver.resolve(
|
||||
maxSlippageBpsExpression
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static void requireNonBlank(
|
||||
String value,
|
||||
String fieldName
|
||||
) {
|
||||
Objects.requireNonNull(value, fieldName);
|
||||
|
||||
if (value.isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
fieldName + " cannot be blank"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record ResolvedPositionDecrease(
|
||||
JupiterPerpsAsset asset,
|
||||
JupiterPerpsPositionDirection direction,
|
||||
JupiterPerpsReceiveToken receiveToken,
|
||||
ΩUSDCAmountΩ sizeUsdDelta,
|
||||
int maxSlippageBps
|
||||
) {
|
||||
public ResolvedPositionDecrease {
|
||||
Objects.requireNonNull(asset, "asset");
|
||||
Objects.requireNonNull(direction, "direction");
|
||||
Objects.requireNonNull(receiveToken, "receiveToken");
|
||||
Objects.requireNonNull(sizeUsdDelta, "sizeUsdDelta");
|
||||
|
||||
if (sizeUsdDelta.signum() <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Position size delta must be greater than zero: "
|
||||
+ sizeUsdDelta
|
||||
);
|
||||
}
|
||||
|
||||
if (maxSlippageBps < 0 || maxSlippageBps > 10_000) {
|
||||
throw new IllegalArgumentException(
|
||||
"Maximum slippage must be between 0 and "
|
||||
+ "10000 basis points: "
|
||||
+ maxSlippageBps
|
||||
);
|
||||
}
|
||||
|
||||
if (receiveToken != JupiterPerpsReceiveToken.USDC
|
||||
&& !receiveToken.receiveTokenMint().equals(
|
||||
asset.tradedTokenMint()
|
||||
)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Receive token must be USDC or match "
|
||||
+ "the position asset: "
|
||||
+ receiveToken
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class JupiterPerpsPositionDecreaseAlarmActionConfigurationParser {
|
||||
public static JupiterPerpsPositionDecreaseAlarmActionConfiguration parse(
|
||||
Path path,
|
||||
AlarmVariableResolver variableResolver
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(variableResolver, "variableResolver");
|
||||
List<String> lines = Files.readAllLines(path);
|
||||
Map<Integer, JupiterPerpsPositionDecreaseAlarmActionConfiguration.PositionDecrease>
|
||||
positionDecreases = new LinkedHashMap<>();
|
||||
ΩSolanaWalletIdΩ walletId = null;
|
||||
String signerKeyName = null;
|
||||
|
||||
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
|
||||
String line = removeComment(lines.get(lineNumber - 1)).trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
String[] columns = line.split("\\s+");
|
||||
String firstColumn = columns[0].toUpperCase(Locale.ROOT);
|
||||
|
||||
if (firstColumn.equals("WALLET_ID")) {
|
||||
requireColumnCount(columns, 2, "WALLET_ID value");
|
||||
if (walletId != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Duplicate WALLET_ID"
|
||||
);
|
||||
}
|
||||
walletId = variableResolver.resolve(columns[1]);
|
||||
} else if (firstColumn.equals("SIGNER_KEY_NAME")) {
|
||||
requireColumnCount(
|
||||
columns,
|
||||
2,
|
||||
"SIGNER_KEY_NAME value"
|
||||
);
|
||||
if (signerKeyName != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Duplicate SIGNER_KEY_NAME"
|
||||
);
|
||||
}
|
||||
signerKeyName = variableResolver.resolve(columns[1]);
|
||||
} else {
|
||||
parsePositionDecrease(
|
||||
columns,
|
||||
positionDecreases,
|
||||
variableResolver
|
||||
);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
path + ":" + lineNumber + ": "
|
||||
+ exception.getMessage(),
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (walletId == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing WALLET_ID in " + path
|
||||
);
|
||||
}
|
||||
if (signerKeyName == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing SIGNER_KEY_NAME in " + path
|
||||
);
|
||||
}
|
||||
|
||||
return new JupiterPerpsPositionDecreaseAlarmActionConfiguration(
|
||||
walletId,
|
||||
signerKeyName,
|
||||
positionDecreases
|
||||
);
|
||||
}
|
||||
|
||||
private static void parsePositionDecrease(
|
||||
String[] columns,
|
||||
Map<Integer, JupiterPerpsPositionDecreaseAlarmActionConfiguration.PositionDecrease>
|
||||
positionDecreases,
|
||||
AlarmVariableResolver variableResolver
|
||||
) {
|
||||
requireColumnCount(
|
||||
columns,
|
||||
6,
|
||||
"alarm id, asset, direction, receive token, "
|
||||
+ "size delta, and maximum slippage"
|
||||
);
|
||||
|
||||
int alarmId = Integer.parseInt(columns[0]);
|
||||
if (positionDecreases.containsKey(alarmId)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Duplicate alarm id: " + alarmId
|
||||
);
|
||||
}
|
||||
|
||||
JupiterPerpsPositionDecreaseAlarmActionConfiguration.PositionDecrease
|
||||
positionDecrease =
|
||||
new JupiterPerpsPositionDecreaseAlarmActionConfiguration.PositionDecrease(
|
||||
columns[1],
|
||||
columns[2],
|
||||
columns[3],
|
||||
columns[4],
|
||||
columns[5]
|
||||
);
|
||||
|
||||
positionDecrease.resolve(variableResolver);
|
||||
positionDecreases.put(alarmId, positionDecrease);
|
||||
}
|
||||
|
||||
private static void requireColumnCount(
|
||||
String[] columns,
|
||||
int expected,
|
||||
String expectedColumns
|
||||
) {
|
||||
if (columns.length != expected) {
|
||||
throw new IllegalArgumentException(
|
||||
"Expected " + expectedColumns + ", but found "
|
||||
+ columns.length + " column(s)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static String removeComment(String line) {
|
||||
int commentStart = line.indexOf('#');
|
||||
return commentStart < 0
|
||||
? line
|
||||
: line.substring(0, commentStart);
|
||||
}
|
||||
|
||||
private JupiterPerpsPositionDecreaseAlarmActionConfigurationParser() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
public enum JupiterPerpsReceiveToken {
|
||||
SOL(JupiterPerpsAsset.SOL.tradedTokenMint()),
|
||||
ETH(JupiterPerpsAsset.ETH.tradedTokenMint()),
|
||||
BTC(JupiterPerpsAsset.BTC.tradedTokenMint()),
|
||||
USDC("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
|
||||
|
||||
JupiterPerpsReceiveToken(
|
||||
ΩSPLMintAddressΩ receiveTokenMint
|
||||
) {
|
||||
this.receiveTokenMint = receiveTokenMint;
|
||||
}
|
||||
|
||||
public ΩSPLMintAddressΩ receiveTokenMint() {
|
||||
return receiveTokenMint;
|
||||
}
|
||||
|
||||
private final ΩSPLMintAddressΩ receiveTokenMint;
|
||||
}
|
||||
Reference in New Issue
Block a user