Proof: npm test (217/217), npm run operator-dashboard:build, and focused intent/trading-config tests cover DB-null request amount and slippage limits. Assumptions: removing request amount and slippage caps is explicitly operator-approved for the current live-funds workflow; preflight remains side-effect-free and submit remains separate. Still fake: request settlement truth still depends on inventory-delta attribution instead of venue-native terminal fill events.
This commit is contained in:
parent
a73ea1c83f
commit
b45d12d37c
9 changed files with 186 additions and 49 deletions
|
|
@ -50,8 +50,10 @@ export function createIntentRequestController({
|
||||||
);
|
);
|
||||||
const slippageBps = Number(body.slippage_bps ?? requestPair.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200);
|
const slippageBps = Number(body.slippage_bps ?? requestPair.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200);
|
||||||
const minDeadlineMs = Number(body.min_deadline_ms || requestPair.minDeadlineMs || config.intentRequestMinDeadlineMs || 60_000);
|
const minDeadlineMs = Number(body.min_deadline_ms || requestPair.minDeadlineMs || config.intentRequestMinDeadlineMs || 60_000);
|
||||||
const maxAmountUnits = parseDecimalToUnits(
|
const maxAmountUnits = requestPair.requestMaxNotional == null
|
||||||
String(requestPair.requestMaxNotional || config.intentRequestMaxAmountEure || 5),
|
? null
|
||||||
|
: parseDecimalToUnits(
|
||||||
|
String(requestPair.requestMaxNotional),
|
||||||
sourceAsset.decimals,
|
sourceAsset.decimals,
|
||||||
{ field: 'intent_request_max_notional' },
|
{ field: 'intent_request_max_notional' },
|
||||||
);
|
);
|
||||||
|
|
@ -80,22 +82,25 @@ export function createIntentRequestController({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
sourceAmountUnits = parseDecimalToUnits(amountEure, sourceAsset.decimals, { field: 'amount_eure' });
|
sourceAmountUnits = parseDecimalToUnits(amountEure, sourceAsset.decimals, { field: 'amount_eure' });
|
||||||
if (BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
|
if (maxAmountUnits != null && BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
|
||||||
blockedBeforeQuote = true;
|
blockedBeforeQuote = true;
|
||||||
throw codedError(
|
throw codedError(
|
||||||
'amount_exceeds_request_limit',
|
'amount_exceeds_request_limit',
|
||||||
`Requested ${amountEure} ${sourceAsset.symbol} exceeds configured live request limit ${requestPair.requestMaxNotional || config.intentRequestMaxAmountEure || 5} ${sourceAsset.symbol}.`,
|
`Requested ${amountEure} ${sourceAsset.symbol} exceeds configured live request limit ${requestPair.requestMaxNotional} ${sourceAsset.symbol}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!Number.isInteger(slippageBps) || slippageBps < 0) {
|
if (!Number.isInteger(slippageBps) || slippageBps < 0) {
|
||||||
blockedBeforeQuote = true;
|
blockedBeforeQuote = true;
|
||||||
throw codedError('invalid_slippage', 'Slippage must be a non-negative integer in basis points.');
|
throw codedError('invalid_slippage', 'Slippage must be a non-negative integer in basis points.');
|
||||||
}
|
}
|
||||||
if (slippageBps > Number(requestPair.requestMaxSlippageBps ?? config.intentRequestMaxSlippageBps ?? 200)) {
|
if (
|
||||||
|
requestPair.requestMaxSlippageBps != null
|
||||||
|
&& slippageBps > Number(requestPair.requestMaxSlippageBps)
|
||||||
|
) {
|
||||||
blockedBeforeQuote = true;
|
blockedBeforeQuote = true;
|
||||||
throw codedError(
|
throw codedError(
|
||||||
'slippage_exceeds_request_limit',
|
'slippage_exceeds_request_limit',
|
||||||
`Slippage ${slippageBps} bps exceeds configured limit ${requestPair.requestMaxSlippageBps ?? config.intentRequestMaxSlippageBps ?? 200} bps.`,
|
`Slippage ${slippageBps} bps exceeds configured limit ${requestPair.requestMaxSlippageBps} bps.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -560,11 +565,9 @@ async function resolveIntentRequestPair({ body, config, getTradingConfig }) {
|
||||||
priceRoute: pair.priceRoute,
|
priceRoute: pair.priceRoute,
|
||||||
requestDefaultNotional:
|
requestDefaultNotional:
|
||||||
strategyConfig.requestDefaultNotional || config.intentRequestDefaultAmountEure,
|
strategyConfig.requestDefaultNotional || config.intentRequestDefaultAmountEure,
|
||||||
requestMaxNotional:
|
requestMaxNotional: strategyConfig.requestMaxNotional ?? null,
|
||||||
strategyConfig.requestMaxNotional || config.intentRequestMaxAmountEure,
|
|
||||||
slippageBps: strategyConfig.slippageBps ?? config.intentRequestDefaultSlippageBps,
|
slippageBps: strategyConfig.slippageBps ?? config.intentRequestDefaultSlippageBps,
|
||||||
requestMaxSlippageBps:
|
requestMaxSlippageBps: strategyConfig.requestMaxSlippageBps ?? null,
|
||||||
strategyConfig.requestMaxSlippageBps ?? strategyConfig.slippageBps ?? config.intentRequestMaxSlippageBps,
|
|
||||||
minDeadlineMs: strategyConfig.minDeadlineMs ?? config.intentRequestMinDeadlineMs,
|
minDeadlineMs: strategyConfig.minDeadlineMs ?? config.intentRequestMinDeadlineMs,
|
||||||
priceMaxAgeMs: strategyConfig.priceMaxAgeMs ?? config.intentRequestPriceMaxAgeMs,
|
priceMaxAgeMs: strategyConfig.priceMaxAgeMs ?? config.intentRequestPriceMaxAgeMs,
|
||||||
inventoryMaxAgeMs: strategyConfig.inventoryMaxAgeMs ?? config.intentRequestInventoryMaxAgeMs,
|
inventoryMaxAgeMs: strategyConfig.inventoryMaxAgeMs ?? config.intentRequestInventoryMaxAgeMs,
|
||||||
|
|
|
||||||
|
|
@ -900,14 +900,23 @@ function buildFundingSummary({ config, fundingObservations, recentDepositStatuse
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildIntentRequestSummary({ config, intentRequests = [], executorState = {} } = {}) {
|
function buildIntentRequestSummary({ config, intentRequests = [], executorState = {} } = {}) {
|
||||||
|
const defaultPair = config.defaultTakerPair || null;
|
||||||
|
const strategyConfig = defaultPair?.strategyConfig || null;
|
||||||
|
const usingDbPairConfig = Boolean(config.tradingConfigLoaded && strategyConfig);
|
||||||
|
const sourceAsset = defaultPair?.assetIn || config.tradingEure;
|
||||||
|
const destinationAsset = defaultPair?.assetOut || config.tradingBtc;
|
||||||
return {
|
return {
|
||||||
defaults: {
|
defaults: {
|
||||||
source_symbol: config.tradingEure.symbol,
|
source_symbol: sourceAsset?.symbol || 'Source',
|
||||||
destination_symbol: config.tradingBtc.symbol,
|
destination_symbol: destinationAsset?.symbol || 'Destination',
|
||||||
amount_eure: String(config.intentRequestDefaultAmountEure || 5),
|
amount_eure: String(strategyConfig?.requestDefaultNotional ?? config.intentRequestDefaultAmountEure ?? 5),
|
||||||
max_amount_eure: String(config.intentRequestMaxAmountEure || 5),
|
max_amount_eure: usingDbPairConfig
|
||||||
slippage_bps: Number(config.intentRequestDefaultSlippageBps ?? 200),
|
? strategyConfig.requestMaxNotional ?? null
|
||||||
max_slippage_bps: Number(config.intentRequestMaxSlippageBps ?? 200),
|
: String(config.intentRequestMaxAmountEure || 5),
|
||||||
|
slippage_bps: Number(strategyConfig?.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200),
|
||||||
|
max_slippage_bps: usingDbPairConfig
|
||||||
|
? strategyConfig.requestMaxSlippageBps ?? null
|
||||||
|
: Number(config.intentRequestMaxSlippageBps ?? 200),
|
||||||
},
|
},
|
||||||
executor_armed: executorState.armed ?? null,
|
executor_armed: executorState.armed ?? null,
|
||||||
executor_paused: executorState.paused ?? null,
|
executor_paused: executorState.paused ?? null,
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ export const CURRENT_REVERSE_PAIR_KEY = pairKey(CURRENT_EURE_ASSET_ID, CURRENT_N
|
||||||
export const CURRENT_EDGE_BPS = 49;
|
export const CURRENT_EDGE_BPS = 49;
|
||||||
export const CURRENT_STRATEGY_MAX_NOTIONAL = '150';
|
export const CURRENT_STRATEGY_MAX_NOTIONAL = '150';
|
||||||
export const CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE = '5';
|
export const CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE = '5';
|
||||||
export const CURRENT_REQUEST_MAX_NOTIONAL_EURE = '5';
|
|
||||||
export const CURRENT_SLIPPAGE_BPS = 200;
|
export const CURRENT_SLIPPAGE_BPS = 200;
|
||||||
|
export const CURRENT_REQUEST_MAX_NOTIONAL_EURE = null;
|
||||||
|
export const CURRENT_REQUEST_MAX_SLIPPAGE_BPS = null;
|
||||||
export const CURRENT_MIN_DEADLINE_MS = 60_000;
|
export const CURRENT_MIN_DEADLINE_MS = 60_000;
|
||||||
export const CURRENT_PRICE_MAX_AGE_MS = 30_000;
|
export const CURRENT_PRICE_MAX_AGE_MS = 30_000;
|
||||||
export const CURRENT_INVENTORY_MAX_AGE_MS = 30_000;
|
export const CURRENT_INVENTORY_MAX_AGE_MS = 30_000;
|
||||||
|
|
@ -188,7 +189,7 @@ export function buildSeedStrategyConfig(pairId, {
|
||||||
inventoryMaxAgeMs: CURRENT_INVENTORY_MAX_AGE_MS,
|
inventoryMaxAgeMs: CURRENT_INVENTORY_MAX_AGE_MS,
|
||||||
requestDefaultNotional: CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE,
|
requestDefaultNotional: CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE,
|
||||||
requestMaxNotional: CURRENT_REQUEST_MAX_NOTIONAL_EURE,
|
requestMaxNotional: CURRENT_REQUEST_MAX_NOTIONAL_EURE,
|
||||||
requestMaxSlippageBps: CURRENT_SLIPPAGE_BPS,
|
requestMaxSlippageBps: CURRENT_REQUEST_MAX_SLIPPAGE_BPS,
|
||||||
createdBy,
|
createdBy,
|
||||||
reason,
|
reason,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -719,9 +719,9 @@ export async function createPairStrategyConfigVersion(pool, {
|
||||||
minDeadlineMs = null,
|
minDeadlineMs = null,
|
||||||
priceMaxAgeMs = null,
|
priceMaxAgeMs = null,
|
||||||
inventoryMaxAgeMs = null,
|
inventoryMaxAgeMs = null,
|
||||||
requestDefaultNotional = null,
|
requestDefaultNotional = undefined,
|
||||||
requestMaxNotional = null,
|
requestMaxNotional = undefined,
|
||||||
requestMaxSlippageBps = null,
|
requestMaxSlippageBps = undefined,
|
||||||
changedBy = 'operator',
|
changedBy = 'operator',
|
||||||
reason = 'operator config update',
|
reason = 'operator config update',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
|
@ -763,17 +763,17 @@ export async function createPairStrategyConfigVersion(pool, {
|
||||||
inventoryMaxAgeMs:
|
inventoryMaxAgeMs:
|
||||||
inventoryMaxAgeMs == null ? Number(active.inventory_max_age_ms) : Number(inventoryMaxAgeMs),
|
inventoryMaxAgeMs == null ? Number(active.inventory_max_age_ms) : Number(inventoryMaxAgeMs),
|
||||||
requestDefaultNotional:
|
requestDefaultNotional:
|
||||||
requestDefaultNotional == null
|
requestDefaultNotional === undefined
|
||||||
? active.request_default_notional == null ? null : String(active.request_default_notional)
|
? active.request_default_notional == null ? null : String(active.request_default_notional)
|
||||||
: String(requestDefaultNotional),
|
: nullablePositiveNumberString(requestDefaultNotional, 'request_default_notional'),
|
||||||
requestMaxNotional:
|
requestMaxNotional:
|
||||||
requestMaxNotional == null
|
requestMaxNotional === undefined
|
||||||
? active.request_max_notional == null ? null : String(active.request_max_notional)
|
? active.request_max_notional == null ? null : String(active.request_max_notional)
|
||||||
: String(requestMaxNotional),
|
: nullablePositiveNumberString(requestMaxNotional, 'request_max_notional'),
|
||||||
requestMaxSlippageBps:
|
requestMaxSlippageBps:
|
||||||
requestMaxSlippageBps == null
|
requestMaxSlippageBps === undefined
|
||||||
? active.request_max_slippage_bps == null ? null : Number(active.request_max_slippage_bps)
|
? active.request_max_slippage_bps == null ? null : Number(active.request_max_slippage_bps)
|
||||||
: Number(requestMaxSlippageBps),
|
: nullableNonNegativeInteger(requestMaxSlippageBps, 'request_max_slippage_bps'),
|
||||||
createdBy: changedBy,
|
createdBy: changedBy,
|
||||||
reason,
|
reason,
|
||||||
};
|
};
|
||||||
|
|
@ -874,9 +874,9 @@ export async function setTradingPairMode(pool, {
|
||||||
minDeadlineMs = null,
|
minDeadlineMs = null,
|
||||||
priceMaxAgeMs = null,
|
priceMaxAgeMs = null,
|
||||||
inventoryMaxAgeMs = null,
|
inventoryMaxAgeMs = null,
|
||||||
requestDefaultNotional = null,
|
requestDefaultNotional = undefined,
|
||||||
requestMaxNotional = null,
|
requestMaxNotional = undefined,
|
||||||
requestMaxSlippageBps = null,
|
requestMaxSlippageBps = undefined,
|
||||||
changedBy = 'operator',
|
changedBy = 'operator',
|
||||||
reason = 'operator pair mode update',
|
reason = 'operator pair mode update',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
|
@ -1336,9 +1336,9 @@ function buildInitialPairStrategyConfig(pairId, {
|
||||||
minDeadlineMs = null,
|
minDeadlineMs = null,
|
||||||
priceMaxAgeMs = null,
|
priceMaxAgeMs = null,
|
||||||
inventoryMaxAgeMs = null,
|
inventoryMaxAgeMs = null,
|
||||||
requestDefaultNotional = null,
|
requestDefaultNotional = undefined,
|
||||||
requestMaxNotional = null,
|
requestMaxNotional = undefined,
|
||||||
requestMaxSlippageBps = null,
|
requestMaxSlippageBps = undefined,
|
||||||
changedBy = 'operator',
|
changedBy = 'operator',
|
||||||
reason = 'operator pair strategy config initialization',
|
reason = 'operator pair strategy config initialization',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
|
@ -1396,11 +1396,6 @@ function nonNegativeIntegerOrDefault(value, fallback, field) {
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nullableNonNegativeIntegerOrDefault(value, fallback, field) {
|
|
||||||
if (!hasConfigOverride(value)) return fallback == null ? null : nonNegativeIntegerOrDefault(fallback, 0, field);
|
|
||||||
return nonNegativeIntegerOrDefault(value, 0, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
function positiveNumberStringOrDefault(value, fallback, field) {
|
function positiveNumberStringOrDefault(value, fallback, field) {
|
||||||
if (!hasConfigOverride(value)) return String(fallback);
|
if (!hasConfigOverride(value)) return String(fallback);
|
||||||
const next = String(value).trim();
|
const next = String(value).trim();
|
||||||
|
|
@ -1416,10 +1411,25 @@ function nonNegativeNumberStringOrDefault(value, fallback, field) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function nullablePositiveNumberStringOrDefault(value, fallback, field) {
|
function nullablePositiveNumberStringOrDefault(value, fallback, field) {
|
||||||
if (!hasConfigOverride(value)) return fallback == null ? null : positiveNumberStringOrDefault(fallback, '1', field);
|
if (value === undefined) return fallback == null ? null : positiveNumberStringOrDefault(fallback, '1', field);
|
||||||
|
return nullablePositiveNumberString(value, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nullablePositiveNumberString(value, field) {
|
||||||
|
if (!hasConfigOverride(value)) return null;
|
||||||
return positiveNumberStringOrDefault(value, '1', field);
|
return positiveNumberStringOrDefault(value, '1', field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nullableNonNegativeIntegerOrDefault(value, fallback, field) {
|
||||||
|
if (value === undefined) return fallback == null ? null : nonNegativeIntegerOrDefault(fallback, 0, field);
|
||||||
|
return nullableNonNegativeInteger(value, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nullableNonNegativeInteger(value, field) {
|
||||||
|
if (!hasConfigOverride(value)) return null;
|
||||||
|
return nonNegativeIntegerOrDefault(value, 0, field);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeStrategyConfigRow(row) {
|
function normalizeStrategyConfigRow(row) {
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,10 @@ function IdentifierLine({ label, value }) {
|
||||||
|
|
||||||
function IntentRequestForm({ summary, onControl }) {
|
function IntentRequestForm({ summary, onControl }) {
|
||||||
const defaults = summary?.defaults || {};
|
const defaults = summary?.defaults || {};
|
||||||
|
const sourceSymbol = defaults.source_symbol || 'EURe';
|
||||||
|
const destinationSymbol = defaults.destination_symbol || 'BTC';
|
||||||
|
const hasAmountCap = defaults.max_amount_eure != null && defaults.max_amount_eure !== '';
|
||||||
|
const hasSlippageCap = defaults.max_slippage_bps != null && defaults.max_slippage_bps !== '';
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
amount_eure: defaults.amount_eure || '5',
|
amount_eure: defaults.amount_eure || '5',
|
||||||
slippage_bps: String(defaults.slippage_bps ?? 200),
|
slippage_bps: String(defaults.slippage_bps ?? 200),
|
||||||
|
|
@ -303,36 +307,40 @@ function IntentRequestForm({ summary, onControl }) {
|
||||||
<form onSubmit={handlePreflight}>
|
<form onSubmit={handlePreflight}>
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="intent-request-amount">Spend EURe</label>
|
<label htmlFor="intent-request-amount">{`Spend ${sourceSymbol}`}</label>
|
||||||
<input
|
<input
|
||||||
id="intent-request-amount"
|
id="intent-request-amount"
|
||||||
max={defaults.max_amount_eure || '5'}
|
|
||||||
min="0.01"
|
min="0.01"
|
||||||
name="amount_eure"
|
name="amount_eure"
|
||||||
onChange={(event) => setForm((current) => ({ ...current, amount_eure: event.target.value }))}
|
onChange={(event) => setForm((current) => ({ ...current, amount_eure: event.target.value }))}
|
||||||
step="0.01"
|
step="0.01"
|
||||||
type="number"
|
type="number"
|
||||||
value={form.amount_eure}
|
value={form.amount_eure}
|
||||||
|
{...(hasAmountCap ? { max: defaults.max_amount_eure } : {})}
|
||||||
/>
|
/>
|
||||||
<div className="status-subtle">{`Max ${defaults.max_amount_eure || '5'} EURe per live test request`}</div>
|
<div className="status-subtle">
|
||||||
|
{hasAmountCap ? `Max ${defaults.max_amount_eure} ${sourceSymbol}` : 'No request amount cap'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label htmlFor="intent-request-slippage">Max slippage bps</label>
|
<label htmlFor="intent-request-slippage">Max slippage bps</label>
|
||||||
<input
|
<input
|
||||||
id="intent-request-slippage"
|
id="intent-request-slippage"
|
||||||
max={defaults.max_slippage_bps ?? 200}
|
|
||||||
min="0"
|
min="0"
|
||||||
name="slippage_bps"
|
name="slippage_bps"
|
||||||
onChange={(event) => setForm((current) => ({ ...current, slippage_bps: event.target.value }))}
|
onChange={(event) => setForm((current) => ({ ...current, slippage_bps: event.target.value }))}
|
||||||
step="1"
|
step="1"
|
||||||
type="number"
|
type="number"
|
||||||
value={form.slippage_bps}
|
value={form.slippage_bps}
|
||||||
|
{...(hasSlippageCap ? { max: defaults.max_slippage_bps } : {})}
|
||||||
/>
|
/>
|
||||||
<div className="status-subtle">{`Max ${defaults.max_slippage_bps ?? 200} bps / 2%`}</div>
|
<div className="status-subtle">
|
||||||
|
{hasSlippageCap ? `Max ${defaults.max_slippage_bps} bps` : 'No slippage cap'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
<button className="button" type="submit">Preflight EURe to BTC</button>
|
<button className="button" type="submit">{`Preflight ${sourceSymbol} to ${destinationSymbol}`}</button>
|
||||||
<button
|
<button
|
||||||
className="button secondary"
|
className="button secondary"
|
||||||
onClick={() => onControl('trade-executor', 'intent-request-refresh-outcomes')}
|
onClick={() => onControl('trade-executor', 'intent-request-refresh-outcomes')}
|
||||||
|
|
|
||||||
|
|
@ -630,6 +630,16 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
||||||
<td>{strategyConfig.edge_bps ?? 'Unavailable'} bps</td>
|
<td>{strategyConfig.edge_bps ?? 'Unavailable'} bps</td>
|
||||||
<td>
|
<td>
|
||||||
<div>{strategyConfig.max_notional || 'Unavailable'} max</div>
|
<div>{strategyConfig.max_notional || 'Unavailable'} max</div>
|
||||||
|
<div className="status-subtle">
|
||||||
|
{strategyConfig.request_max_notional == null
|
||||||
|
? 'No request cap'
|
||||||
|
: `${strategyConfig.request_max_notional} request max`}
|
||||||
|
</div>
|
||||||
|
<div className="status-subtle">
|
||||||
|
{strategyConfig.request_max_slippage_bps == null
|
||||||
|
? 'No slippage cap'
|
||||||
|
: `${strategyConfig.request_max_slippage_bps} bps slippage max`}
|
||||||
|
</div>
|
||||||
<div className="status-subtle">{strategyConfig.price_max_age_ms || 'Unavailable'} ms price max age</div>
|
<div className="status-subtle">{strategyConfig.price_max_age_ms || 'Unavailable'} ms price max age</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{route.source || 'Unavailable'}</td>
|
<td>{route.source || 'Unavailable'}</td>
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,14 @@ function buildRelay() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildController({ store = buildStore(), relay = buildRelay(), armed = true, verifierRegistered = true, withMakerSuppressed = async (operation) => operation() } = {}) {
|
function buildController({
|
||||||
|
store = buildStore(),
|
||||||
|
relay = buildRelay(),
|
||||||
|
armed = true,
|
||||||
|
verifierRegistered = true,
|
||||||
|
withMakerSuppressed = async (operation) => operation(),
|
||||||
|
getTradingConfig = null,
|
||||||
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
store,
|
store,
|
||||||
relay,
|
relay,
|
||||||
|
|
@ -137,6 +144,7 @@ function buildController({ store = buildStore(), relay = buildRelay(), armed = t
|
||||||
signer: KeyPair.fromRandom('ed25519'),
|
signer: KeyPair.fromRandom('ed25519'),
|
||||||
isArmed: () => armed,
|
isArmed: () => armed,
|
||||||
isPaused: () => false,
|
isPaused: () => false,
|
||||||
|
getTradingConfig,
|
||||||
withMakerSuppressed,
|
withMakerSuppressed,
|
||||||
now: () => Date.parse('2026-04-12T10:00:00.000Z'),
|
now: () => Date.parse('2026-04-12T10:00:00.000Z'),
|
||||||
uuid: (() => {
|
uuid: (() => {
|
||||||
|
|
@ -196,6 +204,60 @@ test('preflight is side-effect-free and does not publish a live intent', async (
|
||||||
assert.equal(relay.publishCalls, 0);
|
assert.equal(relay.publishCalls, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('DB null request limits allow operator-chosen amount and slippage', async () => {
|
||||||
|
const store = buildStore({ inventoryUnits: '6000000000000000000' });
|
||||||
|
const relay = buildRelay();
|
||||||
|
relay.quote = async function quote() {
|
||||||
|
this.quoteCalls += 1;
|
||||||
|
return [{
|
||||||
|
quote_hash: 'uncapped-quote-hash',
|
||||||
|
amount_out: '12000',
|
||||||
|
expiration_time: '2026-04-12T10:01:00.000Z',
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
const pairId = `${EURE.assetId}->${BTC.assetId}`;
|
||||||
|
const pair = {
|
||||||
|
key: pairId,
|
||||||
|
pairId,
|
||||||
|
takerEnabled: true,
|
||||||
|
canTrade: true,
|
||||||
|
assetIn: EURE,
|
||||||
|
assetOut: BTC,
|
||||||
|
strategyConfig: {
|
||||||
|
requestDefaultNotional: '5',
|
||||||
|
requestMaxNotional: null,
|
||||||
|
slippageBps: 200,
|
||||||
|
requestMaxSlippageBps: null,
|
||||||
|
minDeadlineMs: 60_000,
|
||||||
|
priceMaxAgeMs: 30_000,
|
||||||
|
inventoryMaxAgeMs: 30_000,
|
||||||
|
},
|
||||||
|
priceRoute: {
|
||||||
|
source: 'btc_eur_reference',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { controller } = buildController({
|
||||||
|
store,
|
||||||
|
relay,
|
||||||
|
getTradingConfig: async () => ({
|
||||||
|
ok: true,
|
||||||
|
tradingEure: EURE,
|
||||||
|
tradingBtc: BTC,
|
||||||
|
pairByKey: new Map([[pairId, pair]]),
|
||||||
|
defaultTakerPair: pair,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const preflight = await controller.preflight({ amount_eure: '6', slippage_bps: 250 });
|
||||||
|
|
||||||
|
assert.equal(preflight.state, 'draft');
|
||||||
|
assert.equal(preflight.reason_code, 'quote_available');
|
||||||
|
assert.equal(preflight.request_max_notional, null);
|
||||||
|
assert.equal(preflight.slippage_bps, 250);
|
||||||
|
assert.equal(preflight.live_submit_capable, true);
|
||||||
|
assert.equal(relay.quoteCalls, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('insufficient spendable EURe blocks before solver quote or signing', async () => {
|
test('insufficient spendable EURe blocks before solver quote or signing', async () => {
|
||||||
const store = buildStore({ inventoryUnits: '0' });
|
const store = buildStore({ inventoryUnits: '0' });
|
||||||
const relay = buildRelay();
|
const relay = buildRelay();
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ test('operator dashboard exposes DB-backed pair activation and pause controls',
|
||||||
assert.match(source, /control\.action === 'pause-pair'/);
|
assert.match(source, /control\.action === 'pause-pair'/);
|
||||||
assert.match(source, /edgeBps: body\.edge_bps/);
|
assert.match(source, /edgeBps: body\.edge_bps/);
|
||||||
assert.match(source, /maxNotional: body\.max_notional/);
|
assert.match(source, /maxNotional: body\.max_notional/);
|
||||||
|
assert.match(source, /requestMaxNotional: bodyField\(body, 'request_max_notional', 'requestMaxNotional'\)/);
|
||||||
|
assert.match(source, /requestMaxSlippageBps: bodyField\(body, 'request_max_slippage_bps', 'requestMaxSlippageBps'\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('operator dashboard API auth failures are JSON for frontend fetches', () => {
|
test('operator dashboard API auth failures are JSON for frontend fetches', () => {
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,38 @@ test('edge update creates a new active strategy version', async () => {
|
||||||
assert.equal(versions.find((row) => row.version === 1).active, false);
|
assert.equal(versions.find((row) => row.version === 1).active, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('strategy config update can clear request amount and slippage caps', async () => {
|
||||||
|
const pool = createMemoryPool();
|
||||||
|
await seedTradingConfig(pool);
|
||||||
|
const pairId = `${CURRENT_EURE_ASSET_ID}->${CURRENT_NBTC_ASSET_ID}`;
|
||||||
|
|
||||||
|
await createPairStrategyConfigVersion(pool, {
|
||||||
|
pairId,
|
||||||
|
edgeBps: 49,
|
||||||
|
maxNotional: '200',
|
||||||
|
requestMaxNotional: '5',
|
||||||
|
requestMaxSlippageBps: 200,
|
||||||
|
changedBy: 'test',
|
||||||
|
reason: 'operator capped request limits',
|
||||||
|
});
|
||||||
|
const next = await createPairStrategyConfigVersion(pool, {
|
||||||
|
pairId,
|
||||||
|
edgeBps: 49,
|
||||||
|
maxNotional: '200',
|
||||||
|
requestMaxNotional: null,
|
||||||
|
requestMaxSlippageBps: null,
|
||||||
|
changedBy: 'test',
|
||||||
|
reason: 'operator uncapped request limits',
|
||||||
|
});
|
||||||
|
const snapshot = await loadTradingConfig(pool);
|
||||||
|
const pair = snapshot.pairByKey.get(pairId);
|
||||||
|
|
||||||
|
assert.equal(next.requestMaxNotional, null);
|
||||||
|
assert.equal(next.requestMaxSlippageBps, null);
|
||||||
|
assert.equal(pair.strategyConfig.requestMaxNotional, null);
|
||||||
|
assert.equal(pair.strategyConfig.requestMaxSlippageBps, null);
|
||||||
|
});
|
||||||
|
|
||||||
test('strategy config update rejects invalid max notional', async () => {
|
test('strategy config update rejects invalid max notional', async () => {
|
||||||
const pool = createMemoryPool();
|
const pool = createMemoryPool();
|
||||||
await seedTradingConfig(pool);
|
await seedTradingConfig(pool);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue