# Jupiter Perps Price Alarm

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 independently configured console, Pushover, and Jupiter position-increase or position-decrease actions.

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 Registry service manager currently constructs and registers the alarm as
`jupiter-perps-alarm.default`, but deliberately does not start it. Its
`SolanaWallet` and `JupiterPerpsService` are selected by the manager's hardcoded
composition and injected before registration. The alarm action configuration
files do not select a wallet or signing key.

> **Important:** This application is not necessarily notification-only. Every active entry in a Jupiter Perps position action file can sign and submit a real transaction. Review both transaction action files before running it.

## Startup flow

The alarm performs the following steps before it starts monitoring prices:

1. Parse the configured alarm path, WebSocket endpoint environment and
   `alarms.conf`.
2. Parse the position-increase and position-decrease action files, including
   their structure and reserve settings.
3. Fetch the injected wallet's current open Jupiter Perps positions and 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. Parse the console and Pushover action files and construct all alarm actions.
7. Open one oracle WebSocket connection per configured asset and RPC endpoint.

Startup fails before WebSockets are opened if an alarm condition or an action
value that is resolved during startup contains an unavailable variable or
invalid value. Jupiter transaction-row expressions are resolved and validated
when their alarm triggers.

## Requirements and build

The project uses:

- JDK 25
- Gradle 9.6.1 through the Gradle wrapper
- Java preview features
- Detag-generated Java sources from `src/main/tjava`

Compile the project with:

```bash
./gradlew classes
```

To create the application JAR and collect runtime dependencies in `build/libs`:

```bash
./gradlew prepareLibs
```

There is currently no alarm-specific automated self-test task.

## Running through Nenjim

The alarm no longer has a standalone `main` method. The Registry service
manager constructs and registers the wallet, Jupiter service and alarm, but the
alarm is intentionally absent from the current explicit application startup
list. Running the Hub therefore leaves the registered alarm inactive:

```bash
./gradlew run
```

After `./gradlew prepareLibs`, the same Registry-managed Hub can be invoked with
its main class (the alarm still remains inactive):

```bash
java --enable-preview \
  -cp "build/libs/*" \
  com.r35157.nenjim.hubd.Main
```

The current manager composition binds the alarm to `conf/alarms.conf` and to the
test Jupiter Perps wallet and service. Until Nenjim provides configured
injection and activation, a different configuration, wallet, or startup choice
must be selected in the Registry manager's composition code, not in an alarm
action file.

The repository contains example files for the alarm configuration and every
action configuration. The real `conf/*.conf` files are ignored by Git; keep
credentials and operational details out of version control.

### Environment variables

The Registry manager does not currently forward alarm-specific command-line
options. Solana WebSocket endpoints can be overridden with:

```bash
export SOLANA_WS_URLS='wss://first-provider.example,wss://second-provider.example'
```

If `SOLANA_WS_URLS` is absent, the alarm uses
`wss://api.mainnet-beta.solana.com`.

## Configuration files

The application uses five configuration files:

| File | Purpose |
|---|---|
| `alarms.conf` | Shared variables and price-alarm definitions |
| `alarmaction_Console.conf` | Alarm IDs written to the console |
| `alarmaction_Pushover.conf` | Pushover credentials and notifications |
| `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` | USDC reserve and position increases |
| `alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf` | Position-size reserve and position decreases |

The four action files are resolved in the same directory as the file supplied through `--config`. All five files are required at startup, although an action can contain no alarm IDs. Blank lines and lines beginning with `#` are ignored.

Example files:

- [alarms.conf.example](conf/alarms.conf.example)
- [alarmaction_Console.conf.example](conf/alarmaction_Console.conf.example)
- [alarmaction_Pushover.conf.example](conf/alarmaction_Pushover.conf.example)
- [alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf.example](conf/alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf.example)
- [alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example](conf/alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf.example)

### Format version

Every configuration file must declare its own format version as its first
actual configuration entry:

| File | Format version |
|---|---:|
| `alarms.conf` | `2` |
| `alarmaction_Console.conf` | `1` |
| `alarmaction_Pushover.conf` | `1` |
| `alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` | `2` |
| `alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf` | `2` |

Blank lines and full-line comments may appear before it. The application
rejects a missing, duplicate, malformed, misplaced, older, or newer version
before parsing the remaining configuration.

