35: Separate alarm definitions from action configuration
This commit is contained in:
+2
-56
@@ -1,7 +1,5 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -58,16 +56,10 @@ public final class AlarmConfigurationParser {
|
||||
cursor.nextToken("trigger")
|
||||
);
|
||||
|
||||
AlarmSeverity severity = AlarmSeverity.valueOf(
|
||||
cursor.nextToken("severity").toUpperCase()
|
||||
);
|
||||
|
||||
String note = cursor.nextQuotedString("note");
|
||||
|
||||
cursor.skipWhitespace();
|
||||
if (!cursor.atEnd() && cursor.current() != '#') {
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected text after note: " + cursor.remaining()
|
||||
"Unexpected text after trigger: " + cursor.remaining()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,9 +68,7 @@ public final class AlarmConfigurationParser {
|
||||
asset,
|
||||
conditionExpression,
|
||||
triggerConfiguration.trigger(),
|
||||
triggerConfiguration.gracePeriod(),
|
||||
severity,
|
||||
note
|
||||
triggerConfiguration.gracePeriod()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,50 +149,6 @@ public final class AlarmConfigurationParser {
|
||||
return line.substring(start, position);
|
||||
}
|
||||
|
||||
private String nextQuotedString(String fieldName) {
|
||||
skipWhitespace();
|
||||
if (atEnd() || current() != '"') {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing quoted " + fieldName + "; expected \"...\""
|
||||
);
|
||||
}
|
||||
position++;
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean escaped = false;
|
||||
|
||||
while (!atEnd()) {
|
||||
char character = current();
|
||||
position++;
|
||||
|
||||
if (escaped) {
|
||||
result.append(switch (character) {
|
||||
case 'n' -> '\n';
|
||||
case 'r' -> '\r';
|
||||
case 't' -> '\t';
|
||||
case '"' -> '"';
|
||||
case '\\' -> '\\';
|
||||
default -> character;
|
||||
});
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '"') {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
result.append(character);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unterminated quoted " + fieldName);
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (!atEnd() && Character.isWhitespace(current())) {
|
||||
position++;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
public interface ConfiguredAlarmAction extends AlarmAction {
|
||||
String getConfigurationFileName();
|
||||
}
|
||||
@@ -1,8 +1,32 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
public final class ConsoleAlarmAction implements AlarmAction {
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ConsoleAlarmAction implements ConfiguredAlarmAction {
|
||||
public ConsoleAlarmAction(Path configurationDirectory) throws IOException {
|
||||
Objects.requireNonNull(configurationDirectory, "configurationDirectory");
|
||||
alarmIds = parseAlarmIds(
|
||||
configurationDirectory.resolve(getConfigurationFileName())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigurationFileName() {
|
||||
return "alarmaction_Console.conf";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
|
||||
if (!alarmIds.contains(alarm.id())) {
|
||||
return;
|
||||
}
|
||||
|
||||
System.err.println();
|
||||
System.err.println("============================================================");
|
||||
System.err.printf(
|
||||
@@ -21,4 +45,45 @@ public final class ConsoleAlarmAction implements AlarmAction {
|
||||
System.err.println("============================================================");
|
||||
System.err.println();
|
||||
}
|
||||
|
||||
private static Set<Integer> parseAlarmIds(Path path) throws IOException {
|
||||
List<String> lines = Files.readAllLines(path);
|
||||
Set<Integer> result = new LinkedHashSet<>();
|
||||
|
||||
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
|
||||
String line = removeComment(lines.get(lineNumber - 1)).trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (line.chars().anyMatch(Character::isWhitespace)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Expected one alarm id, but found: " + line
|
||||
);
|
||||
}
|
||||
|
||||
int alarmId = Integer.parseInt(line);
|
||||
if (!result.add(alarmId)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Duplicate alarm id: " + alarmId
|
||||
);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
path + ":" + lineNumber + ": " + exception.getMessage(),
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
private static String removeComment(String line) {
|
||||
int commentStart = line.indexOf('#');
|
||||
return commentStart < 0 ? line : line.substring(0, commentStart);
|
||||
}
|
||||
|
||||
private final Set<Integer> alarmIds;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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.JupiterPerpsService;
|
||||
import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl;
|
||||
import com.r35157.libs.objcache.ObjectCache;
|
||||
@@ -82,26 +80,22 @@ public final class JupiterPerpsAlarmImpl {
|
||||
|
||||
entryPriceVariableRefreshWatcher.start();
|
||||
|
||||
List<AlarmAction> actions = new ArrayList<>();
|
||||
actions.add(new ConsoleAlarmAction());
|
||||
SolanaBlockChain sbc = new SolanaBlockChainImpl();
|
||||
JupiterPerpsService jupiter = new AnchorIdlJupiterPerpsServiceImpl(sbc);
|
||||
ΩSolanaWalletIdΩ walletId = "vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf";
|
||||
SolanaWallet wallet = new SolanaWalletImpl(walletId, "evelyn-prod", sbc);
|
||||
actions.add(new JupiterPerpsPositionIncreaseAlarmAction(wallet, jupiter));
|
||||
Path alarmConfigurationDirectory = config.alarmConfiguration()
|
||||
.toAbsolutePath()
|
||||
.getParent();
|
||||
|
||||
if (config.pushoverToken() != null && config.pushoverUserKey() != null) {
|
||||
actions.add(new PushoverAlarmAction(
|
||||
config.pushoverToken(),
|
||||
config.pushoverUserKey()
|
||||
));
|
||||
System.out.println("Pushover emergency alarm is enabled.");
|
||||
} else {
|
||||
System.out.println(
|
||||
"Pushover is disabled. Set PUSHOVER_APP_TOKEN and " +
|
||||
"PUSHOVER_USER_KEY to enable it."
|
||||
);
|
||||
}
|
||||
List<AlarmAction> actions = new ArrayList<>();
|
||||
actions.add(new ConsoleAlarmAction(alarmConfigurationDirectory));
|
||||
actions.add(new JupiterPerpsPositionIncreaseAlarmAction(
|
||||
alarmConfigurationDirectory,
|
||||
solanaBlockChain,
|
||||
jupiterPerpsService,
|
||||
variableResolver
|
||||
));
|
||||
actions.add(new PushoverAlarmAction(
|
||||
alarmConfigurationDirectory,
|
||||
variableResolver
|
||||
));
|
||||
|
||||
AlarmAction action = new CompositeAlarmAction(actions);
|
||||
Map<JupiterPerpsAsset, List<PriceAlarmDefinition>> definitionsByAsset =
|
||||
@@ -145,10 +139,9 @@ public final class JupiterPerpsAlarmImpl {
|
||||
asset.oracleAccount()
|
||||
);
|
||||
assetDefinitions.forEach(definition -> System.out.printf(
|
||||
" %s USD, %s, severity=%s%n",
|
||||
" %s USD, %s%n",
|
||||
definition.conditionExpression(),
|
||||
definition.trigger(),
|
||||
definition.severity()
|
||||
definition.trigger()
|
||||
));
|
||||
});
|
||||
System.out.println("RPC endpoints per asset: " + config.webSocketEndpoints().size());
|
||||
@@ -208,16 +201,12 @@ public final class JupiterPerpsAlarmImpl {
|
||||
Environment:
|
||||
PRICE_ALARMS_CONFIG Alternative default configuration path
|
||||
SOLANA_WS_URLS Comma-separated RPC WebSocket endpoints
|
||||
PUSHOVER_APP_TOKEN Pushover application token
|
||||
PUSHOVER_USER_KEY Pushover user/group key
|
||||
""");
|
||||
}
|
||||
|
||||
private record Config(
|
||||
Path alarmConfiguration,
|
||||
List<URI> webSocketEndpoints,
|
||||
String pushoverToken,
|
||||
String pushoverUserKey
|
||||
List<URI> webSocketEndpoints
|
||||
) {
|
||||
private static Config parse(String[] args, Map<String, String> environment) {
|
||||
Map<String, String> options = Arrays.stream(args)
|
||||
@@ -257,15 +246,9 @@ public final class JupiterPerpsAlarmImpl {
|
||||
|
||||
return new Config(
|
||||
Path.of(configurationText),
|
||||
endpoints,
|
||||
blankToNull(environment.get("PUSHOVER_APP_TOKEN")),
|
||||
blankToNull(environment.get("PUSHOVER_USER_KEY"))
|
||||
endpoints
|
||||
);
|
||||
}
|
||||
|
||||
private static String blankToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
private JupiterPerpsAlarmImpl() {
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
public enum JupiterPerpsAsset {
|
||||
SOL("FYq2BWQ1V5P1WFBqr3qB2Kb5yHVvSv7upzKodgQE5zXh"),
|
||||
ETH("AFZnHPzy4mvVCffrVwhewHbFc93uTHvDSFrVH7GtfXF1"),
|
||||
BTC("hUqAT1KQ7eW1i6Csp9CXYtpPfSAvi835V7wKi5fRfmC");
|
||||
SOL(
|
||||
"FYq2BWQ1V5P1WFBqr3qB2Kb5yHVvSv7upzKodgQE5zXh",
|
||||
"So11111111111111111111111111111111111111112"
|
||||
),
|
||||
ETH(
|
||||
"AFZnHPzy4mvVCffrVwhewHbFc93uTHvDSFrVH7GtfXF1",
|
||||
"7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"
|
||||
),
|
||||
BTC(
|
||||
"hUqAT1KQ7eW1i6Csp9CXYtpPfSAvi835V7wKi5fRfmC",
|
||||
"3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh"
|
||||
);
|
||||
|
||||
JupiterPerpsAsset(String oracleAccount) {
|
||||
JupiterPerpsAsset(
|
||||
String oracleAccount,
|
||||
ΩSPLMintAddressΩ tradedTokenMint
|
||||
) {
|
||||
this.oracleAccount = oracleAccount;
|
||||
this.tradedTokenMint = tradedTokenMint;
|
||||
}
|
||||
|
||||
public String oracleAccount() {
|
||||
return oracleAccount;
|
||||
}
|
||||
|
||||
public ΩSPLMintAddressΩ tradedTokenMint() {
|
||||
return tradedTokenMint;
|
||||
}
|
||||
|
||||
private final String oracleAccount;
|
||||
private final ΩSPLMintAddressΩ tradedTokenMint;
|
||||
}
|
||||
|
||||
+46
-26
@@ -1,43 +1,64 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
|
||||
import com.r35157.cryptowallet.solana.impl.ref.SolanaWalletImpl;
|
||||
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.math.BigDecimal;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class JupiterPerpsPositionIncreaseAlarmAction implements AlarmAction {
|
||||
public JupiterPerpsPositionIncreaseAlarmAction(SolanaWallet wallet, JupiterPerpsService jupiter) {
|
||||
this.wallet = wallet;
|
||||
this.jupiter = jupiter;
|
||||
public final class JupiterPerpsPositionIncreaseAlarmAction implements ConfiguredAlarmAction {
|
||||
public JupiterPerpsPositionIncreaseAlarmAction(
|
||||
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 = JupiterPerpsPositionIncreaseAlarmActionConfigurationParser.parse(
|
||||
configurationDirectory.resolve(getConfigurationFileName()),
|
||||
variableResolver
|
||||
);
|
||||
wallet = new SolanaWalletImpl(
|
||||
configuration.walletId(),
|
||||
configuration.signerKeyName(),
|
||||
solanaBlockChain
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigurationFileName() {
|
||||
return "alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
|
||||
ΩUSDCAmountΩ inputTokenAmount = new ΩUSDCAmountΩ("0.25");
|
||||
ΩUSDCAmountΩ sizeUsdDelta;
|
||||
|
||||
if(alarm.id() == 4) {
|
||||
// Critical
|
||||
sizeUsdDelta = new ΩUSDCAmountΩ("2.50"); // Leverage: 10x
|
||||
} else if(alarm.id() == 5) {
|
||||
// Info
|
||||
sizeUsdDelta = new ΩUSDCAmountΩ("6.25"); // Leverage: 25x
|
||||
} else {
|
||||
JupiterPerpsPositionIncreaseAlarmActionConfiguration.PositionIncrease definition =
|
||||
configuration.positionIncreases().get(alarm.id());
|
||||
if (definition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JupiterPerpsPositionIncreaseAlarmActionConfiguration.ResolvedPositionIncrease
|
||||
positionIncrease = definition.resolve(variableResolver);
|
||||
|
||||
SolanaUnsignedTransaction utrans =
|
||||
jupiter.buildPositionIncreaseTransaction(
|
||||
wallet.getAddress(),
|
||||
SOL_MINT,
|
||||
JupiterPerpsPositionDirection.LONG,
|
||||
inputTokenAmount,
|
||||
sizeUsdDelta,
|
||||
200
|
||||
positionIncrease.asset().tradedTokenMint(),
|
||||
positionIncrease.direction(),
|
||||
positionIncrease.collateralUsd(),
|
||||
positionIncrease.sizeUsdDelta(),
|
||||
positionIncrease.maxSlippageBps()
|
||||
);
|
||||
SolanaSignedTransaction strans = wallet.signTransaction(utrans);
|
||||
ΩSolanaTransactionSignatureΩ signature = jupiter.executePositionIncreaseTransaction(strans);
|
||||
@@ -47,9 +68,8 @@ public final class JupiterPerpsPositionIncreaseAlarmAction implements AlarmActio
|
||||
}
|
||||
}
|
||||
|
||||
private static final ΩSPLMintAddressΩ SOL_MINT = "So11111111111111111111111111111111111111112";
|
||||
private static final ΩSPLMintAddressΩ BTC_MINT = "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh";
|
||||
|
||||
private SolanaWallet wallet;
|
||||
private JupiterPerpsService jupiter;
|
||||
private final JupiterPerpsPositionIncreaseAlarmActionConfiguration configuration;
|
||||
private final SolanaWallet wallet;
|
||||
private final JupiterPerpsService jupiter;
|
||||
private final AlarmVariableResolver variableResolver;
|
||||
}
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
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 JupiterPerpsPositionIncreaseAlarmActionConfiguration(
|
||||
ΩSolanaWalletIdΩ walletId,
|
||||
String signerKeyName,
|
||||
Map<Integer, PositionIncrease> positionIncreases
|
||||
) {
|
||||
public JupiterPerpsPositionIncreaseAlarmActionConfiguration {
|
||||
Objects.requireNonNull(walletId, "walletId");
|
||||
Objects.requireNonNull(signerKeyName, "signerKeyName");
|
||||
positionIncreases = Map.copyOf(positionIncreases);
|
||||
|
||||
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 PositionIncrease(
|
||||
String assetExpression,
|
||||
String directionExpression,
|
||||
String collateralUsdExpression,
|
||||
String sizeUsdDeltaExpression,
|
||||
String maxSlippageBpsExpression
|
||||
) {
|
||||
public PositionIncrease {
|
||||
requireNonBlank(assetExpression, "asset");
|
||||
requireNonBlank(directionExpression, "direction");
|
||||
requireNonBlank(collateralUsdExpression, "collateral");
|
||||
requireNonBlank(sizeUsdDeltaExpression, "position size delta");
|
||||
requireNonBlank(maxSlippageBpsExpression, "maximum slippage");
|
||||
}
|
||||
|
||||
public ResolvedPositionIncrease resolve(AlarmVariableResolver variableResolver) {
|
||||
Objects.requireNonNull(variableResolver, "variableResolver");
|
||||
|
||||
return new ResolvedPositionIncrease(
|
||||
JupiterPerpsAsset.valueOf(
|
||||
variableResolver.resolve(assetExpression).toUpperCase(Locale.ROOT)
|
||||
),
|
||||
JupiterPerpsPositionDirection.valueOf(
|
||||
variableResolver.resolve(directionExpression)
|
||||
.toUpperCase(Locale.ROOT)
|
||||
),
|
||||
new ΩUSDCAmountΩ(variableResolver.resolve(collateralUsdExpression)),
|
||||
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 ResolvedPositionIncrease(
|
||||
JupiterPerpsAsset asset,
|
||||
JupiterPerpsPositionDirection direction,
|
||||
ΩUSDCAmountΩ collateralUsd,
|
||||
ΩUSDCAmountΩ sizeUsdDelta,
|
||||
int maxSlippageBps
|
||||
) {
|
||||
public ResolvedPositionIncrease {
|
||||
Objects.requireNonNull(asset, "asset");
|
||||
Objects.requireNonNull(direction, "direction");
|
||||
Objects.requireNonNull(collateralUsd, "collateralUsd");
|
||||
Objects.requireNonNull(sizeUsdDelta, "sizeUsdDelta");
|
||||
|
||||
if (collateralUsd.signum() <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Collateral must be greater than zero: " + collateralUsd
|
||||
);
|
||||
}
|
||||
if (sizeUsdDelta.signum() < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Position size delta cannot be negative: " + sizeUsdDelta
|
||||
);
|
||||
}
|
||||
if (maxSlippageBps < 0 || maxSlippageBps > 10_000) {
|
||||
throw new IllegalArgumentException(
|
||||
"Maximum slippage must be between 0 and 10000 basis points: "
|
||||
+ maxSlippageBps
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
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 JupiterPerpsPositionIncreaseAlarmActionConfigurationParser {
|
||||
public static JupiterPerpsPositionIncreaseAlarmActionConfiguration parse(
|
||||
Path path,
|
||||
AlarmVariableResolver variableResolver
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(variableResolver, "variableResolver");
|
||||
List<String> lines = Files.readAllLines(path);
|
||||
Map<Integer, JupiterPerpsPositionIncreaseAlarmActionConfiguration.PositionIncrease>
|
||||
positionIncreases = 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 {
|
||||
parsePositionIncrease(
|
||||
columns,
|
||||
positionIncreases,
|
||||
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 JupiterPerpsPositionIncreaseAlarmActionConfiguration(
|
||||
walletId,
|
||||
signerKeyName,
|
||||
positionIncreases
|
||||
);
|
||||
}
|
||||
|
||||
private static void parsePositionIncrease(
|
||||
String[] columns,
|
||||
Map<Integer, JupiterPerpsPositionIncreaseAlarmActionConfiguration.PositionIncrease>
|
||||
positionIncreases,
|
||||
AlarmVariableResolver variableResolver
|
||||
) {
|
||||
requireColumnCount(
|
||||
columns,
|
||||
6,
|
||||
"alarm id, asset, direction, collateral, size delta, and maximum slippage"
|
||||
);
|
||||
|
||||
int alarmId = Integer.parseInt(columns[0]);
|
||||
if (positionIncreases.containsKey(alarmId)) {
|
||||
throw new IllegalArgumentException("Duplicate alarm id: " + alarmId);
|
||||
}
|
||||
|
||||
JupiterPerpsPositionIncreaseAlarmActionConfiguration.PositionIncrease positionIncrease =
|
||||
new JupiterPerpsPositionIncreaseAlarmActionConfiguration.PositionIncrease(
|
||||
columns[1],
|
||||
columns[2],
|
||||
columns[3],
|
||||
columns[4],
|
||||
columns[5]
|
||||
);
|
||||
|
||||
positionIncrease.resolve(variableResolver);
|
||||
positionIncreases.put(alarmId, positionIncrease);
|
||||
}
|
||||
|
||||
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 JupiterPerpsPositionIncreaseAlarmActionConfigurationParser() {
|
||||
}
|
||||
}
|
||||
@@ -95,27 +95,12 @@ public final class PriceAlarm {
|
||||
triggerCount++;
|
||||
lastTriggeredAt = Instant.now();
|
||||
|
||||
String note;
|
||||
|
||||
try {
|
||||
note = variableResolver.resolve(definition.note());
|
||||
} catch (RuntimeException exception) {
|
||||
System.err.printf(
|
||||
"Could not resolve note for alarm %d: %s%n",
|
||||
definition.id(),
|
||||
exception.getMessage()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ResolvedPriceAlarm resolvedAlarm = new ResolvedPriceAlarm(
|
||||
definition.id(),
|
||||
definition.asset(),
|
||||
condition,
|
||||
definition.trigger(),
|
||||
definition.triggerGracePeriod(),
|
||||
definition.severity(),
|
||||
note
|
||||
definition.triggerGracePeriod()
|
||||
);
|
||||
|
||||
action.trigger(price, resolvedAlarm);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record PriceAlarmDefinition(
|
||||
@@ -9,16 +7,12 @@ public record PriceAlarmDefinition(
|
||||
JupiterPerpsAsset asset,
|
||||
String conditionExpression,
|
||||
AlarmTrigger trigger,
|
||||
ΩsecondsΩ triggerGracePeriod,
|
||||
AlarmSeverity severity,
|
||||
String note
|
||||
ΩsecondsΩ triggerGracePeriod
|
||||
) {
|
||||
public PriceAlarmDefinition {
|
||||
Objects.requireNonNull(asset, "asset");
|
||||
Objects.requireNonNull(conditionExpression, "conditionExpression");
|
||||
Objects.requireNonNull(trigger, "trigger");
|
||||
Objects.requireNonNull(severity, "severity");
|
||||
Objects.requireNonNull(note, "note");
|
||||
|
||||
if (conditionExpression.isBlank()) {
|
||||
throw new IllegalArgumentException("Condition expression cannot be blank");
|
||||
|
||||
@@ -2,31 +2,57 @@ package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class PushoverAlarmAction implements AlarmAction {
|
||||
public final class PushoverAlarmAction implements ConfiguredAlarmAction {
|
||||
|
||||
public PushoverAlarmAction(String applicationToken, String userKey) {
|
||||
this.applicationToken = applicationToken;
|
||||
this.userKey = userKey;
|
||||
public PushoverAlarmAction(
|
||||
Path configurationDirectory,
|
||||
AlarmVariableResolver variableResolver
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(configurationDirectory, "configurationDirectory");
|
||||
this.variableResolver = Objects.requireNonNull(variableResolver, "variableResolver");
|
||||
configuration = PushoverAlarmActionConfigurationParser.parse(
|
||||
configurationDirectory.resolve(getConfigurationFileName()),
|
||||
variableResolver
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigurationFileName() {
|
||||
return "alarmaction_Pushover.conf";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
|
||||
String title = "Jupiter Perps " + price.asset() + " alarm";
|
||||
String message = createMessage(price, alarm);
|
||||
PushoverAlarmActionConfiguration.Notification notification =
|
||||
configuration.notifications().get(alarm.id());
|
||||
if (notification == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String body = form("token", applicationToken) + "&" +
|
||||
form("user", userKey) + "&" +
|
||||
String title = "Jupiter Perps " + price.asset() + " alarm";
|
||||
String message = createMessage(
|
||||
price,
|
||||
alarm,
|
||||
notification.severity(),
|
||||
variableResolver.resolve(notification.note())
|
||||
);
|
||||
|
||||
String body = form("token", configuration.applicationToken()) + "&" +
|
||||
form("user", configuration.userKey()) + "&" +
|
||||
form("title", title) + "&" +
|
||||
form("message", message) + "&" +
|
||||
createPushoverSeverityParameters(alarm.severity());
|
||||
createPushoverSeverityParameters(notification.severity());
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI)
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
@@ -48,7 +74,7 @@ public final class PushoverAlarmAction implements AlarmAction {
|
||||
response.body()
|
||||
);
|
||||
} else {
|
||||
System.out.println("Pushover alarm sent: " + alarm.severity());
|
||||
System.out.println("Pushover alarm sent: " + notification.severity());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -69,12 +95,17 @@ public final class PushoverAlarmAction implements AlarmAction {
|
||||
};
|
||||
}
|
||||
|
||||
private static String createMessage(OraclePrice price, ResolvedPriceAlarm alarm) {
|
||||
private static String createMessage(
|
||||
OraclePrice price,
|
||||
ResolvedPriceAlarm alarm,
|
||||
AlarmSeverity severity,
|
||||
String note
|
||||
) {
|
||||
return String.format(
|
||||
"%d - %s: %s%n%n%s is %s USD.%nCondition: %s USD.%nOracle time: %s.%nSlot: %d.",
|
||||
alarm.id(),
|
||||
alarm.severity(),
|
||||
alarm.note(),
|
||||
severity,
|
||||
note,
|
||||
price.asset(),
|
||||
price.priceUsd().toPlainString(),
|
||||
alarm.condition().expression(),
|
||||
@@ -84,8 +115,8 @@ public final class PushoverAlarmAction implements AlarmAction {
|
||||
}
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
private final String applicationToken;
|
||||
private final String userKey;
|
||||
private final PushoverAlarmActionConfiguration configuration;
|
||||
private final AlarmVariableResolver variableResolver;
|
||||
|
||||
private static final URI PUSHOVER_URI =
|
||||
URI.create("https://api.pushover.net/1/messages.json");
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public record PushoverAlarmActionConfiguration(
|
||||
String applicationToken,
|
||||
String userKey,
|
||||
Map<Integer, Notification> notifications
|
||||
) {
|
||||
public PushoverAlarmActionConfiguration {
|
||||
Objects.requireNonNull(applicationToken, "applicationToken");
|
||||
Objects.requireNonNull(userKey, "userKey");
|
||||
notifications = Map.copyOf(notifications);
|
||||
|
||||
if (applicationToken.isBlank()) {
|
||||
throw new IllegalArgumentException("Application token cannot be blank");
|
||||
}
|
||||
if (userKey.isBlank()) {
|
||||
throw new IllegalArgumentException("User key cannot be blank");
|
||||
}
|
||||
}
|
||||
|
||||
public record Notification(
|
||||
AlarmSeverity severity,
|
||||
String note
|
||||
) {
|
||||
public Notification {
|
||||
Objects.requireNonNull(severity, "severity");
|
||||
Objects.requireNonNull(note, "note");
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
||||
|
||||
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 PushoverAlarmActionConfigurationParser {
|
||||
public static PushoverAlarmActionConfiguration parse(
|
||||
Path path,
|
||||
AlarmVariableResolver variableResolver
|
||||
) throws IOException {
|
||||
Objects.requireNonNull(variableResolver, "variableResolver");
|
||||
List<String> lines = Files.readAllLines(path);
|
||||
Map<Integer, PushoverAlarmActionConfiguration.Notification> notifications =
|
||||
new LinkedHashMap<>();
|
||||
String applicationToken = null;
|
||||
String userKey = null;
|
||||
|
||||
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
|
||||
String line = lines.get(lineNumber - 1);
|
||||
String trimmed = line.trim();
|
||||
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Cursor cursor = new Cursor(line);
|
||||
String firstToken = cursor.nextToken("setting or alarm id");
|
||||
|
||||
switch (firstToken.toUpperCase(Locale.ROOT)) {
|
||||
case "APPLICATION_TOKEN" -> {
|
||||
if (applicationToken != null) {
|
||||
throw new IllegalArgumentException("Duplicate APPLICATION_TOKEN");
|
||||
}
|
||||
applicationToken = variableResolver.resolve(
|
||||
cursor.nextToken("application token")
|
||||
);
|
||||
cursor.requireEndOrComment();
|
||||
}
|
||||
case "USER_KEY" -> {
|
||||
if (userKey != null) {
|
||||
throw new IllegalArgumentException("Duplicate USER_KEY");
|
||||
}
|
||||
userKey = variableResolver.resolve(
|
||||
cursor.nextToken("user key")
|
||||
);
|
||||
cursor.requireEndOrComment();
|
||||
}
|
||||
default -> parseNotification(
|
||||
firstToken,
|
||||
cursor,
|
||||
notifications,
|
||||
variableResolver
|
||||
);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
path + ":" + lineNumber + ": " + exception.getMessage(),
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (applicationToken == null) {
|
||||
throw new IllegalArgumentException("Missing APPLICATION_TOKEN in " + path);
|
||||
}
|
||||
if (userKey == null) {
|
||||
throw new IllegalArgumentException("Missing USER_KEY in " + path);
|
||||
}
|
||||
|
||||
return new PushoverAlarmActionConfiguration(
|
||||
applicationToken,
|
||||
userKey,
|
||||
notifications
|
||||
);
|
||||
}
|
||||
|
||||
private static void parseNotification(
|
||||
String alarmIdText,
|
||||
Cursor cursor,
|
||||
Map<Integer, PushoverAlarmActionConfiguration.Notification> notifications,
|
||||
AlarmVariableResolver variableResolver
|
||||
) {
|
||||
int alarmId;
|
||||
try {
|
||||
alarmId = Integer.parseInt(alarmIdText);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
"Expected APPLICATION_TOKEN, USER_KEY, or alarm id: " + alarmIdText,
|
||||
exception
|
||||
);
|
||||
}
|
||||
|
||||
AlarmSeverity severity = AlarmSeverity.valueOf(
|
||||
variableResolver.resolve(cursor.nextToken("severity"))
|
||||
.toUpperCase(Locale.ROOT)
|
||||
);
|
||||
String note = cursor.nextQuotedString("note");
|
||||
variableResolver.resolve(note);
|
||||
cursor.requireEndOrComment();
|
||||
|
||||
PushoverAlarmActionConfiguration.Notification previous = notifications.putIfAbsent(
|
||||
alarmId,
|
||||
new PushoverAlarmActionConfiguration.Notification(severity, note)
|
||||
);
|
||||
if (previous != null) {
|
||||
throw new IllegalArgumentException("Duplicate alarm id: " + alarmId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Cursor {
|
||||
private Cursor(String line) {
|
||||
this.line = line;
|
||||
}
|
||||
|
||||
private String nextToken(String fieldName) {
|
||||
skipWhitespace();
|
||||
if (atEnd()) {
|
||||
throw new IllegalArgumentException("Missing " + fieldName);
|
||||
}
|
||||
|
||||
int start = position;
|
||||
while (!atEnd() && !Character.isWhitespace(current())) {
|
||||
position++;
|
||||
}
|
||||
return line.substring(start, position);
|
||||
}
|
||||
|
||||
private String nextQuotedString(String fieldName) {
|
||||
skipWhitespace();
|
||||
if (atEnd() || current() != '"') {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing quoted " + fieldName + "; expected \"...\""
|
||||
);
|
||||
}
|
||||
position++;
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean escaped = false;
|
||||
|
||||
while (!atEnd()) {
|
||||
char character = current();
|
||||
position++;
|
||||
|
||||
if (escaped) {
|
||||
result.append(switch (character) {
|
||||
case 'n' -> '\n';
|
||||
case 'r' -> '\r';
|
||||
case 't' -> '\t';
|
||||
case '"' -> '"';
|
||||
case '\\' -> '\\';
|
||||
default -> character;
|
||||
});
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '"') {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
result.append(character);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unterminated quoted " + fieldName);
|
||||
}
|
||||
|
||||
private void requireEndOrComment() {
|
||||
skipWhitespace();
|
||||
if (!atEnd() && current() != '#') {
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected text: " + line.substring(position)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (!atEnd() && Character.isWhitespace(current())) {
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean atEnd() {
|
||||
return position >= line.length();
|
||||
}
|
||||
|
||||
private char current() {
|
||||
return line.charAt(position);
|
||||
}
|
||||
|
||||
private final String line;
|
||||
private int position;
|
||||
}
|
||||
|
||||
private PushoverAlarmActionConfigurationParser() {
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
||||
|
||||
public record ResolvedPriceAlarm(
|
||||
int id,
|
||||
JupiterPerpsAsset asset,
|
||||
PriceCondition condition,
|
||||
AlarmTrigger trigger,
|
||||
ΩsecondsΩ triggerGracePeriod,
|
||||
AlarmSeverity severity,
|
||||
String note
|
||||
ΩsecondsΩ triggerGracePeriod
|
||||
) {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user