34: Combine alarm direction and target into one condition
This commit is contained in:
+268
-75
@@ -1,122 +1,315 @@
|
|||||||
# Jupiter Perps Price Alarm
|
# Jupiter Perps Price Alarm
|
||||||
|
|
||||||
A small Java program that listens to Jupiter Perps' on-chain aggregated oracle accounts through Solana WebSocket `accountSubscribe`.
|
The Jupiter Perps Price Alarm monitors the Jupiter Perps aggregated oracle accounts for SOL, ETH, and BTC. It evaluates configurable price conditions and dispatches matching alarms to the console, an optional Pushover account, and the currently hard-coded Jupiter position-increase action.
|
||||||
|
|
||||||
It does **not** poll once per second. Every account update observed by the connected RPC node is decoded immediately. The program reconnects automatically, performs an initial/reconnect state fetch, and can connect to multiple independent RPC endpoints for redundancy.
|
Oracle prices are received through Solana WebSocket `accountSubscribe` using `processed` commitment. The application does not poll prices once per second. It also fetches the current oracle account state whenever a WebSocket connection opens, reconnects automatically, and supports multiple RPC endpoints for redundancy.
|
||||||
|
|
||||||
The program supports any number of alarms for SOL, ETH, and BTC. Only one oracle stream is created per configured asset and RPC endpoint, regardless of how many alarms use that asset.
|
> **Important:** This application is not currently notification-only. Alarm IDs `4` and `5` can sign and submit real Jupiter Perps position-increase transactions. See [Automatic position increase](#automatic-position-increase) before running it.
|
||||||
|
|
||||||
## Java version
|
## Startup flow
|
||||||
|
|
||||||
The project uses the current JDK compiler but generates Java 17-compatible class files:
|
The application performs the following steps before it starts monitoring prices:
|
||||||
|
|
||||||
```gradle
|
1. Parse command-line options and the alarm configuration file.
|
||||||
tasks.withType(JavaCompile).configureEach {
|
2. Fetch the wallet's current open Jupiter Perps positions.
|
||||||
options.release = 17
|
3. Populate entry-price and liquidation-price variables.
|
||||||
}
|
4. Resolve and validate every alarm condition.
|
||||||
|
5. Start the periodic variable refresher and file-trigger watcher.
|
||||||
|
6. Configure console, automatic Jupiter, and optional Pushover actions.
|
||||||
|
7. Open one oracle WebSocket connection per configured asset and RPC endpoint.
|
||||||
|
|
||||||
|
Startup fails before WebSockets and alarm actions are started if a condition contains an unresolved variable, invalid syntax, or an invalid range.
|
||||||
|
|
||||||
|
## Requirements and build
|
||||||
|
|
||||||
|
The project uses:
|
||||||
|
|
||||||
|
- JDK 25
|
||||||
|
- Gradle 9.3.1 through the Gradle wrapper
|
||||||
|
- Java preview features
|
||||||
|
- Detag-generated Java sources from `src/main/tjava`
|
||||||
|
|
||||||
|
Compile the project with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew classes
|
||||||
```
|
```
|
||||||
|
|
||||||
## Alarm configuration
|
To create the application JAR and collect runtime dependencies in `build/libs`:
|
||||||
|
|
||||||
The default file is `price-alarms.conf`:
|
```bash
|
||||||
|
./gradlew prepareLibs
|
||||||
|
```
|
||||||
|
|
||||||
|
There is currently no alarm-specific automated self-test task.
|
||||||
|
|
||||||
|
## Running the alarm application
|
||||||
|
|
||||||
|
The alarm entry point is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
# Asset Direction Target TRIGGER SEVERITY NOTE
|
com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl
|
||||||
##################################################################################################
|
|
||||||
SOL ABOVE 75.7 ONETIME 2 "ALARM: Risiko for Perps Solana short LIKVIDERING!"
|
|
||||||
SOL BELOW 60.8 ONETIME 2 "ALARM: Risiko for Perps Solana long LIKVIDERING!"
|
|
||||||
SOL BELOW 71.4 ONETIME 2 "ALARM: Risiko for Solana Raydium LÅN LIKVIDERING!"
|
|
||||||
ETH ABOVE 1848.41 ONETIME 2 "ALARM: Risiko for Perps Ethereum short LIKVIDERING!"
|
|
||||||
ETH BELOW 1789 ONETIME 1 "OK: Perps Ethereum short er lukket!"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported values:
|
The Gradle application plugin currently points at the Nenjim Hub main class, so `./gradlew run` does **not** directly start the alarm application. Run `JupiterPerpsAlarmImpl` from the IDE, or run the prepared JAR and dependencies explicitly:
|
||||||
|
|
||||||
- Asset: `SOL`, `ETH`, or `BTC`
|
```bash
|
||||||
- Direction: `ABOVE` or `BELOW`
|
java --enable-preview \
|
||||||
- Trigger: `ONETIME` or `PERSISTENT`
|
-cp "build/libs/*" \
|
||||||
- Target: positive decimal USD price
|
com.r35157.jupiterperpsalarm.impl.ref.JupiterPerpsAlarmImpl \
|
||||||
- Severity: zero or positive integer
|
--config=conf/alarms.conf
|
||||||
- Note: quoted text; escaped quotes can be written as `\"`
|
```
|
||||||
|
|
||||||
`SEVERITY` and `NOTE` are parsed and retained in `PriceAlarmDefinition`, but are intentionally not used by the alarm actions yet.
|
The repository contains [the example configuration](conf/alarms.conf.example). Keep the real configuration outside version control if it contains operational wallet details.
|
||||||
|
|
||||||
|
### Command-line options and environment variables
|
||||||
|
|
||||||
|
| Purpose | Command-line option | Environment variable | Default |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Alarm configuration | `--config=<path>` | `PRICE_ALARMS_CONFIG` | `price-alarms.conf` |
|
||||||
|
| Solana WebSocket endpoints | `--ws=<url1,url2,...>` | `SOLANA_WS_URLS` | `wss://api.mainnet-beta.solana.com` |
|
||||||
|
| Pushover application token | — | `PUSHOVER_APP_TOKEN` | Disabled |
|
||||||
|
| Pushover user/group key | — | `PUSHOVER_USER_KEY` | Disabled |
|
||||||
|
|
||||||
|
The command-line option takes precedence over its environment variable. Pushover is enabled only when both Pushover variables are non-blank.
|
||||||
|
|
||||||
|
The variable-refresh file watcher uses the configuration file's parent directory. Until that is changed, use a configuration path with an explicit directory component, such as `conf/alarms.conf` or `./price-alarms.conf`, rather than relying on the bare default filename.
|
||||||
|
|
||||||
|
## Configuration format
|
||||||
|
|
||||||
|
Blank lines and lines beginning with `#` are ignored. The file contains variable definitions and alarm definitions.
|
||||||
|
|
||||||
|
### Variable definitions
|
||||||
|
|
||||||
|
A variable definition consists of a name and a value:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{{JUPITER_PERPS_WALLET}} vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf
|
||||||
|
```
|
||||||
|
|
||||||
|
Variable names must use uppercase letters, digits, and underscores. Variables can be referenced in conditions and notes as `{{NAME}}`.
|
||||||
|
|
||||||
|
`JUPITER_PERPS_WALLET` is required for the initial and periodic Jupiter position fetches. For open SOL, BTC, and ETH positions, the refresher supplies these dynamic variables:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{{SOL_LONG_ENTRY_PRICE}} {{SOL_LONG_LIQ_PRICE}}
|
||||||
|
{{SOL_SHORT_ENTRY_PRICE}} {{SOL_SHORT_LIQ_PRICE}}
|
||||||
|
{{BTC_LONG_ENTRY_PRICE}} {{BTC_LONG_LIQ_PRICE}}
|
||||||
|
{{BTC_SHORT_ENTRY_PRICE}} {{BTC_SHORT_LIQ_PRICE}}
|
||||||
|
{{ETH_LONG_ENTRY_PRICE}} {{ETH_LONG_LIQ_PRICE}}
|
||||||
|
{{ETH_SHORT_ENTRY_PRICE}} {{ETH_SHORT_LIQ_PRICE}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only variables for positions returned by Jupiter are populated. Startup validation fails if an alarm references a variable that is unavailable after the initial position fetch.
|
||||||
|
|
||||||
|
### Alarm definitions
|
||||||
|
|
||||||
|
Each alarm is one line with six columns:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ID ASSET CONDITION TRIGGER SEVERITY "NOTE"
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:60 CRITICAL "🚨 SOL Long add 1.1x"
|
||||||
|
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600 INFO "🌱 SOL Long add 3x"
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported column values:
|
||||||
|
|
||||||
|
- `ID`: integer alarm identifier. IDs should be unique. IDs `4` and `5` have special transaction behavior in the current implementation.
|
||||||
|
- `ASSET`: `SOL`, `ETH`, or `BTC`.
|
||||||
|
- `CONDITION`: a comparison or range expression. It must be one token with no whitespace.
|
||||||
|
- `TRIGGER`: `ONETIME`, `PERSISTENT`, or `PERSISTENT:<seconds>`.
|
||||||
|
- `SEVERITY`: `EMERGENCY`, `CRITICAL`, `WARN`, `INFO`, `SILENT`, or `GHOST`.
|
||||||
|
- `NOTE`: quoted text. Supported escapes include `\n`, `\r`, `\t`, `\"`, and `\\`.
|
||||||
|
|
||||||
|
A trailing comment is allowed after a variable value or the quoted note.
|
||||||
|
|
||||||
|
## Condition expressions
|
||||||
|
|
||||||
|
A condition is evaluated against the current oracle price in USD. Conditions are resolved again for every accepted price event, so periodically refreshed entry and liquidation prices take effect without restarting the application.
|
||||||
|
|
||||||
|
The complete condition must not contain whitespace.
|
||||||
|
|
||||||
|
### Target expressions
|
||||||
|
|
||||||
|
Each target can be:
|
||||||
|
|
||||||
|
```text
|
||||||
|
75.50
|
||||||
|
{{SOL_LONG_LIQ_PRICE}}
|
||||||
|
{{SOL_LONG_LIQ_PRICE}}+1%
|
||||||
|
{{SOL_SHORT_LIQ_PRICE}}-3%
|
||||||
|
```
|
||||||
|
|
||||||
|
`BASE+P%` adds `P` percent of `BASE`; `BASE-P%` subtracts it. The base, percentage, and resulting target must be zero or positive. General arithmetic expressions are not supported.
|
||||||
|
|
||||||
|
### Comparisons
|
||||||
|
|
||||||
|
| Syntax | Matches when |
|
||||||
|
|---|---|
|
||||||
|
| `<X` | `price < X` |
|
||||||
|
| `<=X` | `price <= X` |
|
||||||
|
| `>X` | `price > X` |
|
||||||
|
| `>=X` | `price >= X` |
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<={{SOL_LONG_LIQ_PRICE}}+1%
|
||||||
|
>={{SOL_SHORT_LIQ_PRICE}}-1%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ranges
|
||||||
|
|
||||||
|
Ranges use `-->` between a lower and an upper target. Parentheses exclude a boundary; square brackets include it.
|
||||||
|
|
||||||
|
| Syntax | Matches when |
|
||||||
|
|---|---|
|
||||||
|
| `(A-->B)` | `A < price < B` |
|
||||||
|
| `[A-->B)` | `A <= price < B` |
|
||||||
|
| `(A-->B]` | `A < price <= B` |
|
||||||
|
| `[A-->B]` | `A <= price <= B` |
|
||||||
|
|
||||||
|
The lower resolved target must not exceed the upper resolved target.
|
||||||
|
|
||||||
|
Example of two adjacent, non-overlapping long-position conditions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<={{SOL_LONG_LIQ_PRICE}}+1%
|
||||||
|
({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%]
|
||||||
|
```
|
||||||
|
|
||||||
|
At exactly `LIQ+1%`, only the first condition matches. The corresponding short-position split is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
>={{SOL_SHORT_LIQ_PRICE}}-1%
|
||||||
|
[{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%)
|
||||||
|
```
|
||||||
|
|
||||||
## Trigger behavior
|
## Trigger behavior
|
||||||
|
|
||||||
On the first received price after program start:
|
On the first accepted oracle price after startup or reconnect, an already-satisfied alarm can trigger immediately.
|
||||||
|
|
||||||
- An already satisfied alarm triggers immediately.
|
### `ONETIME`
|
||||||
- An unsatisfied alarm waits for the price to cross into its triggered side.
|
|
||||||
|
|
||||||
Alarm state is retained across WebSocket reconnects within the same process. If the price moves from the safe side to the triggered side during a connection outage, the first price received after reconnect will therefore trigger the alarm.
|
- Triggers on the first accepted price that satisfies the condition.
|
||||||
|
- Triggers at most once during the current process lifetime.
|
||||||
|
- 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.
|
||||||
|
|
||||||
After that:
|
### `PERSISTENT`
|
||||||
|
|
||||||
- `ONETIME` triggers only once during the current program run.
|
- Triggers immediately on the first accepted matching price.
|
||||||
- `PERSISTENT` triggers each time the price crosses from the safe side into the triggered side.
|
- With no grace period, it can trigger for every accepted matching price event.
|
||||||
- Remaining on the triggered side does not repeatedly fire the alarm.
|
- `PERSISTENT:<seconds>` 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.
|
||||||
|
|
||||||
`ONETIME` state is currently kept in memory. Restarting the process arms the alarm again.
|
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.
|
||||||
|
|
||||||
## Build and test
|
## Dynamic price-variable refresh
|
||||||
|
|
||||||
|
Entry-price and liquidation-price variables are refreshed:
|
||||||
|
|
||||||
|
- once during startup, before condition validation;
|
||||||
|
- every 60 seconds in a daemon task;
|
||||||
|
- when `jupiter-perps-alarm-var.refresh` is created in the configuration directory.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gradle classes
|
touch conf/jupiter-perps-alarm-var.refresh
|
||||||
gradle run --args='--self-test'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The self-test covers:
|
The watcher detects creation of the file, refreshes the variables, and then deletes the trigger file.
|
||||||
|
|
||||||
- binary oracle decoding
|
When a dynamic price variable changes, the application logs its old and new value.
|
||||||
- configuration parsing
|
|
||||||
- initial satisfied alarm behavior
|
|
||||||
- `ONETIME` behavior
|
|
||||||
- `PERSISTENT` crossing behavior
|
|
||||||
|
|
||||||
## Run
|
## Alarm actions
|
||||||
|
|
||||||
Using the default `price-alarms.conf` in the working directory:
|
### Console
|
||||||
|
|
||||||
```bash
|
Console output is always enabled. It includes the asset, current oracle price, resolved condition, trigger type, oracle time, slot, and source endpoint.
|
||||||
gradle run
|
|
||||||
|
Actions are executed sequentially through `CompositeAlarmAction`. A runtime failure in one action is logged so the remaining actions can still run.
|
||||||
|
|
||||||
|
### Pushover
|
||||||
|
|
||||||
|
Pushover is enabled when both `PUSHOVER_APP_TOKEN` and `PUSHOVER_USER_KEY` are configured.
|
||||||
|
|
||||||
|
| Severity | Pushover parameters |
|
||||||
|
|---|---|
|
||||||
|
| `EMERGENCY` | `priority=2`, `retry=30`, `expire=10800`, `sound=persistent` |
|
||||||
|
| `CRITICAL` | `priority=1`, `sound=spacealarm` |
|
||||||
|
| `WARN` | `priority=0`, `sound=siren` |
|
||||||
|
| `INFO` | `priority=0` |
|
||||||
|
| `SILENT` | `priority=-1` |
|
||||||
|
| `GHOST` | `priority=-2` |
|
||||||
|
|
||||||
|
Pushover requests are sent asynchronously with a 15-second request timeout. Rejected requests and asynchronous failures are logged.
|
||||||
|
|
||||||
|
### Automatic position increase
|
||||||
|
|
||||||
|
The current reference implementation always registers `JupiterPerpsPositionIncreaseAlarmAction`. It performs no transaction for most alarms, but alarm IDs `4` and `5` have hard-coded live behavior:
|
||||||
|
|
||||||
|
| Alarm ID | Asset/direction | USDC collateral | Position-size delta | Maximum slippage |
|
||||||
|
|---|---|---:|---:|---:|
|
||||||
|
| `4` | SOL long | `0.25` | `2.50` USD | `200` bps |
|
||||||
|
| `5` | SOL long | `0.25` | `6.25` USD | `200` bps |
|
||||||
|
|
||||||
|
For these IDs, the action builds a Jupiter position-increase transaction, signs it by running the external `jup sign` command with key name `evelyn-prod`, verifies that the returned signer matches the hard-coded wallet address, and submits the signed transaction.
|
||||||
|
|
||||||
|
The hard-coded wallet address is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf
|
||||||
```
|
```
|
||||||
|
|
||||||
Using another file:
|
Changing which alarm uses ID `4` or `5` does not change this action's hard-coded SOL-long behavior. Treat those IDs as operationally significant, and do not run the application unless the wallet, signer key, amounts, slippage, and conditions have been reviewed.
|
||||||
|
|
||||||
```bash
|
## RPC redundancy and reconnect behavior
|
||||||
gradle run --args='--config=/path/to/price-alarms.conf'
|
|
||||||
```
|
|
||||||
|
|
||||||
The path can also be selected through:
|
Provide multiple comma-separated WebSocket endpoints for redundancy:
|
||||||
|
|
||||||
```bash
|
|
||||||
export PRICE_ALARMS_CONFIG='/path/to/price-alarms.conf'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use two RPC WebSocket streams
|
|
||||||
|
|
||||||
A single WebSocket/RPC provider is not a durable event log. For better resilience, provide two independent endpoints:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SOLANA_WS_URLS='wss://first-provider.example,wss://second-provider.example'
|
export SOLANA_WS_URLS='wss://first-provider.example,wss://second-provider.example'
|
||||||
gradle run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
With two configured assets and two RPC endpoints, the program opens four WebSocket connections.
|
The application creates one WebSocket connection for every configured asset/endpoint combination. With three configured assets and two endpoints, it creates six connections.
|
||||||
|
|
||||||
## Pushover emergency alarm
|
Each connection:
|
||||||
|
|
||||||
```bash
|
- subscribes with `processed` commitment and Base64 account encoding;
|
||||||
export PUSHOVER_APP_TOKEN='...'
|
- fetches the current account state after opening;
|
||||||
export PUSHOVER_USER_KEY='...'
|
- sends a heartbeat every 20 seconds;
|
||||||
gradle run
|
- reconnects with exponential delays from 1 to 30 seconds.
|
||||||
```
|
|
||||||
|
|
||||||
The current implementation sends `priority=2`, `retry=30`, `expire=10800`, and `sound=persistent`.
|
## Validation and errors
|
||||||
|
|
||||||
## Important limitations
|
Structural configuration errors include the file path and line number. After the initial Jupiter position fetch, every condition is resolved and parsed before periodic refresh, WebSockets, and alarm actions are started.
|
||||||
|
|
||||||
- `processed` is intentionally used for minimum delay, but a processed update may belong to a fork that is later abandoned.
|
Startup validation rejects, among other things:
|
||||||
- Solana PubSub is not guaranteed delivery. Two independent RPC streams reduce, but do not eliminate, the risk of missing an update.
|
|
||||||
- The alarm reports the Jupiter Perps oracle price. It does not prove that a specific position was liquidated.
|
- unknown or unavailable variables;
|
||||||
- This is an alerting aid, not a substitute for placing an on-platform stop-loss or reducing leverage.
|
- missing or unknown comparison operators;
|
||||||
|
- conditions split by whitespace;
|
||||||
|
- malformed ranges or multiple `-->` separators;
|
||||||
|
- missing range targets;
|
||||||
|
- negative target values or percentages;
|
||||||
|
- ranges whose lower target exceeds the upper target.
|
||||||
|
|
||||||
|
At runtime, a later resolution/parsing failure is logged for the affected alarm and that price event is skipped for that alarm.
|
||||||
|
|
||||||
|
## Current limitations
|
||||||
|
|
||||||
|
- `processed` commitment minimizes delay but an observed update may belong to a fork that is later abandoned.
|
||||||
|
- Solana PubSub is not a durable event log and does not guarantee delivery. Multiple independent endpoints reduce, but do not eliminate, this risk.
|
||||||
|
- Alarm state and grace-period timestamps are kept only in memory and reset on restart.
|
||||||
|
- Dynamic variables exist only for currently returned open positions.
|
||||||
|
- Target expressions support only a decimal value with an optional single percentage adjustment; they are not a general expression language.
|
||||||
|
- The automatic position-increase action is hard-coded to one wallet, signer key, asset, direction, and two alarm IDs.
|
||||||
|
- A bare default configuration path has no parent directory for the refresh watcher; use an explicit path such as `./price-alarms.conf` or `conf/alarms.conf`.
|
||||||
|
- This application is a monitoring and automation aid, not a substitute for independent risk controls.
|
||||||
+19
-19
@@ -16,35 +16,35 @@
|
|||||||
#{{BTC_SHORT_LIQ_PRICE}} 63275.97
|
#{{BTC_SHORT_LIQ_PRICE}} 63275.97
|
||||||
|
|
||||||
|
|
||||||
# Id Asset Direction Target Trigger Severity Note
|
# Id Asset Condition Trigger Severity Note
|
||||||
#################################################################################################################################
|
#################################################################################################################################
|
||||||
|
|
||||||
# SOL SHORT
|
# SOL SHORT
|
||||||
1 SOL ABOVE {{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:60 CRITICAL "🚨 SOL Short add 1.1x"
|
1 SOL >={{SOL_SHORT_LIQ_PRICE}}-1% PERSISTENT:60 CRITICAL "🚨 SOL Short add 1.1x"
|
||||||
2 SOL ABOVE {{SOL_SHORT_LIQ_PRICE}}-3% PERSISTENT:3600 INFO "🌱 SOL Short add 3x"
|
2 SOL [{{SOL_SHORT_LIQ_PRICE}}-3%-->{{SOL_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600 INFO "🌱 SOL Short add 3x"
|
||||||
3 SOL BELOW {{SOL_SHORT_ENTRY_PRICE}}-3% PERSISTENT:3600 INFO "💵 SOL Short close"
|
3 SOL <={{SOL_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600 INFO "💵 SOL Short close"
|
||||||
|
|
||||||
# SOL LONG
|
# SOL LONG
|
||||||
4 SOL BELOW {{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:60 CRITICAL "🚨 SOL Long add 1.1x"
|
4 SOL <={{SOL_LONG_LIQ_PRICE}}+1% PERSISTENT:60 CRITICAL "🚨 SOL Long add 1.1x"
|
||||||
5 SOL BELOW {{SOL_LONG_LIQ_PRICE}}+3% PERSISTENT:3600 INFO "🌱 SOL Long add 3x"
|
5 SOL ({{SOL_LONG_LIQ_PRICE}}+1%-->{{SOL_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600 INFO "🌱 SOL Long add 3x"
|
||||||
6 SOL ABOVE {{SOL_LONG_ENTRY_PRICE}}+3% PERSISTENT:3600 INFO "💵 SOL Long close"
|
6 SOL >={{SOL_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600 INFO "💵 SOL Long close"
|
||||||
|
|
||||||
# ETH SHORT
|
# ETH SHORT
|
||||||
7 ETH ABOVE {{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:60 CRITICAL "🚨 ETH Short add 1.1x"
|
7 ETH >={{ETH_SHORT_LIQ_PRICE}}-1% PERSISTENT:60 CRITICAL "🚨 ETH Short add 1.1x"
|
||||||
8 ETH ABOVE {{ETH_SHORT_LIQ_PRICE}}-3% PERSISTENT:3600 INFO "🌱 ETH Short add 3x"
|
8 ETH [{{ETH_SHORT_LIQ_PRICE}}-3%-->{{ETH_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600 INFO "🌱 ETH Short add 3x"
|
||||||
9 ETH BELOW {{ETH_SHORT_ENTRY_PRICE}}-3% PERSISTENT:3600 INFO "💵 ETH Short close"
|
9 ETH <={{ETH_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600 INFO "💵 ETH Short close"
|
||||||
|
|
||||||
# ETH LONG
|
# ETH LONG
|
||||||
10 ETH BELOW {{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:60 CRITICAL "🚨 ETH Long add 1.1x"
|
10 ETH <={{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:60 CRITICAL "🚨 ETH Long add 1.1x"
|
||||||
11 ETH BELOW {{ETH_LONG_LIQ_PRICE}}+1% PERSISTENT:3600 INFO "🌱 ETH Long add 3x"
|
11 ETH ({{ETH_LONG_LIQ_PRICE}}+1%-->{{ETH_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600 INFO "🌱 ETH Long add 3x"
|
||||||
12 ETH ABOVE {{ETH_LONG_ENTRY_PRICE}}+3% PERSISTENT:3600 INFO "💵 ETH Long close"
|
12 ETH >={{ETH_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600 INFO "💵 ETH Long close"
|
||||||
|
|
||||||
# BTC SHORT
|
# BTC SHORT
|
||||||
13 BTC ABOVE {{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:60 CRITICAL "🚨 BTC Short add 1.1x"
|
13 BTC >={{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:60 CRITICAL "🚨 BTC Short add 1.1x"
|
||||||
14 BTC ABOVE {{BTC_SHORT_LIQ_PRICE}}-1% PERSISTENT:3600 INFO "🌱 BTC Short add 3x"
|
14 BTC [{{BTC_SHORT_LIQ_PRICE}}-3%-->{{BTC_SHORT_LIQ_PRICE}}-1%) PERSISTENT:3600 INFO "🌱 BTC Short add 3x"
|
||||||
15 BTC BELOW {{BTC_SHORT_ENTRY_PRICE}}-3% PERSISTENT:3600 INFO "💵 BTC Short close"
|
15 BTC <={{BTC_SHORT_ENTRY_PRICE}}-5% PERSISTENT:3600 INFO "💵 BTC Short close"
|
||||||
|
|
||||||
# BTC LONG
|
# BTC LONG
|
||||||
16 BTC BELOW {{BTC_LONG_LIQ_PRICE}}+0.5% PERSISTENT:60 CRITICAL "🚨 BTC Long add 1.1x"
|
16 BTC <={{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:60 CRITICAL "🚨 BTC Long add 1.1x"
|
||||||
17 BTC BELOW {{BTC_LONG_LIQ_PRICE}}+1% PERSISTENT:3600 INFO "🌱 BTC Long add 3x"
|
17 BTC ({{BTC_LONG_LIQ_PRICE}}+1%-->{{BTC_LONG_LIQ_PRICE}}+3%] PERSISTENT:3600 INFO "🌱 BTC Long add 3x"
|
||||||
18 BTC ABOVE {{BTC_LONG_ENTRY_PRICE}}+3% PERSISTENT:3600 INFO "💵 BTC Long close"
|
18 BTC >={{BTC_LONG_ENTRY_PRICE}}+5% PERSISTENT:3600 INFO "💵 BTC Long close"
|
||||||
|
|||||||
@@ -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)
|
cursor.nextToken("asset").toUpperCase(Locale.ROOT)
|
||||||
);
|
);
|
||||||
|
|
||||||
PriceDirection direction = PriceDirection.valueOf(
|
String conditionExpression = parseConditionExpression(cursor);
|
||||||
cursor.nextToken("direction").toUpperCase(Locale.ROOT)
|
|
||||||
);
|
|
||||||
|
|
||||||
String targetExpression = cursor.nextToken("target");
|
|
||||||
|
|
||||||
TriggerConfiguration triggerConfiguration = parseTrigger(
|
TriggerConfiguration triggerConfiguration = parseTrigger(
|
||||||
cursor.nextToken("trigger")
|
cursor.nextToken("trigger")
|
||||||
@@ -78,8 +74,7 @@ public final class AlarmConfigurationParser {
|
|||||||
return new PriceAlarmDefinition(
|
return new PriceAlarmDefinition(
|
||||||
id,
|
id,
|
||||||
asset,
|
asset,
|
||||||
direction,
|
conditionExpression,
|
||||||
targetExpression,
|
|
||||||
triggerConfiguration.trigger(),
|
triggerConfiguration.trigger(),
|
||||||
triggerConfiguration.gracePeriod(),
|
triggerConfiguration.gracePeriod(),
|
||||||
severity,
|
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) {
|
private static TriggerConfiguration parseTrigger(String triggerText) {
|
||||||
String normalized = triggerText.toUpperCase(Locale.ROOT);
|
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.println("============================================================");
|
System.err.println("============================================================");
|
||||||
System.err.printf(
|
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.asset(),
|
||||||
price.priceUsd().toPlainString(),
|
price.priceUsd().toPlainString(),
|
||||||
alarm.direction(),
|
alarm.condition().expression()
|
||||||
alarm.target().toPlainString()
|
|
||||||
);
|
);
|
||||||
System.err.printf(
|
System.err.printf(
|
||||||
"Trigger: %s, oracle time: %s, slot: %d, source: %s%n",
|
"Trigger: %s, oracle time: %s, slot: %d, source: %s%n",
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ public final class JupiterPerpsAlarmImpl {
|
|||||||
entryPriceVariableRefresher.refresh();
|
entryPriceVariableRefresher.refresh();
|
||||||
System.out.println("Fetching done.");
|
System.out.println("Fetching done.");
|
||||||
|
|
||||||
|
System.out.print("Validating alarm conditions... ");
|
||||||
|
validateAlarmConditions(definitions, variableResolver);
|
||||||
|
System.out.println("Done.");
|
||||||
|
|
||||||
entryPriceVariableRefresher.startPeriodicRefresh();
|
entryPriceVariableRefresher.startPeriodicRefresh();
|
||||||
|
|
||||||
JupiterPerpsEntryPriceVariableRefreshWatcher entryPriceVariableRefreshWatcher =
|
JupiterPerpsEntryPriceVariableRefreshWatcher entryPriceVariableRefreshWatcher =
|
||||||
@@ -141,9 +145,8 @@ public final class JupiterPerpsAlarmImpl {
|
|||||||
asset.oracleAccount()
|
asset.oracleAccount()
|
||||||
);
|
);
|
||||||
assetDefinitions.forEach(definition -> System.out.printf(
|
assetDefinitions.forEach(definition -> System.out.printf(
|
||||||
" %s %s USD, %s, severity=%s%n",
|
" %s USD, %s, severity=%s%n",
|
||||||
definition.direction(),
|
definition.conditionExpression(),
|
||||||
definition.targetExpression(),
|
|
||||||
definition.trigger(),
|
definition.trigger(),
|
||||||
definition.severity()
|
definition.severity()
|
||||||
));
|
));
|
||||||
@@ -155,6 +158,29 @@ public final class JupiterPerpsAlarmImpl {
|
|||||||
new CountDownLatch(1).await();
|
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(
|
private static Map<JupiterPerpsAsset, List<PriceAlarmDefinition>> groupByAsset(
|
||||||
List<PriceAlarmDefinition> definitions
|
List<PriceAlarmDefinition> definitions
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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 {
|
||||||
@@ -22,24 +21,21 @@ public final class PriceAlarm {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal target;
|
PriceCondition condition;
|
||||||
try {
|
try {
|
||||||
target = AlarmTargetParser.parse(
|
condition = AlarmConditionParser.parse(
|
||||||
variableResolver.resolve(definition.targetExpression())
|
variableResolver.resolve(definition.conditionExpression())
|
||||||
);
|
);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
System.err.printf(
|
System.err.printf(
|
||||||
"Could not resolve target for alarm %d: %s%n",
|
"Could not resolve condition for alarm %d: %s%n",
|
||||||
definition.id(),
|
definition.id(),
|
||||||
exception.getMessage()
|
exception.getMessage()
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean reached = definition.direction().reached(
|
boolean reached = condition.matches(price.priceUsd());
|
||||||
price.priceUsd(),
|
|
||||||
target
|
|
||||||
);
|
|
||||||
|
|
||||||
boolean enteredTriggeredSide = previousReached == null
|
boolean enteredTriggeredSide = previousReached == null
|
||||||
? reached
|
? reached
|
||||||
@@ -57,13 +53,13 @@ public final class PriceAlarm {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger(price, target);
|
trigger(price, condition);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (definition.trigger() == AlarmTrigger.PERSISTENT) {
|
if (definition.trigger() == AlarmTrigger.PERSISTENT) {
|
||||||
if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) {
|
if (lastTriggeredAt == null || persistentGracePeriodHasPassed()) {
|
||||||
trigger(price, target);
|
trigger(price, condition);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,7 +91,7 @@ public final class PriceAlarm {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void trigger(OraclePrice price, BigDecimal target) {
|
private void trigger(OraclePrice price, PriceCondition condition) {
|
||||||
triggerCount++;
|
triggerCount++;
|
||||||
lastTriggeredAt = Instant.now();
|
lastTriggeredAt = Instant.now();
|
||||||
|
|
||||||
@@ -115,8 +111,7 @@ public final class PriceAlarm {
|
|||||||
ResolvedPriceAlarm resolvedAlarm = new ResolvedPriceAlarm(
|
ResolvedPriceAlarm resolvedAlarm = new ResolvedPriceAlarm(
|
||||||
definition.id(),
|
definition.id(),
|
||||||
definition.asset(),
|
definition.asset(),
|
||||||
definition.direction(),
|
condition,
|
||||||
target,
|
|
||||||
definition.trigger(),
|
definition.trigger(),
|
||||||
definition.triggerGracePeriod(),
|
definition.triggerGracePeriod(),
|
||||||
definition.severity(),
|
definition.severity(),
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import java.util.Objects;
|
|||||||
public record PriceAlarmDefinition(
|
public record PriceAlarmDefinition(
|
||||||
int id,
|
int id,
|
||||||
JupiterPerpsAsset asset,
|
JupiterPerpsAsset asset,
|
||||||
PriceDirection direction,
|
String conditionExpression,
|
||||||
String targetExpression,
|
|
||||||
AlarmTrigger trigger,
|
AlarmTrigger trigger,
|
||||||
ΩsecondsΩ triggerGracePeriod,
|
ΩsecondsΩ triggerGracePeriod,
|
||||||
AlarmSeverity severity,
|
AlarmSeverity severity,
|
||||||
@@ -16,14 +15,13 @@ public record PriceAlarmDefinition(
|
|||||||
) {
|
) {
|
||||||
public PriceAlarmDefinition {
|
public PriceAlarmDefinition {
|
||||||
Objects.requireNonNull(asset, "asset");
|
Objects.requireNonNull(asset, "asset");
|
||||||
Objects.requireNonNull(direction, "direction");
|
Objects.requireNonNull(conditionExpression, "conditionExpression");
|
||||||
Objects.requireNonNull(targetExpression, "targetExpression");
|
|
||||||
Objects.requireNonNull(trigger, "trigger");
|
Objects.requireNonNull(trigger, "trigger");
|
||||||
Objects.requireNonNull(severity, "severity");
|
Objects.requireNonNull(severity, "severity");
|
||||||
Objects.requireNonNull(note, "note");
|
Objects.requireNonNull(note, "note");
|
||||||
|
|
||||||
if (targetExpression.isBlank()) {
|
if (conditionExpression.isBlank()) {
|
||||||
throw new IllegalArgumentException("Target expression cannot be blank");
|
throw new IllegalArgumentException("Condition expression cannot be blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggerGracePeriod < 0) {
|
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) {
|
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.%nCondition: %s USD.%nOracle time: %s.%nSlot: %d.",
|
||||||
alarm.id(),
|
alarm.id(),
|
||||||
alarm.severity(),
|
alarm.severity(),
|
||||||
alarm.note(),
|
alarm.note(),
|
||||||
price.asset(),
|
price.asset(),
|
||||||
price.priceUsd().toPlainString(),
|
price.priceUsd().toPlainString(),
|
||||||
alarm.direction(),
|
alarm.condition().expression(),
|
||||||
alarm.target().toPlainString(),
|
|
||||||
price.oracleTime(),
|
price.oracleTime(),
|
||||||
price.slot()
|
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;
|
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||||
|
|
||||||
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
import com.r35157.jupiterperpsalarm.AlarmSeverity;
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
public record ResolvedPriceAlarm(
|
public record ResolvedPriceAlarm(
|
||||||
int id,
|
int id,
|
||||||
JupiterPerpsAsset asset,
|
JupiterPerpsAsset asset,
|
||||||
PriceDirection direction,
|
PriceCondition condition,
|
||||||
BigDecimal target,
|
|
||||||
AlarmTrigger trigger,
|
AlarmTrigger trigger,
|
||||||
ΩsecondsΩ triggerGracePeriod,
|
ΩsecondsΩ triggerGracePeriod,
|
||||||
AlarmSeverity severity,
|
AlarmSeverity severity,
|
||||||
String note
|
String note
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user