Each file format has an independent version. Increment only the affected
format when making an incompatible change, and update both its parser constant
and its runtime and example files. See
[Configuration format versions](README.md#configuration-format-versions) for
the project-wide policy.

## Shared variables

### Variable definitions

Variable definitions live in `alarms.conf`. A definition consists of a name and a single-token value:

```text
{{SOL_WARNING_PRICE}}  150.00
```

Variable names must use uppercase letters, digits, and underscores. User-defined variables can be referenced from alarm conditions and action configuration values as `{{NAME}}`.

The wallet address used for initial and periodic Jupiter position fetches comes
from the injected `SolanaWallet`; it is not a shared configuration variable.
For open SOL, BTC, and ETH positions belonging to that wallet, 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}}
```

The refresher replaces user-supplied variables with these names when position
data is fetched. Only variables for positions returned by Jupiter are
populated. Startup validation fails if an alarm condition references a variable
that is unavailable after the initial position fetch. Transaction-row
expressions are resolved later, when their alarm triggers.

Alarm conditions, Pushover notes, and Jupiter position-increase and
position-decrease rows are resolved again when an alarm triggers, so refreshed
values take effect without restarting the application. Pushover credentials
and severity are resolved during startup. The wallet and signer belong to the
injected `SolanaWallet` and are not resolved from alarm configuration.

## Alarm definitions

Each alarm is one line with four columns:

```text
ID  ASSET  CONDITION  TRIGGER
```

Example:

```text
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:

- `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:<duration>`.

A trailing comment is allowed after a variable value or trigger.

An alarm can be listed in zero, one, or several action files. Absence means that action ignores the alarm; absence from all action files means that the condition is monitored but produces no action.

## 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

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`

- 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.

### `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.
- Without hysteresis, leaving the condition re-arms the alarm, so the next entry triggers again.
- An optional hysteresis can be appended to the condition as `/H` or `/P%`.
- With hysteresis, the alarm remains disarmed until the price reaches the re-arm boundary.
- A grace period is not supported; `CROSSING:1m` is rejected.
- Only the strict directional comparisons `<` and `>` are supported.
- `=`, `<=`, `>=`, and range conditions are rejected during configuration parsing.

The condition determines the crossing direction. Without hysteresis:

```text
19  SOL  >{{SOL_LONG_ENTRY_PRICE}}  CROSSING
20  SOL  <{{SOL_LONG_ENTRY_PRICE}}  CROSSING
```

Alarm 19 triggers when the price moves from at or below the entry price to above it. Alarm 20 triggers when the price moves from at or above the entry price to below it. Landing exactly on the entry price is not a crossing.

Hysteresis prevents repeated triggers when the price fluctuates around the target:

```text
21  SOL  >{{SOL_LONG_ENTRY_PRICE}}/0.25  CROSSING
22  SOL  <{{SOL_LONG_ENTRY_PRICE}}/0.25%  CROSSING
```

Alarm 21 triggers above the entry price and re-arms only at or below `entry price - 0.25`. Alarm 22 triggers below the entry price and re-arms only at or above `entry price + 0.25%`.

Percentage hysteresis is calculated from the fully resolved comparison target. Hysteresis is an unsigned distance, so a leading `+` or `-` is rejected. Hysteresis is supported only for `CROSSING` alarms.

### `PERSISTENT`

- Triggers immediately on the first accepted matching price.
- With no grace period, it can trigger for every accepted matching price event.
- `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

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
touch conf/jupiter-perps-alarm-var.refresh
```

The watcher detects creation of the file, refreshes the variables, and then deletes the trigger file.

When a dynamic price variable changes, the application logs its old and new value.

## Alarm actions

Actions are executed sequentially through `CompositeAlarmAction`. A runtime failure in one action is logged so the remaining actions can still run.

### Console

`alarmaction_Console.conf` contains one alarm ID per line:

```text
FORMAT_VERSION=1

1
4
5
```

Only listed alarms are written to the console. The output includes the asset, current oracle price, resolved condition, trigger type, oracle time, slot, and source endpoint. Duplicate or non-integer IDs are rejected. An empty file disables console output for all alarms.

### Pushover

`alarmaction_Pushover.conf` contains the credentials followed by zero or more notification definitions:

```text
FORMAT_VERSION=1

APPLICATION_TOKEN  <pushover-application-token>
USER_KEY            <pushover-user-key>

# ID  SEVERITY  "NOTE"
4     CRITICAL  "🚨 SOL Long add 1.1x"
5     INFO      "🌱 SOL Long add 3x"
```

The application token and user/group key are required. Only listed alarm IDs send a Pushover notification. Notes must be quoted and support `\n`, `\r`, `\t`, `\"`, and `\\` escapes.

| 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` |

Credentials, severity, and notes may reference shared variables. Credentials and severity are resolved during startup; notes are also resolved for every trigger so refreshed variables take effect. Pushover requests are sent asynchronously with a 15-second request timeout. Rejected requests and asynchronous failures are logged.

### Automatic position increase

`alarmaction_JupiterPerpsPositionIncreaseAlarmAction.conf` contains a minimum
USDC reserve followed by zero or more position increases:

```text
FORMAT_VERSION=2

