Files
com_r35157_nenjim-hubd-impl…/README_alarm.md
T

21 KiB

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:

./gradlew classes

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

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

./gradlew run

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

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:

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:

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

{{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:

{{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:

ID  ASSET  CONDITION  TRIGGER

Example:

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:

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:

<={{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:

<={{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:

>={{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:

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:

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:

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:

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:

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:

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:

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:

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.