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:
|
Example:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:60
|
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||||
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600
|
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported column values:
|
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.
|
- `ID`: integer alarm identifier. IDs should be unique. Action files use this ID to select alarms.
|
||||||
- `ASSET`: `SOL`, `ETH`, or `BTC`.
|
- `ASSET`: `SOL`, `ETH`, or `BTC`.
|
||||||
- `CONDITION`: a comparison or range expression. It must be one token with no whitespace.
|
- `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.
|
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.
|
- The first accepted price establishes the initial state and never triggers the alarm.
|
||||||
- Remaining inside the condition does not trigger again.
|
- Remaining inside the condition does not trigger again.
|
||||||
- Leaving the condition re-arms the alarm, so the next entry triggers 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:
|
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.
|
- Triggers immediately on the first accepted matching price.
|
||||||
- With no grace period, it can trigger for every accepted matching price event.
|
- 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.
|
- 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.
|
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
|
## Dynamic price-variable refresh
|
||||||
|
|||||||
+18
-18
@@ -20,31 +20,31 @@
|
|||||||
################################################################################################
|
################################################################################################
|
||||||
|
|
||||||
# SOL SHORT
|
# SOL SHORT
|
||||||
1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:60
|
1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||||
2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600
|
2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||||
3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600
|
3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||||
|
|
||||||
# SOL LONG
|
# SOL LONG
|
||||||
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:60
|
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||||
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600
|
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||||
6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600
|
6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||||
|
|
||||||
# ETH SHORT
|
# ETH SHORT
|
||||||
7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:60
|
7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||||
8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600
|
8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||||
9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600
|
9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||||
|
|
||||||
# ETH LONG
|
# ETH LONG
|
||||||
10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:60
|
10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||||
11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600
|
11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||||
12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600
|
12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||||
|
|
||||||
# BTC SHORT
|
# BTC SHORT
|
||||||
13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:60
|
13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:1m
|
||||||
14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600
|
14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:1h
|
||||||
15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600
|
15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:1h
|
||||||
|
|
||||||
# BTC LONG
|
# BTC LONG
|
||||||
16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:60
|
16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:1m
|
||||||
17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600
|
17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:1h
|
||||||
18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600
|
18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:1h
|
||||||
|
|||||||
+59
-26
@@ -4,6 +4,8 @@ import java.io.IOException;
|
|||||||
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.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public final class AlarmConfigurationParser {
|
public final class AlarmConfigurationParser {
|
||||||
|
|
||||||
@@ -91,41 +93,24 @@ public final class AlarmConfigurationParser {
|
|||||||
String normalized = triggerText.toUpperCase(Locale.ROOT);
|
String normalized = triggerText.toUpperCase(Locale.ROOT);
|
||||||
|
|
||||||
if (normalized.equals("ONETIME")) {
|
if (normalized.equals("ONETIME")) {
|
||||||
return new TriggerConfiguration(AlarmTrigger.ONETIME, 0);
|
return new TriggerConfiguration(AlarmTrigger.ONETIME, 0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.equals("CROSSING")) {
|
if (normalized.equals("CROSSING")) {
|
||||||
return new TriggerConfiguration(AlarmTrigger.CROSSING, 0);
|
return new TriggerConfiguration(AlarmTrigger.CROSSING, 0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.equals("PERSISTENT")) {
|
if (normalized.equals("PERSISTENT")) {
|
||||||
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, 0);
|
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, 0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.startsWith("PERSISTENT:")) {
|
if (normalized.startsWith("PERSISTENT:")) {
|
||||||
String graceText = normalized.substring("PERSISTENT:".length());
|
String graceText = triggerText.substring("PERSISTENT:".length());
|
||||||
|
|
||||||
if (graceText.isEmpty()) {
|
return new TriggerConfiguration(
|
||||||
throw new IllegalArgumentException("Missing persistent grace period: " + triggerText);
|
AlarmTrigger.PERSISTENT,
|
||||||
}
|
parsePersistentGracePeriod(graceText)
|
||||||
|
);
|
||||||
ΩsecondsΩ gracePeriodSeconds;
|
|
||||||
try {
|
|
||||||
gracePeriodSeconds = Integer.parseInt(graceText);
|
|
||||||
} catch (NumberFormatException exception) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Invalid persistent grace period: " + triggerText,
|
|
||||||
exception
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gracePeriodSeconds < 0) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Persistent grace period cannot be negative: " + triggerText
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, gracePeriodSeconds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.startsWith("ONETIME:")) {
|
if (normalized.startsWith("ONETIME:")) {
|
||||||
@@ -139,6 +124,51 @@ public final class AlarmConfigurationParser {
|
|||||||
throw new IllegalArgumentException("Unknown trigger: " + triggerText);
|
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 static final class Cursor {
|
||||||
private Cursor(String line) {
|
private Cursor(String line) {
|
||||||
this.line = line;
|
this.line = line;
|
||||||
@@ -245,10 +275,13 @@ public final class AlarmConfigurationParser {
|
|||||||
|
|
||||||
private record TriggerConfiguration(
|
private record TriggerConfiguration(
|
||||||
AlarmTrigger trigger,
|
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() {
|
private AlarmConfigurationParser() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,14 +91,14 @@ public final class PriceAlarm {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ΩsecondsΩ gracePeriod = definition.triggerGracePeriod();
|
ΩmilliSecondsΩ gracePeriod = definition.triggerGracePeriod();
|
||||||
|
|
||||||
if (gracePeriod == 0) {
|
if (gracePeriod == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !Instant.now().isBefore(
|
return !Instant.now().isBefore(
|
||||||
lastTriggeredAt.plusSeconds(gracePeriod)
|
lastTriggeredAt.plusMillis(gracePeriod)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public record PriceAlarmDefinition(
|
|||||||
JupiterPerpsAsset asset,
|
JupiterPerpsAsset asset,
|
||||||
String conditionExpression,
|
String conditionExpression,
|
||||||
AlarmTrigger trigger,
|
AlarmTrigger trigger,
|
||||||
ΩsecondsΩ triggerGracePeriod
|
ΩmilliSecondsΩ triggerGracePeriod
|
||||||
) {
|
) {
|
||||||
public PriceAlarmDefinition {
|
public PriceAlarmDefinition {
|
||||||
Objects.requireNonNull(asset, "asset");
|
Objects.requireNonNull(asset, "asset");
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ public record ResolvedPriceAlarm(
|
|||||||
JupiterPerpsAsset asset,
|
JupiterPerpsAsset asset,
|
||||||
PriceCondition condition,
|
PriceCondition condition,
|
||||||
AlarmTrigger trigger,
|
AlarmTrigger trigger,
|
||||||
ΩsecondsΩ triggerGracePeriod
|
ΩmilliSecondsΩ triggerGracePeriod
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user