34: Combine alarm direction and target into one condition
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class AlarmConditionParser {
|
||||
|
||||
private static final String RANGE_SEPARATOR = "-->";
|
||||
|
||||
private AlarmConditionParser() {
|
||||
}
|
||||
|
||||
public static PriceCondition parse(String conditionExpression) {
|
||||
String trimmedExpression = conditionExpression.trim();
|
||||
|
||||
if (trimmedExpression.startsWith("[") || trimmedExpression.startsWith("(")) {
|
||||
return parseRange(trimmedExpression);
|
||||
}
|
||||
|
||||
PriceComparisonOperator operator;
|
||||
int operatorLength;
|
||||
|
||||
if (trimmedExpression.startsWith("<=")) {
|
||||
operator = PriceComparisonOperator.LESS_THAN_OR_EQUAL;
|
||||
operatorLength = 2;
|
||||
} else if (trimmedExpression.startsWith(">=")) {
|
||||
operator = PriceComparisonOperator.GREATER_THAN_OR_EQUAL;
|
||||
operatorLength = 2;
|
||||
} else if (trimmedExpression.startsWith("<")) {
|
||||
operator = PriceComparisonOperator.LESS_THAN;
|
||||
operatorLength = 1;
|
||||
} else if (trimmedExpression.startsWith(">")) {
|
||||
operator = PriceComparisonOperator.GREATER_THAN;
|
||||
operatorLength = 1;
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown price condition operator: " + conditionExpression
|
||||
);
|
||||
}
|
||||
|
||||
String targetExpression = trimmedExpression.substring(operatorLength).trim();
|
||||
if (targetExpression.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing target in price condition: " + conditionExpression
|
||||
);
|
||||
}
|
||||
|
||||
BigDecimal target = AlarmTargetParser.parse(targetExpression);
|
||||
return new ComparisonPriceCondition(operator, target);
|
||||
}
|
||||
|
||||
private static PriceCondition parseRange(String rangeExpression) {
|
||||
char lowerDelimiter = rangeExpression.charAt(0);
|
||||
char upperDelimiter = rangeExpression.charAt(rangeExpression.length() - 1);
|
||||
|
||||
if (upperDelimiter != ')' && upperDelimiter != ']') {
|
||||
throw new IllegalArgumentException(
|
||||
"Range must end with ')' or ']': " + rangeExpression
|
||||
);
|
||||
}
|
||||
|
||||
String rangeBody = rangeExpression.substring(1, rangeExpression.length() - 1);
|
||||
int separatorIndex = rangeBody.indexOf(RANGE_SEPARATOR);
|
||||
|
||||
if (separatorIndex < 0 || separatorIndex != rangeBody.lastIndexOf(RANGE_SEPARATOR)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Range must contain exactly one '" + RANGE_SEPARATOR + "': " + rangeExpression
|
||||
);
|
||||
}
|
||||
|
||||
String lowerTargetExpression = rangeBody.substring(0, separatorIndex).trim();
|
||||
String upperTargetExpression = rangeBody
|
||||
.substring(separatorIndex + RANGE_SEPARATOR.length())
|
||||
.trim();
|
||||
|
||||
if (lowerTargetExpression.isEmpty() || upperTargetExpression.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Range must contain both a lower and an upper target: " + rangeExpression
|
||||
);
|
||||
}
|
||||
|
||||
BigDecimal lowerTarget = AlarmTargetParser.parse(lowerTargetExpression);
|
||||
BigDecimal upperTarget = AlarmTargetParser.parse(upperTargetExpression);
|
||||
|
||||
return new RangePriceCondition(
|
||||
lowerTarget,
|
||||
lowerDelimiter == '[',
|
||||
upperTarget,
|
||||
upperDelimiter == ']'
|
||||
);
|
||||
}
|
||||
}
|
||||
+17
-7
@@ -52,11 +52,7 @@ public final class AlarmConfigurationParser {
|
||||
cursor.nextToken("asset").toUpperCase(Locale.ROOT)
|
||||
);
|
||||
|
||||
PriceDirection direction = PriceDirection.valueOf(
|
||||
cursor.nextToken("direction").toUpperCase(Locale.ROOT)
|
||||
);
|
||||
|
||||
String targetExpression = cursor.nextToken("target");
|
||||
String conditionExpression = parseConditionExpression(cursor);
|
||||
|
||||
TriggerConfiguration triggerConfiguration = parseTrigger(
|
||||
cursor.nextToken("trigger")
|
||||
@@ -78,8 +74,7 @@ public final class AlarmConfigurationParser {
|
||||
return new PriceAlarmDefinition(
|
||||
id,
|
||||
asset,
|
||||
direction,
|
||||
targetExpression,
|
||||
conditionExpression,
|
||||
triggerConfiguration.trigger(),
|
||||
triggerConfiguration.gracePeriod(),
|
||||
severity,
|
||||
@@ -87,6 +82,21 @@ public final class AlarmConfigurationParser {
|
||||
);
|
||||
}
|
||||
|
||||
private static String parseConditionExpression(Cursor cursor) {
|
||||
String conditionExpression = cursor.nextToken("condition");
|
||||
|
||||
if (conditionExpression.equals("<")
|
||||
|| conditionExpression.equals("<=")
|
||||
|| conditionExpression.equals(">")
|
||||
|| conditionExpression.equals(">=")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Condition must not contain whitespace: " + conditionExpression
|
||||
);
|
||||
}
|
||||
|
||||
return conditionExpression;
|
||||
}
|
||||
|
||||
private static TriggerConfiguration parseTrigger(String triggerText) {
|
||||
String normalized = triggerText.toUpperCase(Locale.ROOT);
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ComparisonPriceCondition(
|
||||
PriceComparisonOperator operator,
|
||||
BigDecimal target
|
||||
) implements PriceCondition {
|
||||
|
||||
public ComparisonPriceCondition {
|
||||
Objects.requireNonNull(operator, "operator");
|
||||
Objects.requireNonNull(target, "target");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(BigDecimal price) {
|
||||
return operator.matches(price, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String expression() {
|
||||
return operator.symbol() + target.toPlainString();
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,10 @@ public final class ConsoleAlarmAction implements AlarmAction {
|
||||
System.err.println();
|
||||
System.err.println("============================================================");
|
||||
System.err.printf(
|
||||
"ALARM: %s is %s USD; target %s %s USD%n",
|
||||
"ALARM: %s is %s USD; condition %s USD%n",
|
||||
price.asset(),
|
||||
price.priceUsd().toPlainString(),
|
||||
alarm.direction(),
|
||||
alarm.target().toPlainString()
|
||||
alarm.condition().expression()
|
||||
);
|
||||
System.err.printf(
|
||||
"Trigger: %s, oracle time: %s, slot: %d, source: %s%n",
|
||||
|
||||
@@ -68,6 +68,10 @@ public final class JupiterPerpsAlarmImpl {
|
||||
entryPriceVariableRefresher.refresh();
|
||||
System.out.println("Fetching done.");
|
||||
|
||||
System.out.print("Validating alarm conditions... ");
|
||||
validateAlarmConditions(definitions, variableResolver);
|
||||
System.out.println("Done.");
|
||||
|
||||
entryPriceVariableRefresher.startPeriodicRefresh();
|
||||
|
||||
JupiterPerpsEntryPriceVariableRefreshWatcher entryPriceVariableRefreshWatcher =
|
||||
@@ -141,9 +145,8 @@ public final class JupiterPerpsAlarmImpl {
|
||||
asset.oracleAccount()
|
||||
);
|
||||
assetDefinitions.forEach(definition -> System.out.printf(
|
||||
" %s %s USD, %s, severity=%s%n",
|
||||
definition.direction(),
|
||||
definition.targetExpression(),
|
||||
" %s USD, %s, severity=%s%n",
|
||||
definition.conditionExpression(),
|
||||
definition.trigger(),
|
||||
definition.severity()
|
||||
));
|
||||
@@ -155,6 +158,29 @@ public final class JupiterPerpsAlarmImpl {
|
||||
new CountDownLatch(1).await();
|
||||
}
|
||||
|
||||
private static void validateAlarmConditions(
|
||||
List<PriceAlarmDefinition> definitions,
|
||||
AlarmVariableResolver variableResolver
|
||||
) {
|
||||
for (PriceAlarmDefinition definition : definitions) {
|
||||
try {
|
||||
AlarmConditionParser.parse(
|
||||
variableResolver.resolve(definition.conditionExpression())
|
||||
);
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid condition for alarm "
|
||||
+ definition.id()
|
||||
+ " ("
|
||||
+ definition.conditionExpression()
|
||||
+ "): "
|
||||
+ exception.getMessage(),
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<JupiterPerpsAsset, List<PriceAlarmDefinition>> groupByAsset(
|
||||
List<PriceAlarmDefinition> definitions
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
|
||||
public final class PriceAlarm {
|
||||
@@ -22,24 +21,21 @@ public final class PriceAlarm {
|
||||
);
|
||||
}
|
||||
|
||||
BigDecimal target;
|
||||
PriceCondition condition;
|
||||
try {
|
||||
target = AlarmTargetParser.parse(
|
||||
variableResolver.resolve(definition.targetExpression())
|
||||
condition = AlarmConditionParser.parse(
|
||||
variableResolver.resolve(definition.conditionExpression())
|
||||
);
|
||||
} catch (RuntimeException exception) {
|
||||
System.err.printf(
|
||||
"Could not resolve target for alarm %d: %s%n",
|
||||
"Could not resolve condition for alarm %d: %s%n",
|
||||
definition.id(),
|
||||
exception.getMessage()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean reached = definition.direction().reached(
|
||||
price.priceUsd(),
|
||||
target
|
||||
);
|
||||
boolean reached = condition.matches(price.priceUsd());
|
||||
|
||||
boolean enteredTriggeredSide = previousReached == null
|
||||
? reached
|
||||
@@ -57,13 +53,13 @@ public final class PriceAlarm {
|
||||
return;
|
||||
}
|
||||
|
||||
trigger(price, target);
|
||||
trigger(price, condition);
|
||||
return;
|
||||
}
|
||||
|
||||
if (definition.trigger() == AlarmTrigger.PERSISTENT) {
|
||||
if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) {
|
||||
trigger(price, target);
|
||||
trigger(price, condition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -95,7 +91,7 @@ public final class PriceAlarm {
|
||||
);
|
||||
}
|
||||
|
||||
private void trigger(OraclePrice price, BigDecimal target) {
|
||||
private void trigger(OraclePrice price, PriceCondition condition) {
|
||||
triggerCount++;
|
||||
lastTriggeredAt = Instant.now();
|
||||
|
||||
@@ -115,8 +111,7 @@ public final class PriceAlarm {
|
||||
ResolvedPriceAlarm resolvedAlarm = new ResolvedPriceAlarm(
|
||||
definition.id(),
|
||||
definition.asset(),
|
||||
definition.direction(),
|
||||
target,
|
||||
condition,
|
||||
definition.trigger(),
|
||||
definition.triggerGracePeriod(),
|
||||
definition.severity(),
|
||||
|
||||
@@ -7,8 +7,7 @@ import java.util.Objects;
|
||||
public record PriceAlarmDefinition(
|
||||
int id,
|
||||
JupiterPerpsAsset asset,
|
||||
PriceDirection direction,
|
||||
String targetExpression,
|
||||
String conditionExpression,
|
||||
AlarmTrigger trigger,
|
||||
ΩsecondsΩ triggerGracePeriod,
|
||||
AlarmSeverity severity,
|
||||
@@ -16,14 +15,13 @@ public record PriceAlarmDefinition(
|
||||
) {
|
||||
public PriceAlarmDefinition {
|
||||
Objects.requireNonNull(asset, "asset");
|
||||
Objects.requireNonNull(direction, "direction");
|
||||
Objects.requireNonNull(targetExpression, "targetExpression");
|
||||
Objects.requireNonNull(conditionExpression, "conditionExpression");
|
||||
Objects.requireNonNull(trigger, "trigger");
|
||||
Objects.requireNonNull(severity, "severity");
|
||||
Objects.requireNonNull(note, "note");
|
||||
|
||||
if (targetExpression.isBlank()) {
|
||||
throw new IllegalArgumentException("Target expression cannot be blank");
|
||||
if (conditionExpression.isBlank()) {
|
||||
throw new IllegalArgumentException("Condition expression cannot be blank");
|
||||
}
|
||||
|
||||
if (triggerGracePeriod < 0) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public enum PriceComparisonOperator {
|
||||
LESS_THAN("<") {
|
||||
@Override
|
||||
public boolean matches(BigDecimal price, BigDecimal target) {
|
||||
return price.compareTo(target) < 0;
|
||||
}
|
||||
},
|
||||
LESS_THAN_OR_EQUAL("<=") {
|
||||
@Override
|
||||
public boolean matches(BigDecimal price, BigDecimal target) {
|
||||
return price.compareTo(target) <= 0;
|
||||
}
|
||||
},
|
||||
GREATER_THAN(">") {
|
||||
@Override
|
||||
public boolean matches(BigDecimal price, BigDecimal target) {
|
||||
return price.compareTo(target) > 0;
|
||||
}
|
||||
},
|
||||
GREATER_THAN_OR_EQUAL(">=") {
|
||||
@Override
|
||||
public boolean matches(BigDecimal price, BigDecimal target) {
|
||||
return price.compareTo(target) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
PriceComparisonOperator(String symbol) {
|
||||
this.symbol = symbol;
|
||||
}
|
||||
|
||||
public abstract boolean matches(BigDecimal price, BigDecimal target);
|
||||
|
||||
public String symbol() {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
private final String symbol;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public interface PriceCondition {
|
||||
|
||||
boolean matches(BigDecimal price);
|
||||
|
||||
String expression();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public enum PriceDirection {
|
||||
ABOVE {
|
||||
@Override
|
||||
public boolean reached(BigDecimal price, BigDecimal target) {
|
||||
return price.compareTo(target) >= 0;
|
||||
}
|
||||
},
|
||||
BELOW {
|
||||
@Override
|
||||
public boolean reached(BigDecimal price, BigDecimal target) {
|
||||
return price.compareTo(target) <= 0;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract boolean reached(BigDecimal price, BigDecimal target);
|
||||
}
|
||||
@@ -71,14 +71,13 @@ public final class PushoverAlarmAction implements AlarmAction {
|
||||
|
||||
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.",
|
||||
"%d - %s: %s%n%n%s is %s USD.%nCondition: %s USD.%nOracle time: %s.%nSlot: %d.",
|
||||
alarm.id(),
|
||||
alarm.severity(),
|
||||
alarm.note(),
|
||||
price.asset(),
|
||||
price.priceUsd().toPlainString(),
|
||||
alarm.direction(),
|
||||
alarm.target().toPlainString(),
|
||||
alarm.condition().expression(),
|
||||
price.oracleTime(),
|
||||
price.slot()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
public record RangePriceCondition(
|
||||
BigDecimal lowerTarget,
|
||||
boolean lowerInclusive,
|
||||
BigDecimal upperTarget,
|
||||
boolean upperInclusive
|
||||
) implements PriceCondition {
|
||||
|
||||
public RangePriceCondition {
|
||||
Objects.requireNonNull(lowerTarget, "lowerTarget");
|
||||
Objects.requireNonNull(upperTarget, "upperTarget");
|
||||
|
||||
if (lowerTarget.compareTo(upperTarget) > 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Range lower target must not exceed upper target: "
|
||||
+ lowerTarget.toPlainString()
|
||||
+ "-->"
|
||||
+ upperTarget.toPlainString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(BigDecimal price) {
|
||||
int lowerComparison = price.compareTo(lowerTarget);
|
||||
int upperComparison = price.compareTo(upperTarget);
|
||||
|
||||
boolean matchesLower = lowerInclusive
|
||||
? lowerComparison >= 0
|
||||
: lowerComparison > 0;
|
||||
|
||||
boolean matchesUpper = upperInclusive
|
||||
? upperComparison <= 0
|
||||
: upperComparison < 0;
|
||||
|
||||
return matchesLower && matchesUpper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String expression() {
|
||||
char lowerDelimiter = lowerInclusive ? '[' : '(';
|
||||
char upperDelimiter = upperInclusive ? ']' : ')';
|
||||
|
||||
return lowerDelimiter
|
||||
+ lowerTarget.toPlainString()
|
||||
+ "-->"
|
||||
+ upperTarget.toPlainString()
|
||||
+ upperDelimiter;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
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,
|
||||
PriceCondition condition,
|
||||
AlarmTrigger trigger,
|
||||
ΩsecondsΩ triggerGracePeriod,
|
||||
AlarmSeverity severity,
|
||||
String note
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user