RESERVE_USDC_LIMIT   10

# ID  ASSET  DIRECTION  COLLATERAL_USDC  SIZE_DELTA_USD  MAX_SLIPPAGE_BPS
4     SOL    LONG       0.25             2.50            200
```

Each active row can submit a real transaction when its alarm triggers. Supported values are:

- `ASSET`: `SOL`, `ETH`, or `BTC`.
- `DIRECTION`: `LONG` or `SHORT`.
- `COLLATERAL_USDC`: greater than zero.
- `SIZE_DELTA_USD`: zero or greater.
- `MAX_SLIPPAGE_BPS`: from `0` to `10000`.

`RESERVE_USDC_LIMIT` is the amount of USDC that must remain after collateral is
deducted. An increase is skipped unless the injected wallet's USDC balance can
cover both the requested collateral and this reserve.

Asset, direction, amounts, and slippage may reference shared variables. Every
transaction row is structurally parsed during startup, then resolved and
validated at trigger time. This allows refreshed variables to change the
transaction parameters without a restart.

For a configured ID, the injected `JupiterPerpsService` builds the position
increase, uses its injected wallet to sign it, submits it and awaits the
transaction confirmation. IDs absent from this file never perform a
transaction.

The example transaction rows are commented out. Do not activate a row until
its alarm condition, injected wallet binding, asset, direction, amounts,
reserve, and slippage have been reviewed.

### Automatic position decrease

`alarmaction_JupiterPerpsPositionDecreaseAlarmAction.conf` contains a minimum
remaining position-size reserve followed by zero or more position decreases:

```text
FORMAT_VERSION=2

RESERVE_POSITION_SIZE_USD_LIMIT   10

# ID  ASSET  DIRECTION  RECEIVE_TOKEN  SIZE_DELTA_USD  MAX_SLIPPAGE_BPS
3     SOL    SHORT      USDC           5.00            200
```

Supported values are:

- `ASSET`: `SOL`, `ETH`, or `BTC`.
- `DIRECTION`: `LONG` or `SHORT`.
- `RECEIVE_TOKEN`: `USDC` or the token matching `ASSET`.
- `SIZE_DELTA_USD`: greater than zero.
- `MAX_SLIPPAGE_BPS`: from `0` to `10000`.

The action finds the injected wallet's single matching open position. It skips
the decrease if the resulting position size would be below
`RESERVE_POSITION_SIZE_USD_LIMIT`. Asset, direction, receive token, amount and
slippage may reference shared variables and are resolved again at trigger time.

For a configured ID, the injected `JupiterPerpsService` builds the position
decrease, uses its injected wallet to sign it, submits it and awaits the
transaction confirmation. IDs absent from this file never perform a
transaction.

## RPC redundancy and reconnect behavior

Provide multiple comma-separated WebSocket endpoints for redundancy:

```bash
export SOLANA_WS_URLS='wss://first-provider.example,wss://second-provider.example'
```

The application creates one WebSocket connection for every configured asset/endpoint combination. With three configured assets and two endpoints, it creates six connections.

Each connection:

- subscribes with `processed` commitment and Base64 account encoding;
- fetches the current account state after opening;
- sends a heartbeat every 20 seconds;
- reconnects with exponential delays from 1 to 30 seconds.

## Validation and errors

Structural configuration errors include the file path and line number. The two
Jupiter action files are parsed before the initial position fetch. After that
fetch, every alarm condition is resolved and parsed, and the console and
Pushover files are loaded before WebSockets are opened. Jupiter transaction-row
expressions are resolved and validated only when their alarm triggers.

Startup validation rejects, among other things:

- unknown or unavailable variables;
- 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;
- missing action credentials or reserve settings;
- duplicate action IDs;
- invalid Pushover severities.

At runtime, a later condition resolution/parsing failure skips that price event
for the affected alarm. Invalid Jupiter assets, directions, receive tokens,
amounts, slippage, or unavailable action variables are logged by the action
dispatcher, and the remaining actions are still attempted.

## 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.
- Configuration files are read only during startup; editing an action file requires a restart.
- Action IDs are not cross-validated against `alarms.conf`; an unknown ID is accepted but never triggered.
- Pushover credentials and severity are resolved only during startup. Dynamic
  re-resolution applies to conditions, Pushover notes, and Jupiter transaction
  rows. Wallet and signer selection requires changing the Hub composition.
- This application is a monitoring and automation aid, not a substitute for independent risk controls.
