Proof: npm test; npm run operator-dashboard:build; focused regressions cover maker/taker activation creating config, invalid initial edge rejection, and invalid max-notional rejection. Assumptions: Adding maker/taker/both mode should create an initial pair strategy config with operator-provided edge and max notional, but pairs without a real price route must remain blocked. Still fake: No BTC/USDC external reference price route or liquidity model exists; non-nBTC/EURe pairs still fail closed on missing price route until that path is built.
This commit is contained in:
parent
acdc3c061e
commit
a0e7a698a1
6 changed files with 322 additions and 18 deletions
|
|
@ -625,8 +625,9 @@ async function invokeControl(control, body) {
|
|||
const result = await createPairStrategyConfigVersion(pool, {
|
||||
pairId: body.pair_id || body.pair,
|
||||
edgeBps: Number(body.edge_bps),
|
||||
maxNotional: body.max_notional,
|
||||
changedBy: body.changed_by || 'operator',
|
||||
reason: body.reason || 'dashboard edge update',
|
||||
reason: body.reason || 'dashboard pair strategy config update',
|
||||
});
|
||||
await tradingConfigStore.forceRefresh();
|
||||
return result;
|
||||
|
|
@ -649,6 +650,8 @@ async function invokeControl(control, body) {
|
|||
assetIn: body.asset_in,
|
||||
assetOut: body.asset_out,
|
||||
mode: body.mode,
|
||||
edgeBps: body.edge_bps,
|
||||
maxNotional: body.max_notional,
|
||||
changedBy: body.changed_by || 'operator',
|
||||
reason: body.reason || 'dashboard pair mode update',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -737,13 +737,14 @@ export async function createPairStrategyConfigVersion(pool, {
|
|||
if (!Number.isInteger(nextEdgeBps) || nextEdgeBps <= 0) {
|
||||
throw new Error('edge_bps must be a positive integer');
|
||||
}
|
||||
const nextMaxNotional = positiveNumberStringOrDefault(maxNotional, active.max_notional, 'max_notional');
|
||||
|
||||
const nextConfig = {
|
||||
configId: `${resolvedPairId}:v${nextVersion}`,
|
||||
pairId: resolvedPairId,
|
||||
version: nextVersion,
|
||||
edgeBps: nextEdgeBps,
|
||||
maxNotional: maxNotional == null ? String(active.max_notional) : String(maxNotional),
|
||||
maxNotional: nextMaxNotional,
|
||||
minNotional: minNotional == null ? String(active.min_notional) : String(minNotional),
|
||||
slippageBps: slippageBps == null ? Number(active.slippage_bps) : Number(slippageBps),
|
||||
minDeadlineMs: minDeadlineMs == null ? Number(active.min_deadline_ms) : Number(minDeadlineMs),
|
||||
|
|
@ -855,6 +856,16 @@ export async function setTradingPairMode(pool, {
|
|||
assetIn = null,
|
||||
assetOut = null,
|
||||
mode = 'observe_only',
|
||||
edgeBps = null,
|
||||
maxNotional = null,
|
||||
minNotional = null,
|
||||
slippageBps = null,
|
||||
minDeadlineMs = null,
|
||||
priceMaxAgeMs = null,
|
||||
inventoryMaxAgeMs = null,
|
||||
requestDefaultNotional = null,
|
||||
requestMaxNotional = null,
|
||||
requestMaxSlippageBps = null,
|
||||
changedBy = 'operator',
|
||||
reason = 'operator pair mode update',
|
||||
} = {}) {
|
||||
|
|
@ -893,6 +904,69 @@ export async function setTradingPairMode(pool, {
|
|||
status: normalizedMode,
|
||||
};
|
||||
await upsertSeedPair(client, { pair: nextPair, now: new Date().toISOString() });
|
||||
|
||||
let strategyConfig = null;
|
||||
if (pairCanMake(nextPair) || pairCanTake(nextPair)) {
|
||||
const activeConfigResult = await client.query(
|
||||
`
|
||||
SELECT *
|
||||
FROM ${PAIR_STRATEGY_CONFIGS_TABLE}
|
||||
WHERE pair_id = $1 AND active = true
|
||||
ORDER BY version DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
[resolvedPairId],
|
||||
);
|
||||
strategyConfig = activeConfigResult.rows[0]
|
||||
? normalizeStrategyConfigRow(activeConfigResult.rows[0])
|
||||
: null;
|
||||
|
||||
if (!strategyConfig) {
|
||||
const nextConfig = buildInitialPairStrategyConfig(resolvedPairId, {
|
||||
edgeBps,
|
||||
maxNotional,
|
||||
minNotional,
|
||||
slippageBps,
|
||||
minDeadlineMs,
|
||||
priceMaxAgeMs,
|
||||
inventoryMaxAgeMs,
|
||||
requestDefaultNotional,
|
||||
requestMaxNotional,
|
||||
requestMaxSlippageBps,
|
||||
changedBy,
|
||||
reason,
|
||||
});
|
||||
await insertPairStrategyConfig(client, { config: nextConfig, active: true });
|
||||
strategyConfig = normalizeStrategyConfigRow({
|
||||
config_id: nextConfig.configId,
|
||||
pair_id: nextConfig.pairId,
|
||||
version: nextConfig.version,
|
||||
active: true,
|
||||
edge_bps: nextConfig.edgeBps,
|
||||
max_notional: nextConfig.maxNotional,
|
||||
min_notional: nextConfig.minNotional,
|
||||
slippage_bps: nextConfig.slippageBps,
|
||||
min_deadline_ms: nextConfig.minDeadlineMs,
|
||||
price_max_age_ms: nextConfig.priceMaxAgeMs,
|
||||
inventory_max_age_ms: nextConfig.inventoryMaxAgeMs,
|
||||
request_default_notional: nextConfig.requestDefaultNotional,
|
||||
request_max_notional: nextConfig.requestMaxNotional,
|
||||
request_max_slippage_bps: nextConfig.requestMaxSlippageBps,
|
||||
created_by: changedBy,
|
||||
reason,
|
||||
});
|
||||
await insertConfigAuditLog(client, {
|
||||
entityType: 'pair_strategy_config',
|
||||
entityId: resolvedPairId,
|
||||
action: 'initial_version_created',
|
||||
oldValue: null,
|
||||
newValue: strategyConfig,
|
||||
changedBy,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await insertConfigAuditLog(client, {
|
||||
entityType: 'trading_pair',
|
||||
entityId: resolvedPairId,
|
||||
|
|
@ -902,7 +976,10 @@ export async function setTradingPairMode(pool, {
|
|||
changedBy,
|
||||
reason,
|
||||
});
|
||||
return nextPair;
|
||||
return {
|
||||
...nextPair,
|
||||
strategyConfig,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1227,6 +1304,98 @@ function splitPairId(pairId) {
|
|||
return parts;
|
||||
}
|
||||
|
||||
function buildInitialPairStrategyConfig(pairId, {
|
||||
edgeBps = null,
|
||||
maxNotional = null,
|
||||
minNotional = null,
|
||||
slippageBps = null,
|
||||
minDeadlineMs = null,
|
||||
priceMaxAgeMs = null,
|
||||
inventoryMaxAgeMs = null,
|
||||
requestDefaultNotional = null,
|
||||
requestMaxNotional = null,
|
||||
requestMaxSlippageBps = null,
|
||||
changedBy = 'operator',
|
||||
reason = 'operator pair strategy config initialization',
|
||||
} = {}) {
|
||||
const baseConfig = buildSeedStrategyConfig(pairId, {
|
||||
createdBy: changedBy,
|
||||
reason,
|
||||
});
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
edgeBps: positiveIntegerOrDefault(edgeBps, baseConfig.edgeBps, 'edge_bps'),
|
||||
maxNotional: positiveNumberStringOrDefault(maxNotional, baseConfig.maxNotional, 'max_notional'),
|
||||
minNotional: nonNegativeNumberStringOrDefault(minNotional, baseConfig.minNotional, 'min_notional'),
|
||||
slippageBps: nonNegativeIntegerOrDefault(slippageBps, baseConfig.slippageBps, 'slippage_bps'),
|
||||
minDeadlineMs: positiveIntegerOrDefault(minDeadlineMs, baseConfig.minDeadlineMs, 'min_deadline_ms'),
|
||||
priceMaxAgeMs: positiveIntegerOrDefault(priceMaxAgeMs, baseConfig.priceMaxAgeMs, 'price_max_age_ms'),
|
||||
inventoryMaxAgeMs:
|
||||
positiveIntegerOrDefault(inventoryMaxAgeMs, baseConfig.inventoryMaxAgeMs, 'inventory_max_age_ms'),
|
||||
requestDefaultNotional:
|
||||
nullablePositiveNumberStringOrDefault(
|
||||
requestDefaultNotional,
|
||||
baseConfig.requestDefaultNotional,
|
||||
'request_default_notional',
|
||||
),
|
||||
requestMaxNotional:
|
||||
nullablePositiveNumberStringOrDefault(
|
||||
requestMaxNotional,
|
||||
baseConfig.requestMaxNotional,
|
||||
'request_max_notional',
|
||||
),
|
||||
requestMaxSlippageBps:
|
||||
nullableNonNegativeIntegerOrDefault(
|
||||
requestMaxSlippageBps,
|
||||
baseConfig.requestMaxSlippageBps,
|
||||
'request_max_slippage_bps',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function hasConfigOverride(value) {
|
||||
return value != null && String(value).trim() !== '';
|
||||
}
|
||||
|
||||
function positiveIntegerOrDefault(value, fallback, field) {
|
||||
if (!hasConfigOverride(value)) return Number(fallback);
|
||||
const next = Number(value);
|
||||
if (!Number.isInteger(next) || next <= 0) throw new Error(`${field} must be a positive integer`);
|
||||
return next;
|
||||
}
|
||||
|
||||
function nonNegativeIntegerOrDefault(value, fallback, field) {
|
||||
if (!hasConfigOverride(value)) return Number(fallback);
|
||||
const next = Number(value);
|
||||
if (!Number.isInteger(next) || next < 0) throw new Error(`${field} must be a non-negative integer`);
|
||||
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) {
|
||||
if (!hasConfigOverride(value)) return String(fallback);
|
||||
const next = String(value).trim();
|
||||
if (!(Number(next) > 0)) throw new Error(`${field} must be greater than zero`);
|
||||
return next;
|
||||
}
|
||||
|
||||
function nonNegativeNumberStringOrDefault(value, fallback, field) {
|
||||
if (!hasConfigOverride(value)) return String(fallback);
|
||||
const next = String(value).trim();
|
||||
if (!(Number(next) >= 0)) throw new Error(`${field} must be zero or greater`);
|
||||
return next;
|
||||
}
|
||||
|
||||
function nullablePositiveNumberStringOrDefault(value, fallback, field) {
|
||||
if (!hasConfigOverride(value)) return fallback == null ? null : positiveNumberStringOrDefault(fallback, '1', field);
|
||||
return positiveNumberStringOrDefault(value, '1', field);
|
||||
}
|
||||
|
||||
function normalizeStrategyConfigRow(row) {
|
||||
if (!row) return null;
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -402,8 +402,11 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
|||
asset_in: '',
|
||||
asset_out: '',
|
||||
mode: 'observe_only',
|
||||
edge_bps: '49',
|
||||
max_notional: '150',
|
||||
});
|
||||
const [edgeDrafts, setEdgeDrafts] = useState({});
|
||||
const [maxNotionalDrafts, setMaxNotionalDrafts] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!assets.length) return;
|
||||
|
|
@ -418,17 +421,46 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
|||
setEdgeDrafts(Object.fromEntries(pairs.map((pair) => {
|
||||
const pairId = pair.pair_id || pair.pairId;
|
||||
const strategyConfig = pair.strategyConfig || pair.strategy_config || {};
|
||||
return [pairId, String(strategyConfig.edge_bps ?? pair.edge_bps ?? '')];
|
||||
const tradingMode = TRADING_PAIR_MODES.has(pair.mode);
|
||||
return [pairId, String(strategyConfig.edge_bps ?? pair.edge_bps ?? (tradingMode ? '49' : ''))];
|
||||
})));
|
||||
setMaxNotionalDrafts(Object.fromEntries(pairs.map((pair) => {
|
||||
const pairId = pair.pair_id || pair.pairId;
|
||||
const strategyConfig = pair.strategyConfig || pair.strategy_config || {};
|
||||
const tradingMode = TRADING_PAIR_MODES.has(pair.mode);
|
||||
return [pairId, String(strategyConfig.max_notional ?? pair.max_notional ?? (tradingMode ? '150' : ''))];
|
||||
})));
|
||||
}, [pairs]);
|
||||
|
||||
async function updateEdge(pair) {
|
||||
async function updatePairConfig(pair) {
|
||||
const pairId = pair.pair_id || pair.pairId;
|
||||
const next = edgeDrafts[pairId];
|
||||
if (!next) return;
|
||||
const strategyConfig = pair.strategyConfig || pair.strategy_config || {};
|
||||
const hasStrategyConfig = Boolean(strategyConfig.config_id || strategyConfig.configId);
|
||||
const edgeBps = edgeDrafts[pairId];
|
||||
const maxNotional = maxNotionalDrafts[pairId];
|
||||
if (!edgeBps || !maxNotional) return;
|
||||
|
||||
if (!hasStrategyConfig) {
|
||||
const mode = pair.mode || pair.status || 'observe_only';
|
||||
if (
|
||||
TRADING_PAIR_MODES.has(mode)
|
||||
&& !window.confirm('Initialize strategy config for this trading pair?')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await onControl?.('operator-dashboard', 'set-pair-mode', {
|
||||
pair_id: pairId,
|
||||
mode,
|
||||
edge_bps: Number(edgeBps),
|
||||
max_notional: maxNotional,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await onControl?.('operator-dashboard', 'update-pair-edge', {
|
||||
pair_id: pairId,
|
||||
edge_bps: Number(next),
|
||||
edge_bps: Number(edgeBps),
|
||||
max_notional: maxNotional,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -445,6 +477,8 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
|||
asset_in: pairForm.asset_in,
|
||||
asset_out: pairForm.asset_out,
|
||||
mode: pairForm.mode,
|
||||
edge_bps: Number(pairForm.edge_bps),
|
||||
max_notional: pairForm.max_notional,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -469,6 +503,11 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
|||
});
|
||||
}
|
||||
|
||||
const tradingModeSelected = TRADING_PAIR_MODES.has(pairForm.mode);
|
||||
const pairFormDisabled = !assets.length
|
||||
|| pairForm.asset_in === pairForm.asset_out
|
||||
|| (tradingModeSelected && (!pairForm.edge_bps || !pairForm.max_notional));
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<div className="panel-head">
|
||||
|
|
@ -524,9 +563,35 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
|||
<option value="both">Maker and taker</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="pair-edge-bps">Edge bps</label>
|
||||
<input
|
||||
disabled={!tradingModeSelected}
|
||||
id="pair-edge-bps"
|
||||
min="1"
|
||||
onChange={(event) => setPairForm((current) => ({ ...current, edge_bps: event.target.value }))}
|
||||
required={tradingModeSelected}
|
||||
step="1"
|
||||
type="number"
|
||||
value={pairForm.edge_bps}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="pair-max-notional">Max notional</label>
|
||||
<input
|
||||
disabled={!tradingModeSelected}
|
||||
id="pair-max-notional"
|
||||
min="0.00000001"
|
||||
onChange={(event) => setPairForm((current) => ({ ...current, max_notional: event.target.value }))}
|
||||
required={tradingModeSelected}
|
||||
step="0.00000001"
|
||||
type="number"
|
||||
value={pairForm.max_notional}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<button className="button" disabled={!assets.length || pairForm.asset_in === pairForm.asset_out} type="submit">
|
||||
<button className="button" disabled={pairFormDisabled} type="submit">
|
||||
Add / activate pair
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -547,13 +612,19 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
|||
</thead>
|
||||
<tbody>
|
||||
{pairs.length ? pairs.map((pair) => {
|
||||
const pairId = pair.pair_id || pair.pairId;
|
||||
const strategyConfig = pair.strategyConfig || pair.strategy_config || {};
|
||||
const route = pair.priceRoute || pair.price_route || {};
|
||||
const hasStrategyConfig = Boolean(strategyConfig.config_id || strategyConfig.configId);
|
||||
const tradingMode = TRADING_PAIR_MODES.has(pair.mode);
|
||||
const configButtonDisabled = !edgeDrafts[pairId]
|
||||
|| !maxNotionalDrafts[pairId]
|
||||
|| (!hasStrategyConfig && !tradingMode);
|
||||
return (
|
||||
<tr key={pair.pair_id || pair.pairId}>
|
||||
<tr key={pairId}>
|
||||
<td>
|
||||
<div>{pair.asset_in_symbol || pair.asset_in} {'->'} {pair.asset_out_symbol || pair.asset_out}</div>
|
||||
<div className="status-subtle mono">{truncateMiddle(pair.pair_id || pair.pairId, 42)}</div>
|
||||
<div className="status-subtle mono">{truncateMiddle(pairId, 42)}</div>
|
||||
</td>
|
||||
<td><Pill label={pair.mode || pair.status} stateLabel={pair.canTrade || pair.can_trade ? 'healthy' : 'warning'} /></td>
|
||||
<td>{strategyConfig.edge_bps ?? 'Unavailable'} bps</td>
|
||||
|
|
@ -566,25 +637,43 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
|||
<td>
|
||||
<div>v{strategyConfig.version || 'Unavailable'}</div>
|
||||
<div className="trace-row">
|
||||
<span className="status-subtle">Edge</span>
|
||||
<input
|
||||
aria-label={`Edge bps for ${pair.pair_id || pair.pairId}`}
|
||||
aria-label={`Edge bps for ${pairId}`}
|
||||
min="1"
|
||||
onChange={(event) => setEdgeDrafts((current) => ({
|
||||
...current,
|
||||
[pair.pair_id || pair.pairId]: event.target.value,
|
||||
[pairId]: event.target.value,
|
||||
}))}
|
||||
step="1"
|
||||
style={{ maxWidth: 92 }}
|
||||
type="number"
|
||||
value={edgeDrafts[pair.pair_id || pair.pairId] ?? ''}
|
||||
value={edgeDrafts[pairId] ?? ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="trace-row">
|
||||
<span className="status-subtle">Max</span>
|
||||
<input
|
||||
aria-label={`Max notional for ${pairId}`}
|
||||
min="0.00000001"
|
||||
onChange={(event) => setMaxNotionalDrafts((current) => ({
|
||||
...current,
|
||||
[pairId]: event.target.value,
|
||||
}))}
|
||||
step="0.00000001"
|
||||
style={{ maxWidth: 112 }}
|
||||
type="number"
|
||||
value={maxNotionalDrafts[pairId] ?? ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="trace-row">
|
||||
<button
|
||||
className="button secondary trace-copy-button"
|
||||
disabled={!strategyConfig.config_id && !strategyConfig.configId}
|
||||
onClick={() => updateEdge(pair)}
|
||||
disabled={configButtonDisabled}
|
||||
onClick={() => updatePairConfig(pair)}
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
{hasStrategyConfig ? 'Save' : 'Init'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,6 @@ test('operator dashboard exposes DB-backed pair activation and pause controls',
|
|||
assert.match(source, /pauseTradingPair/);
|
||||
assert.match(source, /control\.action === 'set-pair-mode'/);
|
||||
assert.match(source, /control\.action === 'pause-pair'/);
|
||||
assert.match(source, /edgeBps: body\.edge_bps/);
|
||||
assert.match(source, /maxNotional: body\.max_notional/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,7 +68,11 @@ test('strategy page exposes pair activation, pause, edge, and deposit address co
|
|||
assert.match(strategySource, /pause-pair/);
|
||||
assert.match(strategySource, /Add, pause, and tune directed pairs/);
|
||||
assert.match(strategySource, /Add \/ activate pair/);
|
||||
assert.match(strategySource, /pair-edge-bps/);
|
||||
assert.match(strategySource, /pair-max-notional/);
|
||||
assert.match(strategySource, /Edge bps for/);
|
||||
assert.match(strategySource, /Max notional for/);
|
||||
assert.match(strategySource, /Init/);
|
||||
assert.match(strategySource, /deposit_address/);
|
||||
assert.match(strategySource, /Copy/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ test('edge update creates a new active strategy version', async () => {
|
|||
const next = await createPairStrategyConfigVersion(pool, {
|
||||
pairId: `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`,
|
||||
edgeBps: 75,
|
||||
maxNotional: '42',
|
||||
changedBy: 'test',
|
||||
reason: 'test edge update',
|
||||
});
|
||||
|
|
@ -132,9 +133,24 @@ test('edge update creates a new active strategy version', async () => {
|
|||
|
||||
assert.equal(next.version, 2);
|
||||
assert.equal(snapshot.pairByKey.get(`${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`).strategyConfig.edgeBps, 75);
|
||||
assert.equal(snapshot.pairByKey.get(`${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`).strategyConfig.maxNotional, '42');
|
||||
assert.equal(versions.find((row) => row.version === 1).active, false);
|
||||
});
|
||||
|
||||
test('strategy config update rejects invalid max notional', async () => {
|
||||
const pool = createMemoryPool();
|
||||
await seedTradingConfig(pool);
|
||||
|
||||
await assert.rejects(
|
||||
createPairStrategyConfigVersion(pool, {
|
||||
pairId: `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`,
|
||||
edgeBps: 75,
|
||||
maxNotional: '0',
|
||||
}),
|
||||
/max_notional must be greater than zero/,
|
||||
);
|
||||
});
|
||||
|
||||
test('observe-only enable does not downgrade an active trading pair', async () => {
|
||||
const pool = createMemoryPool();
|
||||
await seedTradingConfig(pool);
|
||||
|
|
@ -212,6 +228,8 @@ test('pair mode updates activate a directed pair without inventing a price route
|
|||
const updated = await setTradingPairMode(pool, {
|
||||
pairId,
|
||||
mode: 'maker',
|
||||
edgeBps: 75,
|
||||
maxNotional: '25',
|
||||
changedBy: 'test',
|
||||
reason: 'operator activation test',
|
||||
});
|
||||
|
|
@ -219,10 +237,29 @@ test('pair mode updates activate a directed pair without inventing a price route
|
|||
const pair = snapshot.pairByKey.get(pairId);
|
||||
|
||||
assert.equal(updated.mode, 'maker');
|
||||
assert.equal(updated.strategyConfig.edgeBps, 75);
|
||||
assert.equal(updated.strategyConfig.maxNotional, '25');
|
||||
assert.equal(pair.enabled, true);
|
||||
assert.equal(pair.makerEnabled, true);
|
||||
assert.equal(pair.strategyConfig.edgeBps, 75);
|
||||
assert.equal(pair.strategyConfig.maxNotional, '25');
|
||||
assert.equal(pair.canTrade, false);
|
||||
assert.equal(pair.blockReason, 'pair_strategy_config_missing');
|
||||
assert.equal(pair.blockReason, 'price_route_missing');
|
||||
});
|
||||
|
||||
test('pair mode activation rejects invalid initial edge config', async () => {
|
||||
const pool = createMemoryPool();
|
||||
await seedTradingConfig(pool);
|
||||
|
||||
await assert.rejects(
|
||||
setTradingPairMode(pool, {
|
||||
assetIn: LEGACY_OMFT_BTC_ASSET_ID,
|
||||
assetOut: CURRENT_EURE_ASSET_ID,
|
||||
mode: 'both',
|
||||
edgeBps: 0,
|
||||
}),
|
||||
/edge_bps must be a positive integer/,
|
||||
);
|
||||
});
|
||||
|
||||
test('pair pause disables trading without deleting strategy config or seed restoring it', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue