mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
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.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
export type SwingDirection = "long" | "short" | "both";
|
||||
|
||||
export interface SwingLogicConfig {
|
||||
direction: SwingDirection;
|
||||
rsiHigh: number; // e.g. 70
|
||||
rsiLow: number; // e.g. 30
|
||||
}
|
||||
|
||||
export interface SwingState {
|
||||
prevRsi: number | null;
|
||||
armedShortEntry: boolean;
|
||||
armedShortExit: boolean;
|
||||
armedLongEntry: boolean;
|
||||
armedLongExit: boolean;
|
||||
}
|
||||
|
||||
export type SwingAction =
|
||||
| { type: "OPEN_SHORT"; reason: string }
|
||||
| { type: "OPEN_LONG"; reason: string }
|
||||
| { type: "CLOSE_POSITION"; reason: string };
|
||||
|
||||
export interface SwingStepInput {
|
||||
rsi: number | null;
|
||||
positionAmt: number;
|
||||
pnl: number;
|
||||
}
|
||||
|
||||
export function createInitialSwingState(): SwingState {
|
||||
return {
|
||||
prevRsi: null,
|
||||
armedShortEntry: false,
|
||||
armedShortExit: false,
|
||||
armedLongEntry: false,
|
||||
armedLongExit: false,
|
||||
};
|
||||
}
|
||||
|
||||
const EPS = 1e-8;
|
||||
|
||||
function crossUp(prev: number | null, next: number, threshold: number): boolean {
|
||||
if (prev == null) return false;
|
||||
return prev <= threshold && next > threshold;
|
||||
}
|
||||
|
||||
function crossDown(prev: number | null, next: number, threshold: number): boolean {
|
||||
if (prev == null) return false;
|
||||
return prev >= threshold && next < threshold;
|
||||
}
|
||||
|
||||
export function stepSwing(
|
||||
state: SwingState,
|
||||
config: SwingLogicConfig,
|
||||
input: SwingStepInput
|
||||
): { nextState: SwingState; actions: SwingAction[] } {
|
||||
const nextState: SwingState = { ...state };
|
||||
const actions: SwingAction[] = [];
|
||||
|
||||
const rsi = input.rsi;
|
||||
const hasRsi = typeof rsi === "number" && Number.isFinite(rsi);
|
||||
const prevRsi = nextState.prevRsi;
|
||||
|
||||
const direction = config.direction;
|
||||
const allowLong = direction === "long" || direction === "both";
|
||||
const allowShort = direction === "short" || direction === "both";
|
||||
|
||||
const positionAmt = Number(input.positionAmt);
|
||||
const pnl = Number(input.pnl);
|
||||
const isFlat = !Number.isFinite(positionAmt) || Math.abs(positionAmt) <= EPS;
|
||||
const isLong = Number.isFinite(positionAmt) && positionAmt > EPS;
|
||||
const isShort = Number.isFinite(positionAmt) && positionAmt < -EPS;
|
||||
|
||||
// If RSI is missing/invalid, avoid mutating state.
|
||||
if (!hasRsi) {
|
||||
return { nextState, actions };
|
||||
}
|
||||
|
||||
// Keep prevRsi updated once we have a valid reading.
|
||||
nextState.prevRsi = rsi;
|
||||
|
||||
if (isFlat) {
|
||||
// When flat, exit arms are irrelevant.
|
||||
nextState.armedShortExit = false;
|
||||
nextState.armedLongExit = false;
|
||||
|
||||
if (!allowShort) {
|
||||
nextState.armedShortEntry = false;
|
||||
} else {
|
||||
if (crossUp(prevRsi, rsi, config.rsiHigh)) {
|
||||
nextState.armedShortEntry = true;
|
||||
}
|
||||
if (nextState.armedShortEntry && crossDown(prevRsi, rsi, config.rsiHigh)) {
|
||||
actions.push({ type: "OPEN_SHORT", reason: "RSI armed above high, then crossed below high" });
|
||||
nextState.armedShortEntry = false;
|
||||
// Avoid impossible dual-entries.
|
||||
nextState.armedLongEntry = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allowLong) {
|
||||
nextState.armedLongEntry = false;
|
||||
} else {
|
||||
if (crossDown(prevRsi, rsi, config.rsiLow)) {
|
||||
nextState.armedLongEntry = true;
|
||||
}
|
||||
if (nextState.armedLongEntry && crossUp(prevRsi, rsi, config.rsiLow)) {
|
||||
actions.push({ type: "OPEN_LONG", reason: "RSI armed below low, then crossed above low" });
|
||||
nextState.armedLongEntry = false;
|
||||
nextState.armedShortEntry = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length > 1) {
|
||||
// Defensive: avoid opening both directions in one step.
|
||||
return { nextState: { ...nextState, armedShortEntry: false, armedLongEntry: false }, actions: [] };
|
||||
}
|
||||
|
||||
return { nextState, actions };
|
||||
}
|
||||
|
||||
// When exposed, entry arms are irrelevant (strategy does not pyramid).
|
||||
nextState.armedShortEntry = false;
|
||||
nextState.armedLongEntry = false;
|
||||
|
||||
// Exits are always allowed (even if direction config changes) to avoid trapping positions.
|
||||
if (isShort) {
|
||||
nextState.armedLongExit = false;
|
||||
if (crossDown(prevRsi, rsi, config.rsiLow)) {
|
||||
nextState.armedShortExit = true;
|
||||
}
|
||||
if (nextState.armedShortExit && crossUp(prevRsi, rsi, config.rsiLow) && pnl > 0) {
|
||||
actions.push({ type: "CLOSE_POSITION", reason: "RSI exit armed below low, then crossed above low with profit" });
|
||||
nextState.armedShortExit = false;
|
||||
}
|
||||
return { nextState, actions };
|
||||
}
|
||||
|
||||
if (isLong) {
|
||||
nextState.armedShortExit = false;
|
||||
if (crossUp(prevRsi, rsi, config.rsiHigh)) {
|
||||
nextState.armedLongExit = true;
|
||||
}
|
||||
if (nextState.armedLongExit && crossDown(prevRsi, rsi, config.rsiHigh) && pnl > 0) {
|
||||
actions.push({ type: "CLOSE_POSITION", reason: "RSI exit armed above high, then crossed below high with profit" });
|
||||
nextState.armedLongExit = false;
|
||||
}
|
||||
return { nextState, actions };
|
||||
}
|
||||
|
||||
return { nextState, actions };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user