Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 9 additions & 29 deletions .github/workflows/deploy-keeper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -447,35 +447,15 @@ jobs:
fi
echo "✅ Service converged on the deployed revision."

- name: Smoke-test /health reports the deployed version
if: steps.rollout.outputs.diagnose == 'true'
env:
HEALTH_URL: ${{ needs.build.outputs.keeper_health_url }}
EXPECTED_VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
echo "🔎 GET ${HEALTH_URL} (expecting version=${EXPECTED_VERSION})"

# Give the new task a moment to bind the health port, then poll.
ATTEMPTS=12
for i in $(seq 1 "$ATTEMPTS"); do
BODY=$(curl -fsS -m 10 "$HEALTH_URL" 2>/dev/null || true)
if [ -n "$BODY" ]; then
STATUS=$(echo "$BODY" | jq -r '.status // "?"')
LIVE_VERSION=$(echo "$BODY" | jq -r '.info.version // "?"')
echo " attempt ${i}: status=${STATUS} version=${LIVE_VERSION}"
if [ "$STATUS" = "ok" ] && [ "$LIVE_VERSION" = "$EXPECTED_VERSION" ]; then
echo "✅ /health is OK and serving the deployed version."
exit 0
fi
else
echo " attempt ${i}: no response yet"
fi
sleep 10
done

echo "::error::/health never reported status=ok with version=${EXPECTED_VERSION}. Last body: ${BODY:-<none>}"
exit 1
# NOTE: There is intentionally no public HTTP smoke-test of /health here.
# The keeper sits behind an INTERNAL ALB (see
# .bedrock/.terragrunt/06_col_mar_keeper_svc.tf) that only accepts traffic
# from the VPC CIDR and VPN range — keeper.{env}.hashpower.exchange is not
# resolvable/reachable from GitHub-hosted runners on the public internet.
# Health is already proven above: the ALB target group runs a /health
# check, and ECS only reports rolloutState=COMPLETED once the new tasks
# pass it, which the rollout verification asserts alongside the exact
# deployed task-def ARN.

- name: Diagnose failed rollout
if: failure() && steps.rollout.outputs.diagnose == 'true'
Expand Down
9 changes: 2 additions & 7 deletions .github/workflows/keeper-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,18 @@ jobs:
working-directory: ./contracts
run: pnpm install --frozen-lockfile

# TEMPORARY: pinned to feat/liquidate-to-im-buffer because the keeper's
# liquidate-to-IM-buffer orchestration calls contract functions
# (`liquidatePositions` on futures, `liquidatePosition(user, closeQty)` on
# perps) that only exist on those feature branches. Revert both refs back
# to `dev` once derivatives-marketplace#73 and futures-marketplace#203 merge.
- name: Checkout derivatives-marketplace (perps contracts)
uses: actions/checkout@v5
with:
repository: Lumerin-protocol/derivatives-marketplace
ref: feat/liquidate-to-im-buffer
ref: c8078b9e430df8a5ec8bc64865d692b38cc69d8c
path: perps

- name: Checkout futures-marketplace
uses: actions/checkout@v5
with:
repository: Lumerin-protocol/futures-marketplace
ref: feat/liquidate-to-im-buffer
ref: 5e28bb346b125d90eb138370a8a78c69487d1c26
path: futures-marketplace

- name: Install perps contracts dependencies
Expand Down
3 changes: 2 additions & 1 deletion contracts/contracts/PortfolioMarginEngine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ contract PortfolioMarginEngine is
netDelta += pos.netQuantity * int256(WAD) / qtyScale;
}

// Futures delta: sum(deliveryDurationDays * qty) * WAD per active position (optional)
// Futures delta: sum(±qty) * WAD per active position — one WAD per contract
// (1 PH/s/day), sign per side. No duration multiplier. (optional)
if (address(futures) != address(0)) {
netDelta += futures.getNetPositionDelta(user);
}
Expand Down
2 changes: 1 addition & 1 deletion indexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"remove-local": "graph remove --node http://localhost:8020/ collateral-vault",
"deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 --version-label 0 collateral-vault",
"setup-local": "pnpm prepare-local && pnpm codegen && pnpm build && pnpm create-local && pnpm deploy-local",
"test": "graph test",
"test": "graph test -v 0.6.0",
"indexer": "docker compose --env-file ../.env up",
"graph:api": "open http://localhost:8030/graphql/playground"
},
Expand Down
4 changes: 2 additions & 2 deletions keeper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,14 @@ Suites cover:

