46: Support human-readable time units for PERSISTENT alarm grace periods
This commit is contained in:
+20
-5
@@ -131,8 +131,8 @@ ID ASSET CONDITION TRIGGER
|
||||
Example:
|
||||
|
||||
```text
|
||||
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:60
|
||||
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600
|
||||
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
```
|
||||
|
||||
Supported column values:
|
||||
@@ -140,7 +140,7 @@ Supported column values:
|
||||
- `ID`: integer alarm identifier. IDs should be unique. Action files use this ID to select alarms.
|
||||
- `ASSET`: `SOL`, `ETH`, or `BTC`.
|
||||
- `CONDITION`: a comparison or range expression. It must be one token with no whitespace.
|
||||
- `TRIGGER`: `ONETIME`, `CROSSING`, `PERSISTENT`, or `PERSISTENT:<seconds>`.
|
||||
- `TRIGGER`: `ONETIME`, `CROSSING`, `PERSISTENT`, or `PERSISTENT:<duration>`.
|
||||
|
||||
A trailing comment is allowed after a variable value or trigger.
|
||||
|
||||
@@ -225,7 +225,7 @@ On the first accepted oracle price after startup, an already-satisfied `ONETIME`
|
||||
- The first accepted price establishes the initial state and never triggers the alarm.
|
||||
- Remaining inside the condition does not trigger again.
|
||||
- Leaving the condition re-arms the alarm, so the next entry triggers again.
|
||||
- A grace period is not supported; `CROSSING:<seconds>` is rejected.
|
||||
- A grace period is not supported; `CROSSING:1m` is rejected.
|
||||
|
||||
The condition determines the crossing direction. For example:
|
||||
|
||||
@@ -240,9 +240,24 @@ Alarm 19 triggers when the price enters the profitable side from below. Alarm 20
|
||||
|
||||
- Triggers immediately on the first accepted matching price.
|
||||
- With no grace period, it can trigger for every accepted matching price event.
|
||||
- `PERSISTENT:<seconds>` limits repeated triggering to at most once per configured interval while the condition remains satisfied.
|
||||
- `PERSISTENT:<duration>` limits repeated triggering to at most once per configured interval while the condition remains satisfied.
|
||||
- Leaving and re-entering a condition does not bypass the grace period measured from the previous trigger.
|
||||
|
||||
A duration is a non-negative whole number followed by exactly one suffix:
|
||||
|
||||
| Suffix | Unit |
|
||||
|---|---|
|
||||
| `ms` | millisecond |
|
||||
| `s` | second |
|
||||
| `m` | minute |
|
||||
| `h` | hour |
|
||||
| `d` | day |
|
||||
| `w` | week |
|
||||
| `M` | month (30 days) |
|
||||
| `y` | year (365 days) |
|
||||
|
||||
Suffixes are case-sensitive, so `m` means minute and `M` means month. Combined durations such as `1w3d` are not supported.
|
||||
|
||||
Duplicate price events received from redundant RPC endpoints are suppressed using the raw price, exponent, and oracle timestamp. Up to 512 recent event keys are retained per asset monitor.
|
||||
|
||||
## Dynamic price-variable refresh
|
||||
|
||||
+18
-18
@@ -20,31 +20,31 @@
|
||||
################################################################################################
|
||||
|
||||
# SOL SHORT
|
||||
1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:60
|
||||
2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600
|
||||
3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600
|
||||
1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
|
||||
# SOL LONG
|
||||
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:60
|
||||
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600
|
||||
6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600
|
||||
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
|
||||
# ETH SHORT
|
||||
7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:60
|
||||
8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600
|
||||
9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600
|
||||
7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
|
||||
# ETH LONG
|
||||
10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:60
|
||||
11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600
|
||||
12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600
|
||||
10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
|
||||
# BTC SHORT
|
||||
13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:60
|
||||
14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600
|
||||
15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600
|
||||
13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||
14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||
15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||
|
||||
# BTC LONG
|
||||
16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:60
|
||||
17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600
|
||||
18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600
|
||||
16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||
17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||
18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||
|
||||
+58
-25
@@ -4,6 +4,8 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class AlarmConfigurationParser {
|
||||
|
||||
@@ -91,43 +93,26 @@ public final class AlarmConfigurationParser {
|
||||
String normalized = triggerText.toUpperCase(Locale.ROOT);
|
||||
|
||||
if (normalized.equals("ONETIME")) {
|
||||
return new TriggerConfiguration(AlarmTrigger.ONETIME, 0);
|
||||
return new TriggerConfiguration(AlarmTrigger.ONETIME, 0L);
|
||||
}
|
||||
|
||||
if (normalized.equals("CROSSING")) {
|
||||
return new TriggerConfiguration(AlarmTrigger.CROSSING, 0);
|
||||
return new TriggerConfiguration(AlarmTrigger.CROSSING, 0L);
|
||||
}
|
||||
|
||||
if (normalized.equals("PERSISTENT")) {
|
||||
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, 0);
|
||||
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, 0L);
|
||||
}
|
||||
|
||||
if (normalized.startsWith("PERSISTENT:")) {
|
||||
String graceText = normalized.substring("PERSISTENT:".length());
|
||||
String graceText = triggerText.substring("PERSISTENT:".length());
|
||||
|
||||
if (graceText.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing persistent grace period: " + triggerText);
|
||||
}
|
||||
|
||||
ΩsecondsΩ gracePeriodSeconds;
|
||||
try {
|
||||
gracePeriodSeconds = Integer.parseInt(graceText);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid persistent grace period: " + triggerText,
|
||||
exception
|
||||
return new TriggerConfiguration(
|
||||
AlarmTrigger.PERSISTENT,
|
||||
parsePersistentGracePeriod(graceText)
|
||||
);
|
||||
}
|
||||
|
||||
if (gracePeriodSeconds < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Persistent grace period cannot be negative: " + triggerText
|
||||
);
|
||||
}
|
||||
|
||||
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, gracePeriodSeconds);
|
||||
}
|
||||
|
||||
if (normalized.startsWith("ONETIME:")) {
|
||||
throw new IllegalArgumentException("ONETIME cannot have a grace period: " + triggerText);
|
||||
}
|
||||
@@ -139,6 +124,51 @@ public final class AlarmConfigurationParser {
|
||||
throw new IllegalArgumentException("Unknown trigger: " + triggerText);
|
||||
}
|
||||
|
||||
private static ΩmilliSecondsΩ parsePersistentGracePeriod(String graceText) {
|
||||
Matcher matcher = PERSISTENT_GRACE_PERIOD_PATTERN.matcher(graceText);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
throw invalidPersistentGracePeriod(graceText);
|
||||
}
|
||||
|
||||
try {
|
||||
long value = Long.parseLong(matcher.group(1));
|
||||
long millisecondsPerUnit = switch (matcher.group(2)) {
|
||||
case "ms" -> 1L;
|
||||
case "s" -> 1_000L;
|
||||
case "m" -> 60_000L;
|
||||
case "h" -> 3_600_000L;
|
||||
case "d" -> 86_400_000L;
|
||||
case "w" -> 604_800_000L;
|
||||
case "M" -> 2_592_000_000L;
|
||||
case "y" -> 31_536_000_000L;
|
||||
default -> throw new AssertionError("Unsupported grace period suffix");
|
||||
};
|
||||
|
||||
return Math.multiplyExact(value, millisecondsPerUnit);
|
||||
} catch (NumberFormatException | ArithmeticException exception) {
|
||||
throw invalidPersistentGracePeriod(graceText, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static IllegalArgumentException invalidPersistentGracePeriod(
|
||||
String graceText
|
||||
) {
|
||||
return invalidPersistentGracePeriod(graceText, null);
|
||||
}
|
||||
|
||||
private static IllegalArgumentException invalidPersistentGracePeriod(
|
||||
String graceText,
|
||||
Exception cause
|
||||
) {
|
||||
return new IllegalArgumentException(
|
||||
"Invalid persistent grace period '" + graceText
|
||||
+ "'. Expected a non-negative whole number followed by one of: "
|
||||
+ "ms, s, m, h, d, w, M, y",
|
||||
cause
|
||||
);
|
||||
}
|
||||
|
||||
private static final class Cursor {
|
||||
private Cursor(String line) {
|
||||
this.line = line;
|
||||
@@ -245,10 +275,13 @@ public final class AlarmConfigurationParser {
|
||||
|
||||
private record TriggerConfiguration(
|
||||
AlarmTrigger trigger,
|
||||
ΩsecondsΩ gracePeriod
|
||||
ΩmilliSecondsΩ gracePeriod
|
||||
) {
|
||||
}
|
||||
|
||||
private static final Pattern PERSISTENT_GRACE_PERIOD_PATTERN =
|
||||
Pattern.compile("(\\d+)(ms|s|m|h|d|w|M|y)");
|
||||
|
||||
private AlarmConfigurationParser() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,14 +91,14 @@ public final class PriceAlarm {
|
||||
return true;
|
||||
}
|
||||
|
||||
ΩsecondsΩ gracePeriod = definition.triggerGracePeriod();
|
||||
ΩmilliSecondsΩ gracePeriod = definition.triggerGracePeriod();
|
||||
|
||||
if (gracePeriod == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !Instant.now().isBefore(
|
||||
lastTriggeredAt.plusSeconds(gracePeriod)
|
||||
lastTriggeredAt.plusMillis(gracePeriod)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ public record PriceAlarmDefinition(
|
||||
JupiterPerpsAsset asset,
|
||||
String conditionExpression,
|
||||
AlarmTrigger trigger,
|
||||
ΩsecondsΩ triggerGracePeriod
|
||||
ΩmilliSecondsΩ triggerGracePeriod
|
||||
) {
|
||||
public PriceAlarmDefinition {
|
||||
Objects.requireNonNull(asset, "asset");
|
||||
|
||||
@@ -5,6 +5,6 @@ public record ResolvedPriceAlarm(
|
||||
JupiterPerpsAsset asset,
|
||||
PriceCondition condition,
|
||||
AlarmTrigger trigger,
|
||||
ΩsecondsΩ triggerGracePeriod
|
||||
ΩmilliSecondsΩ triggerGracePeriod
|
||||
) {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user