@vulcan-js/strategies 0.0.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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +157 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present enpitsulin <enpitsulin@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @vulcan-js/strategies
|
|
2
|
+
|
|
3
|
+
Composable trading strategies for the [Vulcan](../../README.md) library. Combines multiple indicators into structured signal output with position management.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @vulcan-js/strategies
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Built-in strategies
|
|
14
|
+
|
|
15
|
+
Every strategy is a generator function (just like indicators). Pass OHLCV bars and iterate over signals:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { collect } from '@vulcan-js/core'
|
|
19
|
+
import { goldenCross } from '@vulcan-js/strategies'
|
|
20
|
+
|
|
21
|
+
const bars = [
|
|
22
|
+
{ o: 10, h: 12, l: 9, c: 11, v: 1000 },
|
|
23
|
+
{ o: 11, h: 13, l: 10, c: 12, v: 1200 },
|
|
24
|
+
// ...
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
// Collect all signals
|
|
28
|
+
const signals = collect(goldenCross(bars, { fastPeriod: 10, slowPeriod: 30 }))
|
|
29
|
+
|
|
30
|
+
// Or iterate lazily
|
|
31
|
+
for (const signal of goldenCross(bars)) {
|
|
32
|
+
console.log(signal.action) // 'long' | 'short' | 'close' | 'hold'
|
|
33
|
+
console.log(signal.reason) // human-readable explanation
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use `.create()` for real-time / streaming scenarios:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { goldenCross } from '@vulcan-js/strategies'
|
|
41
|
+
|
|
42
|
+
const process = goldenCross.create({ fastPeriod: 10, slowPeriod: 30 })
|
|
43
|
+
|
|
44
|
+
// Feed bars one by one
|
|
45
|
+
const signal = process({ o: 10, h: 12, l: 9, c: 11, v: 1000 })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Custom strategies
|
|
49
|
+
|
|
50
|
+
Use `createStrategy` to build your own strategy. It mirrors `createSignal` from core, but adds a rolling window of historical bars and structured signal output:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { ema } from '@vulcan-js/indicators'
|
|
54
|
+
import { createStrategy } from '@vulcan-js/strategies'
|
|
55
|
+
|
|
56
|
+
const myStrategy = createStrategy(
|
|
57
|
+
({ emaPeriod, threshold }) => {
|
|
58
|
+
const emaProc = ema.create({ period: emaPeriod })
|
|
59
|
+
|
|
60
|
+
return (ctx) => {
|
|
61
|
+
const price = ctx.bar.c
|
|
62
|
+
const emaValue = emaProc(price)
|
|
63
|
+
|
|
64
|
+
// Access historical bars via ctx.bars (oldest first)
|
|
65
|
+
// Access current bar index via ctx.index
|
|
66
|
+
|
|
67
|
+
return { action: 'hold' }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{ windowSize: 10, emaPeriod: 20, threshold: 0.05 },
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
### `createStrategy(factory, defaultOptions)`
|
|
77
|
+
|
|
78
|
+
Creates a generator-based strategy from a factory function.
|
|
79
|
+
|
|
80
|
+
Returns a `StrategyGenerator` with:
|
|
81
|
+
- **Generator iteration** — `strategy(source, options?)` yields `StrategySignal` for each bar
|
|
82
|
+
- **`.create(options?)`** — returns a stateful `Processor<CandleData, StrategySignal>` for point-by-point feeding
|
|
83
|
+
- **`.defaultOptions`** — the default options for the strategy
|
|
84
|
+
|
|
85
|
+
The factory receives resolved options and returns a function `(ctx: StrategyContext) => StrategySignal`. The `StrategyContext` provides:
|
|
86
|
+
|
|
87
|
+
| Property | Type | Description |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `bar` | `CandleData` | The current OHLCV bar |
|
|
90
|
+
| `bars` | `readonly CandleData[]` | Rolling window of historical bars (oldest first, includes current bar) |
|
|
91
|
+
| `index` | `number` | Zero-based index of the current bar since the strategy started |
|
|
92
|
+
|
|
93
|
+
### `StrategySignal`
|
|
94
|
+
|
|
95
|
+
| Property | Type | Description |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `action` | `'long' \| 'short' \| 'close' \| 'hold'` | The recommended action |
|
|
98
|
+
| `size?` | `number` | Position size as a fraction (0–1) |
|
|
99
|
+
| `stopLoss?` | `number` | Stop-loss price level |
|
|
100
|
+
| `takeProfit?` | `number` | Take-profit price level |
|
|
101
|
+
| `reason?` | `string` | Human-readable reason for the signal |
|
|
102
|
+
|
|
103
|
+
### `BaseStrategyOptions`
|
|
104
|
+
|
|
105
|
+
All strategy options must extend `BaseStrategyOptions`:
|
|
106
|
+
|
|
107
|
+
| Property | Type | Description |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| `windowSize` | `number` | Number of historical bars to keep in the rolling window |
|
|
110
|
+
|
|
111
|
+
## Built-in Strategies
|
|
112
|
+
|
|
113
|
+
| Strategy | Function | Alias | Description |
|
|
114
|
+
| --- | --- | --- | --- |
|
|
115
|
+
| Golden Cross / Death Cross | `goldenCross` | `goldenCrossStrategy` | Detects fast SMA crossing above/below slow SMA |
|
|
116
|
+
| RSI Oversold/Overbought | `rsiOversoldOverbought` | `rsiOversoldOverboughtStrategy` | Detects RSI crossing oversold/overbought levels |
|
|
117
|
+
|
|
118
|
+
### Golden Cross
|
|
119
|
+
|
|
120
|
+
Detects when a fast SMA crosses above (golden cross) or below (death cross) a slow SMA. Includes stop-loss based on a configurable percentage.
|
|
121
|
+
|
|
122
|
+
| Option | Type | Default | Description |
|
|
123
|
+
| --- | --- | --- | --- |
|
|
124
|
+
| `fastPeriod` | `number` | `50` | Fast SMA period |
|
|
125
|
+
| `slowPeriod` | `number` | `200` | Slow SMA period |
|
|
126
|
+
| `stopLossPercent` | `number` | `0.02` | Stop-loss percentage (0–1) |
|
|
127
|
+
| `windowSize` | `number` | `2` | Rolling window size |
|
|
128
|
+
|
|
129
|
+
### RSI Oversold/Overbought
|
|
130
|
+
|
|
131
|
+
Uses the Relative Strength Index to detect oversold and overbought reversal conditions.
|
|
132
|
+
|
|
133
|
+
| Option | Type | Default | Description |
|
|
134
|
+
| --- | --- | --- | --- |
|
|
135
|
+
| `period` | `number` | `14` | RSI calculation period |
|
|
136
|
+
| `overboughtLevel` | `number` | `70` | RSI level considered overbought |
|
|
137
|
+
| `oversoldLevel` | `number` | `30` | RSI level considered oversold |
|
|
138
|
+
| `windowSize` | `number` | `2` | Rolling window size |
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { CandleData, SignalGenerator } from "@vulcan-js/core";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The action a strategy recommends.
|
|
6
|
+
*/
|
|
7
|
+
type StrategyAction = 'long' | 'short' | 'close' | 'hold';
|
|
8
|
+
/**
|
|
9
|
+
* Structured signal output from a strategy.
|
|
10
|
+
*/
|
|
11
|
+
interface StrategySignal {
|
|
12
|
+
/** The recommended action */
|
|
13
|
+
action: StrategyAction;
|
|
14
|
+
/** Position size as a fraction (0–1), defaults to 1 */
|
|
15
|
+
size?: number;
|
|
16
|
+
/** Stop-loss price level */
|
|
17
|
+
stopLoss?: number;
|
|
18
|
+
/** Take-profit price level */
|
|
19
|
+
takeProfit?: number;
|
|
20
|
+
/** Human-readable reason for the signal */
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Base options that every strategy must include.
|
|
25
|
+
*/
|
|
26
|
+
interface BaseStrategyOptions {
|
|
27
|
+
/** Number of historical bars to keep in the rolling window */
|
|
28
|
+
windowSize: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Context passed to the strategy evaluation function on each bar.
|
|
32
|
+
*/
|
|
33
|
+
interface StrategyContext {
|
|
34
|
+
/** The current bar */
|
|
35
|
+
bar: CandleData;
|
|
36
|
+
/** Historical bars in the rolling window (oldest first, includes current bar) */
|
|
37
|
+
bars: readonly CandleData[];
|
|
38
|
+
/** Zero-based index of the current bar since the strategy started */
|
|
39
|
+
index: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A strategy generator — type alias for `SignalGenerator<CandleData, StrategySignal, Opts>`.
|
|
43
|
+
*/
|
|
44
|
+
type StrategyGenerator<Opts extends BaseStrategyOptions> = SignalGenerator<CandleData, StrategySignal, Opts>;
|
|
45
|
+
type StrategyFactory<Opts extends BaseStrategyOptions> = (opts: Required<Opts>) => (ctx: StrategyContext) => StrategySignal;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/base.d.ts
|
|
48
|
+
/**
|
|
49
|
+
* Create a generator-based strategy from a strategy factory.
|
|
50
|
+
*
|
|
51
|
+
* Mirrors `createSignal` from core, but manages a rolling window of bars
|
|
52
|
+
* and constructs a `StrategyContext` for each evaluation.
|
|
53
|
+
*
|
|
54
|
+
* The returned generator is fully compatible with `SignalGenerator<CandleData, StrategySignal, Opts>`.
|
|
55
|
+
*/
|
|
56
|
+
declare function createStrategy<Opts extends BaseStrategyOptions>(factory: StrategyFactory<Opts>, defaultOptions: Opts): StrategyGenerator<Opts>;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/builtin/goldenCross.d.ts
|
|
59
|
+
interface GoldenCrossOptions extends BaseStrategyOptions {
|
|
60
|
+
/** Fast SMA period */
|
|
61
|
+
fastPeriod: number;
|
|
62
|
+
/** Slow SMA period */
|
|
63
|
+
slowPeriod: number;
|
|
64
|
+
/** Stop-loss percentage (0–1), e.g. 0.02 = 2% */
|
|
65
|
+
stopLossPercent: number;
|
|
66
|
+
}
|
|
67
|
+
declare const defaultGoldenCrossOptions: GoldenCrossOptions;
|
|
68
|
+
/**
|
|
69
|
+
* Golden Cross / Death Cross Strategy
|
|
70
|
+
*
|
|
71
|
+
* Detects when a fast SMA crosses above (golden cross → long) or
|
|
72
|
+
* below (death cross → short) a slow SMA.
|
|
73
|
+
*
|
|
74
|
+
* - **Golden Cross**: fast SMA crosses above slow SMA → `long`
|
|
75
|
+
* - **Death Cross**: fast SMA crosses below slow SMA → `short`
|
|
76
|
+
* - Otherwise → `hold`
|
|
77
|
+
*
|
|
78
|
+
* Stop-loss is set as a percentage below/above the entry price.
|
|
79
|
+
*/
|
|
80
|
+
declare const goldenCross: StrategyGenerator<GoldenCrossOptions>;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/builtin/rsiOversoldOverbought.d.ts
|
|
83
|
+
interface RsiOversoldOverboughtOptions extends BaseStrategyOptions {
|
|
84
|
+
/** RSI calculation period */
|
|
85
|
+
period: number;
|
|
86
|
+
/** RSI level above which the asset is considered overbought */
|
|
87
|
+
overboughtLevel: number;
|
|
88
|
+
/** RSI level below which the asset is considered oversold */
|
|
89
|
+
oversoldLevel: number;
|
|
90
|
+
}
|
|
91
|
+
declare const defaultRsiOversoldOverboughtOptions: RsiOversoldOverboughtOptions;
|
|
92
|
+
/**
|
|
93
|
+
* RSI Oversold/Overbought Strategy
|
|
94
|
+
*
|
|
95
|
+
* Uses the Relative Strength Index to detect oversold and overbought conditions.
|
|
96
|
+
*
|
|
97
|
+
* - RSI crosses below `oversoldLevel` then back above → `long` (oversold reversal)
|
|
98
|
+
* - RSI crosses above `overboughtLevel` then back below → `short` (overbought reversal)
|
|
99
|
+
* - Otherwise → `hold`
|
|
100
|
+
*/
|
|
101
|
+
declare const rsiOversoldOverbought: StrategyGenerator<RsiOversoldOverboughtOptions>;
|
|
102
|
+
//#endregion
|
|
103
|
+
export { type BaseStrategyOptions, GoldenCrossOptions, RsiOversoldOverboughtOptions, type StrategyAction, type StrategyContext, type StrategyFactory, type StrategyGenerator, type StrategySignal, createStrategy, defaultGoldenCrossOptions, defaultRsiOversoldOverboughtOptions, goldenCross, goldenCross as goldenCrossStrategy, rsiOversoldOverbought, rsiOversoldOverbought as rsiOversoldOverboughtStrategy };
|
|
104
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { assert } from "@vulcan-js/core";
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
import { rsi, sma } from "@vulcan-js/indicators";
|
|
4
|
+
import { from, gt, lt, toNumber } from "dnum";
|
|
5
|
+
|
|
6
|
+
//#region src/base.ts
|
|
7
|
+
/**
|
|
8
|
+
* A fixed-capacity ring buffer with O(1) push and O(n) snapshot.
|
|
9
|
+
*/
|
|
10
|
+
function createRingBuffer(capacity) {
|
|
11
|
+
const buf = Array.from({ length: capacity });
|
|
12
|
+
let head = 0;
|
|
13
|
+
let size = 0;
|
|
14
|
+
return {
|
|
15
|
+
push(item) {
|
|
16
|
+
buf[head] = item;
|
|
17
|
+
head = (head + 1) % capacity;
|
|
18
|
+
if (size < capacity) size++;
|
|
19
|
+
},
|
|
20
|
+
toArray() {
|
|
21
|
+
if (size < capacity) return buf.slice(0, size);
|
|
22
|
+
return [...buf.slice(head), ...buf.slice(0, head)];
|
|
23
|
+
},
|
|
24
|
+
get size() {
|
|
25
|
+
return size;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a generator-based strategy from a strategy factory.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors `createSignal` from core, but manages a rolling window of bars
|
|
33
|
+
* and constructs a `StrategyContext` for each evaluation.
|
|
34
|
+
*
|
|
35
|
+
* The returned generator is fully compatible with `SignalGenerator<CandleData, StrategySignal, Opts>`.
|
|
36
|
+
*/
|
|
37
|
+
function createStrategy(factory, defaultOptions) {
|
|
38
|
+
function buildProcessor(options) {
|
|
39
|
+
const opt = defu(options, defaultOptions);
|
|
40
|
+
assert(Number.isInteger(opt.windowSize) && opt.windowSize >= 1, /* @__PURE__ */ new RangeError(`Expected windowSize to be a positive integer, got ${opt.windowSize}`));
|
|
41
|
+
const ring = createRingBuffer(opt.windowSize);
|
|
42
|
+
const process = factory(opt);
|
|
43
|
+
let index = 0;
|
|
44
|
+
return (bar) => {
|
|
45
|
+
ring.push(bar);
|
|
46
|
+
const signal = process({
|
|
47
|
+
bar,
|
|
48
|
+
bars: ring.toArray(),
|
|
49
|
+
index
|
|
50
|
+
});
|
|
51
|
+
index++;
|
|
52
|
+
return signal;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function* generator(source, options) {
|
|
56
|
+
const process = buildProcessor(options);
|
|
57
|
+
for (const bar of source) yield process(bar);
|
|
58
|
+
}
|
|
59
|
+
generator.create = (options) => {
|
|
60
|
+
return buildProcessor(options);
|
|
61
|
+
};
|
|
62
|
+
Object.defineProperty(generator, "defaultOptions", { get() {
|
|
63
|
+
return JSON.parse(JSON.stringify(defaultOptions));
|
|
64
|
+
} });
|
|
65
|
+
return generator;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/builtin/goldenCross.ts
|
|
70
|
+
const defaultGoldenCrossOptions = {
|
|
71
|
+
windowSize: 2,
|
|
72
|
+
fastPeriod: 50,
|
|
73
|
+
slowPeriod: 200,
|
|
74
|
+
stopLossPercent: .02
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Golden Cross / Death Cross Strategy
|
|
78
|
+
*
|
|
79
|
+
* Detects when a fast SMA crosses above (golden cross → long) or
|
|
80
|
+
* below (death cross → short) a slow SMA.
|
|
81
|
+
*
|
|
82
|
+
* - **Golden Cross**: fast SMA crosses above slow SMA → `long`
|
|
83
|
+
* - **Death Cross**: fast SMA crosses below slow SMA → `short`
|
|
84
|
+
* - Otherwise → `hold`
|
|
85
|
+
*
|
|
86
|
+
* Stop-loss is set as a percentage below/above the entry price.
|
|
87
|
+
*/
|
|
88
|
+
const goldenCross = createStrategy(({ fastPeriod, slowPeriod, stopLossPercent }) => {
|
|
89
|
+
const fastSma = sma.create({ period: fastPeriod });
|
|
90
|
+
const slowSma = sma.create({ period: slowPeriod });
|
|
91
|
+
let prevFastAbove;
|
|
92
|
+
return (ctx) => {
|
|
93
|
+
const close = ctx.bar.c;
|
|
94
|
+
const fastAbove = gt(fastSma(close), slowSma(close));
|
|
95
|
+
const price = toNumber(from(close, 18));
|
|
96
|
+
let signal = { action: "hold" };
|
|
97
|
+
if (prevFastAbove !== void 0) {
|
|
98
|
+
if (fastAbove && !prevFastAbove) signal = {
|
|
99
|
+
action: "long",
|
|
100
|
+
stopLoss: price * (1 - stopLossPercent),
|
|
101
|
+
reason: "Golden cross: fast SMA crossed above slow SMA"
|
|
102
|
+
};
|
|
103
|
+
else if (!fastAbove && prevFastAbove) signal = {
|
|
104
|
+
action: "short",
|
|
105
|
+
stopLoss: price * (1 + stopLossPercent),
|
|
106
|
+
reason: "Death cross: fast SMA crossed below slow SMA"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
prevFastAbove = fastAbove;
|
|
110
|
+
return signal;
|
|
111
|
+
};
|
|
112
|
+
}, defaultGoldenCrossOptions);
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/builtin/rsiOversoldOverbought.ts
|
|
116
|
+
const defaultRsiOversoldOverboughtOptions = {
|
|
117
|
+
windowSize: 2,
|
|
118
|
+
period: 14,
|
|
119
|
+
overboughtLevel: 70,
|
|
120
|
+
oversoldLevel: 30
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* RSI Oversold/Overbought Strategy
|
|
124
|
+
*
|
|
125
|
+
* Uses the Relative Strength Index to detect oversold and overbought conditions.
|
|
126
|
+
*
|
|
127
|
+
* - RSI crosses below `oversoldLevel` then back above → `long` (oversold reversal)
|
|
128
|
+
* - RSI crosses above `overboughtLevel` then back below → `short` (overbought reversal)
|
|
129
|
+
* - Otherwise → `hold`
|
|
130
|
+
*/
|
|
131
|
+
const rsiOversoldOverbought = createStrategy(({ period, overboughtLevel, oversoldLevel }) => {
|
|
132
|
+
const rsiProc = rsi.create({ period });
|
|
133
|
+
let prevRsi;
|
|
134
|
+
const obLevel = from(overboughtLevel, 18);
|
|
135
|
+
const osLevel = from(oversoldLevel, 18);
|
|
136
|
+
return (ctx) => {
|
|
137
|
+
const close = ctx.bar.c;
|
|
138
|
+
const currentRsi = rsiProc(close);
|
|
139
|
+
let signal = { action: "hold" };
|
|
140
|
+
if (prevRsi !== void 0) {
|
|
141
|
+
if (lt(prevRsi, osLevel) && gt(currentRsi, osLevel)) signal = {
|
|
142
|
+
action: "long",
|
|
143
|
+
reason: `RSI crossed above oversold level (${oversoldLevel})`
|
|
144
|
+
};
|
|
145
|
+
else if (gt(prevRsi, obLevel) && lt(currentRsi, obLevel)) signal = {
|
|
146
|
+
action: "short",
|
|
147
|
+
reason: `RSI crossed below overbought level (${overboughtLevel})`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
prevRsi = currentRsi;
|
|
151
|
+
return signal;
|
|
152
|
+
};
|
|
153
|
+
}, defaultRsiOversoldOverboughtOptions);
|
|
154
|
+
|
|
155
|
+
//#endregion
|
|
156
|
+
export { createStrategy, defaultGoldenCrossOptions, defaultRsiOversoldOverboughtOptions, goldenCross, goldenCross as goldenCrossStrategy, rsiOversoldOverbought, rsiOversoldOverbought as rsiOversoldOverboughtStrategy };
|
|
157
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/base.ts","../src/builtin/goldenCross.ts","../src/builtin/rsiOversoldOverbought.ts"],"sourcesContent":["import type { BaseStrategyOptions, CandleData, Processor, StrategyContext, StrategyFactory, StrategyGenerator, StrategySignal } from './types'\nimport { assert } from '@vulcan-js/core'\nimport { defu } from 'defu'\n\n/**\n * A fixed-capacity ring buffer with O(1) push and O(n) snapshot.\n */\nfunction createRingBuffer<T>(capacity: number) {\n const buf: T[] = Array.from({ length: capacity })\n let head = 0\n let size = 0\n\n return {\n push(item: T) {\n buf[head] = item\n head = (head + 1) % capacity\n if (size < capacity)\n size++\n },\n toArray(): readonly T[] {\n if (size < capacity) {\n return buf.slice(0, size)\n }\n // head points to the oldest element when buffer is full\n return [...buf.slice(head), ...buf.slice(0, head)]\n },\n get size() {\n return size\n },\n }\n}\n\n/**\n * Create a generator-based strategy from a strategy factory.\n *\n * Mirrors `createSignal` from core, but manages a rolling window of bars\n * and constructs a `StrategyContext` for each evaluation.\n *\n * The returned generator is fully compatible with `SignalGenerator<CandleData, StrategySignal, Opts>`.\n */\nexport function createStrategy<Opts extends BaseStrategyOptions>(\n factory: StrategyFactory<Opts>,\n defaultOptions: Opts,\n): StrategyGenerator<Opts> {\n function buildProcessor(options?: Partial<Opts>): Processor<CandleData, StrategySignal> {\n const opt = defu(options, defaultOptions) as Required<Opts>\n assert(Number.isInteger(opt.windowSize) && opt.windowSize >= 1, new RangeError(`Expected windowSize to be a positive integer, got ${opt.windowSize}`))\n const ring = createRingBuffer<CandleData>(opt.windowSize)\n const process = factory(opt)\n let index = 0\n return (bar: CandleData) => {\n ring.push(bar)\n const ctx: StrategyContext = { bar, bars: ring.toArray(), index }\n const signal = process(ctx)\n index++\n return signal\n }\n }\n\n function* generator(\n source: Iterable<CandleData>,\n options?: Partial<Opts>,\n ): Generator<StrategySignal, void, unknown> {\n const process = buildProcessor(options)\n for (const bar of source) {\n yield process(bar)\n }\n }\n\n generator.create = (options?: Partial<Opts>): Processor<CandleData, StrategySignal> => {\n return buildProcessor(options)\n }\n\n Object.defineProperty(generator, 'defaultOptions', {\n get() {\n return JSON.parse(JSON.stringify(defaultOptions))\n },\n })\n\n return generator as StrategyGenerator<Opts>\n}\n","import type { Dnum, Numberish } from 'dnum'\nimport type { BaseStrategyOptions, StrategySignal } from '../types'\nimport { sma } from '@vulcan-js/indicators'\nimport { from, gt, toNumber } from 'dnum'\nimport { createStrategy } from '../base'\n\nexport interface GoldenCrossOptions extends BaseStrategyOptions {\n /** Fast SMA period */\n fastPeriod: number\n /** Slow SMA period */\n slowPeriod: number\n /** Stop-loss percentage (0–1), e.g. 0.02 = 2% */\n stopLossPercent: number\n}\n\nexport const defaultGoldenCrossOptions: GoldenCrossOptions = {\n windowSize: 2,\n fastPeriod: 50,\n slowPeriod: 200,\n stopLossPercent: 0.02,\n}\n\n/**\n * Golden Cross / Death Cross Strategy\n *\n * Detects when a fast SMA crosses above (golden cross → long) or\n * below (death cross → short) a slow SMA.\n *\n * - **Golden Cross**: fast SMA crosses above slow SMA → `long`\n * - **Death Cross**: fast SMA crosses below slow SMA → `short`\n * - Otherwise → `hold`\n *\n * Stop-loss is set as a percentage below/above the entry price.\n */\nexport const goldenCross = createStrategy(\n ({ fastPeriod, slowPeriod, stopLossPercent }) => {\n const fastSma = sma.create({ period: fastPeriod })\n const slowSma = sma.create({ period: slowPeriod })\n let prevFastAbove: boolean | undefined\n\n return (ctx) => {\n const close = ctx.bar.c as Numberish\n const fast: Dnum = fastSma(close)\n const slow: Dnum = slowSma(close)\n const fastAbove = gt(fast, slow)\n const price = toNumber(from(close, 18))\n\n let signal: StrategySignal = { action: 'hold' }\n\n if (prevFastAbove !== undefined) {\n if (fastAbove && !prevFastAbove) {\n signal = {\n action: 'long',\n stopLoss: price * (1 - stopLossPercent),\n reason: 'Golden cross: fast SMA crossed above slow SMA',\n }\n }\n else if (!fastAbove && prevFastAbove) {\n signal = {\n action: 'short',\n stopLoss: price * (1 + stopLossPercent),\n reason: 'Death cross: fast SMA crossed below slow SMA',\n }\n }\n }\n\n prevFastAbove = fastAbove\n\n return signal\n }\n },\n defaultGoldenCrossOptions,\n)\n\nexport { goldenCross as goldenCrossStrategy }\n","import type { Dnum, Numberish } from 'dnum'\nimport type { BaseStrategyOptions, StrategySignal } from '../types'\nimport { rsi } from '@vulcan-js/indicators'\nimport { from, gt, lt } from 'dnum'\nimport { createStrategy } from '../base'\n\nexport interface RsiOversoldOverboughtOptions extends BaseStrategyOptions {\n /** RSI calculation period */\n period: number\n /** RSI level above which the asset is considered overbought */\n overboughtLevel: number\n /** RSI level below which the asset is considered oversold */\n oversoldLevel: number\n}\n\nexport const defaultRsiOversoldOverboughtOptions: RsiOversoldOverboughtOptions = {\n windowSize: 2,\n period: 14,\n overboughtLevel: 70,\n oversoldLevel: 30,\n}\n\n/**\n * RSI Oversold/Overbought Strategy\n *\n * Uses the Relative Strength Index to detect oversold and overbought conditions.\n *\n * - RSI crosses below `oversoldLevel` then back above → `long` (oversold reversal)\n * - RSI crosses above `overboughtLevel` then back below → `short` (overbought reversal)\n * - Otherwise → `hold`\n */\nexport const rsiOversoldOverbought = createStrategy(\n ({ period, overboughtLevel, oversoldLevel }) => {\n const rsiProc = rsi.create({ period })\n let prevRsi: Dnum | undefined\n const obLevel = from(overboughtLevel, 18)\n const osLevel = from(oversoldLevel, 18)\n\n return (ctx) => {\n const close = ctx.bar.c as Numberish\n const currentRsi = rsiProc(close)\n\n let signal: StrategySignal = { action: 'hold' }\n\n if (prevRsi !== undefined) {\n // Oversold reversal: RSI was below oversoldLevel, now crosses above\n if (lt(prevRsi, osLevel) && gt(currentRsi, osLevel)) {\n signal = {\n action: 'long',\n reason: `RSI crossed above oversold level (${oversoldLevel})`,\n }\n }\n // Overbought reversal: RSI was above overboughtLevel, now crosses below\n else if (gt(prevRsi, obLevel) && lt(currentRsi, obLevel)) {\n signal = {\n action: 'short',\n reason: `RSI crossed below overbought level (${overboughtLevel})`,\n }\n }\n }\n\n prevRsi = currentRsi\n\n return signal\n }\n },\n defaultRsiOversoldOverboughtOptions,\n)\n\nexport { rsiOversoldOverbought as rsiOversoldOverboughtStrategy }\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vulcan-js/strategies",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js",
|
|
8
|
+
"./package.json": "./package.json"
|
|
9
|
+
},
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"defu": "^6.1.4",
|
|
16
|
+
"dnum": "^2.17.0",
|
|
17
|
+
"@vulcan-js/core": "^0.0.0",
|
|
18
|
+
"@vulcan-js/indicators": "^0.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"vite-tsconfig-paths": "^6.1.1",
|
|
22
|
+
"vitest": "^4.0.18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsdown",
|
|
26
|
+
"test": "vitest run"
|
|
27
|
+
}
|
|
28
|
+
}
|