From bbba18dcaec9560a2f0537126b899816d3449679 Mon Sep 17 00:00:00 2001 From: Minimons Date: Sat, 25 Jul 2026 09:15:47 +0200 Subject: [PATCH] 39: Add configurable Jupiter Perps position decrease alarm action --- ...psPositionDecreaseAlarmAction.conf.example | 12 ++ .../impl/ref/JupiterPerpsAlarmImpl.tjava | 6 + ...iterPerpsPositionDecreaseAlarmAction.tjava | 143 +++++++++++++++++ ...tionDecreaseAlarmActionConfiguration.tjava | 140 +++++++++++++++++ ...creaseAlarmActionConfigurationParser.tjava | 144 ++++++++++++++++++ .../impl/ref/JupiterPerpsReceiveToken.tjava | 20 +++ 6 files changed, 465 insertions(+) create mode 100644 conf/alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmAction.tjava create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmActionConfiguration.tjava create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmActionConfigurationParser.tjava create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsReceiveToken.tjava diff --git a/conf/alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example b/conf/alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example new file mode 100644 index 0000000..7dca901 --- /dev/null +++ b/conf/alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example @@ -0,0 +1,12 @@ +# Wallet and signer used for position-decrease transactions +################################################################ +WALLET_ID {{JUPITER_PERPS_WALLET}} +SIGNER_KEY_NAME + + +# 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 \ No newline at end of file diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava index 0a69509..f881508 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava @@ -92,6 +92,12 @@ public final class JupiterPerpsAlarmImpl { jupiterPerpsService, variableResolver )); + actions.add(new JupiterPerpsPositionDecreaseAlarmAction( + alarmConfigurationDirectory, + solanaBlockChain, + jupiterPerpsService, + variableResolver + )); actions.add(new PushoverAlarmAction( alarmConfigurationDirectory, variableResolver diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmAction.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmAction.tjava new file mode 100644 index 0000000..1b68979 --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmAction.tjava @@ -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; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmActionConfiguration.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmActionConfiguration.tjava new file mode 100644 index 0000000..1e4ac57 --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmActionConfiguration.tjava @@ -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 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 + ); + } + } + } +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmActionConfigurationParser.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmActionConfigurationParser.tjava new file mode 100644 index 0000000..e58cc6a --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsPositionDecreaseAlarmActionConfigurationParser.tjava @@ -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 lines = Files.readAllLines(path); + Map + 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 + 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() { + } +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsReceiveToken.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsReceiveToken.tjava new file mode 100644 index 0000000..80524e5 --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsReceiveToken.tjava @@ -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; +} \ No newline at end of file