From 8f8677378619733dccb50dc17e0e27cfbee43224 Mon Sep 17 00:00:00 2001 From: Minimons Date: Sat, 27 Jun 2026 11:01:28 +0200 Subject: [PATCH] 20: Add dynamic Jupiter Perps entry price variables to alarm config --- conf/alarms.conf.example | 64 +++++++++++-- .../impl/ref/AlarmAction.tjava | 2 +- .../impl/ref/AlarmConfiguration.tjava | 19 ++++ .../impl/ref/AlarmConfigurationParser.tjava | 79 ++++------------ .../impl/ref/AlarmTargetParser.tjava | 60 ++++++++++++ .../impl/ref/AlarmVariableResolver.tjava | 30 ++++++ .../impl/ref/AssetPriceAlarmMonitor.tjava | 3 +- .../impl/ref/CompositeAlarmAction.tjava | 2 +- .../impl/ref/ConsoleAlarmAction.tjava | 2 +- .../impl/ref/JupiterPerpsAlarmImpl.tjava | 34 ++++++- ...erpsEntryPriceVariableRefreshWatcher.tjava | 92 +++++++++++++++++++ ...iterPerpsEntryPriceVariableRefresher.tjava | 86 +++++++++++++++++ .../impl/ref/PriceAlarm.tjava | 58 ++++++++++-- .../impl/ref/PriceAlarmDefinition.tjava | 10 +- .../impl/ref/PushoverAlarmAction.tjava | 6 +- .../impl/ref/ResolvedPriceAlarm.tjava | 16 ++++ 16 files changed, 474 insertions(+), 89 deletions(-) create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmConfiguration.tjava create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmTargetParser.tjava create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmVariableResolver.tjava create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsEntryPriceVariableRefreshWatcher.tjava create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsEntryPriceVariableRefresher.tjava create mode 100644 src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/ResolvedPriceAlarm.tjava diff --git a/conf/alarms.conf.example b/conf/alarms.conf.example index 7060a3d..ebe210a 100644 --- a/conf/alarms.conf.example +++ b/conf/alarms.conf.example @@ -1,8 +1,56 @@ -# Id Asset Direction Target Trigger Severity Note -###################################################################################################################### -1 SOL ABOVE 97.03-1.25% ONETIME CRITICAL "CRITICAL: Risiko for Perps Solana short LIKVIDERING!" -2 SOL BELOW 48.72+1.25% ONETIME CRITICAL "CRITICAL: Risiko for Perps Solana long LIKVIDERING!" -3 BTC ABOVE 85032.87-1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Bitcoin short LIKVIDERING!" -4 BTC BELOW 42779.40+1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Bitcoin long LIKVIDERING!" -5 ETH ABOVE 2296.13-1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Ethereum short LIKVIDERING!" -6 ETH BELOW 1155.19+1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Ethereum long LIKVIDERING!" +# General configurations +##################################################### +{{JUPITER_PERPS_WALLET}} 8abcYourWalletAddressHere + + +# Constant Name Value +#################################### + +{{SOL_LONG_ENTRY_PRICE}} 73.67 +{{SOL_LONG_LIQ_PRICE}} 70.28 +{{SOL_SHORT_ENTRY_PRICE}} 70.47 +{{SOL_SHORT_LIQ_PRICE}} 75.01 + +{{ETH_LONG_ENTRY_PRICE}} 1589.63 +{{ETH_LONG_LIQ_PRICE}} 1461.11 +{{ETH_SHORT_ENTRY_PRICE}} 1545.29 +{{ETH_SHORT_LIQ_PRICE}} 1698.42 + +{{BTC_LONG_ENTRY_PRICE}} 61236.03 +{{BTC_LONG_LIQ_PRICE}} 40984.70 +{{BTC_SHORT_ENTRY_PRICE}} 59451.94 +{{BTC_SHORT_LIQ_PRICE}} 65084.53 + + +# Id Asset Direction Target Trigger Severity Note +############################################################################################################################### + +# SOL SHORT +1 SOL ABOVE {{SOL_SHORT_LIQ_PRICE}}-1.25% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko SOL Short!" +2 SOL ABOVE {{SOL_SHORT_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "🌱 Short SOL now" +3 SOL BELOW {{SOL_SHORT_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "💵 Close SOL Short" + +# SOL LONG +4 SOL BELOW {{SOL_LONG_LIQ_PRICE}}+1.25% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko SOL Long!" +5 SOL BELOW {{SOL_LONG_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "🌱 Long SOL now" +6 SOL ABOVE {{SOL_LONG_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "💵 Close SOL Long" + +# ETH SHORT +7 ETH ABOVE {{ETH_SHORT_LIQ_PRICE}}-1% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko ETH Short!" +8 ETH ABOVE {{ETH_SHORT_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "🌱 Short ETH now" +9 ETH BELOW {{ETH_SHORT_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "💵 Close ETH Short" + +# ETH LONG +10 ETH BELOW {{ETH_LONG_LIQ_PRICE}}+1% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko ETH Long!" +11 ETH BELOW {{ETH_LONG_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "🌱 Long ETH now" +12 ETH ABOVE {{ETH_LONG_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "💵 Close ETH Long" + +# BTC SHORT +13 BTC ABOVE {{BTC_SHORT_LIQ_PRICE}}-1% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko BTC Short!" +14 BTC ABOVE {{BTC_SHORT_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "🌱 Short BTC now" +15 BTC BELOW {{BTC_SHORT_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "💵 Close BTC Short" + +# BTC LONG +16 BTC BELOW {{BTC_LONG_LIQ_PRICE}}+1% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko BTC Long!" +17 BTC BELOW {{BTC_LONG_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "🌱 Long BTC now" +18 BTC ABOVE {{BTC_LONG_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "💵 Close BTC Long" diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmAction.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmAction.tjava index bdbf3db..b095b71 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmAction.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmAction.tjava @@ -2,5 +2,5 @@ package com.r35157.jupiterperpsalarm.impl.ref; @FunctionalInterface public interface AlarmAction { - void trigger(OraclePrice price, PriceAlarmDefinition alarm); + void trigger(OraclePrice price, ResolvedPriceAlarm alarm); } diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmConfiguration.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmConfiguration.tjava new file mode 100644 index 0000000..3850a35 --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmConfiguration.tjava @@ -0,0 +1,19 @@ +package com.r35157.jupiterperpsalarm.impl.ref; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public record AlarmConfiguration( + List definitions, + Map variables +) { + public AlarmConfiguration { + Objects.requireNonNull(definitions, "definitions"); + Objects.requireNonNull(variables, "variables"); + + definitions = List.copyOf(definitions); + variables = new ConcurrentHashMap<>(variables); + } +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmConfigurationParser.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmConfigurationParser.tjava index 0075032..37ffda1 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmConfigurationParser.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmConfigurationParser.tjava @@ -3,14 +3,13 @@ package com.r35157.jupiterperpsalarm.impl.ref; import com.r35157.jupiterperpsalarm.AlarmSeverity; import java.io.IOException; -import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; public final class AlarmConfigurationParser { - public static List parse(Path path) throws IOException { + public static AlarmConfiguration parse(Path path) throws IOException { List lines = Files.readAllLines(path); List alarms = new ArrayList<>(); Map constants = new LinkedHashMap<>(); @@ -27,8 +26,7 @@ public final class AlarmConfigurationParser { if(isConstantDefinition(trimmed)) { parseConstantDefinition(constants, line); } else { - String resolvedLine = replaceConstants(line, constants); - alarms.add(parseLine(resolvedLine)); + alarms.add(parseLine(line)); } } catch (RuntimeException exception) { throw new IllegalArgumentException( @@ -42,7 +40,7 @@ public final class AlarmConfigurationParser { throw new IllegalArgumentException("No alarms found in " + path); } - return List.copyOf(alarms); + return new AlarmConfiguration(alarms, constants); } static PriceAlarmDefinition parseLine(String line) { @@ -58,7 +56,7 @@ public final class AlarmConfigurationParser { cursor.nextToken("direction").toUpperCase(Locale.ROOT) ); - BigDecimal target = parseTarget(cursor.nextToken("target")); + String targetExpression = cursor.nextToken("target"); TriggerConfiguration triggerConfiguration = parseTrigger( cursor.nextToken("trigger") @@ -81,7 +79,7 @@ public final class AlarmConfigurationParser { id, asset, direction, - target, + targetExpression, triggerConfiguration.trigger(), triggerConfiguration.gracePeriod(), severity, @@ -89,28 +87,6 @@ public final class AlarmConfigurationParser { ); } - private static BigDecimal parseTargetPercentage( - String targetStr, - int operatorIndex, - boolean add - ) { - BigDecimal base = new BigDecimal(targetStr.substring(0, operatorIndex)); - String percentStr = targetStr.substring(operatorIndex + 1, targetStr.length() - 1); - BigDecimal percent = new BigDecimal(percentStr); - - BigDecimal delta = base - .multiply(percent) - .divide(BigDecimal.valueOf(100)); - - BigDecimal target = add ? base.add(delta) : base.subtract(delta); - - validateTarget(base, targetStr); - validateTarget(percent, targetStr); - validateTarget(target, targetStr); - - return target; - } - private static TriggerConfiguration parseTrigger(String triggerText) { String normalized = triggerText.toUpperCase(Locale.ROOT); @@ -155,35 +131,6 @@ public final class AlarmConfigurationParser { throw new IllegalArgumentException("Unknown trigger: " + triggerText); } - private static void validateTarget(BigDecimal target, String originalTargetStr) { - if (target.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException( - "Target must be zero or positive: " + originalTargetStr - ); - } - } - - private static BigDecimal parseTarget(String targetStr) { - String trimmedTargetStr = targetStr.trim(); - - if (trimmedTargetStr.endsWith("%")) { - int plusIndex = trimmedTargetStr.indexOf('+'); - int minusIndex = trimmedTargetStr.indexOf('-', 1); - - if (plusIndex >= 0) { - return parseTargetPercentage(trimmedTargetStr, plusIndex, true); - } - - if (minusIndex >= 0) { - return parseTargetPercentage(trimmedTargetStr, minusIndex, false); - } - } - - BigDecimal target = new BigDecimal(trimmedTargetStr); - validateTarget(target, targetStr); - return target; - } - private static final class Cursor { private Cursor(String line) { this.line = line; @@ -290,9 +237,21 @@ public final class AlarmConfigurationParser { validateConstantName(name); - if (constants.putIfAbsent(name, value) != null) { - throw new IllegalArgumentException("Duplicate constant: " + name); + constants.put(parseVariableName(name), value); + } + + private static String parseVariableName(String variableName) { + if (!variableName.startsWith("{{") || !variableName.endsWith("}}")) { + throw new IllegalArgumentException("Invalid variable name: " + variableName); } + + String parsedVariableName = variableName.substring(2, variableName.length() - 2); + + if (parsedVariableName.isBlank()) { + throw new IllegalArgumentException("Variable name cannot be blank: " + variableName); + } + + return parsedVariableName; } private static String replaceConstants( diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmTargetParser.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmTargetParser.tjava new file mode 100644 index 0000000..1ba2f3d --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmTargetParser.tjava @@ -0,0 +1,60 @@ +package com.r35157.jupiterperpsalarm.impl.ref; + +import java.math.BigDecimal; + +public final class AlarmTargetParser { + + private AlarmTargetParser() { + } + + public static BigDecimal parse(String targetStr) { + String trimmedTargetStr = targetStr.trim(); + + if (trimmedTargetStr.endsWith("%")) { + int plusIndex = trimmedTargetStr.indexOf('+'); + int minusIndex = trimmedTargetStr.indexOf('-', 1); + + if (plusIndex >= 0) { + return parseTargetPercentage(trimmedTargetStr, plusIndex, true); + } + + if (minusIndex >= 0) { + return parseTargetPercentage(trimmedTargetStr, minusIndex, false); + } + } + + BigDecimal target = new BigDecimal(trimmedTargetStr); + validateTarget(target, targetStr); + return target; + } + + private static BigDecimal parseTargetPercentage( + String targetStr, + int operatorIndex, + boolean add + ) { + BigDecimal base = new BigDecimal(targetStr.substring(0, operatorIndex)); + String percentStr = targetStr.substring(operatorIndex + 1, targetStr.length() - 1); + BigDecimal percent = new BigDecimal(percentStr); + + BigDecimal delta = base + .multiply(percent) + .divide(BigDecimal.valueOf(100)); + + BigDecimal target = add ? base.add(delta) : base.subtract(delta); + + validateTarget(base, targetStr); + validateTarget(percent, targetStr); + validateTarget(target, targetStr); + + return target; + } + + private static void validateTarget(BigDecimal target, String originalTargetStr) { + if (target.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException( + "Target must be zero or positive: " + originalTargetStr + ); + } + } +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmVariableResolver.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmVariableResolver.tjava new file mode 100644 index 0000000..61263bf --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AlarmVariableResolver.tjava @@ -0,0 +1,30 @@ +package com.r35157.jupiterperpsalarm.impl.ref; + +import java.util.Map; +import java.util.Objects; + +public final class AlarmVariableResolver { + + public AlarmVariableResolver(Map variables) { + this.variables = Objects.requireNonNull(variables, "variables"); + } + + public String resolve(String text) { + Objects.requireNonNull(text, "text"); + + String resolvedText = text; + + for (Map.Entry entry : variables.entrySet()) { + String placeholder = "{{" + entry.getKey() + "}}"; + resolvedText = resolvedText.replace(placeholder, entry.getValue()); + } + + if (resolvedText.contains("{{") || resolvedText.contains("}}")) { + throw new IllegalArgumentException("Unresolved variable in text: " + text); + } + + return resolvedText; + } + + private final Map variables; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AssetPriceAlarmMonitor.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AssetPriceAlarmMonitor.tjava index 4a35553..1e6dae3 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AssetPriceAlarmMonitor.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/AssetPriceAlarmMonitor.tjava @@ -12,6 +12,7 @@ public final class AssetPriceAlarmMonitor { public AssetPriceAlarmMonitor( JupiterPerpsAsset asset, List definitions, + AlarmVariableResolver variableResolver, AlarmAction action ) { this.asset = asset; @@ -22,7 +23,7 @@ public final class AssetPriceAlarmMonitor { "Alarm asset " + definition.asset() + " does not match monitor " + asset ); } - return new PriceAlarm(definition, action); + return new PriceAlarm(definition, variableResolver, action); }) .toList(); diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/CompositeAlarmAction.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/CompositeAlarmAction.tjava index 02e8e1b..4215a9a 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/CompositeAlarmAction.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/CompositeAlarmAction.tjava @@ -9,7 +9,7 @@ public final class CompositeAlarmAction implements AlarmAction { } @Override - public void trigger(OraclePrice price, PriceAlarmDefinition alarm) { + public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) { for (AlarmAction action : actions) { try { action.trigger(price, alarm); diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/ConsoleAlarmAction.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/ConsoleAlarmAction.tjava index 5223d4f..c228fb8 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/ConsoleAlarmAction.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/ConsoleAlarmAction.tjava @@ -2,7 +2,7 @@ package com.r35157.jupiterperpsalarm.impl.ref; public final class ConsoleAlarmAction implements AlarmAction { @Override - public void trigger(OraclePrice price, PriceAlarmDefinition alarm) { + public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) { System.err.println(); System.err.println("============================================================"); System.err.printf( 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 848a5d7..224e8f8 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava @@ -1,5 +1,10 @@ package com.r35157.jupiterperpsalarm.impl.ref; +import com.r35157.libs.jupiter.perps.JupiterPerpsService; +import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl; +import com.r35157.libs.solana.SolanaBlockChain; +import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl; + import java.net.URI; import java.nio.file.Path; import java.util.ArrayList; @@ -13,6 +18,7 @@ public final class JupiterPerpsAlarmImpl { public static void main(String[] args) throws Exception { Config config; + try { config = Config.parse(args, System.getenv()); } catch (IllegalArgumentException exception) { @@ -22,13 +28,34 @@ public final class JupiterPerpsAlarmImpl { } List definitions; + AlarmConfiguration alarmConfiguration; + try { - definitions = AlarmConfigurationParser.parse(config.alarmConfiguration()); + alarmConfiguration = AlarmConfigurationParser.parse(config.alarmConfiguration()); + definitions = alarmConfiguration.definitions(); } catch (Exception exception) { String errMsg = "Could not load alarm configuration: " + exception.getMessage() + "!"; throw new IllegalStateException(errMsg, exception); } + AlarmVariableResolver variableResolver = new AlarmVariableResolver( + alarmConfiguration.variables() + ); + + SolanaBlockChain solanaBlockChain = new SolanaBlockChainImpl(); + JupiterPerpsService jupiterPerpsService = new AnchorIdlJupiterPerpsServiceImpl(solanaBlockChain); + JupiterPerpsEntryPriceVariableRefresher entryPriceVariableRefresher = + new JupiterPerpsEntryPriceVariableRefresher(alarmConfiguration.variables(), jupiterPerpsService); + entryPriceVariableRefresher.refresh(); + + JupiterPerpsEntryPriceVariableRefreshWatcher entryPriceVariableRefreshWatcher = + new JupiterPerpsEntryPriceVariableRefreshWatcher( + config.alarmConfiguration().getParent(), + entryPriceVariableRefresher + ); + + entryPriceVariableRefreshWatcher.start(); + List actions = new ArrayList<>(); actions.add(new ConsoleAlarmAction()); @@ -52,9 +79,10 @@ public final class JupiterPerpsAlarmImpl { Map monitors = new EnumMap<>( JupiterPerpsAsset.class ); + definitionsByAsset.forEach((asset, assetDefinitions) -> monitors.put( asset, - new AssetPriceAlarmMonitor(asset, assetDefinitions, action) + new AssetPriceAlarmMonitor(asset, assetDefinitions, variableResolver, action) )); List clients = new ArrayList<>(); @@ -85,7 +113,7 @@ public final class JupiterPerpsAlarmImpl { assetDefinitions.forEach(definition -> System.out.printf( " %s %s USD, %s, severity=%s%n", definition.direction(), - definition.target().toPlainString(), + definition.targetExpression(), definition.trigger(), definition.severity() )); diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsEntryPriceVariableRefreshWatcher.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsEntryPriceVariableRefreshWatcher.tjava new file mode 100644 index 0000000..28f8f4b --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsEntryPriceVariableRefreshWatcher.tjava @@ -0,0 +1,92 @@ +package com.r35157.jupiterperpsalarm.impl.ref; + +import java.nio.file.*; +import java.util.Objects; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.FileSystems; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; + +public final class JupiterPerpsEntryPriceVariableRefreshWatcher { + + public JupiterPerpsEntryPriceVariableRefreshWatcher( + Path confDirectory, + JupiterPerpsEntryPriceVariableRefresher refresher + ) { + this.confDirectory = Objects.requireNonNull(confDirectory, "confDirectory"); + this.refresher = Objects.requireNonNull(refresher, "refresher"); + } + + public void start() { + Thread thread = new Thread( + this::run, + "jupiter-perps-entry-price-variable-refresh-watcher" + ); + + thread.setDaemon(true); + thread.start(); + } + + private void run() { + try (WatchService watchService = FileSystems.getDefault().newWatchService()) { + + confDirectory.register( + watchService, + StandardWatchEventKinds.ENTRY_CREATE + ); + + System.out.println( + "Jupiter Perps entry price variable refresh watcher started for directory: " + + confDirectory + ); + while (true) { + WatchKey key = watchService.take(); + + for (WatchEvent event : key.pollEvents()) { + Path path = (Path) event.context(); + + if (!REFRESH_TRIGGER_FILE_NAME.equals(path.getFileName().toString())) { + continue; + } + + Path triggerFile = confDirectory.resolve(path); + + System.out.println("Refresh trigger file detected: " + triggerFile); + + try { + refresher.refresh(); + } finally { + Files.deleteIfExists(triggerFile); + } + } + + if (!key.reset()) { + System.err.println( + "Jupiter Perps entry price variable refresh watcher stopped: watch key is no longer valid" + ); + return; + } + } + } catch (IOException exception) { + System.err.println( + "Could not start Jupiter Perps entry price variable refresh watcher: " + + exception.getMessage() + ); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + + System.err.println( + "Jupiter Perps entry price variable refresh watcher interrupted" + ); + } + } + + private static final String REFRESH_TRIGGER_FILE_NAME = + "jupiter-perps-alarm-var.refresh"; + + private final Path confDirectory; + private final JupiterPerpsEntryPriceVariableRefresher refresher; +} diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsEntryPriceVariableRefresher.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsEntryPriceVariableRefresher.tjava new file mode 100644 index 0000000..271cec5 --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsEntryPriceVariableRefresher.tjava @@ -0,0 +1,86 @@ +package com.r35157.jupiterperpsalarm.impl.ref; + +import com.r35157.libs.jupiter.perps.JupiterPerpsPosition; +import com.r35157.libs.jupiter.perps.JupiterPerpsService; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public final class JupiterPerpsEntryPriceVariableRefresher { + + public JupiterPerpsEntryPriceVariableRefresher( + Map variables, + JupiterPerpsService jupiterPerpsService + ) { + this.variables = Objects.requireNonNull(variables, "variables"); + this.jupiterPerpsService = Objects.requireNonNull(jupiterPerpsService, "jupiterPerpsService"); + } + + public void refresh() { + ΩSolanaWalletIdΩ wallet = variables.get("JUPITER_PERPS_WALLET"); + + if (wallet == null || wallet.isBlank()) { + System.err.println("Cannot refresh Jupiter Perps entry price variables: JUPITER_PERPS_WALLET is not configured"); + return; + } + + try { + Set positions = jupiterPerpsService.getOpenPositions(wallet); + + System.out.println( + "Fetched " + positions.size() + + " open Jupiter Perps positions for wallet: " + wallet + ); + + removeEntryPriceVariables(); + + for (JupiterPerpsPosition position : positions) { + String variableName = createEntryPriceVariableName(position); + + System.out.println( + "Jupiter Perps position maps to variable " + + variableName + + " = " + + position.entryPrice() + ); + variables.put(variableName, position.entryPrice().toPlainString()); + } + } catch (IOException | InterruptedException exception) { + if (exception instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + + System.err.println( + "Could not refresh Jupiter Perps entry price variables: " + + exception.getMessage() + ); + } + } + + private void removeEntryPriceVariables() { + variables.remove("SOL_LONG_ENTRY_PRICE"); + variables.remove("SOL_SHORT_ENTRY_PRICE"); + variables.remove("BTC_LONG_ENTRY_PRICE"); + variables.remove("BTC_SHORT_ENTRY_PRICE"); + variables.remove("ETH_LONG_ENTRY_PRICE"); + variables.remove("ETH_SHORT_ENTRY_PRICE"); + } + + private static String createEntryPriceVariableName(JupiterPerpsPosition position) { + String asset = switch (position.tradedTokenMint()) { + case "So11111111111111111111111111111111111111112" -> "SOL"; + case "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh" -> "BTC"; + case "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs" -> "ETH"; + default -> throw new IllegalArgumentException( + "Unsupported Jupiter Perps traded token mint: " + position.tradedTokenMint() + ); + }; + + return asset + "_" + position.direction() + "_ENTRY_PRICE"; + } + + private final Map variables; + private final JupiterPerpsService jupiterPerpsService; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PriceAlarm.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PriceAlarm.tjava index cdb8ed1..7275183 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PriceAlarm.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PriceAlarm.tjava @@ -1,11 +1,17 @@ package com.r35157.jupiterperpsalarm.impl.ref; +import java.math.BigDecimal; import java.time.Instant; public final class PriceAlarm { - public PriceAlarm(PriceAlarmDefinition definition, AlarmAction action) { + public PriceAlarm( + PriceAlarmDefinition definition, + AlarmVariableResolver variableResolver, + AlarmAction action + ) { this.definition = definition; + this.variableResolver = variableResolver; this.action = action; } @@ -16,9 +22,23 @@ public final class PriceAlarm { ); } + BigDecimal target; + try { + target = AlarmTargetParser.parse( + variableResolver.resolve(definition.targetExpression()) + ); + } catch (RuntimeException exception) { + System.err.printf( + "Could not resolve target for alarm %d: %s%n", + definition.id(), + exception.getMessage() + ); + return; + } + boolean reached = definition.direction().reached( price.priceUsd(), - definition.target() + target ); boolean enteredTriggeredSide = previousReached == null @@ -37,13 +57,13 @@ public final class PriceAlarm { return; } - trigger(price); + trigger(price, target); return; } if (definition.trigger() == AlarmTrigger.PERSISTENT) { if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) { - trigger(price); + trigger(price, target); } return; } @@ -75,13 +95,39 @@ public final class PriceAlarm { ); } - private void trigger(OraclePrice price) { + private void trigger(OraclePrice price, BigDecimal target) { triggerCount++; lastTriggeredAt = Instant.now(); - action.trigger(price, definition); + + 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(), + definition.direction(), + target, + definition.trigger(), + definition.triggerGracePeriod(), + definition.severity(), + note + ); + + action.trigger(price, resolvedAlarm); } private final PriceAlarmDefinition definition; + private final AlarmVariableResolver variableResolver; private final AlarmAction action; private Instant lastTriggeredAt; diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PriceAlarmDefinition.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PriceAlarmDefinition.tjava index 3664b76..8047d0b 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PriceAlarmDefinition.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PriceAlarmDefinition.tjava @@ -2,14 +2,13 @@ package com.r35157.jupiterperpsalarm.impl.ref; import com.r35157.jupiterperpsalarm.AlarmSeverity; -import java.math.BigDecimal; import java.util.Objects; public record PriceAlarmDefinition( int id, JupiterPerpsAsset asset, PriceDirection direction, - BigDecimal target, + String targetExpression, AlarmTrigger trigger, ΩsecondsΩ triggerGracePeriod, AlarmSeverity severity, @@ -18,12 +17,13 @@ public record PriceAlarmDefinition( public PriceAlarmDefinition { Objects.requireNonNull(asset, "asset"); Objects.requireNonNull(direction, "direction"); - Objects.requireNonNull(target, "target"); + Objects.requireNonNull(targetExpression, "targetExpression"); Objects.requireNonNull(trigger, "trigger"); + Objects.requireNonNull(severity, "severity"); Objects.requireNonNull(note, "note"); - if (target.signum() < 0) { - throw new IllegalArgumentException("Target price cannot be negative (was: " + target.signum() + ")!"); + if (targetExpression.isBlank()) { + throw new IllegalArgumentException("Target expression cannot be blank"); } if (triggerGracePeriod < 0) { diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PushoverAlarmAction.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PushoverAlarmAction.tjava index be4e7fa..0f42790 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PushoverAlarmAction.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/PushoverAlarmAction.tjava @@ -18,7 +18,7 @@ public final class PushoverAlarmAction implements AlarmAction { } @Override - public void trigger(OraclePrice price, PriceAlarmDefinition alarm) { + public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) { String title = "Jupiter Perps " + price.asset() + " alarm"; String message = createMessage(price, alarm); @@ -26,7 +26,7 @@ public final class PushoverAlarmAction implements AlarmAction { form("user", userKey) + "&" + form("title", title) + "&" + form("message", message) + "&" + - createPushoverSeverityParameters(alarm.severity());; + createPushoverSeverityParameters(alarm.severity()); HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI) .timeout(Duration.ofSeconds(15)) @@ -69,7 +69,7 @@ public final class PushoverAlarmAction implements AlarmAction { }; } - private static String createMessage(OraclePrice price, PriceAlarmDefinition alarm) { + private static String createMessage(OraclePrice price, ResolvedPriceAlarm alarm) { return String.format( "%d - %s: %s%n%n%s is %s USD.%nTarget: %s %s USD.%nOracle time: %s.%nSlot: %d.", alarm.id(), diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/ResolvedPriceAlarm.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/ResolvedPriceAlarm.tjava new file mode 100644 index 0000000..502f4e3 --- /dev/null +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/ResolvedPriceAlarm.tjava @@ -0,0 +1,16 @@ +package com.r35157.jupiterperpsalarm.impl.ref; + +import com.r35157.jupiterperpsalarm.AlarmSeverity; +import java.math.BigDecimal; + +public record ResolvedPriceAlarm( + int id, + JupiterPerpsAsset asset, + PriceDirection direction, + BigDecimal target, + AlarmTrigger trigger, + ΩsecondsΩ triggerGracePeriod, + AlarmSeverity severity, + String note +) { +} \ No newline at end of file