37: Add re-armable CROSSING alarm trigger

This commit is contained in:
2026-08-05 10:12:38 +02:00
parent e59aff95b8
commit d2f28af1bc
4 changed files with 39 additions and 2 deletions
+19 -2
View File
@@ -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`, `PERSISTENT`, or `PERSISTENT:<seconds>`. - `TRIGGER`: `ONETIME`, `CROSSING`, `PERSISTENT`, or `PERSISTENT:<seconds>`.
A trailing comment is allowed after a variable value or trigger. A trailing comment is allowed after a variable value or trigger.
@@ -210,7 +210,7 @@ At exactly `LIQ+1%`, only the first condition matches. The corresponding short-p
## Trigger behavior ## Trigger behavior
On the first accepted oracle price after startup or reconnect, an already-satisfied alarm can trigger immediately. On the first accepted oracle price after startup, an already-satisfied `ONETIME` or `PERSISTENT` alarm can trigger immediately. A `CROSSING` alarm only establishes its initial state.
### `ONETIME` ### `ONETIME`
@@ -219,6 +219,23 @@ On the first accepted oracle price after startup or reconnect, an already-satisf
- Leaving and re-entering the condition does not re-arm it. - Leaving and re-entering the condition does not re-arm it.
- Restarting the application re-arms it because alarm state is held only in memory. - Restarting the application re-arms it because alarm state is held only in memory.
### `CROSSING`
- Triggers when the condition changes from not satisfied to satisfied.
- 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.
The condition determines the crossing direction. For example:
```text
19 SOL >={{SOL_LONG_ENTRY_PRICE}} CROSSING
20 SOL <{{SOL_LONG_ENTRY_PRICE}} CROSSING
```
Alarm 19 triggers when the price enters the profitable side from below. Alarm 20 triggers when it enters the losing side from above. With a range condition, `CROSSING` triggers whenever the price enters the range from either side.
### `PERSISTENT` ### `PERSISTENT`
- Triggers immediately on the first accepted matching price. - Triggers immediately on the first accepted matching price.
@@ -94,6 +94,10 @@ public final class AlarmConfigurationParser {
return new TriggerConfiguration(AlarmTrigger.ONETIME, 0); return new TriggerConfiguration(AlarmTrigger.ONETIME, 0);
} }
if (normalized.equals("CROSSING")) {
return new TriggerConfiguration(AlarmTrigger.CROSSING, 0);
}
if (normalized.equals("PERSISTENT")) { if (normalized.equals("PERSISTENT")) {
return new TriggerConfiguration(AlarmTrigger.PERSISTENT, 0); return new TriggerConfiguration(AlarmTrigger.PERSISTENT, 0);
} }
@@ -128,6 +132,10 @@ public final class AlarmConfigurationParser {
throw new IllegalArgumentException("ONETIME cannot have a grace period: " + triggerText); throw new IllegalArgumentException("ONETIME cannot have a grace period: " + triggerText);
} }
if (normalized.startsWith("CROSSING:")) {
throw new IllegalArgumentException("CROSSING cannot have a grace period: " + triggerText);
}
throw new IllegalArgumentException("Unknown trigger: " + triggerText); throw new IllegalArgumentException("Unknown trigger: " + triggerText);
} }
@@ -2,5 +2,6 @@ package com.r35157.jupiterperpsalarm.impl.ref;
public enum AlarmTrigger { public enum AlarmTrigger {
ONETIME, ONETIME,
CROSSING,
PERSISTENT PERSISTENT
} }
@@ -41,6 +41,10 @@ public final class PriceAlarm {
? reached ? reached
: reached && !previousReached; : reached && !previousReached;
boolean crossedIntoTriggeredSide = previousReached != null
&& reached
&& !previousReached;
previousReached = reached; previousReached = reached;
if (!reached) { if (!reached) {
@@ -57,6 +61,13 @@ public final class PriceAlarm {
return; return;
} }
if (definition.trigger() == AlarmTrigger.CROSSING) {
if (crossedIntoTriggeredSide) {
trigger(price, condition);
}
return;
}
if (definition.trigger() == AlarmTrigger.PERSISTENT) { if (definition.trigger() == AlarmTrigger.PERSISTENT) {
if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) { if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) {
trigger(price, condition); trigger(price, condition);