Files
ritmex-bot/src/strategy/swing-logic.test.ts
T
discountry c4559cb0d7 Add swing trading strategy with RSI signals and Binance integration
- Introduced a new swing trading strategy utilizing the RSI indicator on the ETHBTC pair from Binance.
- Implemented the `SwingEngine` to manage trading logic, including entry and exit conditions based on RSI thresholds.
- Added configuration options for swing direction, trade amount, and RSI parameters in `config.ts`.
- Created new documentation for the swing strategy, detailing its behavior and configuration.
- Enhanced CLI to support the new swing strategy option.
- Added tests for swing logic to ensure correct behavior under various market conditions.
2026-01-31 16:15:07 +08:00

94 lines
4.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createInitialSwingState, stepSwing, type SwingLogicConfig } from "./swing-logic";
const baseConfig: SwingLogicConfig = {
direction: "both",
rsiHigh: 70,
rsiLow: 30,
};
describe("swing logic", () => {
it("arms short entry on RSI cross up 70, then opens on cross down 70", () => {
let state = createInitialSwingState();
// First observation sets prevRsi, no cross yet.
({ nextState: state } = stepSwing(state, { ...baseConfig, direction: "short" }, { rsi: 69, positionAmt: 0, pnl: 0 }));
const a1 = stepSwing(state, { ...baseConfig, direction: "short" }, { rsi: 71, positionAmt: 0, pnl: 0 });
expect(a1.actions).toEqual([]);
expect(a1.nextState.armedShortEntry).toBe(true);
state = a1.nextState;
const a2 = stepSwing(state, { ...baseConfig, direction: "short" }, { rsi: 69, positionAmt: 0, pnl: 0 });
expect(a2.actions.map((a) => a.type)).toEqual(["OPEN_SHORT"]);
expect(a2.nextState.armedShortEntry).toBe(false);
});
it("arms long entry on RSI cross down 30, then opens on cross up 30", () => {
let state = createInitialSwingState();
({ nextState: state } = stepSwing(state, { ...baseConfig, direction: "long" }, { rsi: 31, positionAmt: 0, pnl: 0 }));
const a1 = stepSwing(state, { ...baseConfig, direction: "long" }, { rsi: 29, positionAmt: 0, pnl: 0 });
expect(a1.actions).toEqual([]);
expect(a1.nextState.armedLongEntry).toBe(true);
state = a1.nextState;
const a2 = stepSwing(state, { ...baseConfig, direction: "long" }, { rsi: 31, positionAmt: 0, pnl: 0 });
expect(a2.actions.map((a) => a.type)).toEqual(["OPEN_LONG"]);
expect(a2.nextState.armedLongEntry).toBe(false);
});
it("short exit requires profit: arms on cross down 30, closes on cross up 30 if pnl > 0", () => {
let state = createInitialSwingState();
({ nextState: state } = stepSwing(state, baseConfig, { rsi: 31, positionAmt: -1, pnl: -1 }));
const a1 = stepSwing(state, baseConfig, { rsi: 29, positionAmt: -1, pnl: -1 });
expect(a1.actions).toEqual([]);
expect(a1.nextState.armedShortExit).toBe(true);
state = a1.nextState;
const a2 = stepSwing(state, baseConfig, { rsi: 31, positionAmt: -1, pnl: 0 });
expect(a2.actions).toEqual([]); // pnl not strictly positive
expect(a2.nextState.armedShortExit).toBe(true);
state = a2.nextState;
const a3 = stepSwing(state, baseConfig, { rsi: 31, positionAmt: -1, pnl: 0.01 });
// No cross (prev=31 -> 31), still armed.
expect(a3.actions).toEqual([]);
// Cross down then up to trigger close with profit
const a4 = stepSwing(a3.nextState, baseConfig, { rsi: 29, positionAmt: -1, pnl: 0.01 });
const a5 = stepSwing(a4.nextState, baseConfig, { rsi: 31, positionAmt: -1, pnl: 0.01 });
expect(a5.actions.map((a) => a.type)).toEqual(["CLOSE_POSITION"]);
expect(a5.nextState.armedShortExit).toBe(false);
});
it("long exit requires profit: arms on cross up 70, closes on cross down 70 if pnl > 0", () => {
let state = createInitialSwingState();
({ nextState: state } = stepSwing(state, baseConfig, { rsi: 69, positionAmt: 1, pnl: 0 }));
const a1 = stepSwing(state, baseConfig, { rsi: 71, positionAmt: 1, pnl: 0 });
expect(a1.actions).toEqual([]);
expect(a1.nextState.armedLongExit).toBe(true);
state = a1.nextState;
const a2 = stepSwing(state, baseConfig, { rsi: 69, positionAmt: 1, pnl: 0.01 });
expect(a2.actions.map((a) => a.type)).toEqual(["CLOSE_POSITION"]);
expect(a2.nextState.armedLongExit).toBe(false);
});
it("clears entry arms when a position is present", () => {
let state = createInitialSwingState();
({ nextState: state } = stepSwing(state, baseConfig, { rsi: 69, positionAmt: 0, pnl: 0 }));
// Arm short entry.
state = stepSwing(state, baseConfig, { rsi: 71, positionAmt: 0, pnl: 0 }).nextState;
expect(state.armedShortEntry).toBe(true);
// Now position appears: entry arms should be reset.
const next = stepSwing(state, baseConfig, { rsi: 71, positionAmt: -1, pnl: 0 });
expect(next.nextState.armedShortEntry).toBe(false);
expect(next.nextState.armedLongEntry).toBe(false);
});
});