39: Add configurable Jupiter Perps position decrease alarm action

This commit is contained in:
2026-08-05 10:12:39 +02:00
parent 9a770e1801
commit bbba18dcae
6 changed files with 465 additions and 0 deletions
@@ -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, jupiterPerpsService,
variableResolver variableResolver
)); ));
actions.add(new JupiterPerpsPositionDecreaseAlarmAction(
alarmConfigurationDirectory,
solanaBlockChain,
jupiterPerpsService,
variableResolver
));
actions.add(new PushoverAlarmAction( actions.add(new PushoverAlarmAction(
alarmConfigurationDirectory, alarmConfigurationDirectory,
variableResolver variableResolver
@@ -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;
}
@@ -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
);
}
}
}
}
@@ -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;
}