- `pme/health` — multicall batching + `imUtilization` precision
- `venues/perps` — long/short PnL math, `PERPS_MARKET_ID` sentinel, position id
- `venues/futures` — buyer/seller PnL, `deliveryAt` → marketId, `deliveryDurationDays` caching
- `venues/futures` — buyer/seller PnL (one contract = 1 PH/s·day, duration-free), `deliveryAt` → marketId
- `coordinator/queue` — BigInt-safe ordering, `upsert` re-ranking, snapshot semantics
- `coordinator/planner` — orders-leg, position ranking, `OrdersStillOpen`-replay, bad-debt
- `alert/notifier` — dedupe window, severity promotion, ordering, retry-on-failure
- `discovery/tracker` — checksum dedupe, `onAdded` / `onChanged` listeners, startup backfill
- `discovery/webhook` — payload extraction across `data` / `records` / array shapes
- `runtime/scheduler` — alert ladder thresholds, queue upsert + executor kick wiring
- `oracle/priceFeed` — rebase to token decimals, dispatch, no-op on unchanged answer
- `oracle/priceFeed` — rebase to token decimals + contract-size unit (`CONTRACT_SIZE_HPS_DAY / ORACLE_UNIT_HPS_DAY`, ×10 at defaults), dispatch, no-op on unchanged answer
- `predict/mm` — net delta, stress, perp/futures unrealized loss, mm/im surplus
- `predict/solve` — long/short downside & upside thresholds, drag from orderMargin/funding
- `predict/predictiveIndex` — upsert/invalidate, sorted crossings on rise & drop
Expand Down
4 changes: 2 additions & 2 deletions keeper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"dependencies": {
"amaro": "^1.1.9",
"collateral-margin-abi": "github:Lumerin-protocol/collateral-margin#dev&path:/contracts/abi",
"derivatives-marketplace-abi": "github:Lumerin-protocol/derivatives-marketplace#feat/liquidate-to-im-buffer&path:/contracts/abi",
"futures-marketplace-abi": "github:Lumerin-protocol/futures-marketplace#feat/liquidate-to-im-buffer&path:/contracts/abi",
"derivatives-marketplace-abi": "github:Lumerin-protocol/derivatives-marketplace#c8078b9e430df8a5ec8bc64865d692b38cc69d8c&path:/contracts/abi",
"futures-marketplace-abi": "github:Lumerin-protocol/futures-marketplace#5e28bb346b125d90eb138370a8a78c69487d1c26&path:/contracts/abi",
"pino": "^10.3.1",
"viem": "^2.48.8"
},
Expand Down
20 changes: 10 additions & 10 deletions keeper/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions keeper/scripts/debug-delivery-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ const now = BigInt(Math.floor(Date.now() / 1000));
const block = await client.getBlock();
console.log("wall-clock now:", now, " block.timestamp:", block.timestamp);

