39 Commits

Author SHA256 Message Date
minimons 244a6f19fe 20: Add actual refreshing logic to loop 2026-06-28 18:40:05 +02:00
minimons ff4f5a0356 20: Adding refresher file filtering 2026-06-28 18:33:20 +02:00
minimons f86652df7d 20: More refresher logic 2026-06-28 18:30:58 +02:00
minimons 5e5f8ebd51 20: Prepare background refresher thread 2026-06-28 18:19:20 +02:00
minimons d216c06ba1 20: Add watcher wire-up 2026-06-28 18:14:13 +02:00
minimons c1cbaf0c52 20: Add new JupiterPerpsEntryPriceVariableRefreshWatcher class 2026-06-28 18:06:50 +02:00
minimons 76446f8b73 20: Clean-up before update 2026-06-28 11:48:08 +02:00
minimons dac1b62628 20: Update variable map with new data 2026-06-28 11:45:04 +02:00
minimons 0aff69429a 20: Map mint addresses to asset symbols 2026-06-28 11:40:13 +02:00
minimons 384fad01bf 20: Fetch open Perps positions JupiterPerpsEntryPriceVariableRefresher 2026-06-28 11:36:05 +02:00
minimons 520a0bcd92 20: Inject JupiterPerpsService into JupiterPerpsEntryPriceVariableRefresher 2026-06-28 11:30:03 +02:00
minimons 896b5235d0 20: Add new JupiterPerpsEntryPriceVariableRefresher class 2026-06-28 11:08:14 +02:00
minimons 403d7af6e9 20: CompositeAlarmAction compilation fix 2026-06-27 23:32:51 +02:00
minimons 90fd1694fb 20: Fix compilatoin of PushoverAlarmAction 2026-06-27 23:27:47 +02:00
minimons 1fb712b61d 20: Fix compilation of ConsoleAlarmAction 2026-06-27 23:25:04 +02:00
minimons 1355422597 20: Use resolved node in PriceAlarm trigger 2026-06-27 21:12:26 +02:00
minimons c6ba8dc009 20: Change AlarmAction to use resolved data 2026-06-27 21:07:17 +02:00
minimons a5e5470c2b 20: Create new ResolvedPriceAlarm and resolve note 2026-06-27 21:03:21 +02:00
minimons 2953b07609 20: Switch variables with brackets 2026-06-27 20:54:02 +02:00
minimons 0d14bb3538 20: Store variables keys without brackets 2026-06-27 20:48:28 +02:00
minimons 41bd1898c2 20: Change AlarmConfiguration to use ConcurrentHashMap 2026-06-27 20:43:09 +02:00
minimons 873086eeaf 20: Handle missing variables gracefully 2026-06-27 20:35:10 +02:00
minimons d33289d8ce 20: Wire-up resolver 2026-06-27 20:29:24 +02:00
minimons b0eb4e6d93 20: Make AlarmConfigurationParser return AlarmConfiguration 2026-06-27 20:20:03 +02:00
minimons 2d359f59e4 20: Add new AlarmConfiguration class 2026-06-27 20:15:41 +02:00
minimons 3fc769a687 20: Initialize AlarmVariableResolver 2026-06-27 20:12:10 +02:00
minimons 4edd5af9d7 20: Inject AlarmVariableResolver 2026-06-27 20:07:14 +02:00
minimons 7a06d87f4a 20: Use resolver in PriceAlarm 2026-06-27 20:01:47 +02:00
minimons 05b0b6cb6a 20: Add AlarmVariableResolver class 2026-06-27 19:57:02 +02:00
minimons 4a5450b4b0 20: Do not resolve while reading config 2026-06-27 19:54:12 +02:00
minimons 1410c959e6 20: Fix target price compilation issues 2026-06-27 19:49:36 +02:00
minimons 6e75cf3725 20: Update PriceAlarm to use parser 2026-06-27 19:40:46 +02:00
minimons c8c443fdc8 20: Move target price parsing to new AlarmTargetParser class 2026-06-27 19:27:25 +02:00
minimons 545b79de3b 20: Do not resolve target price 2026-06-27 19:14:01 +02:00
minimons 97ecb7572a 20: Update PriceAlarmDefinition to support expressions 2026-06-27 19:05:59 +02:00
minimons 940e1ece94 20: Change config file 2026-06-27 19:05:59 +02:00
minimons a212791ba9 X: Add publishGitHub 2026-06-27 19:05:59 +02:00
minimons d9f773c1f9 X: Updated for some manual testing in Main 2026-06-26 22:27:57 +02:00
minimons 2d68b83cf4 17: Add mint address field in JupiterPerpsPosition 2026-06-26 22:27:57 +02:00
21 changed files with 621 additions and 109 deletions
+55 -7
View File
@@ -1,8 +1,56 @@
# 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 # 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!" # SOL SHORT
3 BTC ABOVE 85032.87-1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Bitcoin short LIKVIDERING!" 1 SOL ABOVE {{SOL_SHORT_LIQ_PRICE}}-1.25% ONETIME CRITICAL "🚨 LIKVIDERINGS-risiko SOL Short!"
4 BTC BELOW 42779.40+1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Bitcoin long LIKVIDERING!" 2 SOL ABOVE {{SOL_SHORT_ENTRY_PRICE}}+3% PERSISTENT:900 INFO "🌱 Short SOL now"
5 ETH ABOVE 2296.13-1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Ethereum short LIKVIDERING!" 3 SOL BELOW {{SOL_SHORT_ENTRY_PRICE}}-3% PERSISTENT:900 INFO "💵 Close SOL Short"
6 ETH BELOW 1155.19+1% ONETIME CRITICAL "CRITICAL: Risiko for Perps Ethereum long LIKVIDERING!"
# 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"
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
SOURCE="$HOME/projects/com_r35157_nenjim-hubd-impl_ref"
TARGET="$HOME/projects/com_r35157_nenjim-hubd-impl_ref_github_snapshot"
rsync -a --delete \
--exclude '.git' \
--exclude 'conf/*.conf' \
--exclude 'conf/*.xml' \
"$SOURCE/" \
"$TARGET/"
cd "$TARGET"
git add -A
if git diff --cached --quiet; then
echo "No snapshot changes to publish."
exit 0
fi
git commit -m "Mirror snapshot"
git push
@@ -2,5 +2,5 @@ package com.r35157.jupiterperpsalarm.impl.ref;
@FunctionalInterface @FunctionalInterface
public interface AlarmAction { public interface AlarmAction {
void trigger(OraclePrice price, PriceAlarmDefinition alarm); void trigger(OraclePrice price, ResolvedPriceAlarm alarm);
} }
@@ -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<PriceAlarmDefinition> definitions,
Map<String, String> variables
) {
public AlarmConfiguration {
Objects.requireNonNull(definitions, "definitions");
Objects.requireNonNull(variables, "variables");
definitions = List.copyOf(definitions);
variables = new ConcurrentHashMap<>(variables);
}
}
@@ -3,14 +3,13 @@ package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity; import com.r35157.jupiterperpsalarm.AlarmSeverity;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
public final class AlarmConfigurationParser { public final class AlarmConfigurationParser {
public static List<PriceAlarmDefinition> parse(Path path) throws IOException { public static AlarmConfiguration parse(Path path) throws IOException {
List<String> lines = Files.readAllLines(path); List<String> lines = Files.readAllLines(path);
List<PriceAlarmDefinition> alarms = new ArrayList<>(); List<PriceAlarmDefinition> alarms = new ArrayList<>();
Map<String, String> constants = new LinkedHashMap<>(); Map<String, String> constants = new LinkedHashMap<>();
@@ -27,8 +26,7 @@ public final class AlarmConfigurationParser {
if(isConstantDefinition(trimmed)) { if(isConstantDefinition(trimmed)) {
parseConstantDefinition(constants, line); parseConstantDefinition(constants, line);
} else { } else {
String resolvedLine = replaceConstants(line, constants); alarms.add(parseLine(line));
alarms.add(parseLine(resolvedLine));
} }
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
@@ -42,7 +40,7 @@ public final class AlarmConfigurationParser {
throw new IllegalArgumentException("No alarms found in " + path); throw new IllegalArgumentException("No alarms found in " + path);
} }
return List.copyOf(alarms); return new AlarmConfiguration(alarms, constants);
} }
static PriceAlarmDefinition parseLine(String line) { static PriceAlarmDefinition parseLine(String line) {
@@ -58,7 +56,7 @@ public final class AlarmConfigurationParser {
cursor.nextToken("direction").toUpperCase(Locale.ROOT) cursor.nextToken("direction").toUpperCase(Locale.ROOT)
); );
BigDecimal target = parseTarget(cursor.nextToken("target")); String targetExpression = cursor.nextToken("target");
TriggerConfiguration triggerConfiguration = parseTrigger( TriggerConfiguration triggerConfiguration = parseTrigger(
cursor.nextToken("trigger") cursor.nextToken("trigger")
@@ -81,7 +79,7 @@ public final class AlarmConfigurationParser {
id, id,
asset, asset,
direction, direction,
target, targetExpression,
triggerConfiguration.trigger(), triggerConfiguration.trigger(),
triggerConfiguration.gracePeriod(), triggerConfiguration.gracePeriod(),
severity, 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) { private static TriggerConfiguration parseTrigger(String triggerText) {
String normalized = triggerText.toUpperCase(Locale.ROOT); String normalized = triggerText.toUpperCase(Locale.ROOT);
@@ -155,35 +131,6 @@ public final class AlarmConfigurationParser {
throw new IllegalArgumentException("Unknown trigger: " + triggerText); 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 static final class Cursor {
private Cursor(String line) { private Cursor(String line) {
this.line = line; this.line = line;
@@ -290,9 +237,21 @@ public final class AlarmConfigurationParser {
validateConstantName(name); validateConstantName(name);
if (constants.putIfAbsent(name, value) != null) { constants.put(parseVariableName(name), value);
throw new IllegalArgumentException("Duplicate constant: " + name);
} }
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( private static String replaceConstants(
@@ -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
);
}
}
}
@@ -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<String, String> variables) {
this.variables = Objects.requireNonNull(variables, "variables");
}
public String resolve(String text) {
Objects.requireNonNull(text, "text");
String resolvedText = text;
for (Map.Entry<String, String> 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<String, String> variables;
}
@@ -12,6 +12,7 @@ public final class AssetPriceAlarmMonitor {
public AssetPriceAlarmMonitor( public AssetPriceAlarmMonitor(
JupiterPerpsAsset asset, JupiterPerpsAsset asset,
List<PriceAlarmDefinition> definitions, List<PriceAlarmDefinition> definitions,
AlarmVariableResolver variableResolver,
AlarmAction action AlarmAction action
) { ) {
this.asset = asset; this.asset = asset;
@@ -22,7 +23,7 @@ public final class AssetPriceAlarmMonitor {
"Alarm asset " + definition.asset() + " does not match monitor " + asset "Alarm asset " + definition.asset() + " does not match monitor " + asset
); );
} }
return new PriceAlarm(definition, action); return new PriceAlarm(definition, variableResolver, action);
}) })
.toList(); .toList();
@@ -9,7 +9,7 @@ public final class CompositeAlarmAction implements AlarmAction {
} }
@Override @Override
public void trigger(OraclePrice price, PriceAlarmDefinition alarm) { public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
for (AlarmAction action : actions) { for (AlarmAction action : actions) {
try { try {
action.trigger(price, alarm); action.trigger(price, alarm);
@@ -2,7 +2,7 @@ package com.r35157.jupiterperpsalarm.impl.ref;
public final class ConsoleAlarmAction implements AlarmAction { public final class ConsoleAlarmAction implements AlarmAction {
@Override @Override
public void trigger(OraclePrice price, PriceAlarmDefinition alarm) { public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
System.err.println(); System.err.println();
System.err.println("============================================================"); System.err.println("============================================================");
System.err.printf( System.err.printf(
@@ -1,5 +1,10 @@
package com.r35157.jupiterperpsalarm.impl.ref; 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.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
@@ -13,6 +18,7 @@ public final class JupiterPerpsAlarmImpl {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
Config config; Config config;
try { try {
config = Config.parse(args, System.getenv()); config = Config.parse(args, System.getenv());
} catch (IllegalArgumentException exception) { } catch (IllegalArgumentException exception) {
@@ -22,13 +28,34 @@ public final class JupiterPerpsAlarmImpl {
} }
List<PriceAlarmDefinition> definitions; List<PriceAlarmDefinition> definitions;
AlarmConfiguration alarmConfiguration;
try { try {
definitions = AlarmConfigurationParser.parse(config.alarmConfiguration()); alarmConfiguration = AlarmConfigurationParser.parse(config.alarmConfiguration());
definitions = alarmConfiguration.definitions();
} catch (Exception exception) { } catch (Exception exception) {
String errMsg = "Could not load alarm configuration: " + exception.getMessage() + "!"; String errMsg = "Could not load alarm configuration: " + exception.getMessage() + "!";
throw new IllegalStateException(errMsg, exception); 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<AlarmAction> actions = new ArrayList<>(); List<AlarmAction> actions = new ArrayList<>();
actions.add(new ConsoleAlarmAction()); actions.add(new ConsoleAlarmAction());
@@ -52,9 +79,10 @@ public final class JupiterPerpsAlarmImpl {
Map<JupiterPerpsAsset, AssetPriceAlarmMonitor> monitors = new EnumMap<>( Map<JupiterPerpsAsset, AssetPriceAlarmMonitor> monitors = new EnumMap<>(
JupiterPerpsAsset.class JupiterPerpsAsset.class
); );
definitionsByAsset.forEach((asset, assetDefinitions) -> monitors.put( definitionsByAsset.forEach((asset, assetDefinitions) -> monitors.put(
asset, asset,
new AssetPriceAlarmMonitor(asset, assetDefinitions, action) new AssetPriceAlarmMonitor(asset, assetDefinitions, variableResolver, action)
)); ));
List<OracleWebSocketClient> clients = new ArrayList<>(); List<OracleWebSocketClient> clients = new ArrayList<>();
@@ -85,7 +113,7 @@ public final class JupiterPerpsAlarmImpl {
assetDefinitions.forEach(definition -> System.out.printf( assetDefinitions.forEach(definition -> System.out.printf(
" %s %s USD, %s, severity=%s%n", " %s %s USD, %s, severity=%s%n",
definition.direction(), definition.direction(),
definition.target().toPlainString(), definition.targetExpression(),
definition.trigger(), definition.trigger(),
definition.severity() definition.severity()
)); ));
@@ -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;
}
@@ -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<String, String> 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<JupiterPerpsPosition> 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<String, String> variables;
private final JupiterPerpsService jupiterPerpsService;
}
@@ -1,11 +1,17 @@
package com.r35157.jupiterperpsalarm.impl.ref; package com.r35157.jupiterperpsalarm.impl.ref;
import java.math.BigDecimal;
import java.time.Instant; import java.time.Instant;
public final class PriceAlarm { public final class PriceAlarm {
public PriceAlarm(PriceAlarmDefinition definition, AlarmAction action) { public PriceAlarm(
PriceAlarmDefinition definition,
AlarmVariableResolver variableResolver,
AlarmAction action
) {
this.definition = definition; this.definition = definition;
this.variableResolver = variableResolver;
this.action = action; 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( boolean reached = definition.direction().reached(
price.priceUsd(), price.priceUsd(),
definition.target() target
); );
boolean enteredTriggeredSide = previousReached == null boolean enteredTriggeredSide = previousReached == null
@@ -37,13 +57,13 @@ public final class PriceAlarm {
return; return;
} }
trigger(price); trigger(price, target);
return; return;
} }
if (definition.trigger() == AlarmTrigger.PERSISTENT) { if (definition.trigger() == AlarmTrigger.PERSISTENT) {
if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) { if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) {
trigger(price); trigger(price, target);
} }
return; return;
} }
@@ -75,13 +95,39 @@ public final class PriceAlarm {
); );
} }
private void trigger(OraclePrice price) { private void trigger(OraclePrice price, BigDecimal target) {
triggerCount++; triggerCount++;
lastTriggeredAt = Instant.now(); 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 PriceAlarmDefinition definition;
private final AlarmVariableResolver variableResolver;
private final AlarmAction action; private final AlarmAction action;
private Instant lastTriggeredAt; private Instant lastTriggeredAt;
@@ -2,14 +2,13 @@ package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.jupiterperpsalarm.AlarmSeverity; import com.r35157.jupiterperpsalarm.AlarmSeverity;
import java.math.BigDecimal;
import java.util.Objects; import java.util.Objects;
public record PriceAlarmDefinition( public record PriceAlarmDefinition(
int id, int id,
JupiterPerpsAsset asset, JupiterPerpsAsset asset,
PriceDirection direction, PriceDirection direction,
BigDecimal target, String targetExpression,
AlarmTrigger trigger, AlarmTrigger trigger,
ΩsecondsΩ triggerGracePeriod, ΩsecondsΩ triggerGracePeriod,
AlarmSeverity severity, AlarmSeverity severity,
@@ -18,12 +17,13 @@ public record PriceAlarmDefinition(
public PriceAlarmDefinition { public PriceAlarmDefinition {
Objects.requireNonNull(asset, "asset"); Objects.requireNonNull(asset, "asset");
Objects.requireNonNull(direction, "direction"); Objects.requireNonNull(direction, "direction");
Objects.requireNonNull(target, "target"); Objects.requireNonNull(targetExpression, "targetExpression");
Objects.requireNonNull(trigger, "trigger"); Objects.requireNonNull(trigger, "trigger");
Objects.requireNonNull(severity, "severity");
Objects.requireNonNull(note, "note"); Objects.requireNonNull(note, "note");
if (target.signum() < 0) { if (targetExpression.isBlank()) {
throw new IllegalArgumentException("Target price cannot be negative (was: " + target.signum() + ")!"); throw new IllegalArgumentException("Target expression cannot be blank");
} }
if (triggerGracePeriod < 0) { if (triggerGracePeriod < 0) {
@@ -18,7 +18,7 @@ public final class PushoverAlarmAction implements AlarmAction {
} }
@Override @Override
public void trigger(OraclePrice price, PriceAlarmDefinition alarm) { public void trigger(OraclePrice price, ResolvedPriceAlarm alarm) {
String title = "Jupiter Perps " + price.asset() + " alarm"; String title = "Jupiter Perps " + price.asset() + " alarm";
String message = createMessage(price, alarm); String message = createMessage(price, alarm);
@@ -26,7 +26,7 @@ public final class PushoverAlarmAction implements AlarmAction {
form("user", userKey) + "&" + form("user", userKey) + "&" +
form("title", title) + "&" + form("title", title) + "&" +
form("message", message) + "&" + form("message", message) + "&" +
createPushoverSeverityParameters(alarm.severity());; createPushoverSeverityParameters(alarm.severity());
HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI) HttpRequest request = HttpRequest.newBuilder(PUSHOVER_URI)
.timeout(Duration.ofSeconds(15)) .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( return String.format(
"%d - %s: %s%n%n%s is %s USD.%nTarget: %s %s USD.%nOracle time: %s.%nSlot: %d.", "%d - %s: %s%n%n%s is %s USD.%nTarget: %s %s USD.%nOracle time: %s.%nSlot: %d.",
alarm.id(), alarm.id(),
@@ -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
) {
}
@@ -0,0 +1,50 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.r35157.libs.codec.Base58Codec;
import com.r35157.libs.codec.impl.ref.Base58CodecImpl;
import com.r35157.libs.solana.SolanaAccountInfo;
import java.util.Base64;
class AnchorIdlJupiterPerpsCustodyDecoder {
ΩSPLMintAddressΩ decodeMint(
SolanaAccountInfo custodyAccountInfo
) {
byte[] data = Base64.getDecoder().decode(custodyAccountInfo.dataBase64());
if (data.length < MINT_OFFSET + PUBLIC_KEY_LENGTH) {
throw new IllegalArgumentException(
"Jupiter Perps custody account data is too short: " + data.length
);
}
return readPublicKey(data, MINT_OFFSET);
}
private ΩSPLMintAddressΩ readPublicKey(
byte[] data,
int offset
) {
byte[] publicKeyBytes = new byte[PUBLIC_KEY_LENGTH];
System.arraycopy(
data,
offset,
publicKeyBytes,
0,
PUBLIC_KEY_LENGTH
);
return base58.encode(publicKeyBytes);
}
private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8;
private static final int PUBLIC_KEY_LENGTH = 32;
private static final int MINT_OFFSET =
ANCHOR_DISCRIMINATOR_LENGTH
+ PUBLIC_KEY_LENGTH; // pool
private static final Base58Codec base58 = new Base58CodecImpl();
}
@@ -1,5 +1,7 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl; package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.r35157.libs.codec.Base58Codec;
import com.r35157.libs.codec.impl.ref.Base58CodecImpl;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition; import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection; import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
import com.r35157.libs.solana.SolanaAccountInfo; import com.r35157.libs.solana.SolanaAccountInfo;
@@ -13,7 +15,8 @@ class AnchorIdlJupiterPerpsPositionDecoder {
JupiterPerpsPosition decode( JupiterPerpsPosition decode(
ΩJupiterPerpsPositionAccountΩ positionAccount, ΩJupiterPerpsPositionAccountΩ positionAccount,
SolanaAccountInfo accountInfo SolanaAccountInfo accountInfo,
ΩSPLMintAddressΩ tradedTokenMint
) { ) {
byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64()); byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64());
@@ -38,12 +41,28 @@ class AnchorIdlJupiterPerpsPositionDecoder {
JupiterPerpsPosition pos = new JupiterPerpsPosition( JupiterPerpsPosition pos = new JupiterPerpsPosition(
positionAccount, positionAccount,
entryPrice, entryPrice,
direction direction,
tradedTokenMint
); );
return pos; return pos;
} }
ΩSolanaAddressΩ decodeCustodyAccount(SolanaAccountInfo accountInfo) {
byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64());
if (data.length < CUSTODY_OFFSET + PUBLIC_KEY_LENGTH) {
throw new IllegalArgumentException(
"Jupiter Perps position account data is too short: " + data.length
);
}
return readPublicKey(
data,
CUSTODY_OFFSET
);
}
private JupiterPerpsPositionDirection decodeDirection( private JupiterPerpsPositionDirection decodeDirection(
byte rawSide byte rawSide
) { ) {
@@ -59,22 +78,37 @@ class AnchorIdlJupiterPerpsPositionDecoder {
return direction; return direction;
} }
private ΩSolanaAddressΩ readPublicKey(
byte[] data,
int offset
) {
byte[] publicKeyBytes = new byte[PUBLIC_KEY_LENGTH];
System.arraycopy(
data,
offset,
publicKeyBytes,
0,
PUBLIC_KEY_LENGTH
);
return base58.encode(publicKeyBytes);
}
private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8; private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8;
private static final int PUBLIC_KEY_LENGTH = 32; private static final int PUBLIC_KEY_LENGTH = 32;
private static final int I64_LENGTH = 8; private static final int I64_LENGTH = 8;
private static final int U64_LENGTH = 8;
private static final int SIDE_ENUM_LENGTH = 1; private static final int SIDE_ENUM_LENGTH = 1;
private static final int U64_LENGTH = 8;
private static final int SIDE_OFFSET = private static final int OWNER_OFFSET = ANCHOR_DISCRIMINATOR_LENGTH;
ANCHOR_DISCRIMINATOR_LENGTH private static final int POOL_OFFSET = OWNER_OFFSET + PUBLIC_KEY_LENGTH;
+ PUBLIC_KEY_LENGTH // owner private static final int CUSTODY_OFFSET = POOL_OFFSET + PUBLIC_KEY_LENGTH;
+ PUBLIC_KEY_LENGTH // pool private static final int COLLATERAL_CUSTODY_OFFSET = CUSTODY_OFFSET + PUBLIC_KEY_LENGTH; // custody
+ PUBLIC_KEY_LENGTH // custody private static final int OPEN_TIME_OFFSET = COLLATERAL_CUSTODY_OFFSET + PUBLIC_KEY_LENGTH;
+ PUBLIC_KEY_LENGTH // collateralCustody private static final int UPDATE_TIME_OFFSET = OPEN_TIME_OFFSET + I64_LENGTH; // openTime
+ I64_LENGTH // openTime private static final int SIDE_OFFSET = UPDATE_TIME_OFFSET + I64_LENGTH;
+ I64_LENGTH; // updateTime private static final int PRICE_OFFSET = SIDE_OFFSET + SIDE_ENUM_LENGTH;
private static final int PRICE_OFFSET = private static final Base58Codec base58 = new Base58CodecImpl();
SIDE_OFFSET
+ SIDE_ENUM_LENGTH; // side
} }
@@ -17,6 +17,7 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
) { ) {
this.solanaBlockChain = solanaBlockChain; this.solanaBlockChain = solanaBlockChain;
this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder(); this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
} }
@Override @Override
@@ -34,7 +35,9 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
); );
} }
JupiterPerpsPosition pos = positionDecoder.decode(positionAccount, accountInfo); ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo);
JupiterPerpsPosition pos = positionDecoder.decode(positionAccount, accountInfo, tradedTokenMint);
return pos; return pos;
} }
@@ -61,20 +64,36 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
throw new IllegalArgumentException(errorMsg); throw new IllegalArgumentException(errorMsg);
} }
JupiterPerpsPosition position = positionDecoder.decode( ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo);
address,
accountInfo JupiterPerpsPosition position = positionDecoder.decode(address, accountInfo, tradedTokenMint);
);
positions.add(position); positions.add(position);
} }
return Set.copyOf(positions); return Set.copyOf(positions);
} }
private ΩSPLMintAddressΩ getTradedTokenMint(SolanaAccountInfo positionAccountInfo)
throws IOException, InterruptedException
{
ΩSolanaAddressΩ custodyAccount = positionDecoder.decodeCustodyAccount(positionAccountInfo);
SolanaAccountInfo custodyAccountInfo = solanaBlockChain.getAccountInfo(custodyAccount);
if (custodyAccountInfo == null) {
throw new IllegalArgumentException(
"Jupiter Perps custody account does not exist: " + custodyAccount
);
}
ΩSPLMintAddressΩ mintAddress = custodyDecoder.decodeMint(custodyAccountInfo);
return mintAddress;
}
private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID = private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID =
"PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"; "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu";
private static final int POSITION_OWNER_OFFSET = 8; private static final int POSITION_OWNER_OFFSET = 8;
private final SolanaBlockChain solanaBlockChain; private final SolanaBlockChain solanaBlockChain;
private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder; private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder;
private final AnchorIdlJupiterPerpsCustodyDecoder custodyDecoder;
} }
@@ -24,9 +24,10 @@ public class Main {
// TODO: Consider if we really need a Main class or we just need to move the main method to NenjimHubImpl? // TODO: Consider if we really need a Main class or we just need to move the main method to NenjimHubImpl?
static void main(String[] args) throws Exception { static void main(String[] args) throws Exception {
NenjimHubImpl nenjimHub = new NenjimHubImpl(); NenjimHubImpl nenjimHub = new NenjimHubImpl();
/* /*
SolanaBlockChain sbc = new SolanaBlockChainImpl(); SolanaBlockChain sbc = new SolanaBlockChainImpl();
JupiterPerpsPositionService jupiter = new AnchorIdlJupiterPerpsPositionServiceImpl(sbc); JupiterPerpsService jupiter = new AnchorIdlJupiterPerpsServiceImpl(sbc);
ΩSolanaWalletIdΩ walletId = "vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf"; ΩSolanaWalletIdΩ walletId = "vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf";
Set<JupiterPerpsPosition> positions = jupiter.getOpenPositions(walletId); Set<JupiterPerpsPosition> positions = jupiter.getOpenPositions(walletId);
int a=0; int a=0;