Compare commits
37 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
| 244a6f19fe | |||
| ff4f5a0356 | |||
| f86652df7d | |||
| 5e5f8ebd51 | |||
| d216c06ba1 | |||
| c1cbaf0c52 | |||
| 76446f8b73 | |||
| dac1b62628 | |||
| 0aff69429a | |||
| 384fad01bf | |||
| 520a0bcd92 | |||
| 896b5235d0 | |||
| 403d7af6e9 | |||
| 90fd1694fb | |||
| 1fb712b61d | |||
| 1355422597 | |||
| c6ba8dc009 | |||
| a5e5470c2b | |||
| 2953b07609 | |||
| 0d14bb3538 | |||
| 41bd1898c2 | |||
| 873086eeaf | |||
| d33289d8ce | |||
| b0eb4e6d93 | |||
| 2d359f59e4 | |||
| 3fc769a687 | |||
| 4edd5af9d7 | |||
| 7a06d87f4a | |||
| 05b0b6cb6a | |||
| 4a5450b4b0 | |||
| 1410c959e6 | |||
| 6e75cf3725 | |||
| c8c443fdc8 | |||
| 545b79de3b | |||
| 97ecb7572a | |||
| 940e1ece94 | |||
| a212791ba9 |
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-60
@@ -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()
|
||||||
));
|
));
|
||||||
|
|||||||
+92
@@ -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;
|
||||||
|
}
|
||||||
+86
@@ -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,7 +2,6 @@ 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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user