const deliveryDurationDays = (await client.readContract({
const expirationIntervalDays = (await client.readContract({
address: FUTURES,
abi: FuturesAbi,
functionName: "deliveryDurationDays",
functionName: "expirationIntervalDays",
})) as number;
const window = BigInt(deliveryDurationDays) * 86_400n;
const window = BigInt(expirationIntervalDays) * 86_400n;
console.log(
"deliveryDurationDays:",
deliveryDurationDays,
"expirationIntervalDays:",
expirationIntervalDays,
"→ window:",
window,
"s",
Expand Down
39 changes: 38 additions & 1 deletion keeper/src/oracle/priceFeed.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HashPowerPerpsDEXAbi } from "derivatives-marketplace-abi/HashPowerPerpsDEX.ts";
import type pino from "pino";
import type { Chain } from "../chain.ts";
import type { Config } from "../config.ts";
Expand Down Expand Up @@ -40,6 +41,12 @@ export type PriceListener = (update: PriceUpdate) => void;
* we rescale to the perps/futures token decimals (USDC = 6) so consumers
* compare apples to apples with `getMarketPrice()`.
*
* The oracle quotes the price of `ORACLE_UNIT_HPS_DAY` (100 TH/s over a day), but the
* venues denominate one contract in `contractSizeHpsDay` (default 1e15 = 1 PH/s over a
* day). We read that contract-size multiplier from the perps venue once at `start()` and
* apply `contractSizeHpsDay / ORACLE_UNIT_HPS_DAY` so the streamed price matches on-chain
* `getMarketPrice()`. Both venues are assumed to share the same contract size.
*
* Lifecycle:
* - `start()`: read decimals, prime `current` via one `latestRoundData`,
* then attach the watcher. Returns once the first read has resolved.
Expand All @@ -53,6 +60,10 @@ export class PriceFeed {
private unwatch: (() => void) | undefined;
/** 10^(oracleDecimals - tokenDecimals). Set during `start()`. */
private rescaleDivisor: bigint = 1n;
/** Contract size in hashes/s·day (`contractSizeHpsDay`). Set during `start()`. */
private contractSizeHpsDay: bigint = 1n;
/** Oracle quote basis in hashes/s·day (`ORACLE_UNIT_HPS_DAY`). Set during `start()`. */
private oracleUnitHpsDay: bigint = 1n;

private readonly chain: Chain;
private readonly config: Config;
Expand Down Expand Up @@ -91,6 +102,28 @@ export class PriceFeed {
}
this.rescaleDivisor = 10n ** BigInt(oracleDecimals - this.tokenDecimals);

// Rebase from the oracle's quote basis (100 TH/s/day) to one contract unit
// (contractSizeHpsDay/day), matching `getMarketPrice()` on-chain.
const [contractSizeHpsDay, oracleUnitHpsDay] = await Promise.all([
this.chain.publicClient.readContract({
address: this.config.perps.address,
abi: HashPowerPerpsDEXAbi,
functionName: "CONTRACT_SIZE_HPS_DAY",
}) as Promise<bigint>,
this.chain.publicClient.readContract({
address: this.config.perps.address,
abi: HashPowerPerpsDEXAbi,
functionName: "ORACLE_UNIT_HPS_DAY",
}) as Promise<bigint>,
]);
if (contractSizeHpsDay <= 0n || oracleUnitHpsDay <= 0n) {
throw new Error(
`PriceFeed: invalid contract size (contractSizeHpsDay=${contractSizeHpsDay}, ORACLE_UNIT_HPS_DAY=${oracleUnitHpsDay})`,
);
}
this.contractSizeHpsDay = contractSizeHpsDay;
this.oracleUnitHpsDay = oracleUnitHpsDay;

await this.refresh("start");

// We watch BTC/USDC (not HashpriceUSD) because HashpriceUSD is a pure
Expand All @@ -114,6 +147,8 @@ export class PriceFeed {
btcUsdcFeed: this.config.oracle.btcUsdcFeedAddress,
oracleDecimals,
tokenDecimals: this.tokenDecimals,
contractSizeHpsDay: this.contractSizeHpsDay,
oracleUnitHpsDay: this.oracleUnitHpsDay,
currentPrice: this.currentPrice,
},
"PriceFeed started",
Expand Down Expand Up @@ -164,7 +199,9 @@ export class PriceFeed {
return;
}

const next = answer / this.rescaleDivisor;
// Mirror on-chain `getMarketPrice()`: rebase decimals first, then apply the
// contract-size multiplier (contractSizeHpsDay / ORACLE_UNIT_HPS_DAY).
const next = ((answer / this.rescaleDivisor) * this.contractSizeHpsDay) / this.oracleUnitHpsDay;
const prev = this.currentPrice;
if (prev === next) return;

Expand Down
13 changes: 7 additions & 6 deletions keeper/src/predict/mm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { AccountSnapshot, MMParams } from "./types.ts";
* - perp.orderMargin (constant)
* - perp.unrealizedLoss = max(0, -((P - entry) * netQty / qtyScale))
* - futures.orderMargin (constant)
* - futures.unrealizedLoss = sum_i max(0, -(buyer? : ±)(P - entry_i)*deliveryDays)
* - futures.unrealizedLoss = sum_i max(0, -(buyer? : ±)(P - entry_i))
* - perp.fundingOwed (constant — short-term, refreshed on snapshot)
*
* Total mmRequired(P) is therefore piecewise-linear with kinks at the
Expand All @@ -40,7 +40,7 @@ function abs(x: bigint): bigint {
* Aggregate net delta in WAD (matches `_aggregateGreeks` for pure-delta).
*
* perpDelta = perpNetQty * WAD / 10^perpQtyDecimals
* futuresDelta = sum_i (isBuyer ? +1 : -1) * deliveryDays * WAD
* futuresDelta = sum_i (isBuyer ? +1 : -1) * WAD
*
* Note: the on-chain `getNetPositionDelta` already returns this sum for the
* futures leg in WAD; we re-derive it here off-chain because the snapshot
Expand All @@ -52,7 +52,7 @@ export function netDeltaWad(snap: AccountSnapshot, params: MMParams): bigint {
let delta = (snap.perp.netQty * WAD) / perpQtyScale;
for (const pos of snap.futures.positions) {
const sign = pos.isBuyer ? 1n : -1n;
delta += sign * snap.futures.deliveryDays * WAD;
delta += sign * WAD;
}
return delta;
}
Expand Down Expand Up @@ -99,17 +99,18 @@ export function perpUnrealizedLoss(snap: AccountSnapshot, params: MMParams, P: b
}

/**
* Sum of per-position futures unrealized losses at price P. Each contract:
* Sum of per-position futures unrealized losses at price P. Each contract
* settles `pricePerDay` of notional (no duration multiplier):
*
* diffPerDay = isBuyer ? (P - entryPerDay) : (entryPerDay - P)
* pnl = diffPerDay * deliveryDays
* pnl = diffPerDay
* loss = max(0, -pnl)
*/
export function futuresUnrealizedLoss(snap: AccountSnapshot, P: bigint): bigint {
let sum = 0n;
for (const pos of snap.futures.positions) {
const diffPerDay = pos.isBuyer ? P - pos.entryPricePerDay : pos.entryPricePerDay - P;
const pnl = diffPerDay * snap.futures.deliveryDays;
const pnl = diffPerDay;
if (pnl < 0n) sum += -pnl;
}
return sum;
Expand Down
9 changes: 1 addition & 8 deletions keeper/src/predict/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function readMMParams(
* function of price. Two RPC round-trips:
*
* 1. Bulk multicall: balance, perp position/orderMargin/funding,
* futures orderMargin/positionIds, deliveryDurationDays.
* futures orderMargin/positionIds.
* 2. Per-position multicall: hydrate each futures position so we know its
* `(buyer, buyPricePerDay, sellPricePerDay)` for off-chain PnL.
*
Expand All @@ -75,7 +75,6 @@ export async function readAccountSnapshot(
perpFunding,
futuresOrderMargin,
futuresPositionIds,
deliveryDurationDays,
] = await chain.publicClient.multicall({
contracts: [
{
Expand Down Expand Up @@ -114,11 +113,6 @@ export async function readAccountSnapshot(
functionName: "getPositionIds" as const,
args: [user] as const,
},
{
address: config.futures.address,
abi: FuturesAbi,
functionName: "deliveryDurationDays" as const,
},
] as const,
allowFailure: false,
});
Expand Down Expand Up @@ -164,7 +158,6 @@ export async function readAccountSnapshot(
futures: {
positions: futuresPositions,
orderMargin: futuresOrderMargin as bigint,
deliveryDays: BigInt(deliveryDurationDays as number),
},
};
}
Loading
Loading