@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,338 @@
|
|
|
1
|
+
// Tool: create_order — order execution with real price data + pre-trade risk gate
|
|
2
|
+
// Readiness gate: BLOCKED unless adapter.readiness === 'READY'.
|
|
3
|
+
import { formatError } from '../logger.js';
|
|
4
|
+
import { getQuoteBalance, getQuoteWalletBalance } from '../balance-utils.js';
|
|
5
|
+
import { fetchCurrentPrice, fetchOrderBook, isError } from './helpers.js';
|
|
6
|
+
import { validateCreateOrder, sanitizeRealizationRule } from './assessment-validation.js';
|
|
7
|
+
import { preTradeRiskCheck, getDefaultPreTradeLimits, computeConsecutiveLosses } from '../risk/pre-trade-check.js';
|
|
8
|
+
import { bracketsEnabled, loadBracketMode, loadBracketRequirements } from '../config/brackets-config.js';
|
|
9
|
+
import { onCreateOrderFilled } from '../ingest/position-auto-capture.js';
|
|
10
|
+
/** Build a PortfolioSnapshot from current simulator state (paper mode). */
|
|
11
|
+
function buildPortfolioSnapshotFromSimulator(simulator) {
|
|
12
|
+
const state = simulator.getState();
|
|
13
|
+
const quoteCurrency = state.config.quoteCurrency;
|
|
14
|
+
const walletBal = state.wallet[quoteCurrency];
|
|
15
|
+
return {
|
|
16
|
+
positions: state.positions.map(p => {
|
|
17
|
+
const ticker = simulator.getLastTicker(p.symbol);
|
|
18
|
+
return {
|
|
19
|
+
symbol: p.symbol,
|
|
20
|
+
side: p.side,
|
|
21
|
+
quantity: p.quantity,
|
|
22
|
+
entryPrice: p.entryPrice,
|
|
23
|
+
markPrice: ticker?.last ?? p.entryPrice,
|
|
24
|
+
};
|
|
25
|
+
}),
|
|
26
|
+
walletTotal: walletBal?.total ?? 0,
|
|
27
|
+
walletAvailable: walletBal?.available ?? 0,
|
|
28
|
+
sessionStartNav: simulator.getSessionStartNav(),
|
|
29
|
+
equity: simulator.computeEquity(),
|
|
30
|
+
isLive: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Build a PortfolioSnapshot from adapter (live mode). */
|
|
34
|
+
async function buildPortfolioSnapshotFromAdapter(adapter) {
|
|
35
|
+
const [balance, positions] = await Promise.all([
|
|
36
|
+
adapter.getBalance(),
|
|
37
|
+
adapter.getPositionsOrNull(),
|
|
38
|
+
]);
|
|
39
|
+
// null = positions fetch FAILED (429 / weight-paced / transient). If we
|
|
40
|
+
// collapsed that to [], existing open positions become invisible to the
|
|
41
|
+
// pre-trade risk check below — gross exposure / max-open-positions / net
|
|
42
|
+
// tilt would all be under-counted and a trade that should be blocked could
|
|
43
|
+
// pass. Refuse to size off a phantom-flat account; abort so the agent
|
|
44
|
+
// retries next heartbeat. Same null≠empty contract as the close/flatten path.
|
|
45
|
+
if (positions === null) {
|
|
46
|
+
throw new Error('Cannot build portfolio snapshot — position fetch failed (exchange data ' +
|
|
47
|
+
'unavailable, likely Binance 429 or transient). Order NOT placed; retry next heartbeat.');
|
|
48
|
+
}
|
|
49
|
+
const walletTotal = getQuoteWalletBalance(balance);
|
|
50
|
+
// Use the session-start NAV captured once at initialization, not the current balance.
|
|
51
|
+
// If not available (shouldn't happen after init), fall back to current balance.
|
|
52
|
+
const sessionNav = adapter.getSessionStartNav?.() ?? walletTotal;
|
|
53
|
+
// Live Binance wallet already includes margin; equity = wallet + unrealized P&L.
|
|
54
|
+
let liveEquity = walletTotal;
|
|
55
|
+
for (const p of positions) {
|
|
56
|
+
const mark = p.markPrice ?? p.entryPrice;
|
|
57
|
+
const pnl = p.side === 'long'
|
|
58
|
+
? (mark - p.entryPrice) * p.contracts
|
|
59
|
+
: (p.entryPrice - mark) * p.contracts;
|
|
60
|
+
liveEquity += pnl;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
positions: positions.map(p => ({
|
|
64
|
+
symbol: p.symbol,
|
|
65
|
+
side: p.side,
|
|
66
|
+
quantity: p.contracts,
|
|
67
|
+
entryPrice: p.entryPrice,
|
|
68
|
+
markPrice: p.markPrice,
|
|
69
|
+
})),
|
|
70
|
+
walletTotal,
|
|
71
|
+
walletAvailable: getQuoteBalance(balance.free),
|
|
72
|
+
sessionStartNav: sessionNav,
|
|
73
|
+
equity: liveEquity,
|
|
74
|
+
isLive: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function createOrderTool(args, deps) {
|
|
78
|
+
// ---- Readiness gate (live mode only) ----
|
|
79
|
+
if (deps.adapter.readiness !== 'READY') {
|
|
80
|
+
return { error: `Trading blocked: adapter is ${deps.adapter.readiness}. Wait for initialization to complete.` };
|
|
81
|
+
}
|
|
82
|
+
// ---- Assessment-schema gate (SKILL.md v2.10.0 — live mode only) ----
|
|
83
|
+
// In live mode the agent must include thesis + setup_type + regime +
|
|
84
|
+
// regime_confidence + scorecard_verdict + confluence_score. This is the
|
|
85
|
+
// same data the heartbeat reassessment uses to detect scorecard reversal /
|
|
86
|
+
// regime flip — if the agent can't produce it at entry, it can't produce
|
|
87
|
+
// it mid-trade either.
|
|
88
|
+
const gateCheck = validateCreateOrder(args, deps.adapter.isLive);
|
|
89
|
+
if (!gateCheck.ok) {
|
|
90
|
+
return { error: gateCheck.error ?? 'Invalid create_order arguments.' };
|
|
91
|
+
}
|
|
92
|
+
// Validate side
|
|
93
|
+
const side = args.side.toLowerCase();
|
|
94
|
+
if (side !== 'buy' && side !== 'sell') {
|
|
95
|
+
return { error: `Invalid side: ${args.side}. Must be 'buy' or 'sell'.` };
|
|
96
|
+
}
|
|
97
|
+
// Validate type
|
|
98
|
+
const type = args.type.toLowerCase();
|
|
99
|
+
if (type !== 'market' && type !== 'limit') {
|
|
100
|
+
return { error: `Invalid type: ${args.type}. Must be 'market' or 'limit'.` };
|
|
101
|
+
}
|
|
102
|
+
// Validate amount
|
|
103
|
+
if (!args.amount || args.amount <= 0) {
|
|
104
|
+
return { error: 'Amount must be a positive number.' };
|
|
105
|
+
}
|
|
106
|
+
// Validate price for limit orders
|
|
107
|
+
if (type === 'limit' && (!args.price || args.price <= 0)) {
|
|
108
|
+
return { error: 'Limit orders require a positive price.' };
|
|
109
|
+
}
|
|
110
|
+
// In paper mode, fetch latest ticker + order book for realistic fills
|
|
111
|
+
let ticker = null;
|
|
112
|
+
if (!deps.adapter.isLive) {
|
|
113
|
+
const paperDeps = { binanceApi: deps.binanceApi, simulator: deps.adapter.getSimulator() };
|
|
114
|
+
const [priceResult] = await Promise.all([
|
|
115
|
+
fetchCurrentPrice(args.symbol, paperDeps),
|
|
116
|
+
fetchOrderBook(args.symbol, paperDeps),
|
|
117
|
+
]);
|
|
118
|
+
if (isError(priceResult))
|
|
119
|
+
return priceResult;
|
|
120
|
+
ticker = priceResult;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Live mode: fetch ticker for risk check reference price
|
|
124
|
+
const lastPrice = await deps.adapter.getLastPrice(args.symbol);
|
|
125
|
+
if (lastPrice != null) {
|
|
126
|
+
ticker = {
|
|
127
|
+
symbol: args.symbol, last: lastPrice,
|
|
128
|
+
bid: lastPrice, ask: lastPrice, baseVolume: 0, quoteVolume: 0,
|
|
129
|
+
change: 0, percentage: 0, timestamp: Date.now(), datetime: new Date().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!ticker) {
|
|
134
|
+
return { error: `Failed to fetch current price for ${args.symbol}` };
|
|
135
|
+
}
|
|
136
|
+
// ---- Pre-trade risk gate ----
|
|
137
|
+
const orderPrice = type === 'limit' ? args.price : ticker.last;
|
|
138
|
+
const proposed = {
|
|
139
|
+
symbol: args.symbol,
|
|
140
|
+
side: side,
|
|
141
|
+
type: type,
|
|
142
|
+
amount: args.amount,
|
|
143
|
+
price: orderPrice,
|
|
144
|
+
stopPrice: args.stopPrice,
|
|
145
|
+
targetPrice: args.target_price,
|
|
146
|
+
};
|
|
147
|
+
// Build portfolio snapshot based on mode
|
|
148
|
+
let portfolio;
|
|
149
|
+
let consecutiveLosses = 0;
|
|
150
|
+
if (!deps.adapter.isLive) {
|
|
151
|
+
const simulator = deps.adapter.getSimulator();
|
|
152
|
+
portfolio = buildPortfolioSnapshotFromSimulator(simulator);
|
|
153
|
+
consecutiveLosses = computeConsecutiveLosses(simulator.getState().tradeHistory);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
portfolio = await buildPortfolioSnapshotFromAdapter(deps.adapter);
|
|
157
|
+
// In live mode, consecutive losses would come from intelligence DB — use 0 for now
|
|
158
|
+
}
|
|
159
|
+
// Bracket enforcement only applies in live mode when the feature is enabled.
|
|
160
|
+
// Paper mode uses the stop-watcher and doesn't care about these flags.
|
|
161
|
+
let bracketEnforcement;
|
|
162
|
+
if (deps.adapter.isLive && bracketsEnabled(loadBracketMode())) {
|
|
163
|
+
bracketEnforcement = loadBracketRequirements();
|
|
164
|
+
}
|
|
165
|
+
const riskCheck = preTradeRiskCheck(proposed, portfolio, getDefaultPreTradeLimits(), {
|
|
166
|
+
volFactor: !deps.adapter.isLive ? deps.adapter.getSimulator().getVolFactor() : 1.0,
|
|
167
|
+
consecutiveLosses,
|
|
168
|
+
bracketEnforcement,
|
|
169
|
+
});
|
|
170
|
+
if (!riskCheck.allowed) {
|
|
171
|
+
const reasons = riskCheck.violations.map(v => v.message).join('; ');
|
|
172
|
+
return {
|
|
173
|
+
error: `Order REJECTED by risk gate [${riskCheck.drawdownZone} zone]: ${reasons}`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// Build position metadata from optional args (only if any metadata provided)
|
|
177
|
+
// Sanitize numeric metadata — reject non-finite values, clamp ranges
|
|
178
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
179
|
+
const clamp = (v, min, max) => {
|
|
180
|
+
const n = num(v);
|
|
181
|
+
return n != null ? Math.max(min, Math.min(max, n)) : undefined;
|
|
182
|
+
};
|
|
183
|
+
const hasMetadata = args.setup_type || args.thesis || args.target_price != null
|
|
184
|
+
|| args.regime || args.scorecard_verdict || args.confluence_score != null
|
|
185
|
+
|| args.invalidation_price != null || args.realization_rule != null;
|
|
186
|
+
const metadata = hasMetadata ? {
|
|
187
|
+
setupType: args.setup_type,
|
|
188
|
+
thesis: args.thesis?.slice(0, 500), // Cap length
|
|
189
|
+
stopPrice: num(args.stopPrice),
|
|
190
|
+
targetPrice: num(args.target_price),
|
|
191
|
+
regime: args.regime,
|
|
192
|
+
regimeConfidence: clamp(args.regime_confidence, 0, 100),
|
|
193
|
+
scorecardVerdict: args.scorecard_verdict,
|
|
194
|
+
confluenceScore: clamp(args.confluence_score, 0, 10),
|
|
195
|
+
invalidationPrice: num(args.invalidation_price),
|
|
196
|
+
realizationRule: sanitizeRealizationRule(args.realization_rule),
|
|
197
|
+
} : undefined;
|
|
198
|
+
// ---- Approval-mode branches ----
|
|
199
|
+
// Both shadow and per_trade need full v2.10.0 metadata so the proposal is
|
|
200
|
+
// operator-meaningful. Metadata is the agent's responsibility; ingest creds
|
|
201
|
+
// are the operator's. Separate so the error messages can be precise.
|
|
202
|
+
const argsMetadataComplete = !!(args.thesis &&
|
|
203
|
+
args.setup_type &&
|
|
204
|
+
args.target_price != null &&
|
|
205
|
+
args.stopPrice != null &&
|
|
206
|
+
args.regime &&
|
|
207
|
+
typeof args.regime_confidence === 'number' &&
|
|
208
|
+
args.scorecard_verdict &&
|
|
209
|
+
typeof args.confluence_score === 'number');
|
|
210
|
+
const proposalPathWired = !!(deps.proposalManager && deps.userId);
|
|
211
|
+
const approvalMode = deps.approvalMode ?? 'off';
|
|
212
|
+
// per_trade mode: propose and EARLY-RETURN. The agent's call doesn't fire;
|
|
213
|
+
// the operator approves and ProposalDecisionListener fires via this same
|
|
214
|
+
// tool with proposalManager omitted (which takes the real path below).
|
|
215
|
+
//
|
|
216
|
+
// If proposalManager isn't wired (operator set approval.mode=per_trade in
|
|
217
|
+
// config but ingest creds aren't present), we fall through to the normal
|
|
218
|
+
// fire path — the startup warning is the operator's signal. We never trap
|
|
219
|
+
// the agent inside an unfireable per_trade branch.
|
|
220
|
+
if (approvalMode === 'per_trade' && proposalPathWired) {
|
|
221
|
+
if (!argsMetadataComplete) {
|
|
222
|
+
return {
|
|
223
|
+
error: 'approval.mode=per_trade requires the full v2.10.0 metadata block on every create_order: ' +
|
|
224
|
+
'thesis, setup_type, target_price, stopPrice, regime, regime_confidence, ' +
|
|
225
|
+
'scorecard_verdict, confluence_score. The operator approves on this metadata.',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const result = deps.proposalManager.propose(deps.userId, {
|
|
230
|
+
symbol: args.symbol,
|
|
231
|
+
side: side,
|
|
232
|
+
orderType: type,
|
|
233
|
+
size: args.amount,
|
|
234
|
+
proposedEntry: orderPrice,
|
|
235
|
+
stopPrice: args.stopPrice,
|
|
236
|
+
targetPrice: args.target_price,
|
|
237
|
+
riskUsd: Math.abs(orderPrice - args.stopPrice) * args.amount,
|
|
238
|
+
thesis: args.thesis,
|
|
239
|
+
setupType: args.setup_type,
|
|
240
|
+
regime: args.regime,
|
|
241
|
+
regimeConfidence: args.regime_confidence,
|
|
242
|
+
scorecardVerdict: args.scorecard_verdict,
|
|
243
|
+
confluenceScore: args.confluence_score,
|
|
244
|
+
...(args.supersedes_id ? { supersedesId: args.supersedes_id } : {}),
|
|
245
|
+
// Real, operator-actionable. Default would also resolve to 'real' but
|
|
246
|
+
// we set it explicitly so the intent reads at the call site.
|
|
247
|
+
origin: 'real',
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
status: 'pending_approval',
|
|
251
|
+
proposal_id: result.proposalUuid,
|
|
252
|
+
hard_expires_at: result.hardExpiresAt.toISOString(),
|
|
253
|
+
setup_bucket: result.setupBucket,
|
|
254
|
+
message: `Order awaits operator approval (${result.setupBucket} bucket, expires ` +
|
|
255
|
+
`${result.hardExpiresAt.toISOString()}). Do not call create_order again ` +
|
|
256
|
+
`for this setup; the listener will fire on approval. If the operator ` +
|
|
257
|
+
`requests modifications in chat, call create_order with supersedes_id=` +
|
|
258
|
+
`<id from the chat-context header>.`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
return { error: `Proposal submission failed: ${formatError(err)}` };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// shadow mode: dual-write a telemetry proposal AND continue to the real
|
|
266
|
+
// fire below. Never blocks the hot path on a propose() failure.
|
|
267
|
+
if (approvalMode === 'shadow' && proposalPathWired && argsMetadataComplete) {
|
|
268
|
+
try {
|
|
269
|
+
deps.proposalManager.propose(deps.userId, {
|
|
270
|
+
symbol: args.symbol,
|
|
271
|
+
side: side,
|
|
272
|
+
orderType: type,
|
|
273
|
+
size: args.amount,
|
|
274
|
+
proposedEntry: orderPrice,
|
|
275
|
+
stopPrice: args.stopPrice,
|
|
276
|
+
targetPrice: args.target_price,
|
|
277
|
+
riskUsd: Math.abs(orderPrice - args.stopPrice) * args.amount,
|
|
278
|
+
thesis: args.thesis,
|
|
279
|
+
setupType: args.setup_type,
|
|
280
|
+
regime: args.regime,
|
|
281
|
+
regimeConfidence: args.regime_confidence,
|
|
282
|
+
scorecardVerdict: args.scorecard_verdict,
|
|
283
|
+
confluenceScore: args.confluence_score,
|
|
284
|
+
...(args.supersedes_id ? { supersedesId: args.supersedes_id } : {}),
|
|
285
|
+
// Phase A telemetry — agent's create_order ALSO fires the real order
|
|
286
|
+
// synchronously below. Hidden from operator UI by origin='shadow'.
|
|
287
|
+
origin: 'shadow',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
// eslint-disable-next-line no-console
|
|
292
|
+
console.warn(`[approval-shadow] propose() threw: ${formatError(err)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const order = await deps.adapter.createOrder(args.symbol, side, type, args.amount, args.price, metadata);
|
|
297
|
+
// Auto-capture entry to the Position Decision Journal — fail-open, never
|
|
298
|
+
// block the trading hot path on a webapp ingest blip.
|
|
299
|
+
if (deps.autoCapture) {
|
|
300
|
+
// ALWAYS stash the metadata first (fixed 2026-07-05). Binance futures
|
|
301
|
+
// market orders routinely come back filled>0 but with NO average price
|
|
302
|
+
// in the immediate REST response — the synchronous capture below then
|
|
303
|
+
// bails ("missing fill price; skipping capture") and, before this fix,
|
|
304
|
+
// the metadata was dropped on the floor because the stash only ran in
|
|
305
|
+
// the not-filled branch. The WS-driven onWsFillObserved captured those
|
|
306
|
+
// entries seconds later with metadata=none → the live journal filled
|
|
307
|
+
// with '(no thesis recorded)' / setup_type='unknown' rows (majority of
|
|
308
|
+
// 2026-07 live entries) even though the agent supplied full v2.10.0
|
|
309
|
+
// metadata every time. When the synchronous capture DOES succeed, the
|
|
310
|
+
// WS dedup (openedFromExchangeTradeId === orderId) skips the duplicate
|
|
311
|
+
// and the unused stash entry simply expires (24h TTL, pruned).
|
|
312
|
+
if (metadata &&
|
|
313
|
+
deps.autoCapture.pendingEntries &&
|
|
314
|
+
typeof order.id === 'string' &&
|
|
315
|
+
order.id.length > 0) {
|
|
316
|
+
deps.autoCapture.pendingEntries.put({
|
|
317
|
+
orderId: order.id,
|
|
318
|
+
symbol: args.symbol,
|
|
319
|
+
side: side,
|
|
320
|
+
metadata,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const filledNow = typeof order.filled === 'number' && order.filled > 0;
|
|
324
|
+
if (filledNow) {
|
|
325
|
+
// Order filled at submit (market, or limit-that-crossed). Capture
|
|
326
|
+
// synchronously — same as the original PR1 path.
|
|
327
|
+
onCreateOrderFilled(deps.autoCapture, { symbol: args.symbol, side: side, metadata }, order).catch((err) => {
|
|
328
|
+
// eslint-disable-next-line no-console
|
|
329
|
+
console.warn(`[position-auto-capture] entry capture threw: ${formatError(err)}`);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return order;
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
return { error: formatError(err) };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type ExitGateMode = 'off' | 'shadow' | 'observe' | 'enforce';
|
|
2
|
+
export type ThesisStatusForGate = 'intact' | 'weakening' | 'invalidated' | 'evolving';
|
|
3
|
+
export type ReviewVerdictForGate = 'hold' | 'add_on' | 'close_recommended';
|
|
4
|
+
export interface ExitGateInputs {
|
|
5
|
+
/** The close_position reason the agent supplied. `operator_command` always
|
|
6
|
+
* passes; the other five reasons go through tier evaluation. */
|
|
7
|
+
reason: 'regime_flip' | 'scorecard_reversal' | 'risk_limit' | 'bracket_integrity' | 'operator_command';
|
|
8
|
+
/** Max favorable excursion in R-multiples ever reached on this position.
|
|
9
|
+
* Undefined => gate skips (PASS with code 'inputs_unavailable_mfe'). */
|
|
10
|
+
mfe_r_peak?: number;
|
|
11
|
+
/** Current R-multiple at the close price. Undefined => gate skips. */
|
|
12
|
+
current_r?: number;
|
|
13
|
+
/** Telemetry only (0a/0b) — provenance of the mfe_r_peak / current_r values
|
|
14
|
+
* above. Ignored by evaluateExitGate; set by buildExitGateInputs and
|
|
15
|
+
* persisted in gate_eval.inputs so the inputs_unavailable_* and
|
|
16
|
+
* computed-fallback paths are diagnosable from the journal. */
|
|
17
|
+
mfe_src?: 'assessment' | 'pos.mfeR' | 'computed_floor';
|
|
18
|
+
current_r_src?: 'assessment' | 'computed';
|
|
19
|
+
/** Implied or explicit vol-shock flag from the caller. The caller computes
|
|
20
|
+
* this from recent ATR vs latest adverse 5m bar. Default false. */
|
|
21
|
+
volShock?: boolean;
|
|
22
|
+
/** Position side — needed for the T2a stop-trail check (long stop ≥ entry,
|
|
23
|
+
* short stop ≤ entry). */
|
|
24
|
+
side?: 'long' | 'short';
|
|
25
|
+
/** Current bracket stop price. Undefined => T2a check is skipped (PASS with
|
|
26
|
+
* code 't2a_skipped_no_stop_known' rather than block). */
|
|
27
|
+
stopPrice?: number;
|
|
28
|
+
/** Entry price for the position. Used as the BE level in T2a. */
|
|
29
|
+
entryPrice?: number;
|
|
30
|
+
/** Most recent record_position_reviews entry for this position. Drives the
|
|
31
|
+
* contradicts-own-review block and the review-override pass. */
|
|
32
|
+
lastReview?: {
|
|
33
|
+
verdict: ReviewVerdictForGate;
|
|
34
|
+
thesis_status: ThesisStatusForGate;
|
|
35
|
+
review_at_ms: number;
|
|
36
|
+
};
|
|
37
|
+
/** Current time in ms — explicit for testability. */
|
|
38
|
+
now_ms: number;
|
|
39
|
+
}
|
|
40
|
+
export type GateOutcome = 'PASS' | 'BLOCK';
|
|
41
|
+
export interface GateVerdict {
|
|
42
|
+
outcome: GateOutcome;
|
|
43
|
+
/** Tier classification: T1 / T2a / T2b / override / skipped. */
|
|
44
|
+
tier: 'T1' | 'T2a' | 'T2b' | 'override' | 'skipped';
|
|
45
|
+
/** Stable code identifying the specific predicate that fired.
|
|
46
|
+
* e.g. 't1_premature_exit', 't2a_stop_not_trailed_to_BE',
|
|
47
|
+
* 'operator_command', 'vol_shock', 'contradicts_own_recent_hold_verdict'. */
|
|
48
|
+
code: string;
|
|
49
|
+
/** Optional recovery hint for BLOCK outcomes — surfaced back to the agent
|
|
50
|
+
* via the tool error so it can self-correct mid-turn. */
|
|
51
|
+
recovery_hint?: string;
|
|
52
|
+
/** Optional discipline-failure flag, currently only set on T2b passes so
|
|
53
|
+
* we capture "winner peaked, reversed, no stop-trail done". */
|
|
54
|
+
discipline_failure?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Evaluate the exit gate against the supplied inputs. Pure; safe to call
|
|
57
|
+
* from anywhere. See module header for semantics. */
|
|
58
|
+
export declare function evaluateExitGate(inputs: ExitGateInputs): GateVerdict;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Exit gate — prevents the agent from closing positions before they've
|
|
2
|
+
// meaningfully developed, while preserving override paths for genuine adverse
|
|
3
|
+
// signals. See docs/EXIT_GATE_DESIGN.md for the full spec and §3 for the
|
|
4
|
+
// tier table this function implements.
|
|
5
|
+
//
|
|
6
|
+
// Pure function — caller supplies all inputs, including the mode flag, and the
|
|
7
|
+
// function returns a structured verdict. Caller (close-position.ts) is
|
|
8
|
+
// responsible for actually blocking, soft-warning, or logging based on mode.
|
|
9
|
+
//
|
|
10
|
+
// Design contract:
|
|
11
|
+
// - In `off` mode the gate is never called at all (don't pay the input
|
|
12
|
+
// computation cost).
|
|
13
|
+
// - In `shadow` / `observe` / `enforce` the gate runs; the caller decides
|
|
14
|
+
// what to do with BLOCK verdicts.
|
|
15
|
+
// - If inputs are partial (e.g., MFE not supplied), the gate returns
|
|
16
|
+
// PASS('inputs_unavailable') — never BLOCK on missing data. The shadow
|
|
17
|
+
// soak surfaces these so we can fix plumbing before enforce.
|
|
18
|
+
// - The "contradicts-own-review" rule applies pre-tier (blocks T2b too).
|
|
19
|
+
// See §8 R4 in the design doc — this is the conservative default; we
|
|
20
|
+
// watch shadow telemetry and demote to T1-only if it blocks legit exits.
|
|
21
|
+
/** Window during which a hold-verdict review blocks a contradicting close. */
|
|
22
|
+
const CONTRADICTS_OWN_REVIEW_WINDOW_MS = 5 * 60 * 1000;
|
|
23
|
+
/** Developed-position threshold in R-multiples. */
|
|
24
|
+
const DEVELOPED_MFE_R = 0.5;
|
|
25
|
+
/** T1 escape threshold — position deteriorating fast. */
|
|
26
|
+
const T1_ESCAPE_CURRENT_R = -0.5;
|
|
27
|
+
/** 0c — floor for the 'weakening' review override. The t1_review_override pass
|
|
28
|
+
* was the dominant exit (~42% of agent closes) and, by construction, every one
|
|
29
|
+
* is a T1-premature position — the exact population the gate exists to BLOCK.
|
|
30
|
+
* It was fully agent-self-served: file verdict='close_recommended' +
|
|
31
|
+
* thesis_status='weakening' and the block lifted with zero objective
|
|
32
|
+
* corroboration. We now split by strength of claim: 'invalidated' (a strong,
|
|
33
|
+
* falsifiable thesis break — regime flip / level broken) keeps the FULL
|
|
34
|
+
* override even at breakeven; 'weakening' (a soft, abuse-prone signal) only
|
|
35
|
+
* overrides once price has ALSO moved adversely to at least this floor.
|
|
36
|
+
* ⚠ This threshold is a value to SOAK in shadow (compare would_have_blocked vs
|
|
37
|
+
* which positions later hit TP), NOT a derived optimum — tightening reclassifies
|
|
38
|
+
* a large population. See docs/EXIT_DISCIPLINE_BUILD_PLAN.md Step 0c. */
|
|
39
|
+
const T1_WEAKENING_OVERRIDE_FLOOR_R = -0.25;
|
|
40
|
+
/** Evaluate the exit gate against the supplied inputs. Pure; safe to call
|
|
41
|
+
* from anywhere. See module header for semantics. */
|
|
42
|
+
export function evaluateExitGate(inputs) {
|
|
43
|
+
// 0. Always-allow overrides — checked first, before any input-availability
|
|
44
|
+
// checks, so the operator can ALWAYS get out.
|
|
45
|
+
if (inputs.reason === 'operator_command') {
|
|
46
|
+
return { outcome: 'PASS', tier: 'override', code: 'operator_command' };
|
|
47
|
+
}
|
|
48
|
+
if (inputs.volShock === true) {
|
|
49
|
+
return { outcome: 'PASS', tier: 'override', code: 'vol_shock' };
|
|
50
|
+
}
|
|
51
|
+
// 1. Contradicts-own-review block — pre-tier.
|
|
52
|
+
// The "intact + hold → close" pattern from the May 12 analysis. If the
|
|
53
|
+
// agent's last review said hold AND was recent, refuse to immediately
|
|
54
|
+
// contradict it. Operator command (above) is the only override.
|
|
55
|
+
const lr = inputs.lastReview;
|
|
56
|
+
if (lr && lr.verdict === 'hold') {
|
|
57
|
+
const age = inputs.now_ms - lr.review_at_ms;
|
|
58
|
+
if (age < CONTRADICTS_OWN_REVIEW_WINDOW_MS && age >= 0) {
|
|
59
|
+
return {
|
|
60
|
+
outcome: 'BLOCK',
|
|
61
|
+
tier: 'override',
|
|
62
|
+
code: 'contradicts_own_recent_hold_verdict',
|
|
63
|
+
recovery_hint: `Last record_position_reviews filed verdict='hold' ${Math.round(age / 1000)}s ago. ` +
|
|
64
|
+
`If conditions have genuinely changed, file a fresh review with verdict='close_recommended' first. ` +
|
|
65
|
+
`Operator can override via reason='operator_command'.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 2. Input availability — we need mfe_r_peak + current_r to do tier work.
|
|
70
|
+
// Missing => PASS with a skip code so shadow telemetry can surface the
|
|
71
|
+
// plumbing gap.
|
|
72
|
+
if (typeof inputs.mfe_r_peak !== 'number' || !Number.isFinite(inputs.mfe_r_peak)) {
|
|
73
|
+
return { outcome: 'PASS', tier: 'skipped', code: 'inputs_unavailable_mfe' };
|
|
74
|
+
}
|
|
75
|
+
if (typeof inputs.current_r !== 'number' || !Number.isFinite(inputs.current_r)) {
|
|
76
|
+
return { outcome: 'PASS', tier: 'skipped', code: 'inputs_unavailable_current_r' };
|
|
77
|
+
}
|
|
78
|
+
// 3. Review override — well-formed close_recommended on a genuine adverse
|
|
79
|
+
// signal. 0c split (see T1_WEAKENING_OVERRIDE_FLOOR_R): 'invalidated' (a
|
|
80
|
+
// strong, falsifiable break) overrides outright; 'weakening' (soft, the
|
|
81
|
+
// scratch-laundering vector) overrides ONLY with price confirmation
|
|
82
|
+
// (current_r ≤ the floor). inputs.current_r is guaranteed finite here
|
|
83
|
+
// (checked in step 2 above).
|
|
84
|
+
const reviewOverride = !!lr &&
|
|
85
|
+
lr.verdict === 'close_recommended' &&
|
|
86
|
+
(lr.thesis_status === 'invalidated' ||
|
|
87
|
+
(lr.thesis_status === 'weakening' && inputs.current_r <= T1_WEAKENING_OVERRIDE_FLOOR_R));
|
|
88
|
+
// 4. Tier classification.
|
|
89
|
+
const developed = inputs.mfe_r_peak >= DEVELOPED_MFE_R;
|
|
90
|
+
if (!developed) {
|
|
91
|
+
// ── Tier 1: never developed (mfe_r_peak < 0.5) ──
|
|
92
|
+
if (inputs.current_r <= T1_ESCAPE_CURRENT_R) {
|
|
93
|
+
return {
|
|
94
|
+
outcome: 'PASS',
|
|
95
|
+
tier: 'T1',
|
|
96
|
+
code: 't1_escape_deteriorating',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (reviewOverride) {
|
|
100
|
+
return {
|
|
101
|
+
outcome: 'PASS',
|
|
102
|
+
tier: 'T1',
|
|
103
|
+
code: 't1_review_override',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
outcome: 'BLOCK',
|
|
108
|
+
tier: 'T1',
|
|
109
|
+
code: 't1_premature_exit',
|
|
110
|
+
recovery_hint: `Position has not developed (mfe_r_peak=${inputs.mfe_r_peak.toFixed(2)}R < 0.5R) and is not deteriorating ` +
|
|
111
|
+
`(current_r=${inputs.current_r.toFixed(2)}R > -0.5R). ` +
|
|
112
|
+
`Let the bracket be the exit. Override paths: ` +
|
|
113
|
+
`(a) file verdict='close_recommended' with thesis_status='invalidated' — a real, falsifiable ` +
|
|
114
|
+
`thesis break (regime flip, key level broken), not a soft read; ` +
|
|
115
|
+
`(b) thesis_status='weakening' overrides ONLY once current_r ≤ -0.25R (soft signals need price confirmation); ` +
|
|
116
|
+
`(c) wait for current_r ≤ -0.5R; (d) operator_command.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ── Tier 2: developed (mfe_r_peak ≥ 0.5) ──
|
|
120
|
+
if (inputs.current_r >= 0) {
|
|
121
|
+
// T2a — in profit. Require stop has been trailed to BE-or-better.
|
|
122
|
+
// Need side + stopPrice + entryPrice to check; if any missing, pass with
|
|
123
|
+
// a skip code so shadow telemetry surfaces it.
|
|
124
|
+
if (!inputs.side ||
|
|
125
|
+
typeof inputs.stopPrice !== 'number' ||
|
|
126
|
+
!Number.isFinite(inputs.stopPrice) ||
|
|
127
|
+
typeof inputs.entryPrice !== 'number' ||
|
|
128
|
+
!Number.isFinite(inputs.entryPrice)) {
|
|
129
|
+
return {
|
|
130
|
+
outcome: 'PASS',
|
|
131
|
+
tier: 'T2a',
|
|
132
|
+
code: 't2a_skipped_no_stop_known',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const stopAtBE = inputs.side === 'long' ? inputs.stopPrice >= inputs.entryPrice : inputs.stopPrice <= inputs.entryPrice;
|
|
136
|
+
if (!stopAtBE) {
|
|
137
|
+
return {
|
|
138
|
+
outcome: 'BLOCK',
|
|
139
|
+
tier: 'T2a',
|
|
140
|
+
code: 't2a_stop_not_trailed_to_BE',
|
|
141
|
+
recovery_hint: `Position developed to mfe_r_peak=${inputs.mfe_r_peak.toFixed(2)}R and is currently in profit ` +
|
|
142
|
+
`(current_r=${inputs.current_r.toFixed(2)}R), but stop is still at original level ` +
|
|
143
|
+
`(stop=${inputs.stopPrice}, entry=${inputs.entryPrice}). ` +
|
|
144
|
+
`Call modify_stop to move stop to ${inputs.entryPrice} (break-even) or better before closing. ` +
|
|
145
|
+
`This locks in profit and is the v2.10.0 winner-protection rule.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
outcome: 'PASS',
|
|
150
|
+
tier: 'T2a',
|
|
151
|
+
code: 't2a_developed_in_profit_stop_trailed',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// T2b — developed, now in red. Horse has bolted; allow the exit but flag
|
|
155
|
+
// as discipline failure so we can study these later.
|
|
156
|
+
return {
|
|
157
|
+
outcome: 'PASS',
|
|
158
|
+
tier: 'T2b',
|
|
159
|
+
code: 't2b_developed_reversed',
|
|
160
|
+
discipline_failure: 'unmanaged_winner_reversed',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BinancePublicApi } from '../ccxt/binance-public.js';
|
|
2
|
+
import type { CcxtOHLCV } from '../types.js';
|
|
3
|
+
export declare function fetchOhlcvTool(args: {
|
|
4
|
+
symbol: string;
|
|
5
|
+
timeframe?: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
}, deps: {
|
|
8
|
+
binanceApi: BinancePublicApi;
|
|
9
|
+
}): Promise<CcxtOHLCV[] | {
|
|
10
|
+
error: string;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Tool: fetch_ohlcv — real Binance OHLCV candles via CCXT
|
|
2
|
+
export async function fetchOhlcvTool(args, deps) {
|
|
3
|
+
const candles = await deps.binanceApi.fetchOHLCV(args.symbol, args.timeframe ?? '1h', args.limit ?? 100);
|
|
4
|
+
if (!candles) {
|
|
5
|
+
return { error: `Failed to fetch OHLCV for ${args.symbol}` };
|
|
6
|
+
}
|
|
7
|
+
return candles;
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BinancePublicApi } from '../ccxt/binance-public.js';
|
|
2
|
+
import type { ExchangeSimulator } from '../simulator/exchange-simulator.js';
|
|
3
|
+
import type { CcxtTicker } from '../types.js';
|
|
4
|
+
export declare function fetchTickerTool(args: {
|
|
5
|
+
symbol: string;
|
|
6
|
+
}, deps: {
|
|
7
|
+
binanceApi: BinancePublicApi;
|
|
8
|
+
simulator: ExchangeSimulator;
|
|
9
|
+
}): Promise<CcxtTicker | {
|
|
10
|
+
error: string;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Tool: get_agent_profile — returns earned autonomy tier and permissions.
|
|
2
|
+
// The agent checks this at session start to know what it's allowed to adjust.
|
|
3
|
+
import { fetchIntelApi } from './intel-api.js';
|
|
4
|
+
export async function getAgentProfileTool(deps) {
|
|
5
|
+
return fetchIntelApi('/api/agent-profile', deps);
|
|
6
|
+
}
|