@reefclaw/openclaw-plugin 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/.gitignore +8 -0
- package/audit/mode-transition-audit.d.ts +11 -0
- package/audit/mode-transition-audit.js +29 -0
- package/balance-utils.d.ts +36 -0
- package/balance-utils.js +98 -0
- package/bridge/bridge.d.ts +109 -0
- package/bridge/bridge.js +1036 -0
- package/bridge/config.d.ts +43 -0
- package/bridge/config.js +176 -0
- package/bridge/connector.d.ts +52 -0
- package/bridge/connector.js +293 -0
- package/bridge/event-replay-buffer.d.ts +43 -0
- package/bridge/event-replay-buffer.js +109 -0
- package/bridge/gateway/event-parser.d.ts +209 -0
- package/bridge/gateway/event-parser.js +793 -0
- package/bridge/gateway/gateway-config.d.ts +39 -0
- package/bridge/gateway/gateway-config.js +86 -0
- package/bridge/gateway/gateway-http-client.d.ts +50 -0
- package/bridge/gateway/gateway-http-client.js +165 -0
- package/bridge/gateway/gateway-ws-client.d.ts +116 -0
- package/bridge/gateway/gateway-ws-client.js +417 -0
- package/bridge/gateway/poller.d.ts +146 -0
- package/bridge/gateway/poller.js +505 -0
- package/bridge/gateway/tool-discovery.d.ts +25 -0
- package/bridge/gateway/tool-discovery.js +198 -0
- package/bridge/index.d.ts +2 -0
- package/bridge/index.js +253 -0
- package/bridge/logger.d.ts +2 -0
- package/bridge/logger.js +2 -0
- package/bridge/provider.d.ts +156 -0
- package/bridge/provider.js +2 -0
- package/bridge/providers/emergency-commands.d.ts +54 -0
- package/bridge/providers/emergency-commands.js +235 -0
- package/bridge/providers/gateway.d.ts +322 -0
- package/bridge/providers/gateway.js +2302 -0
- package/bridge/providers/mock.d.ts +37 -0
- package/bridge/providers/mock.js +385 -0
- package/bridge/providers/onboarding-commands.d.ts +83 -0
- package/bridge/providers/onboarding-commands.js +213 -0
- package/bridge/providers/risk-calculator.d.ts +96 -0
- package/bridge/providers/risk-calculator.js +369 -0
- package/bridge/setup.d.ts +32 -0
- package/bridge/setup.js +226 -0
- package/bridge/types.d.ts +584 -0
- package/bridge/types.js +50 -0
- package/bridge/utils/reconnect.d.ts +6 -0
- package/bridge/utils/reconnect.js +6 -0
- package/bridge/utils/skill-signing.d.ts +51 -0
- package/bridge/utils/skill-signing.js +138 -0
- package/bridge/utils/skill-version.d.ts +17 -0
- package/bridge/utils/skill-version.js +74 -0
- package/ccxt/binance-ban-gate.d.ts +47 -0
- package/ccxt/binance-ban-gate.js +409 -0
- package/ccxt/binance-private.d.ts +325 -0
- package/ccxt/binance-private.js +1415 -0
- package/ccxt/binance-public.d.ts +18 -0
- package/ccxt/binance-public.js +147 -0
- package/config/agent-config-client.d.ts +55 -0
- package/config/agent-config-client.js +145 -0
- package/config/agent-config-poller.d.ts +25 -0
- package/config/agent-config-poller.js +100 -0
- package/config/brackets-config.d.ts +22 -0
- package/config/brackets-config.js +58 -0
- package/config/gate-store.d.ts +18 -0
- package/config/gate-store.js +61 -0
- package/config/plugin-config-io.d.ts +181 -0
- package/config/plugin-config-io.js +84 -0
- package/config/position-review-config.d.ts +35 -0
- package/config/position-review-config.js +105 -0
- package/config/tool-gate.d.ts +53 -0
- package/config/tool-gate.js +125 -0
- package/config/user-data-stream-config.d.ts +85 -0
- package/config/user-data-stream-config.js +224 -0
- package/connector-supervisor.d.ts +36 -0
- package/connector-supervisor.js +149 -0
- package/exchange-adapter.d.ts +49 -0
- package/exchange-adapter.js +4 -0
- package/index.d.ts +30 -0
- package/index.js +2030 -0
- package/ingest/pending-entry-metadata.d.ts +52 -0
- package/ingest/pending-entry-metadata.js +182 -0
- package/ingest/position-auto-capture.d.ts +98 -0
- package/ingest/position-auto-capture.js +394 -0
- package/ingest/position-decisions-client.d.ts +318 -0
- package/ingest/position-decisions-client.js +296 -0
- package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
- package/ingest/reconcile-db-vs-exchange.js +114 -0
- package/ingest/reconciler-cleanup.d.ts +37 -0
- package/ingest/reconciler-cleanup.js +147 -0
- package/ingest/rest-gap-filler.d.ts +191 -0
- package/ingest/rest-gap-filler.js +565 -0
- package/ingest/touched-symbols-store.d.ts +25 -0
- package/ingest/touched-symbols-store.js +96 -0
- package/ingest/trade-store-client.d.ts +40 -0
- package/ingest/trade-store-client.js +125 -0
- package/ingest/ws-ingest.d.ts +43 -0
- package/ingest/ws-ingest.js +126 -0
- package/learning/setup-family.d.ts +21 -0
- package/learning/setup-family.js +103 -0
- package/lifecycle/install-signal-handlers.d.ts +33 -0
- package/lifecycle/install-signal-handlers.js +112 -0
- package/lifecycle/shutdown-coordinator.d.ts +43 -0
- package/lifecycle/shutdown-coordinator.js +131 -0
- package/live/bracket-id.d.ts +18 -0
- package/live/bracket-id.js +81 -0
- package/live/bracket-ledger.d.ts +54 -0
- package/live/bracket-ledger.js +267 -0
- package/live/bracket-manager.d.ts +82 -0
- package/live/bracket-manager.js +478 -0
- package/live/bracket-params.d.ts +22 -0
- package/live/bracket-params.js +124 -0
- package/live/bracket-reconciler.d.ts +95 -0
- package/live/bracket-reconciler.js +573 -0
- package/live/bracket-types.d.ts +102 -0
- package/live/bracket-types.js +8 -0
- package/live/deposit-tracker.d.ts +62 -0
- package/live/deposit-tracker.js +97 -0
- package/live/emergency-controls.d.ts +32 -0
- package/live/emergency-controls.js +226 -0
- package/live/exchange-errors.d.ts +12 -0
- package/live/exchange-errors.js +130 -0
- package/live/exchange-info-cache.d.ts +35 -0
- package/live/exchange-info-cache.js +119 -0
- package/live/fact-subscriber.d.ts +78 -0
- package/live/fact-subscriber.js +182 -0
- package/live/intent-journal.d.ts +42 -0
- package/live/intent-journal.js +122 -0
- package/live/listen-key-manager.d.ts +70 -0
- package/live/listen-key-manager.js +169 -0
- package/live/live-adapter.d.ts +264 -0
- package/live/live-adapter.js +1665 -0
- package/live/live-balance-enricher.d.ts +32 -0
- package/live/live-balance-enricher.js +104 -0
- package/live/live-bracket-api.d.ts +13 -0
- package/live/live-bracket-api.js +20 -0
- package/live/live-state-store.d.ts +194 -0
- package/live/live-state-store.js +450 -0
- package/live/local-signal-service.d.ts +57 -0
- package/live/local-signal-service.js +146 -0
- package/live/local-strategy-evaluator.d.ts +62 -0
- package/live/local-strategy-evaluator.js +127 -0
- package/live/microstructure-assembler.d.ts +54 -0
- package/live/microstructure-assembler.js +148 -0
- package/live/order-poller.d.ts +29 -0
- package/live/order-poller.js +125 -0
- package/live/position-state-store.d.ts +83 -0
- package/live/position-state-store.js +237 -0
- package/live/proposal-decision-listener.d.ts +64 -0
- package/live/proposal-decision-listener.js +288 -0
- package/live/proposal-manager.d.ts +76 -0
- package/live/proposal-manager.js +140 -0
- package/live/rate-limiter.d.ts +47 -0
- package/live/rate-limiter.js +159 -0
- package/live/reconciler.d.ts +39 -0
- package/live/reconciler.js +175 -0
- package/live/setup-buckets.d.ts +7 -0
- package/live/setup-buckets.js +33 -0
- package/live/slippage-tracker.d.ts +45 -0
- package/live/slippage-tracker.js +78 -0
- package/live/stop-watcher.d.ts +34 -0
- package/live/stop-watcher.js +158 -0
- package/live/user-data-active-probe.d.ts +54 -0
- package/live/user-data-active-probe.js +180 -0
- package/live/user-data-stream-controller.d.ts +200 -0
- package/live/user-data-stream-controller.js +579 -0
- package/live/user-data-stream-ws.d.ts +22 -0
- package/live/user-data-stream-ws.js +63 -0
- package/live/user-data-stream.d.ts +243 -0
- package/live/user-data-stream.js +704 -0
- package/logger.d.ts +2 -0
- package/logger.js +2 -0
- package/mfe.d.ts +21 -0
- package/mfe.js +68 -0
- package/onboarding/mode-ladder.d.ts +1 -0
- package/onboarding/mode-ladder.js +3 -0
- package/onboarding/runtime.d.ts +71 -0
- package/onboarding/runtime.js +153 -0
- package/openclaw.plugin.json +94 -0
- package/package.json +27 -0
- package/paper-adapter.d.ts +24 -0
- package/paper-adapter.js +91 -0
- package/persistence/state-manager.d.ts +42 -0
- package/persistence/state-manager.js +164 -0
- package/pinned-plan.d.ts +9 -0
- package/pinned-plan.js +23 -0
- package/risk/pre-trade-check.d.ts +38 -0
- package/risk/pre-trade-check.js +345 -0
- package/risk/pre-trade-types.d.ts +60 -0
- package/risk/pre-trade-types.js +3 -0
- package/scripts/assemble.mjs +114 -0
- package/shadow/shadow-tracker.d.ts +36 -0
- package/shadow/shadow-tracker.js +151 -0
- package/shadow/types.d.ts +42 -0
- package/shadow/types.js +20 -0
- package/shared/indicators-extended.d.ts +52 -0
- package/shared/indicators-extended.js +291 -0
- package/shared/indicators.d.ts +15 -0
- package/shared/indicators.js +114 -0
- package/signals/conditions/registry.d.ts +16 -0
- package/signals/conditions/registry.js +1274 -0
- package/signals/conditions/types.d.ts +1 -0
- package/signals/conditions/types.js +4 -0
- package/signals/direction-rules.d.ts +3 -0
- package/signals/direction-rules.js +24 -0
- package/signals/entry-rules.d.ts +6 -0
- package/signals/entry-rules.js +33 -0
- package/signals/serialize-context.d.ts +4 -0
- package/signals/serialize-context.js +39 -0
- package/signals/stop-rules.d.ts +3 -0
- package/signals/stop-rules.js +48 -0
- package/signals/strategy-adapter.d.ts +14 -0
- package/signals/strategy-adapter.js +122 -0
- package/signals/types.d.ts +1 -0
- package/signals/types.js +8 -0
- package/simulator/exchange-simulator.d.ts +93 -0
- package/simulator/exchange-simulator.js +684 -0
- package/simulator/fill-engine.d.ts +53 -0
- package/simulator/fill-engine.js +276 -0
- package/simulator/paper-market-feed.d.ts +26 -0
- package/simulator/paper-market-feed.js +104 -0
- package/simulator/realistic-fills.d.ts +59 -0
- package/simulator/realistic-fills.js +175 -0
- package/simulator/types.d.ts +219 -0
- package/simulator/types.js +43 -0
- package/skills/reefclaw/SKILL.md +97 -0
- package/strategy/builtin-strategies.d.ts +2 -0
- package/strategy/builtin-strategies.js +109 -0
- package/strategy/condition-registry.d.ts +3 -0
- package/strategy/condition-registry.js +153 -0
- package/strategy/evaluator.d.ts +67 -0
- package/strategy/evaluator.js +93 -0
- package/tools/assessment-validation.d.ts +118 -0
- package/tools/assessment-validation.js +415 -0
- package/tools/attach-brackets.d.ts +34 -0
- package/tools/attach-brackets.js +363 -0
- package/tools/audit-bracket-protection.d.ts +49 -0
- package/tools/audit-bracket-protection.js +527 -0
- package/tools/cancel-all-orders.d.ts +7 -0
- package/tools/cancel-all-orders.js +5 -0
- package/tools/cancel-order.d.ts +10 -0
- package/tools/cancel-order.js +14 -0
- package/tools/check-position-health.d.ts +46 -0
- package/tools/check-position-health.js +194 -0
- package/tools/clear-exchange-credentials.d.ts +24 -0
- package/tools/clear-exchange-credentials.js +70 -0
- package/tools/close-position.d.ts +22 -0
- package/tools/close-position.js +449 -0
- package/tools/create-order.d.ts +54 -0
- package/tools/create-order.js +338 -0
- package/tools/exit-gate.d.ts +58 -0
- package/tools/exit-gate.js +162 -0
- package/tools/fetch-balance.d.ts +5 -0
- package/tools/fetch-balance.js +4 -0
- package/tools/fetch-ohlcv.d.ts +11 -0
- package/tools/fetch-ohlcv.js +8 -0
- package/tools/fetch-open-orders.d.ts +7 -0
- package/tools/fetch-open-orders.js +4 -0
- package/tools/fetch-positions.d.ts +7 -0
- package/tools/fetch-positions.js +4 -0
- package/tools/fetch-ticker.d.ts +11 -0
- package/tools/fetch-ticker.js +5 -0
- package/tools/get-agent-profile.d.ts +4 -0
- package/tools/get-agent-profile.js +6 -0
- package/tools/get-analytics.d.ts +6 -0
- package/tools/get-analytics.js +7 -0
- package/tools/get-backtest.d.ts +12 -0
- package/tools/get-backtest.js +91 -0
- package/tools/get-basis.d.ts +7 -0
- package/tools/get-basis.js +7 -0
- package/tools/get-bracket-config.d.ts +11 -0
- package/tools/get-bracket-config.js +24 -0
- package/tools/get-cascade-risk.d.ts +7 -0
- package/tools/get-cascade-risk.js +8 -0
- package/tools/get-crypto-metrics.d.ts +18 -0
- package/tools/get-crypto-metrics.js +45 -0
- package/tools/get-cvd.d.ts +6 -0
- package/tools/get-cvd.js +6 -0
- package/tools/get-divergences.d.ts +6 -0
- package/tools/get-divergences.js +6 -0
- package/tools/get-funding-context.d.ts +6 -0
- package/tools/get-funding-context.js +16 -0
- package/tools/get-liquidation-levels.d.ts +7 -0
- package/tools/get-liquidation-levels.js +7 -0
- package/tools/get-liquidation-pulse.d.ts +9 -0
- package/tools/get-liquidation-pulse.js +22 -0
- package/tools/get-market-breadth.d.ts +6 -0
- package/tools/get-market-breadth.js +8 -0
- package/tools/get-market-intel.d.ts +19 -0
- package/tools/get-market-intel.js +116 -0
- package/tools/get-market-structure.d.ts +47 -0
- package/tools/get-market-structure.js +198 -0
- package/tools/get-my-mined-patterns.d.ts +20 -0
- package/tools/get-my-mined-patterns.js +61 -0
- package/tools/get-my-proposed-learnings.d.ts +20 -0
- package/tools/get-my-proposed-learnings.js +55 -0
- package/tools/get-my-recent-reviews.d.ts +22 -0
- package/tools/get-my-recent-reviews.js +66 -0
- package/tools/get-orderbook.d.ts +21 -0
- package/tools/get-orderbook.js +32 -0
- package/tools/get-pattern-scan.d.ts +7 -0
- package/tools/get-pattern-scan.js +8 -0
- package/tools/get-regime.d.ts +6 -0
- package/tools/get-regime.js +7 -0
- package/tools/get-relevant-learnings.d.ts +21 -0
- package/tools/get-relevant-learnings.js +65 -0
- package/tools/get-resting-liquidity.d.ts +6 -0
- package/tools/get-resting-liquidity.js +11 -0
- package/tools/get-risk-scenario.d.ts +29 -0
- package/tools/get-risk-scenario.js +47 -0
- package/tools/get-risk-summary.d.ts +51 -0
- package/tools/get-risk-summary.js +118 -0
- package/tools/get-sentiment.d.ts +4 -0
- package/tools/get-sentiment.js +6 -0
- package/tools/get-session-review.d.ts +7 -0
- package/tools/get-session-review.js +8 -0
- package/tools/get-setup-detail.d.ts +7 -0
- package/tools/get-setup-detail.js +303 -0
- package/tools/get-signals.d.ts +15 -0
- package/tools/get-signals.js +54 -0
- package/tools/get-sizing.d.ts +6 -0
- package/tools/get-sizing.js +6 -0
- package/tools/get-trade-feedback.d.ts +7 -0
- package/tools/get-trade-feedback.js +8 -0
- package/tools/get-trade-flow.d.ts +7 -0
- package/tools/get-trade-flow.js +7 -0
- package/tools/get-volume-analysis.d.ts +21 -0
- package/tools/get-volume-analysis.js +74 -0
- package/tools/get-volume-profile.d.ts +7 -0
- package/tools/get-volume-profile.js +7 -0
- package/tools/helpers.d.ts +25 -0
- package/tools/helpers.js +38 -0
- package/tools/intel-api.d.ts +14 -0
- package/tools/intel-api.js +52 -0
- package/tools/intel-cache.d.ts +25 -0
- package/tools/intel-cache.js +133 -0
- package/tools/list-strategies.d.ts +7 -0
- package/tools/list-strategies.js +6 -0
- package/tools/modify-stop.d.ts +17 -0
- package/tools/modify-stop.js +81 -0
- package/tools/modify-target.d.ts +17 -0
- package/tools/modify-target.js +71 -0
- package/tools/propose-learning.d.ts +22 -0
- package/tools/propose-learning.js +65 -0
- package/tools/query-review-outcomes.d.ts +30 -0
- package/tools/query-review-outcomes.js +64 -0
- package/tools/query-trades.d.ts +21 -0
- package/tools/query-trades.js +37 -0
- package/tools/record-position-reviews.d.ts +38 -0
- package/tools/record-position-reviews.js +147 -0
- package/tools/save-strategy.d.ts +16 -0
- package/tools/save-strategy.js +46 -0
- package/tools/scan-pairs.d.ts +18 -0
- package/tools/scan-pairs.js +220 -0
- package/tools/score-setup.d.ts +31 -0
- package/tools/score-setup.js +268 -0
- package/tools/set-bracket-requirement.d.ts +18 -0
- package/tools/set-bracket-requirement.js +81 -0
- package/tools/set-exchange-credentials.d.ts +25 -0
- package/tools/set-exchange-credentials.js +80 -0
- package/tools/set-trading-mode.d.ts +26 -0
- package/tools/set-trading-mode.js +135 -0
- package/tools/test-exchange-credentials.d.ts +16 -0
- package/tools/test-exchange-credentials.js +100 -0
- package/tools/toggle-strategy.d.ts +8 -0
- package/tools/toggle-strategy.js +8 -0
- package/trading-params-cache.d.ts +26 -0
- package/trading-params-cache.js +52 -0
- package/types.d.ts +110 -0
- package/types.js +7 -0
- package/util/plugin-paths.d.ts +3 -0
- package/util/plugin-paths.js +15 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Pure command functions for the operator onboarding RPCs (PR2).
|
|
2
|
+
// Matches the shape of emergency-commands.ts: each function takes a context
|
|
3
|
+
// with `http` + `toolMap` and returns a structured outcome. The
|
|
4
|
+
// GatewayProvider's `setTradingMode` / `setExchangeCredentials` methods
|
|
5
|
+
// are thin wrappers that forward their own http + toolMap. Unit tests
|
|
6
|
+
// exercise these functions directly with a mocked http client.
|
|
7
|
+
import { logger } from '../logger.js';
|
|
8
|
+
const TAG = 'onboarding-commands';
|
|
9
|
+
/** Escape a string for a regex literal. Used to scrub secrets from error messages. */
|
|
10
|
+
function escapeRegex(s) {
|
|
11
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
}
|
|
13
|
+
/** Forward a set_trading_mode invocation to the plugin via HTTP. */
|
|
14
|
+
export async function executeSetTradingMode(ctx, mode, acknowledged) {
|
|
15
|
+
const tool = ctx.toolMap.set_trading_mode;
|
|
16
|
+
if (!tool || !ctx.http) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
message: 'set_trading_mode tool not available on gateway',
|
|
20
|
+
previousMode: 'PAPER',
|
|
21
|
+
mode: 'PAPER',
|
|
22
|
+
readiness: 'UNKNOWN',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const result = await ctx.http.invoke(tool, { mode, acknowledged });
|
|
27
|
+
return result.data;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
31
|
+
logger.error(TAG, `set_trading_mode failed: ${reason}`);
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
message: reason,
|
|
35
|
+
previousMode: 'PAPER',
|
|
36
|
+
mode: 'PAPER',
|
|
37
|
+
readiness: 'UNKNOWN',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Forward a test_exchange_credentials invocation to the plugin. The plugin
|
|
42
|
+
* does a throwaway read-only call to verify permissions without persisting
|
|
43
|
+
* the keys. Error messages are scrubbed of the raw secret before logging
|
|
44
|
+
* (same approach as the set path). */
|
|
45
|
+
export async function executeTestExchangeCredentials(ctx, apiKey, secret, testnet) {
|
|
46
|
+
const tool = ctx.toolMap.test_exchange_credentials;
|
|
47
|
+
if (!tool || !ctx.http) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
message: 'test_exchange_credentials tool not available on gateway',
|
|
51
|
+
fingerprint: '(not-set)',
|
|
52
|
+
canReadBalance: false,
|
|
53
|
+
canReadPositions: false,
|
|
54
|
+
balanceUSDT: null,
|
|
55
|
+
testnet: testnet ?? false,
|
|
56
|
+
errors: ['tool-not-available'],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const result = await ctx.http.invoke(tool, { apiKey, secret, testnet });
|
|
61
|
+
return result.data;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
65
|
+
const sanitized = reason.replace(new RegExp(escapeRegex(secret), 'g'), '***');
|
|
66
|
+
logger.error(TAG, `test_exchange_credentials failed: ${sanitized}`);
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
message: sanitized,
|
|
70
|
+
fingerprint: '(not-set)',
|
|
71
|
+
canReadBalance: false,
|
|
72
|
+
canReadPositions: false,
|
|
73
|
+
balanceUSDT: null,
|
|
74
|
+
testnet: testnet ?? false,
|
|
75
|
+
errors: [sanitized],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Forward a clear_exchange_credentials invocation to the plugin. Sends
|
|
80
|
+
* `confirm: true` unconditionally — the skill RPC only accepts the call
|
|
81
|
+
* at all when the bridge has decided it's legitimate, so the plugin-side
|
|
82
|
+
* safety gate is always satisfied when we reach this function. */
|
|
83
|
+
export async function executeClearExchangeCredentials(ctx) {
|
|
84
|
+
const tool = ctx.toolMap.clear_exchange_credentials;
|
|
85
|
+
if (!tool || !ctx.http) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
message: 'clear_exchange_credentials tool not available on gateway',
|
|
89
|
+
modeBefore: 'PAPER',
|
|
90
|
+
mode: 'PAPER',
|
|
91
|
+
rolledBackToPaper: false,
|
|
92
|
+
readiness: 'UNKNOWN',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const result = await ctx.http.invoke(tool, { confirm: true });
|
|
97
|
+
return result.data;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
101
|
+
logger.error(TAG, `clear_exchange_credentials failed: ${reason}`);
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
message: reason,
|
|
105
|
+
modeBefore: 'PAPER',
|
|
106
|
+
mode: 'PAPER',
|
|
107
|
+
rolledBackToPaper: false,
|
|
108
|
+
readiness: 'UNKNOWN',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Forward a get_bracket_config invocation to the plugin. Returns safe
|
|
113
|
+
* defaults if the tool isn't available (bracket feature not enabled or
|
|
114
|
+
* plugin is older than v2.7) so the dashboard can still render. */
|
|
115
|
+
export async function executeGetBracketConfig(ctx) {
|
|
116
|
+
const tool = ctx.toolMap.get_bracket_config;
|
|
117
|
+
if (!tool || !ctx.http) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
message: 'get_bracket_config tool not available on gateway',
|
|
121
|
+
mode: 'off',
|
|
122
|
+
requireStopLoss: true,
|
|
123
|
+
requireTakeProfit: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const result = await ctx.http.invoke(tool, {});
|
|
128
|
+
if ('error' in result.data) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
message: result.data.error,
|
|
132
|
+
mode: 'off',
|
|
133
|
+
requireStopLoss: true,
|
|
134
|
+
requireTakeProfit: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, ...result.data };
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
141
|
+
logger.error(TAG, `get_bracket_config failed: ${reason}`);
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: reason,
|
|
145
|
+
mode: 'off',
|
|
146
|
+
requireStopLoss: true,
|
|
147
|
+
requireTakeProfit: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Forward a set_bracket_requirement invocation to the plugin. The plugin
|
|
152
|
+
* side enforces flag + boolean validation; the skill layer just forwards. */
|
|
153
|
+
export async function executeSetBracketRequirement(ctx, flag, value) {
|
|
154
|
+
const tool = ctx.toolMap.set_bracket_requirement;
|
|
155
|
+
if (!tool || !ctx.http) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
flag,
|
|
159
|
+
previousValue: true,
|
|
160
|
+
value,
|
|
161
|
+
error: 'set_bracket_requirement tool not available on gateway',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const result = await ctx.http.invoke(tool, { flag, value });
|
|
166
|
+
return result.data;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
170
|
+
logger.error(TAG, `set_bracket_requirement failed: ${reason}`);
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
flag,
|
|
174
|
+
previousValue: true,
|
|
175
|
+
value,
|
|
176
|
+
error: reason,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Forward a set_exchange_credentials invocation to the plugin via HTTP.
|
|
181
|
+
* The raw secret is sent once over TLS and never retained here — no
|
|
182
|
+
* caching, no log lines with the raw value. Errors are scrubbed of the
|
|
183
|
+
* secret before logging in case it ever appears in an error chain. */
|
|
184
|
+
export async function executeSetExchangeCredentials(ctx, apiKey, secret, testnet) {
|
|
185
|
+
const tool = ctx.toolMap.set_exchange_credentials;
|
|
186
|
+
if (!tool || !ctx.http) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
message: 'set_exchange_credentials tool not available on gateway',
|
|
190
|
+
fingerprint: '(not-set)',
|
|
191
|
+
reconnected: false,
|
|
192
|
+
mode: 'PAPER',
|
|
193
|
+
readiness: 'UNKNOWN',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const result = await ctx.http.invoke(tool, { apiKey, secret, testnet });
|
|
198
|
+
return result.data;
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
202
|
+
const sanitized = reason.replace(new RegExp(escapeRegex(secret), 'g'), '***');
|
|
203
|
+
logger.error(TAG, `set_exchange_credentials failed: ${sanitized}`);
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
message: sanitized,
|
|
207
|
+
fingerprint: '(not-set)',
|
|
208
|
+
reconnected: false,
|
|
209
|
+
mode: 'PAPER',
|
|
210
|
+
readiness: 'UNKNOWN',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { RiskLimits, RiskBreach, RiskUpdatePayload } from '../types.js';
|
|
2
|
+
import type { CcxtPosition } from '../gateway/poller.js';
|
|
3
|
+
export declare const DEFAULT_RISK_LIMITS: RiskLimits;
|
|
4
|
+
/** Multipliers applied to base limits per regime. Values < 1 tighten limits. */
|
|
5
|
+
export declare const REGIME_LIMIT_PROFILES: Record<string, Partial<{
|
|
6
|
+
positionSize: number;
|
|
7
|
+
grossExposure: number;
|
|
8
|
+
netExposure: number;
|
|
9
|
+
description: string;
|
|
10
|
+
}>>;
|
|
11
|
+
/** Apply regime-conditional adjustments to limits. */
|
|
12
|
+
export declare function applyRegimeAdjustment(baseLimits: RiskLimits, regime: string): RiskLimits;
|
|
13
|
+
export declare const DRAWDOWN_ZONE_THRESHOLDS: {
|
|
14
|
+
readonly YELLOW: -0.01;
|
|
15
|
+
readonly ORANGE: -0.02;
|
|
16
|
+
readonly RED: -0.025;
|
|
17
|
+
};
|
|
18
|
+
export type DrawdownZone = 'GREEN' | 'YELLOW' | 'ORANGE' | 'RED';
|
|
19
|
+
/** State snapshot needed to compute equity. */
|
|
20
|
+
export interface EquityState {
|
|
21
|
+
balanceTotal: number;
|
|
22
|
+
positions: CcxtPosition[];
|
|
23
|
+
lastTickerPrice: number | null;
|
|
24
|
+
}
|
|
25
|
+
/** State snapshot needed to compute full risk metrics. */
|
|
26
|
+
export interface RiskState {
|
|
27
|
+
balanceTotal: number;
|
|
28
|
+
positions: CcxtPosition[];
|
|
29
|
+
lastTickerPrice: number | null;
|
|
30
|
+
sessionStartNav: number;
|
|
31
|
+
orderTimestamps: number[];
|
|
32
|
+
cancelTimestamps: number[];
|
|
33
|
+
atrData?: {
|
|
34
|
+
currentAtr: number;
|
|
35
|
+
baselineAtr: number;
|
|
36
|
+
};
|
|
37
|
+
regime?: string;
|
|
38
|
+
realizedPnlToday?: number;
|
|
39
|
+
}
|
|
40
|
+
/** Result of computeRiskMetrics including pruned timestamp arrays. */
|
|
41
|
+
export interface RiskMetricsResult {
|
|
42
|
+
metrics: RiskUpdatePayload['metrics'];
|
|
43
|
+
prunedOrderTimestamps: number[];
|
|
44
|
+
prunedCancelTimestamps: number[];
|
|
45
|
+
}
|
|
46
|
+
/** Compute volatility factor from current ATR vs baseline. Clamped [0.5, 3.0]. */
|
|
47
|
+
export declare function computeVolFactor(state: {
|
|
48
|
+
currentAtr: number;
|
|
49
|
+
baselineAtr: number;
|
|
50
|
+
}): number;
|
|
51
|
+
/** Apply volatility adjustment to base limits. Tightens position/exposure, leaves loss/rate unchanged. */
|
|
52
|
+
export declare function applyVolatilityAdjustment(baseLimits: RiskLimits, volFactor: number): RiskLimits;
|
|
53
|
+
/** Determine drawdown zone from drawdown ratio (e.g. -0.015 = -1.5%). */
|
|
54
|
+
export declare function getDrawdownZone(drawdownRatio: number): DrawdownZone;
|
|
55
|
+
/** Human-readable message for a drawdown zone. */
|
|
56
|
+
export declare function getDrawdownZoneMessage(zone: DrawdownZone, ratio: number): string;
|
|
57
|
+
/**
|
|
58
|
+
* Compute true equity: wallet + entry_notional (margin held) + unrealized P&L per position.
|
|
59
|
+
*
|
|
60
|
+
* The paper trading simulator deducts entry_notional from the wallet when opening
|
|
61
|
+
* ANY position (long or short). To recover true equity we add back the margin and
|
|
62
|
+
* the unrealized P&L.
|
|
63
|
+
*
|
|
64
|
+
* The old formula (wallet + |current_notional|) worked for longs but INVERTED the
|
|
65
|
+
* P&L direction for shorts: a losing short increased |notional| and showed phantom profit.
|
|
66
|
+
*/
|
|
67
|
+
export declare function computeEquity(state: EquityState): number;
|
|
68
|
+
/**
|
|
69
|
+
* Compute signed notional value of a position.
|
|
70
|
+
* Positive for long, negative for short.
|
|
71
|
+
*/
|
|
72
|
+
export declare function computePositionNotional(pos: CcxtPosition, lastTickerPrice: number | null): number;
|
|
73
|
+
/**
|
|
74
|
+
* Count events within the last RATE_WINDOW_MS.
|
|
75
|
+
* Returns the count and a new filtered array (does NOT mutate input).
|
|
76
|
+
*/
|
|
77
|
+
export declare function countRecentEvents(timestamps: number[]): {
|
|
78
|
+
count: number;
|
|
79
|
+
filtered: number[];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Detect risk limit breaches from current metrics.
|
|
83
|
+
*/
|
|
84
|
+
export declare function detectBreaches(metrics: {
|
|
85
|
+
grossExposure: number;
|
|
86
|
+
netExposure: number;
|
|
87
|
+
dailyDrawdown: number;
|
|
88
|
+
dailyLoss?: number;
|
|
89
|
+
ordersPerMinute: number;
|
|
90
|
+
cancelsPerMinute: number;
|
|
91
|
+
}, limits: RiskLimits): RiskBreach[];
|
|
92
|
+
/**
|
|
93
|
+
* Compute full risk metrics from a state snapshot.
|
|
94
|
+
* Returns metrics + pruned timestamp arrays for the caller to store.
|
|
95
|
+
*/
|
|
96
|
+
export declare function computeRiskMetrics(state: RiskState): RiskMetricsResult;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// Pure risk calculation functions extracted from GatewayProvider.
|
|
2
|
+
// All functions are stateless — they take snapshots and return results.
|
|
3
|
+
// ---- Default risk limits (configurable in future phases) ----
|
|
4
|
+
export const DEFAULT_RISK_LIMITS = {
|
|
5
|
+
position: {
|
|
6
|
+
maxPositionSize: 10_000,
|
|
7
|
+
maxOpenPositions: 5,
|
|
8
|
+
maxGrossExposure: 1.5,
|
|
9
|
+
maxNetExposure: 1.5,
|
|
10
|
+
},
|
|
11
|
+
loss: {
|
|
12
|
+
maxDailyDrawdown: -0.03,
|
|
13
|
+
maxPerTradeLoss: -500,
|
|
14
|
+
maxDailyLoss: -2000,
|
|
15
|
+
},
|
|
16
|
+
rate: { maxOrdersPerMinute: 60, maxCancelsPerMinute: 30 },
|
|
17
|
+
};
|
|
18
|
+
// ---- Regime-conditional limit multipliers (Phase 9e) ----
|
|
19
|
+
/** Multipliers applied to base limits per regime. Values < 1 tighten limits. */
|
|
20
|
+
export const REGIME_LIMIT_PROFILES = {
|
|
21
|
+
TREND_UP: { positionSize: 1.0, grossExposure: 1.0, netExposure: 1.0, description: 'Standard limits — trending up' },
|
|
22
|
+
TREND_DOWN: { positionSize: 1.0, grossExposure: 1.0, netExposure: 1.0, description: 'Standard limits — trending down' },
|
|
23
|
+
RANGE_TIGHT: { positionSize: 0.5, grossExposure: 0.7, netExposure: 0.7, description: 'Tighter positions — less edge in tight ranges' },
|
|
24
|
+
RANGE_WIDE: { positionSize: 0.5, grossExposure: 0.5, netExposure: 0.5, description: 'Significantly tighter — choppy markets burn capital' },
|
|
25
|
+
VOLATILITY_EXPANSION: { positionSize: 0.6, grossExposure: 0.7, netExposure: 0.7, description: 'Smaller positions — vol needs room' },
|
|
26
|
+
AVOID: { positionSize: 0, grossExposure: 1, netExposure: 1, description: 'No new entries — regime classified as AVOID' },
|
|
27
|
+
UNKNOWN: { positionSize: 0.8, grossExposure: 0.8, netExposure: 0.8, description: 'Cautious — regime unknown' },
|
|
28
|
+
};
|
|
29
|
+
/** Apply regime-conditional adjustments to limits. */
|
|
30
|
+
export function applyRegimeAdjustment(baseLimits, regime) {
|
|
31
|
+
const profile = REGIME_LIMIT_PROFILES[regime];
|
|
32
|
+
if (!profile)
|
|
33
|
+
return baseLimits;
|
|
34
|
+
return {
|
|
35
|
+
position: {
|
|
36
|
+
maxPositionSize: baseLimits.position.maxPositionSize * (profile.positionSize ?? 1),
|
|
37
|
+
maxOpenPositions: baseLimits.position.maxOpenPositions,
|
|
38
|
+
maxGrossExposure: baseLimits.position.maxGrossExposure * (profile.grossExposure ?? 1),
|
|
39
|
+
maxNetExposure: baseLimits.position.maxNetExposure * (profile.netExposure ?? 1),
|
|
40
|
+
},
|
|
41
|
+
loss: baseLimits.loss,
|
|
42
|
+
rate: baseLimits.rate,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// ---- Drawdown zone thresholds ----
|
|
46
|
+
export const DRAWDOWN_ZONE_THRESHOLDS = {
|
|
47
|
+
YELLOW: -0.01, // -1%
|
|
48
|
+
ORANGE: -0.02, // -2%
|
|
49
|
+
RED: -0.025, // -2.5%
|
|
50
|
+
};
|
|
51
|
+
// ---- Rate tracking window ----
|
|
52
|
+
const RATE_WINDOW_MS = 60_000; // 1 minute
|
|
53
|
+
// ---- Volatility + drawdown pure functions ----
|
|
54
|
+
/** Compute volatility factor from current ATR vs baseline. Clamped [0.5, 3.0]. */
|
|
55
|
+
export function computeVolFactor(state) {
|
|
56
|
+
if (state.baselineAtr <= 0 || state.currentAtr <= 0)
|
|
57
|
+
return 1.0;
|
|
58
|
+
return Math.max(0.5, Math.min(3.0, state.currentAtr / state.baselineAtr));
|
|
59
|
+
}
|
|
60
|
+
/** Apply volatility adjustment to base limits. Tightens position/exposure, leaves loss/rate unchanged. */
|
|
61
|
+
export function applyVolatilityAdjustment(baseLimits, volFactor) {
|
|
62
|
+
if (volFactor <= 1.0)
|
|
63
|
+
return baseLimits;
|
|
64
|
+
return {
|
|
65
|
+
position: {
|
|
66
|
+
maxPositionSize: baseLimits.position.maxPositionSize / volFactor,
|
|
67
|
+
maxOpenPositions: baseLimits.position.maxOpenPositions,
|
|
68
|
+
maxGrossExposure: baseLimits.position.maxGrossExposure / volFactor,
|
|
69
|
+
maxNetExposure: baseLimits.position.maxNetExposure / volFactor,
|
|
70
|
+
},
|
|
71
|
+
loss: baseLimits.loss,
|
|
72
|
+
rate: baseLimits.rate,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Determine drawdown zone from drawdown ratio (e.g. -0.015 = -1.5%). */
|
|
76
|
+
export function getDrawdownZone(drawdownRatio) {
|
|
77
|
+
if (drawdownRatio <= DRAWDOWN_ZONE_THRESHOLDS.RED)
|
|
78
|
+
return 'RED';
|
|
79
|
+
if (drawdownRatio <= DRAWDOWN_ZONE_THRESHOLDS.ORANGE)
|
|
80
|
+
return 'ORANGE';
|
|
81
|
+
if (drawdownRatio <= DRAWDOWN_ZONE_THRESHOLDS.YELLOW)
|
|
82
|
+
return 'YELLOW';
|
|
83
|
+
return 'GREEN';
|
|
84
|
+
}
|
|
85
|
+
/** Human-readable message for a drawdown zone. */
|
|
86
|
+
export function getDrawdownZoneMessage(zone, ratio) {
|
|
87
|
+
const pct = (ratio * 100).toFixed(2);
|
|
88
|
+
switch (zone) {
|
|
89
|
+
case 'GREEN': return `Green zone (${pct}%) — full trading`;
|
|
90
|
+
case 'YELLOW': return `Yellow zone (${pct}%) — position sizes halved`;
|
|
91
|
+
case 'ORANGE': return `Orange zone (${pct}%) — new entries blocked, exits only`;
|
|
92
|
+
case 'RED': return `Red zone (${pct}%) — all orders rejected, auto-flatten`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ---- Pure functions ----
|
|
96
|
+
/**
|
|
97
|
+
* Compute true equity: wallet + entry_notional (margin held) + unrealized P&L per position.
|
|
98
|
+
*
|
|
99
|
+
* The paper trading simulator deducts entry_notional from the wallet when opening
|
|
100
|
+
* ANY position (long or short). To recover true equity we add back the margin and
|
|
101
|
+
* the unrealized P&L.
|
|
102
|
+
*
|
|
103
|
+
* The old formula (wallet + |current_notional|) worked for longs but INVERTED the
|
|
104
|
+
* P&L direction for shorts: a losing short increased |notional| and showed phantom profit.
|
|
105
|
+
*/
|
|
106
|
+
export function computeEquity(state) {
|
|
107
|
+
let equity = state.balanceTotal;
|
|
108
|
+
for (const pos of state.positions) {
|
|
109
|
+
const contracts = Math.abs(pos.contracts ?? 0);
|
|
110
|
+
const contractSize = pos.contractSize ?? 1;
|
|
111
|
+
const entryPrice = pos.entryPrice ?? 0;
|
|
112
|
+
// Add back margin deducted from wallet at position open
|
|
113
|
+
equity += contracts * contractSize * entryPrice;
|
|
114
|
+
// Add unrealized P&L (correctly signed: positive when profitable)
|
|
115
|
+
equity += pos.unrealizedPnl ?? 0;
|
|
116
|
+
}
|
|
117
|
+
return equity;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Compute signed notional value of a position.
|
|
121
|
+
* Positive for long, negative for short.
|
|
122
|
+
*/
|
|
123
|
+
export function computePositionNotional(pos, lastTickerPrice) {
|
|
124
|
+
const sign = pos.side === 'short' ? -1 : 1;
|
|
125
|
+
// Prefer exchange-provided notional
|
|
126
|
+
if (typeof pos.notional === 'number' && pos.notional !== 0) {
|
|
127
|
+
return sign * Math.abs(pos.notional);
|
|
128
|
+
}
|
|
129
|
+
// Compute from contracts * contractSize * price
|
|
130
|
+
const contracts = pos.contracts ?? 0;
|
|
131
|
+
const contractSize = pos.contractSize ?? 1;
|
|
132
|
+
const price = pos.markPrice ?? pos.entryPrice ?? lastTickerPrice ?? 0;
|
|
133
|
+
return sign * contracts * contractSize * price;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Count events within the last RATE_WINDOW_MS.
|
|
137
|
+
* Returns the count and a new filtered array (does NOT mutate input).
|
|
138
|
+
*/
|
|
139
|
+
export function countRecentEvents(timestamps) {
|
|
140
|
+
const cutoff = Date.now() - RATE_WINDOW_MS;
|
|
141
|
+
const filtered = timestamps.filter((ts) => ts >= cutoff);
|
|
142
|
+
return { count: filtered.length, filtered };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Detect risk limit breaches from current metrics.
|
|
146
|
+
*/
|
|
147
|
+
export function detectBreaches(metrics, limits) {
|
|
148
|
+
const breaches = [];
|
|
149
|
+
const now = new Date().toISOString();
|
|
150
|
+
// Gross exposure
|
|
151
|
+
if (metrics.grossExposure > limits.position.maxGrossExposure) {
|
|
152
|
+
breaches.push({
|
|
153
|
+
metric: 'grossExposure',
|
|
154
|
+
limit: limits.position.maxGrossExposure,
|
|
155
|
+
current: metrics.grossExposure,
|
|
156
|
+
level: 'CRITICAL',
|
|
157
|
+
action: 'ALERT',
|
|
158
|
+
timestamp: now,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else if (metrics.grossExposure > limits.position.maxGrossExposure * 0.8) {
|
|
162
|
+
breaches.push({
|
|
163
|
+
metric: 'grossExposure',
|
|
164
|
+
limit: limits.position.maxGrossExposure,
|
|
165
|
+
current: metrics.grossExposure,
|
|
166
|
+
level: 'WARNING',
|
|
167
|
+
action: 'ALERT',
|
|
168
|
+
timestamp: now,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Net exposure
|
|
172
|
+
if (Math.abs(metrics.netExposure) > limits.position.maxNetExposure) {
|
|
173
|
+
breaches.push({
|
|
174
|
+
metric: 'netExposure',
|
|
175
|
+
limit: limits.position.maxNetExposure,
|
|
176
|
+
current: metrics.netExposure,
|
|
177
|
+
level: 'CRITICAL',
|
|
178
|
+
action: 'ALERT',
|
|
179
|
+
timestamp: now,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else if (Math.abs(metrics.netExposure) > limits.position.maxNetExposure * 0.8) {
|
|
183
|
+
breaches.push({
|
|
184
|
+
metric: 'netExposure',
|
|
185
|
+
limit: limits.position.maxNetExposure,
|
|
186
|
+
current: metrics.netExposure,
|
|
187
|
+
level: 'WARNING',
|
|
188
|
+
action: 'ALERT',
|
|
189
|
+
timestamp: now,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// Daily drawdown (negative values — breach when current is more negative than limit)
|
|
193
|
+
if (metrics.dailyDrawdown < limits.loss.maxDailyDrawdown) {
|
|
194
|
+
breaches.push({
|
|
195
|
+
metric: 'dailyDrawdown',
|
|
196
|
+
limit: limits.loss.maxDailyDrawdown,
|
|
197
|
+
current: metrics.dailyDrawdown,
|
|
198
|
+
level: 'CRITICAL',
|
|
199
|
+
action: 'AUTO_PAUSE',
|
|
200
|
+
timestamp: now,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// Order rate
|
|
204
|
+
if (metrics.ordersPerMinute > limits.rate.maxOrdersPerMinute) {
|
|
205
|
+
breaches.push({
|
|
206
|
+
metric: 'ordersPerMinute',
|
|
207
|
+
limit: limits.rate.maxOrdersPerMinute,
|
|
208
|
+
current: metrics.ordersPerMinute,
|
|
209
|
+
level: 'CRITICAL',
|
|
210
|
+
action: 'AUTO_PAUSE',
|
|
211
|
+
timestamp: now,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Cancel rate
|
|
215
|
+
if (metrics.cancelsPerMinute > limits.rate.maxCancelsPerMinute) {
|
|
216
|
+
breaches.push({
|
|
217
|
+
metric: 'cancelsPerMinute',
|
|
218
|
+
limit: limits.rate.maxCancelsPerMinute,
|
|
219
|
+
current: metrics.cancelsPerMinute,
|
|
220
|
+
level: 'CRITICAL',
|
|
221
|
+
action: 'AUTO_PAUSE',
|
|
222
|
+
timestamp: now,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Daily loss (negative values — breach when realized loss exceeds limit)
|
|
226
|
+
if (metrics.dailyLoss != null && metrics.dailyLoss < limits.loss.maxDailyLoss) {
|
|
227
|
+
breaches.push({
|
|
228
|
+
metric: 'dailyLoss',
|
|
229
|
+
limit: limits.loss.maxDailyLoss,
|
|
230
|
+
current: metrics.dailyLoss,
|
|
231
|
+
level: 'CRITICAL',
|
|
232
|
+
action: 'AUTO_PAUSE',
|
|
233
|
+
timestamp: now,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return breaches;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Compute full risk metrics from a state snapshot.
|
|
240
|
+
* Returns metrics + pruned timestamp arrays for the caller to store.
|
|
241
|
+
*/
|
|
242
|
+
export function computeRiskMetrics(state) {
|
|
243
|
+
// Use equity (wallet + position notionals) for NAV
|
|
244
|
+
const currentEquity = computeEquity({
|
|
245
|
+
balanceTotal: state.balanceTotal,
|
|
246
|
+
positions: state.positions,
|
|
247
|
+
lastTickerPrice: state.lastTickerPrice,
|
|
248
|
+
});
|
|
249
|
+
const nav = state.sessionStartNav > 0 ? state.sessionStartNav : (currentEquity > 0 ? currentEquity : 1);
|
|
250
|
+
// Compute position metrics
|
|
251
|
+
const positionSizes = {};
|
|
252
|
+
let grossExposure = 0;
|
|
253
|
+
let netExposure = 0;
|
|
254
|
+
for (const pos of state.positions) {
|
|
255
|
+
const signedNotional = computePositionNotional(pos, state.lastTickerPrice);
|
|
256
|
+
const absNotional = Math.abs(signedNotional);
|
|
257
|
+
if (absNotional > 0) {
|
|
258
|
+
positionSizes[pos.symbol] = absNotional;
|
|
259
|
+
grossExposure += absNotional;
|
|
260
|
+
netExposure += signedNotional;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Normalize by NAV
|
|
264
|
+
const grossRatio = nav > 0 ? grossExposure / nav : 0;
|
|
265
|
+
const netRatio = nav > 0 ? netExposure / nav : 0;
|
|
266
|
+
// Daily drawdown: (current equity - session start NAV) / session start NAV
|
|
267
|
+
const dailyDrawdown = state.sessionStartNav > 0
|
|
268
|
+
? (currentEquity - state.sessionStartNav) / state.sessionStartNav
|
|
269
|
+
: 0;
|
|
270
|
+
// Prune and count rate events (non-mutating)
|
|
271
|
+
const orderResult = countRecentEvents(state.orderTimestamps);
|
|
272
|
+
const cancelResult = countRecentEvents(state.cancelTimestamps);
|
|
273
|
+
// Volatility adjustment (when ATR data is available)
|
|
274
|
+
let adjustedLimits = DEFAULT_RISK_LIMITS;
|
|
275
|
+
let effectiveLimits;
|
|
276
|
+
let volatilityInfo;
|
|
277
|
+
let volFactor = 1.0;
|
|
278
|
+
if (state.atrData && state.atrData.currentAtr > 0 && state.atrData.baselineAtr > 0) {
|
|
279
|
+
volFactor = computeVolFactor(state.atrData);
|
|
280
|
+
const adjusted = volFactor > 1.0;
|
|
281
|
+
if (adjusted) {
|
|
282
|
+
adjustedLimits = applyVolatilityAdjustment(DEFAULT_RISK_LIMITS, volFactor);
|
|
283
|
+
}
|
|
284
|
+
volatilityInfo = {
|
|
285
|
+
atr14: state.atrData.currentAtr,
|
|
286
|
+
baselineAtr: state.atrData.baselineAtr,
|
|
287
|
+
volFactor: +volFactor.toFixed(3),
|
|
288
|
+
adjusted,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// Regime-conditional adjustment (Phase 9e) — applied on top of vol adjustment
|
|
292
|
+
let regimeInfo;
|
|
293
|
+
if (state.regime && state.regime !== 'UNKNOWN') {
|
|
294
|
+
const profile = REGIME_LIMIT_PROFILES[state.regime];
|
|
295
|
+
if (profile) {
|
|
296
|
+
const regimeAdjusted = applyRegimeAdjustment(adjustedLimits, state.regime);
|
|
297
|
+
const isAdjusted = state.regime !== 'TREND_UP' && state.regime !== 'TREND_DOWN';
|
|
298
|
+
if (isAdjusted) {
|
|
299
|
+
adjustedLimits = regimeAdjusted;
|
|
300
|
+
}
|
|
301
|
+
regimeInfo = {
|
|
302
|
+
regime: state.regime,
|
|
303
|
+
description: profile.description ?? state.regime,
|
|
304
|
+
adjusted: isAdjusted,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (adjustedLimits !== DEFAULT_RISK_LIMITS) {
|
|
309
|
+
effectiveLimits = adjustedLimits;
|
|
310
|
+
}
|
|
311
|
+
// Drawdown zone
|
|
312
|
+
const drawdownZone = getDrawdownZone(dailyDrawdown);
|
|
313
|
+
const drawdownZoneMessage = getDrawdownZoneMessage(drawdownZone, dailyDrawdown);
|
|
314
|
+
// Detect breaches using effective limits (vol + regime adjusted) when available
|
|
315
|
+
const limitsForBreaches = effectiveLimits ?? DEFAULT_RISK_LIMITS;
|
|
316
|
+
const dailyLoss = state.realizedPnlToday ?? 0;
|
|
317
|
+
const breaches = detectBreaches({
|
|
318
|
+
grossExposure: grossRatio,
|
|
319
|
+
netExposure: netRatio,
|
|
320
|
+
dailyDrawdown,
|
|
321
|
+
dailyLoss,
|
|
322
|
+
ordersPerMinute: orderResult.count,
|
|
323
|
+
cancelsPerMinute: cancelResult.count,
|
|
324
|
+
}, limitsForBreaches);
|
|
325
|
+
// Add zone-specific breaches
|
|
326
|
+
if (drawdownZone === 'ORANGE' || drawdownZone === 'RED') {
|
|
327
|
+
breaches.push({
|
|
328
|
+
metric: 'drawdownZone',
|
|
329
|
+
limit: drawdownZone === 'RED' ? DRAWDOWN_ZONE_THRESHOLDS.RED : DRAWDOWN_ZONE_THRESHOLDS.ORANGE,
|
|
330
|
+
current: dailyDrawdown,
|
|
331
|
+
level: 'CRITICAL',
|
|
332
|
+
action: drawdownZone === 'RED' ? 'AUTO_PAUSE' : 'REJECT_ORDER',
|
|
333
|
+
timestamp: new Date().toISOString(),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
else if (drawdownZone === 'YELLOW') {
|
|
337
|
+
breaches.push({
|
|
338
|
+
metric: 'drawdownZone',
|
|
339
|
+
limit: DRAWDOWN_ZONE_THRESHOLDS.YELLOW,
|
|
340
|
+
current: dailyDrawdown,
|
|
341
|
+
level: 'WARNING',
|
|
342
|
+
action: 'ALERT',
|
|
343
|
+
timestamp: new Date().toISOString(),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
metrics: {
|
|
348
|
+
limits: DEFAULT_RISK_LIMITS,
|
|
349
|
+
utilization: {
|
|
350
|
+
currentPositionSize: positionSizes,
|
|
351
|
+
openPositionCount: state.positions.filter((p) => computePositionNotional(p, state.lastTickerPrice) !== 0).length,
|
|
352
|
+
grossExposure: +grossRatio.toFixed(4),
|
|
353
|
+
netExposure: +netRatio.toFixed(4),
|
|
354
|
+
dailyDrawdown: +dailyDrawdown.toFixed(6),
|
|
355
|
+
dailyLoss: state.realizedPnlToday ?? 0,
|
|
356
|
+
ordersPerMinute: orderResult.count,
|
|
357
|
+
cancelsPerMinute: cancelResult.count,
|
|
358
|
+
},
|
|
359
|
+
activeBreaches: breaches,
|
|
360
|
+
effectiveLimits,
|
|
361
|
+
volatility: volatilityInfo,
|
|
362
|
+
drawdownZone,
|
|
363
|
+
drawdownZoneMessage,
|
|
364
|
+
regime: regimeInfo,
|
|
365
|
+
},
|
|
366
|
+
prunedOrderTimestamps: orderResult.filtered,
|
|
367
|
+
prunedCancelTimestamps: cancelResult.filtered,
|
|
368
|
+
};
|
|
369
|
+
}
|