@raintree-technology/perps 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/adapters/aevo.d.ts +64 -0
- package/dist/adapters/aevo.js +899 -0
- package/dist/adapters/certification.d.ts +33 -0
- package/dist/adapters/certification.js +99 -0
- package/dist/adapters/decibel/order-manager.d.ts +45 -0
- package/dist/adapters/decibel/order-manager.js +140 -0
- package/dist/adapters/decibel/rest-client.d.ts +176 -0
- package/dist/adapters/decibel/rest-client.js +155 -0
- package/dist/adapters/decibel/ws-feed.d.ts +28 -0
- package/dist/adapters/decibel/ws-feed.js +166 -0
- package/dist/adapters/decibel.d.ts +108 -0
- package/dist/adapters/decibel.js +1377 -0
- package/dist/adapters/hyperliquid.d.ts +63 -0
- package/dist/adapters/hyperliquid.js +797 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/interface.d.ts +310 -0
- package/dist/adapters/interface.js +15 -0
- package/dist/adapters/orderly.d.ts +70 -0
- package/dist/adapters/orderly.js +936 -0
- package/dist/adapters/paradex.d.ts +69 -0
- package/dist/adapters/paradex.js +862 -0
- package/dist/adapters/utils.d.ts +17 -0
- package/dist/adapters/utils.js +122 -0
- package/dist/cli/command-metadata.d.ts +2 -0
- package/dist/cli/command-metadata.js +44 -0
- package/dist/cli/context.d.ts +14 -0
- package/dist/cli/context.js +59 -0
- package/dist/cli/experience.d.ts +48 -0
- package/dist/cli/experience.js +243 -0
- package/dist/cli/ink/app/AppShell.d.ts +12 -0
- package/dist/cli/ink/app/AppShell.js +32 -0
- package/dist/cli/ink/app/MetricStrip.d.ts +6 -0
- package/dist/cli/ink/app/MetricStrip.js +14 -0
- package/dist/cli/ink/app/Panel.d.ts +9 -0
- package/dist/cli/ink/app/Panel.js +7 -0
- package/dist/cli/ink/app/ascii.d.ts +2 -0
- package/dist/cli/ink/app/ascii.js +46 -0
- package/dist/cli/ink/app/index.d.ts +5 -0
- package/dist/cli/ink/app/index.js +4 -0
- package/dist/cli/ink/app/types.d.ts +15 -0
- package/dist/cli/ink/app/types.js +1 -0
- package/dist/cli/ink/components/PnL.d.ts +12 -0
- package/dist/cli/ink/components/PnL.js +23 -0
- package/dist/cli/ink/components/Spinner.d.ts +13 -0
- package/dist/cli/ink/components/Spinner.js +13 -0
- package/dist/cli/ink/components/Table.d.ts +14 -0
- package/dist/cli/ink/components/Table.js +42 -0
- package/dist/cli/ink/components/WatchHeader.d.ts +10 -0
- package/dist/cli/ink/components/WatchHeader.js +18 -0
- package/dist/cli/ink/components/index.d.ts +4 -0
- package/dist/cli/ink/components/index.js +4 -0
- package/dist/cli/ink/index.d.ts +4 -0
- package/dist/cli/ink/index.js +4 -0
- package/dist/cli/ink/render.d.ts +12 -0
- package/dist/cli/ink/render.js +21 -0
- package/dist/cli/ink/theme.d.ts +29 -0
- package/dist/cli/ink/theme.js +40 -0
- package/dist/cli/network-defaults.d.ts +10 -0
- package/dist/cli/network-defaults.js +35 -0
- package/dist/cli/output.d.ts +11 -0
- package/dist/cli/output.js +115 -0
- package/dist/cli/program.d.ts +18 -0
- package/dist/cli/program.js +164 -0
- package/dist/cli/watch.d.ts +19 -0
- package/dist/cli/watch.js +35 -0
- package/dist/client/index.d.ts +55 -0
- package/dist/client/index.js +157 -0
- package/dist/commands/account/add.d.ts +2 -0
- package/dist/commands/account/add.js +510 -0
- package/dist/commands/account/balances-simple.d.ts +5 -0
- package/dist/commands/account/balances-simple.js +63 -0
- package/dist/commands/account/index.d.ts +2 -0
- package/dist/commands/account/index.js +17 -0
- package/dist/commands/account/ls.d.ts +2 -0
- package/dist/commands/account/ls.js +95 -0
- package/dist/commands/account/positions-simple.d.ts +5 -0
- package/dist/commands/account/positions-simple.js +77 -0
- package/dist/commands/account/remove.d.ts +2 -0
- package/dist/commands/account/remove.js +47 -0
- package/dist/commands/account/set-default.d.ts +2 -0
- package/dist/commands/account/set-default.js +47 -0
- package/dist/commands/agent/index.d.ts +2 -0
- package/dist/commands/agent/index.js +126 -0
- package/dist/commands/arb/alert.d.ts +6 -0
- package/dist/commands/arb/alert.js +88 -0
- package/dist/commands/arb/basis-execute.d.ts +6 -0
- package/dist/commands/arb/basis-execute.js +332 -0
- package/dist/commands/arb/basis.d.ts +6 -0
- package/dist/commands/arb/basis.js +181 -0
- package/dist/commands/arb/compare.d.ts +6 -0
- package/dist/commands/arb/compare.js +216 -0
- package/dist/commands/arb/execute.d.ts +6 -0
- package/dist/commands/arb/execute.js +467 -0
- package/dist/commands/arb/funding.d.ts +6 -0
- package/dist/commands/arb/funding.js +201 -0
- package/dist/commands/arb/history.d.ts +6 -0
- package/dist/commands/arb/history.js +153 -0
- package/dist/commands/arb/index.d.ts +6 -0
- package/dist/commands/arb/index.js +29 -0
- package/dist/commands/arb/positions.d.ts +6 -0
- package/dist/commands/arb/positions.js +158 -0
- package/dist/commands/arb/spread.d.ts +6 -0
- package/dist/commands/arb/spread.js +253 -0
- package/dist/commands/arb/track.d.ts +6 -0
- package/dist/commands/arb/track.js +259 -0
- package/dist/commands/asset/book-simple.d.ts +5 -0
- package/dist/commands/asset/book-simple.js +77 -0
- package/dist/commands/asset/index.d.ts +2 -0
- package/dist/commands/asset/index.js +5 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +161 -0
- package/dist/commands/config/index.d.ts +5 -0
- package/dist/commands/config/index.js +109 -0
- package/dist/commands/data/index.d.ts +31 -0
- package/dist/commands/data/index.js +1466 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +201 -0
- package/dist/commands/exchange/index.d.ts +2 -0
- package/dist/commands/exchange/index.js +107 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +48 -0
- package/dist/commands/markets/index.d.ts +2 -0
- package/dist/commands/markets/index.js +5 -0
- package/dist/commands/markets/ls-simple.d.ts +7 -0
- package/dist/commands/markets/ls-simple.js +277 -0
- package/dist/commands/operator/index.d.ts +2 -0
- package/dist/commands/operator/index.js +146 -0
- package/dist/commands/order/cancel-simple.d.ts +5 -0
- package/dist/commands/order/cancel-simple.js +104 -0
- package/dist/commands/order/index.d.ts +2 -0
- package/dist/commands/order/index.js +13 -0
- package/dist/commands/order/limit-simple.d.ts +5 -0
- package/dist/commands/order/limit-simple.js +195 -0
- package/dist/commands/order/market-simple.d.ts +5 -0
- package/dist/commands/order/market-simple.js +190 -0
- package/dist/commands/order/shared.d.ts +17 -0
- package/dist/commands/order/shared.js +51 -0
- package/dist/commands/order/trigger-simple.d.ts +5 -0
- package/dist/commands/order/trigger-simple.js +246 -0
- package/dist/commands/referral/index.d.ts +2 -0
- package/dist/commands/referral/index.js +7 -0
- package/dist/commands/referral/set.d.ts +2 -0
- package/dist/commands/referral/set.js +26 -0
- package/dist/commands/referral/status.d.ts +2 -0
- package/dist/commands/referral/status.js +31 -0
- package/dist/commands/replay/index.d.ts +2 -0
- package/dist/commands/replay/index.js +152 -0
- package/dist/commands/risk/analytics.d.ts +2 -0
- package/dist/commands/risk/analytics.js +64 -0
- package/dist/commands/risk/audit.d.ts +2 -0
- package/dist/commands/risk/audit.js +52 -0
- package/dist/commands/risk/index.d.ts +2 -0
- package/dist/commands/risk/index.js +9 -0
- package/dist/commands/risk/rules.d.ts +2 -0
- package/dist/commands/risk/rules.js +102 -0
- package/dist/commands/server.d.ts +2 -0
- package/dist/commands/server.js +208 -0
- package/dist/commands/setup/index.d.ts +2 -0
- package/dist/commands/setup/index.js +478 -0
- package/dist/commands/signal/index.d.ts +2 -0
- package/dist/commands/signal/index.js +129 -0
- package/dist/commands/state/index.d.ts +2 -0
- package/dist/commands/state/index.js +5 -0
- package/dist/commands/state/show.d.ts +2 -0
- package/dist/commands/state/show.js +105 -0
- package/dist/commands/strategy/index.d.ts +4 -0
- package/dist/commands/strategy/index.js +73 -0
- package/dist/commands/traces/index.d.ts +2 -0
- package/dist/commands/traces/index.js +76 -0
- package/dist/commands/ui/demo.d.ts +9 -0
- package/dist/commands/ui/demo.js +195 -0
- package/dist/commands/ui/index.d.ts +2 -0
- package/dist/commands/ui/index.js +7 -0
- package/dist/commands/ui/terminal.d.ts +2 -0
- package/dist/commands/ui/terminal.js +255 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +98 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/lib/agent/audit.d.ts +12 -0
- package/dist/lib/agent/audit.js +13 -0
- package/dist/lib/agent/gateway.d.ts +13 -0
- package/dist/lib/agent/gateway.js +598 -0
- package/dist/lib/agent/metrics.d.ts +33 -0
- package/dist/lib/agent/metrics.js +175 -0
- package/dist/lib/agent/signature.d.ts +8 -0
- package/dist/lib/agent/signature.js +28 -0
- package/dist/lib/agent/tools.d.ts +28 -0
- package/dist/lib/agent/tools.js +453 -0
- package/dist/lib/agent/x402.d.ts +23 -0
- package/dist/lib/agent/x402.js +62 -0
- package/dist/lib/api-wallet.d.ts +69 -0
- package/dist/lib/api-wallet.js +101 -0
- package/dist/lib/balance-watcher.d.ts +25 -0
- package/dist/lib/balance-watcher.js +83 -0
- package/dist/lib/book-watcher.d.ts +25 -0
- package/dist/lib/book-watcher.js +48 -0
- package/dist/lib/config.d.ts +88 -0
- package/dist/lib/config.js +427 -0
- package/dist/lib/constants.d.ts +50 -0
- package/dist/lib/constants.js +84 -0
- package/dist/lib/contracts.d.ts +7 -0
- package/dist/lib/contracts.js +8 -0
- package/dist/lib/credential-vault.d.ts +22 -0
- package/dist/lib/credential-vault.js +109 -0
- package/dist/lib/db/accounts.d.ts +83 -0
- package/dist/lib/db/accounts.js +203 -0
- package/dist/lib/db/funding-history.d.ts +69 -0
- package/dist/lib/db/funding-history.js +183 -0
- package/dist/lib/db/index.d.ts +11 -0
- package/dist/lib/db/index.js +272 -0
- package/dist/lib/events/bus.d.ts +10 -0
- package/dist/lib/events/bus.js +17 -0
- package/dist/lib/events/types.d.ts +51 -0
- package/dist/lib/events/types.js +1 -0
- package/dist/lib/exchange.d.ts +30 -0
- package/dist/lib/exchange.js +84 -0
- package/dist/lib/execution/journal.d.ts +25 -0
- package/dist/lib/execution/journal.js +158 -0
- package/dist/lib/execution/safety.d.ts +34 -0
- package/dist/lib/execution/safety.js +197 -0
- package/dist/lib/exit-codes.d.ts +18 -0
- package/dist/lib/exit-codes.js +60 -0
- package/dist/lib/fetch.d.ts +18 -0
- package/dist/lib/fetch.js +66 -0
- package/dist/lib/fs-security.d.ts +10 -0
- package/dist/lib/fs-security.js +26 -0
- package/dist/lib/funding-tracker.d.ts +40 -0
- package/dist/lib/funding-tracker.js +118 -0
- package/dist/lib/logger.d.ts +27 -0
- package/dist/lib/logger.js +82 -0
- package/dist/lib/network-model.d.ts +13 -0
- package/dist/lib/network-model.js +30 -0
- package/dist/lib/onboarding.d.ts +133 -0
- package/dist/lib/onboarding.js +1459 -0
- package/dist/lib/operator-state.d.ts +25 -0
- package/dist/lib/operator-state.js +82 -0
- package/dist/lib/orders-watcher.d.ts +24 -0
- package/dist/lib/orders-watcher.js +74 -0
- package/dist/lib/paths.d.ts +20 -0
- package/dist/lib/paths.js +23 -0
- package/dist/lib/portfolio-watcher.d.ts +33 -0
- package/dist/lib/portfolio-watcher.js +95 -0
- package/dist/lib/position-watcher.d.ts +16 -0
- package/dist/lib/position-watcher.js +44 -0
- package/dist/lib/price-watcher.d.ts +15 -0
- package/dist/lib/price-watcher.js +84 -0
- package/dist/lib/prompts.d.ts +32 -0
- package/dist/lib/prompts.js +105 -0
- package/dist/lib/rate-limit.d.ts +32 -0
- package/dist/lib/rate-limit.js +88 -0
- package/dist/lib/risk/analytics.d.ts +39 -0
- package/dist/lib/risk/analytics.js +98 -0
- package/dist/lib/risk/drawdown.d.ts +18 -0
- package/dist/lib/risk/drawdown.js +49 -0
- package/dist/lib/risk/evaluation-log.d.ts +29 -0
- package/dist/lib/risk/evaluation-log.js +61 -0
- package/dist/lib/risk/index.d.ts +4 -0
- package/dist/lib/risk/index.js +4 -0
- package/dist/lib/risk/limits.d.ts +23 -0
- package/dist/lib/risk/limits.js +27 -0
- package/dist/lib/risk/manager.d.ts +32 -0
- package/dist/lib/risk/manager.js +85 -0
- package/dist/lib/risk/policy-middleware.d.ts +33 -0
- package/dist/lib/risk/policy-middleware.js +267 -0
- package/dist/lib/risk/position-sizer.d.ts +9 -0
- package/dist/lib/risk/position-sizer.js +14 -0
- package/dist/lib/risk/rules-store.d.ts +16 -0
- package/dist/lib/risk/rules-store.js +47 -0
- package/dist/lib/schema.d.ts +254 -0
- package/dist/lib/schema.js +199 -0
- package/dist/lib/secrets.d.ts +3 -0
- package/dist/lib/secrets.js +62 -0
- package/dist/lib/settings.d.ts +24 -0
- package/dist/lib/settings.js +86 -0
- package/dist/lib/signals.d.ts +73 -0
- package/dist/lib/signals.js +136 -0
- package/dist/lib/stable-stringify.d.ts +6 -0
- package/dist/lib/stable-stringify.js +17 -0
- package/dist/lib/state-context.d.ts +44 -0
- package/dist/lib/state-context.js +133 -0
- package/dist/lib/strategy/basis-trade.d.ts +2 -0
- package/dist/lib/strategy/basis-trade.js +24 -0
- package/dist/lib/strategy/funding-arb.d.ts +2 -0
- package/dist/lib/strategy/funding-arb.js +23 -0
- package/dist/lib/strategy/interface.d.ts +23 -0
- package/dist/lib/strategy/interface.js +1 -0
- package/dist/lib/strategy/registry.d.ts +4 -0
- package/dist/lib/strategy/registry.js +10 -0
- package/dist/lib/telemetry.d.ts +25 -0
- package/dist/lib/telemetry.js +101 -0
- package/dist/lib/trace-queries.d.ts +20 -0
- package/dist/lib/trace-queries.js +133 -0
- package/dist/lib/trace.d.ts +1 -0
- package/dist/lib/trace.js +4 -0
- package/dist/lib/trade-reputation.d.ts +6 -0
- package/dist/lib/trade-reputation.js +99 -0
- package/dist/lib/ui-tokens.d.ts +21 -0
- package/dist/lib/ui-tokens.js +26 -0
- package/dist/lib/validate.d.ts +39 -0
- package/dist/lib/validate.js +108 -0
- package/dist/lib/validation.d.ts +9 -0
- package/dist/lib/validation.js +64 -0
- package/dist/server/cache.d.ts +38 -0
- package/dist/server/cache.js +56 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +89 -0
- package/dist/server/ipc.d.ts +18 -0
- package/dist/server/ipc.js +159 -0
- package/dist/server/subscriptions.d.ts +18 -0
- package/dist/server/subscriptions.js +114 -0
- package/package.json +124 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { copyFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { HL_DIR, DB_PATH, LEGACY_DB_PATH } from "../paths.js";
|
|
4
|
+
import { ensurePrivateDir, hardenPrivateFile } from "../fs-security.js";
|
|
5
|
+
let db = null;
|
|
6
|
+
/**
|
|
7
|
+
* Get or create the database connection
|
|
8
|
+
*/
|
|
9
|
+
export function getDb() {
|
|
10
|
+
if (db) {
|
|
11
|
+
return db;
|
|
12
|
+
}
|
|
13
|
+
// Ensure runtime storage is private to the local user.
|
|
14
|
+
ensurePrivateDir(HL_DIR);
|
|
15
|
+
// One-time migration for pre-rename databases (~/.hl/hl.db -> ~/.perp/perp.db).
|
|
16
|
+
if (!existsSync(DB_PATH) && existsSync(LEGACY_DB_PATH)) {
|
|
17
|
+
copyFileSync(LEGACY_DB_PATH, DB_PATH);
|
|
18
|
+
hardenPrivateFile(DB_PATH);
|
|
19
|
+
}
|
|
20
|
+
db = new Database(DB_PATH);
|
|
21
|
+
hardenPrivateFile(DB_PATH);
|
|
22
|
+
// Enable foreign keys
|
|
23
|
+
db.pragma("foreign_keys = ON");
|
|
24
|
+
// Run migrations
|
|
25
|
+
runMigrations(db);
|
|
26
|
+
return db;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Close the database connection
|
|
30
|
+
*/
|
|
31
|
+
export function closeDb() {
|
|
32
|
+
if (db) {
|
|
33
|
+
db.close();
|
|
34
|
+
db = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Run database migrations
|
|
39
|
+
*/
|
|
40
|
+
function runMigrations(db) {
|
|
41
|
+
// Create migrations table if it doesn't exist
|
|
42
|
+
db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
44
|
+
id INTEGER PRIMARY KEY,
|
|
45
|
+
name TEXT NOT NULL UNIQUE,
|
|
46
|
+
applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
|
47
|
+
)
|
|
48
|
+
`);
|
|
49
|
+
const migrations = [
|
|
50
|
+
{
|
|
51
|
+
name: "001_create_accounts",
|
|
52
|
+
sql: `
|
|
53
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
alias TEXT NOT NULL,
|
|
56
|
+
user_address TEXT NOT NULL,
|
|
57
|
+
type TEXT NOT NULL CHECK (type IN ('readonly', 'api_wallet')),
|
|
58
|
+
source TEXT NOT NULL DEFAULT 'cli_import',
|
|
59
|
+
api_wallet_private_key TEXT,
|
|
60
|
+
api_wallet_public_key TEXT,
|
|
61
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
|
63
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Index for quick default account lookup
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_is_default ON accounts(is_default);
|
|
68
|
+
|
|
69
|
+
-- Index for user address lookups
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_user_address ON accounts(user_address);
|
|
71
|
+
`,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "002_add_exchange_column",
|
|
75
|
+
sql: `
|
|
76
|
+
-- Add exchange column with default 'hyperliquid' for existing accounts
|
|
77
|
+
ALTER TABLE accounts ADD COLUMN exchange TEXT NOT NULL DEFAULT 'hyperliquid';
|
|
78
|
+
|
|
79
|
+
-- Make alias unique per exchange (not globally)
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_exchange ON accounts(exchange);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_alias_exchange ON accounts(alias, exchange);
|
|
82
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_alias_exchange_unique ON accounts(alias, exchange);
|
|
83
|
+
`,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "003_create_funding_history",
|
|
87
|
+
sql: `
|
|
88
|
+
-- Funding rate history for arbitrage analysis
|
|
89
|
+
CREATE TABLE IF NOT EXISTS funding_history (
|
|
90
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
exchange TEXT NOT NULL,
|
|
92
|
+
market TEXT NOT NULL,
|
|
93
|
+
rate REAL NOT NULL, -- Hourly rate as decimal (e.g., 0.0001 = 0.01%)
|
|
94
|
+
annualized REAL NOT NULL, -- Annualized rate
|
|
95
|
+
next_funding INTEGER, -- Next funding timestamp
|
|
96
|
+
recorded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_funding_exchange_market ON funding_history(exchange, market);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_funding_recorded_at ON funding_history(recorded_at);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_funding_market_time ON funding_history(market, recorded_at);
|
|
102
|
+
`,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "004_rebuild_accounts_alias_scope",
|
|
106
|
+
sql: `
|
|
107
|
+
PRAGMA foreign_keys = OFF;
|
|
108
|
+
|
|
109
|
+
CREATE TABLE accounts_new (
|
|
110
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
111
|
+
alias TEXT NOT NULL,
|
|
112
|
+
exchange TEXT NOT NULL,
|
|
113
|
+
user_address TEXT NOT NULL,
|
|
114
|
+
type TEXT NOT NULL CHECK (type IN ('readonly', 'api_wallet')),
|
|
115
|
+
source TEXT NOT NULL DEFAULT 'cli_import',
|
|
116
|
+
api_wallet_private_key TEXT,
|
|
117
|
+
api_wallet_public_key TEXT,
|
|
118
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
119
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
|
120
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
|
121
|
+
UNIQUE(alias, exchange)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
INSERT INTO accounts_new (
|
|
125
|
+
id,
|
|
126
|
+
alias,
|
|
127
|
+
exchange,
|
|
128
|
+
user_address,
|
|
129
|
+
type,
|
|
130
|
+
source,
|
|
131
|
+
api_wallet_private_key,
|
|
132
|
+
api_wallet_public_key,
|
|
133
|
+
is_default,
|
|
134
|
+
created_at,
|
|
135
|
+
updated_at
|
|
136
|
+
)
|
|
137
|
+
SELECT
|
|
138
|
+
id,
|
|
139
|
+
alias,
|
|
140
|
+
exchange,
|
|
141
|
+
user_address,
|
|
142
|
+
type,
|
|
143
|
+
source,
|
|
144
|
+
api_wallet_private_key,
|
|
145
|
+
api_wallet_public_key,
|
|
146
|
+
is_default,
|
|
147
|
+
created_at,
|
|
148
|
+
updated_at
|
|
149
|
+
FROM accounts;
|
|
150
|
+
|
|
151
|
+
DROP TABLE accounts;
|
|
152
|
+
ALTER TABLE accounts_new RENAME TO accounts;
|
|
153
|
+
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_is_default ON accounts(is_default);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_user_address ON accounts(user_address);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_exchange ON accounts(exchange);
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_alias_exchange ON accounts(alias, exchange);
|
|
158
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_alias_exchange_unique ON accounts(alias, exchange);
|
|
159
|
+
|
|
160
|
+
PRAGMA foreign_keys = ON;
|
|
161
|
+
`,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "005_create_execution_journal",
|
|
165
|
+
sql: `
|
|
166
|
+
CREATE TABLE IF NOT EXISTS execution_journal (
|
|
167
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
168
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
169
|
+
request_hash TEXT NOT NULL,
|
|
170
|
+
command TEXT NOT NULL,
|
|
171
|
+
exchange TEXT NOT NULL,
|
|
172
|
+
testnet INTEGER NOT NULL DEFAULT 0,
|
|
173
|
+
market TEXT,
|
|
174
|
+
side TEXT,
|
|
175
|
+
order_type TEXT,
|
|
176
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'succeeded', 'failed')),
|
|
177
|
+
request_json TEXT NOT NULL,
|
|
178
|
+
result_json TEXT,
|
|
179
|
+
error_message TEXT,
|
|
180
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
181
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
182
|
+
completed_at INTEGER
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
CREATE INDEX IF NOT EXISTS idx_execution_journal_status ON execution_journal(status);
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_execution_journal_exchange_created ON execution_journal(exchange, created_at);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_execution_journal_created ON execution_journal(created_at);
|
|
188
|
+
`,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "006_create_risk_rules",
|
|
192
|
+
sql: `
|
|
193
|
+
CREATE TABLE IF NOT EXISTS risk_rules (
|
|
194
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
195
|
+
name TEXT NOT NULL UNIQUE,
|
|
196
|
+
type TEXT NOT NULL CHECK(type IN ('max_position_size', 'max_total_exposure', 'max_leverage', 'max_drawdown', 'min_confidence', 'cooldown_after_loss', 'rate_limit')),
|
|
197
|
+
scope TEXT NOT NULL CHECK(scope IN ('global', 'exchange', 'market', 'strategy')),
|
|
198
|
+
scope_value TEXT,
|
|
199
|
+
value REAL NOT NULL,
|
|
200
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
201
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
202
|
+
);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_risk_rules_scope ON risk_rules(scope, scope_value);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_risk_rules_enabled ON risk_rules(enabled);
|
|
205
|
+
`,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "007_create_risk_evaluation_log",
|
|
209
|
+
sql: `
|
|
210
|
+
CREATE TABLE IF NOT EXISTS risk_evaluation_log (
|
|
211
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
212
|
+
trace_id TEXT,
|
|
213
|
+
exchange TEXT NOT NULL,
|
|
214
|
+
market TEXT,
|
|
215
|
+
side TEXT,
|
|
216
|
+
requested_size_usd REAL NOT NULL,
|
|
217
|
+
equity REAL NOT NULL,
|
|
218
|
+
exposure_usd REAL NOT NULL,
|
|
219
|
+
confidence REAL,
|
|
220
|
+
allowed INTEGER NOT NULL,
|
|
221
|
+
adjusted_size_usd REAL,
|
|
222
|
+
drawdown_pct REAL,
|
|
223
|
+
rules_evaluated TEXT,
|
|
224
|
+
rules_fired TEXT,
|
|
225
|
+
reason TEXT,
|
|
226
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
227
|
+
);
|
|
228
|
+
CREATE INDEX IF NOT EXISTS idx_risk_eval_trace ON risk_evaluation_log(trace_id);
|
|
229
|
+
CREATE INDEX IF NOT EXISTS idx_risk_eval_time ON risk_evaluation_log(created_at);
|
|
230
|
+
CREATE INDEX IF NOT EXISTS idx_risk_eval_allowed ON risk_evaluation_log(allowed);
|
|
231
|
+
`,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "008_add_trace_id_to_journal",
|
|
235
|
+
sql: `ALTER TABLE execution_journal ADD COLUMN trace_id TEXT;
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_journal_trace ON execution_journal(trace_id);`,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "009_create_trade_signals",
|
|
240
|
+
sql: `
|
|
241
|
+
CREATE TABLE IF NOT EXISTS trade_signals (
|
|
242
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
243
|
+
trace_id TEXT,
|
|
244
|
+
exchange TEXT NOT NULL,
|
|
245
|
+
market TEXT,
|
|
246
|
+
strategy TEXT,
|
|
247
|
+
signal TEXT NOT NULL CHECK(signal IN ('up', 'down')),
|
|
248
|
+
reason TEXT,
|
|
249
|
+
pnl_usd REAL,
|
|
250
|
+
metadata TEXT,
|
|
251
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
252
|
+
);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_signals_trace ON trade_signals(trace_id);
|
|
254
|
+
CREATE INDEX IF NOT EXISTS idx_signals_exchange ON trade_signals(exchange);
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_signals_signal ON trade_signals(signal);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_signals_time ON trade_signals(created_at);
|
|
257
|
+
`,
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
const appliedMigrations = db
|
|
261
|
+
.prepare("SELECT name FROM migrations")
|
|
262
|
+
.all();
|
|
263
|
+
const appliedNames = new Set(appliedMigrations.map((m) => m.name));
|
|
264
|
+
for (const migration of migrations) {
|
|
265
|
+
if (!appliedNames.has(migration.name)) {
|
|
266
|
+
db.exec(migration.sql);
|
|
267
|
+
db.prepare("INSERT INTO migrations (name) VALUES (?)").run(migration.name);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
export * from "./accounts.js";
|
|
272
|
+
export * from "./funding-history.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EventMap } from "./types.js";
|
|
2
|
+
export type { EventMap };
|
|
3
|
+
declare class TypedEventBus {
|
|
4
|
+
private emitter;
|
|
5
|
+
on<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void): void;
|
|
6
|
+
off<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void): void;
|
|
7
|
+
once<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void): void;
|
|
8
|
+
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void;
|
|
9
|
+
}
|
|
10
|
+
export declare const eventBus: TypedEventBus;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
class TypedEventBus {
|
|
3
|
+
emitter = new EventEmitter();
|
|
4
|
+
on(event, handler) {
|
|
5
|
+
this.emitter.on(event, handler);
|
|
6
|
+
}
|
|
7
|
+
off(event, handler) {
|
|
8
|
+
this.emitter.off(event, handler);
|
|
9
|
+
}
|
|
10
|
+
once(event, handler) {
|
|
11
|
+
this.emitter.once(event, handler);
|
|
12
|
+
}
|
|
13
|
+
emit(event, data) {
|
|
14
|
+
this.emitter.emit(event, data);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const eventBus = new TypedEventBus();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface EventMap {
|
|
2
|
+
"risk.circuit_breaker.tripped": {
|
|
3
|
+
exchange: string;
|
|
4
|
+
drawdownPct: number;
|
|
5
|
+
peakEquity: number;
|
|
6
|
+
currentEquity: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
};
|
|
9
|
+
"risk.evaluation.denied": {
|
|
10
|
+
exchange: string;
|
|
11
|
+
market: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
traceId?: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
};
|
|
16
|
+
"order.filled": {
|
|
17
|
+
exchange: string;
|
|
18
|
+
market: string;
|
|
19
|
+
orderId: string;
|
|
20
|
+
side: string;
|
|
21
|
+
size: number;
|
|
22
|
+
price: number;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
};
|
|
25
|
+
"position.opened": {
|
|
26
|
+
exchange: string;
|
|
27
|
+
market: string;
|
|
28
|
+
side: string;
|
|
29
|
+
size: number;
|
|
30
|
+
entryPrice: number;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
};
|
|
33
|
+
"position.closed": {
|
|
34
|
+
exchange: string;
|
|
35
|
+
market: string;
|
|
36
|
+
side: string;
|
|
37
|
+
size: number;
|
|
38
|
+
entryPrice: number;
|
|
39
|
+
exitPrice: number;
|
|
40
|
+
pnl: number;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
};
|
|
43
|
+
"operator.halt": {
|
|
44
|
+
reason: string;
|
|
45
|
+
source: "manual" | "drawdown" | "error";
|
|
46
|
+
timestamp: number;
|
|
47
|
+
};
|
|
48
|
+
"operator.resume": {
|
|
49
|
+
timestamp: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exchange Factory and Context
|
|
3
|
+
* Provides adapter instances based on configuration
|
|
4
|
+
*/
|
|
5
|
+
import { type PerpDEXAdapter } from "../adapters/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Set the current exchange context
|
|
8
|
+
*/
|
|
9
|
+
export declare function setExchange(exchangeId: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Get the current exchange ID
|
|
12
|
+
*/
|
|
13
|
+
export declare function getExchangeId(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Get the current exchange adapter
|
|
16
|
+
* Creates a new instance if needed
|
|
17
|
+
*/
|
|
18
|
+
export declare function getExchangeAdapter(): PerpDEXAdapter;
|
|
19
|
+
/**
|
|
20
|
+
* Get a specific exchange adapter by ID
|
|
21
|
+
*/
|
|
22
|
+
export declare function getExchangeAdapterById(exchangeId: string): PerpDEXAdapter;
|
|
23
|
+
/**
|
|
24
|
+
* List all available exchanges
|
|
25
|
+
*/
|
|
26
|
+
export declare function getAvailableExchanges(): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Check if an exchange is supported
|
|
29
|
+
*/
|
|
30
|
+
export declare function isExchangeSupported(exchangeId: string): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exchange Factory and Context
|
|
3
|
+
* Provides adapter instances based on configuration
|
|
4
|
+
*/
|
|
5
|
+
import { getAdapter, listAdapters } from "../adapters/index.js";
|
|
6
|
+
import { getSetting } from "./settings.js";
|
|
7
|
+
// Current exchange context (set by CLI)
|
|
8
|
+
let currentExchange = null;
|
|
9
|
+
let currentAdapter = null;
|
|
10
|
+
/**
|
|
11
|
+
* Get the default exchange from settings
|
|
12
|
+
*/
|
|
13
|
+
function getDefaultExchange() {
|
|
14
|
+
const available = listAdapters();
|
|
15
|
+
const fallback = "hyperliquid";
|
|
16
|
+
try {
|
|
17
|
+
const configured = getSetting("defaultExchange") || fallback;
|
|
18
|
+
if (available.includes(configured)) {
|
|
19
|
+
return configured;
|
|
20
|
+
}
|
|
21
|
+
return available.includes(fallback) ? fallback : available[0] ?? fallback;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return available.includes(fallback) ? fallback : available[0] ?? fallback;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Set the current exchange context
|
|
29
|
+
*/
|
|
30
|
+
export function setExchange(exchangeId) {
|
|
31
|
+
const id = exchangeId.toLowerCase();
|
|
32
|
+
if (!listAdapters().includes(id)) {
|
|
33
|
+
const available = listAdapters().join(", ");
|
|
34
|
+
throw new Error(`Unknown exchange: ${exchangeId}. Available: ${available}`);
|
|
35
|
+
}
|
|
36
|
+
currentExchange = id;
|
|
37
|
+
currentAdapter = null; // Reset adapter so it gets recreated
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the current exchange ID
|
|
41
|
+
*/
|
|
42
|
+
export function getExchangeId() {
|
|
43
|
+
if (!currentExchange) {
|
|
44
|
+
currentExchange = getDefaultExchange();
|
|
45
|
+
}
|
|
46
|
+
return currentExchange;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get the current exchange adapter
|
|
50
|
+
* Creates a new instance if needed
|
|
51
|
+
*/
|
|
52
|
+
export function getExchangeAdapter() {
|
|
53
|
+
const exchangeId = getExchangeId();
|
|
54
|
+
if (!currentAdapter) {
|
|
55
|
+
currentAdapter = getAdapter(exchangeId);
|
|
56
|
+
if (!currentAdapter) {
|
|
57
|
+
throw new Error(`Failed to create adapter for: ${exchangeId}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return currentAdapter;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get a specific exchange adapter by ID
|
|
64
|
+
*/
|
|
65
|
+
export function getExchangeAdapterById(exchangeId) {
|
|
66
|
+
const adapter = getAdapter(exchangeId.toLowerCase());
|
|
67
|
+
if (!adapter) {
|
|
68
|
+
const available = listAdapters().join(", ");
|
|
69
|
+
throw new Error(`Unknown exchange: ${exchangeId}. Available: ${available}`);
|
|
70
|
+
}
|
|
71
|
+
return adapter;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* List all available exchanges
|
|
75
|
+
*/
|
|
76
|
+
export function getAvailableExchanges() {
|
|
77
|
+
return listAdapters();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check if an exchange is supported
|
|
81
|
+
*/
|
|
82
|
+
export function isExchangeSupported(exchangeId) {
|
|
83
|
+
return listAdapters().includes(exchangeId.toLowerCase());
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const AUTO_IDEMPOTENCY_WINDOW_MS = 30000;
|
|
2
|
+
export interface ExecutionJournalMetadata {
|
|
3
|
+
command: string;
|
|
4
|
+
exchange: string;
|
|
5
|
+
testnet: boolean;
|
|
6
|
+
market?: string;
|
|
7
|
+
side?: string;
|
|
8
|
+
orderType?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ExecutionJournalOptions<T> {
|
|
11
|
+
idempotencyKey?: string;
|
|
12
|
+
traceId?: string;
|
|
13
|
+
metadata: ExecutionJournalMetadata;
|
|
14
|
+
request: unknown;
|
|
15
|
+
execute: () => Promise<T>;
|
|
16
|
+
}
|
|
17
|
+
export interface ExecutionJournalResult<T> {
|
|
18
|
+
result: T;
|
|
19
|
+
idempotencyKey: string;
|
|
20
|
+
autoGeneratedKey: boolean;
|
|
21
|
+
replayed: boolean;
|
|
22
|
+
journalId: number;
|
|
23
|
+
traceId?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function runWithExecutionJournal<T>(options: ExecutionJournalOptions<T>): Promise<ExecutionJournalResult<T>>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { getDb } from "../db/index.js";
|
|
3
|
+
import { stableStringify } from "../stable-stringify.js";
|
|
4
|
+
export const AUTO_IDEMPOTENCY_WINDOW_MS = 30_000;
|
|
5
|
+
function hashRequest(request) {
|
|
6
|
+
const canonical = stableStringify(request);
|
|
7
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
8
|
+
}
|
|
9
|
+
function normalizeExplicitKey(input) {
|
|
10
|
+
if (!input) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const key = input.trim();
|
|
14
|
+
if (!key) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (key.length > 128) {
|
|
18
|
+
throw new Error("Idempotency key must be 128 characters or fewer");
|
|
19
|
+
}
|
|
20
|
+
if (!/^[A-Za-z0-9:._/-]+$/.test(key)) {
|
|
21
|
+
throw new Error("Idempotency key contains invalid characters");
|
|
22
|
+
}
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
function resolveIdempotencyKey(explicitKey, requestHash, nowMs) {
|
|
26
|
+
const normalized = normalizeExplicitKey(explicitKey);
|
|
27
|
+
if (normalized) {
|
|
28
|
+
return { key: normalized, autoGenerated: false };
|
|
29
|
+
}
|
|
30
|
+
const bucket = Math.floor(nowMs / AUTO_IDEMPOTENCY_WINDOW_MS);
|
|
31
|
+
return {
|
|
32
|
+
key: `auto:${requestHash.slice(0, 24)}:${bucket}`,
|
|
33
|
+
autoGenerated: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function getByKey(idempotencyKey) {
|
|
37
|
+
const db = getDb();
|
|
38
|
+
const row = db
|
|
39
|
+
.prepare("SELECT * FROM execution_journal WHERE idempotency_key = ?")
|
|
40
|
+
.get(idempotencyKey);
|
|
41
|
+
return row ?? null;
|
|
42
|
+
}
|
|
43
|
+
function parseStoredResult(row) {
|
|
44
|
+
if (!row.result_json) {
|
|
45
|
+
throw new Error(`Execution journal entry ${row.id} is missing result payload`);
|
|
46
|
+
}
|
|
47
|
+
return JSON.parse(row.result_json);
|
|
48
|
+
}
|
|
49
|
+
function handleExistingRow(row, requestHash, resolved) {
|
|
50
|
+
if (row.request_hash !== requestHash) {
|
|
51
|
+
throw new Error(`Idempotency key "${resolved.key}" was already used for a different request`);
|
|
52
|
+
}
|
|
53
|
+
if (row.status === "succeeded") {
|
|
54
|
+
return {
|
|
55
|
+
result: parseStoredResult(row),
|
|
56
|
+
idempotencyKey: row.idempotency_key,
|
|
57
|
+
autoGeneratedKey: resolved.autoGenerated,
|
|
58
|
+
replayed: true,
|
|
59
|
+
journalId: row.id,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (row.status === "pending") {
|
|
63
|
+
throw new Error(`Execution with idempotency key "${resolved.key}" is still pending. ` +
|
|
64
|
+
"Refusing retry to avoid duplicate orders.");
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Execution with idempotency key "${resolved.key}" already failed` +
|
|
67
|
+
(row.error_message ? `: ${row.error_message}` : ". Use a new key to retry."));
|
|
68
|
+
}
|
|
69
|
+
function insertPending(idempotencyKey, requestHash, requestJson, metadata, nowMs, traceId) {
|
|
70
|
+
const db = getDb();
|
|
71
|
+
const result = db.prepare(`
|
|
72
|
+
INSERT INTO execution_journal (
|
|
73
|
+
idempotency_key,
|
|
74
|
+
request_hash,
|
|
75
|
+
command,
|
|
76
|
+
exchange,
|
|
77
|
+
testnet,
|
|
78
|
+
market,
|
|
79
|
+
side,
|
|
80
|
+
order_type,
|
|
81
|
+
status,
|
|
82
|
+
request_json,
|
|
83
|
+
created_at,
|
|
84
|
+
updated_at,
|
|
85
|
+
trace_id
|
|
86
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?)
|
|
87
|
+
`).run(idempotencyKey, requestHash, metadata.command, metadata.exchange, metadata.testnet ? 1 : 0, metadata.market ?? null, metadata.side ?? null, metadata.orderType ?? null, requestJson, nowMs, nowMs, traceId ?? null);
|
|
88
|
+
return Number(result.lastInsertRowid);
|
|
89
|
+
}
|
|
90
|
+
function markSucceeded(idempotencyKey, result, nowMs) {
|
|
91
|
+
const db = getDb();
|
|
92
|
+
const resultJson = JSON.stringify(result ?? null);
|
|
93
|
+
db.prepare(`
|
|
94
|
+
UPDATE execution_journal
|
|
95
|
+
SET
|
|
96
|
+
status = 'succeeded',
|
|
97
|
+
result_json = ?,
|
|
98
|
+
error_message = NULL,
|
|
99
|
+
updated_at = ?,
|
|
100
|
+
completed_at = ?
|
|
101
|
+
WHERE idempotency_key = ?
|
|
102
|
+
`).run(resultJson, nowMs, nowMs, idempotencyKey);
|
|
103
|
+
}
|
|
104
|
+
function markFailed(idempotencyKey, err, nowMs) {
|
|
105
|
+
const db = getDb();
|
|
106
|
+
const errorMessage = err instanceof Error
|
|
107
|
+
? `${err.name}: ${err.message}`
|
|
108
|
+
: String(err);
|
|
109
|
+
db.prepare(`
|
|
110
|
+
UPDATE execution_journal
|
|
111
|
+
SET
|
|
112
|
+
status = 'failed',
|
|
113
|
+
error_message = ?,
|
|
114
|
+
updated_at = ?,
|
|
115
|
+
completed_at = ?
|
|
116
|
+
WHERE idempotency_key = ?
|
|
117
|
+
`).run(errorMessage, nowMs, nowMs, idempotencyKey);
|
|
118
|
+
}
|
|
119
|
+
export async function runWithExecutionJournal(options) {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const requestHash = hashRequest(options.request);
|
|
122
|
+
const requestJson = stableStringify(options.request);
|
|
123
|
+
const resolved = resolveIdempotencyKey(options.idempotencyKey, requestHash, now);
|
|
124
|
+
const existing = getByKey(resolved.key);
|
|
125
|
+
if (existing) {
|
|
126
|
+
return handleExistingRow(existing, requestHash, resolved);
|
|
127
|
+
}
|
|
128
|
+
let journalId;
|
|
129
|
+
try {
|
|
130
|
+
journalId = insertPending(resolved.key, requestHash, requestJson, options.metadata, now, options.traceId);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
134
|
+
if (message.includes("UNIQUE constraint failed: execution_journal.idempotency_key")) {
|
|
135
|
+
const raceExisting = getByKey(resolved.key);
|
|
136
|
+
if (raceExisting) {
|
|
137
|
+
return handleExistingRow(raceExisting, requestHash, resolved);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const result = await options.execute();
|
|
144
|
+
markSucceeded(resolved.key, result, Date.now());
|
|
145
|
+
return {
|
|
146
|
+
result,
|
|
147
|
+
idempotencyKey: resolved.key,
|
|
148
|
+
autoGeneratedKey: resolved.autoGenerated,
|
|
149
|
+
replayed: false,
|
|
150
|
+
journalId,
|
|
151
|
+
traceId: options.traceId,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
markFailed(resolved.key, err, Date.now());
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Order, OrderParams, PerpDEXAdapter } from "../../adapters/interface.js";
|
|
2
|
+
import type { ExecutionSafetyConfig } from "../config.js";
|
|
3
|
+
export interface SafetyExecutionOptions {
|
|
4
|
+
closeThenFlip?: boolean;
|
|
5
|
+
spreadAwarePricing?: boolean;
|
|
6
|
+
spreadOffset?: number;
|
|
7
|
+
attachTpSl?: boolean;
|
|
8
|
+
tpSlConfig?: Pick<ExecutionSafetyConfig, "stopLossPct" | "takeProfitPct">;
|
|
9
|
+
}
|
|
10
|
+
export interface SafetyExecutionResult {
|
|
11
|
+
order: Order;
|
|
12
|
+
closedOppositePosition: boolean;
|
|
13
|
+
tpSlOrderIds: string[];
|
|
14
|
+
pricing?: {
|
|
15
|
+
source: "orderbook" | "ticker";
|
|
16
|
+
referencePrice: number;
|
|
17
|
+
spread?: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface SpreadAwarePrice {
|
|
21
|
+
source: "orderbook" | "ticker";
|
|
22
|
+
referencePrice: number;
|
|
23
|
+
spread?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function executeOrderWithSafety(adapter: PerpDEXAdapter, params: OrderParams, options?: SafetyExecutionOptions): Promise<SafetyExecutionResult>;
|
|
26
|
+
export declare function closeThenFlipIfNeeded(adapter: PerpDEXAdapter, market: string, desiredSide: "long" | "short"): Promise<boolean>;
|
|
27
|
+
export declare function attachTpSlForPosition(adapter: PerpDEXAdapter, position: {
|
|
28
|
+
market: string;
|
|
29
|
+
side: "long" | "short";
|
|
30
|
+
size: string;
|
|
31
|
+
entryPrice: number;
|
|
32
|
+
}, config: Pick<ExecutionSafetyConfig, "stopLossPct" | "takeProfitPct">): Promise<string[]>;
|
|
33
|
+
export declare function resolveSpreadAwarePrice(adapter: PerpDEXAdapter, market: string, side: "long" | "short", spreadOffset?: number): Promise<SpreadAwarePrice>;
|
|
34
|
+
export {};
|