@tradejs/core 3.1.29-beta.290 → 3.1.29-beta.292
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/dist/aiFeatures.d.mts +14 -0
- package/dist/aiFeatures.d.ts +14 -0
- package/dist/aiFeatures.js +179 -0
- package/dist/aiFeatures.mjs +150 -0
- package/dist/backtest.mjs +3 -3
- package/dist/indicators.mjs +2 -2
- package/dist/portfolio.d.mts +23 -0
- package/dist/portfolio.d.ts +23 -0
- package/dist/portfolio.js +314 -0
- package/dist/portfolio.mjs +280 -0
- package/dist/strategies.mjs +2 -2
- package/dist/tickerProfiles.d.mts +17 -0
- package/dist/tickerProfiles.d.ts +17 -0
- package/dist/tickerProfiles.js +211 -0
- package/dist/tickerProfiles.mjs +182 -0
- package/package.json +17 -2
- package/dist/{chunk-OZVRWW2K.mjs → chunk-6XY6Z2DB.mjs} +3 -3
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/portfolio.ts
|
|
21
|
+
var portfolio_exports = {};
|
|
22
|
+
__export(portfolio_exports, {
|
|
23
|
+
assertPortfolioState: () => assertPortfolioState,
|
|
24
|
+
buildPortfolioContext: () => buildPortfolioContext,
|
|
25
|
+
createPortfolioState: () => createPortfolioState,
|
|
26
|
+
parsePortfolioPolicy: () => parsePortfolioPolicy,
|
|
27
|
+
portfolioCloseCommands: () => portfolioCloseCommands,
|
|
28
|
+
portfolioFingerprint: () => portfolioFingerprint,
|
|
29
|
+
portfolioOwns: () => portfolioOwns,
|
|
30
|
+
reconcilePortfolio: () => reconcilePortfolio,
|
|
31
|
+
reservePortfolioEntry: () => reservePortfolioEntry,
|
|
32
|
+
validatePortfolioObservation: () => validatePortfolioObservation
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(portfolio_exports);
|
|
35
|
+
var DAY = 864e5;
|
|
36
|
+
var positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
37
|
+
var record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
38
|
+
function parsePortfolioPolicy(value) {
|
|
39
|
+
if (value === void 0 || value === null || value === false)
|
|
40
|
+
return void 0;
|
|
41
|
+
if (!record(value) || typeof value.id !== "string" || !/^[a-zA-Z0-9_-]+$/.test(value.id) || !positive(value.riskUnit) || !Number.isSafeInteger(value.observationIntervalMs) || !positive(value.observationIntervalMs) || !Number.isSafeInteger(value.maxPriceAgeMs) || !positive(value.maxPriceAgeMs))
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Invalid portfolio policy identity, risk or observation clock"
|
|
44
|
+
);
|
|
45
|
+
const scope = value.scope;
|
|
46
|
+
if (scope !== "strategy" && scope !== "account" && !(record(scope) && Object.keys(scope).length === 1 && Array.isArray(scope.strategies) && scope.strategies.length && scope.strategies.every((s) => typeof s === "string" && s.trim()) && new Set(scope.strategies).size === scope.strategies.length))
|
|
47
|
+
throw new Error("Invalid portfolio scope");
|
|
48
|
+
if (value.maxPositions !== void 0 && (!positive(value.maxPositions) || !Number.isSafeInteger(value.maxPositions)))
|
|
49
|
+
throw new Error("Invalid portfolio position limit");
|
|
50
|
+
if (value.closeOppositeOnFill !== void 0 && typeof value.closeOppositeOnFill !== "boolean")
|
|
51
|
+
throw new Error("Invalid opposite-fill policy");
|
|
52
|
+
const profit = value.profitExit;
|
|
53
|
+
if (profit !== void 0 && (!record(profit) || profit.scope !== "position" && profit.scope !== "basket" || Object.keys(profit).some(
|
|
54
|
+
(k) => !["scope", "multiple", "minPositions"].includes(k)
|
|
55
|
+
) || !positive(profit.multiple) || profit.minPositions !== void 0 && (!positive(profit.minPositions) || !Number.isSafeInteger(profit.minPositions))))
|
|
56
|
+
throw new Error("Invalid portfolio profit exit");
|
|
57
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
58
|
+
"id",
|
|
59
|
+
"scope",
|
|
60
|
+
"riskUnit",
|
|
61
|
+
"observationIntervalMs",
|
|
62
|
+
"maxPriceAgeMs",
|
|
63
|
+
"maxPositions",
|
|
64
|
+
"profitExit",
|
|
65
|
+
"closeOppositeOnFill"
|
|
66
|
+
]);
|
|
67
|
+
if (value.maxPositions === void 0 && profit === void 0 && value.closeOppositeOnFill !== true)
|
|
68
|
+
throw new Error("Portfolio policy has no enabled rules");
|
|
69
|
+
if (Object.keys(value).some((k) => !allowed.has(k)))
|
|
70
|
+
throw new Error("Unknown portfolio policy option");
|
|
71
|
+
return {
|
|
72
|
+
id: value.id,
|
|
73
|
+
scope,
|
|
74
|
+
riskUnit: value.riskUnit,
|
|
75
|
+
observationIntervalMs: value.observationIntervalMs,
|
|
76
|
+
maxPriceAgeMs: value.maxPriceAgeMs,
|
|
77
|
+
...value.maxPositions !== void 0 ? { maxPositions: value.maxPositions } : {},
|
|
78
|
+
...record(profit) ? {
|
|
79
|
+
profitExit: {
|
|
80
|
+
scope: profit.scope,
|
|
81
|
+
multiple: profit.multiple,
|
|
82
|
+
...profit.minPositions !== void 0 ? { minPositions: profit.minPositions } : {}
|
|
83
|
+
}
|
|
84
|
+
} : {},
|
|
85
|
+
...value.closeOppositeOnFill !== void 0 ? { closeOppositeOnFill: value.closeOppositeOnFill } : {}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function portfolioFingerprint(policy, strategy) {
|
|
89
|
+
return JSON.stringify({
|
|
90
|
+
...policy,
|
|
91
|
+
scope: policy.scope === "strategy" ? { strategies: [strategy] } : policy.scope === "account" ? "account" : { strategies: [...policy.scope.strategies].sort() }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function createPortfolioState(fingerprint) {
|
|
95
|
+
return {
|
|
96
|
+
schema: "tradejs-portfolio/v1",
|
|
97
|
+
revision: 0,
|
|
98
|
+
fingerprint,
|
|
99
|
+
reservations: [],
|
|
100
|
+
closing: [],
|
|
101
|
+
completedSignals: {},
|
|
102
|
+
observedAt: -1,
|
|
103
|
+
evaluatedAt: -1,
|
|
104
|
+
blockedThrough: -1
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function portfolioOwns(policy, strategy, owner) {
|
|
108
|
+
return owner !== null && (policy.scope === "account" || (policy.scope === "strategy" ? owner === strategy : policy.scope.strategies.includes(owner)));
|
|
109
|
+
}
|
|
110
|
+
function validatePortfolioObservation(o) {
|
|
111
|
+
if (!record(o) || !Number.isSafeInteger(o.asOf) || Number(o.asOf) < 0 || !Array.isArray(o.positions) || !Array.isArray(o.orders))
|
|
112
|
+
throw new Error("Invalid portfolio observation");
|
|
113
|
+
const ids = /* @__PURE__ */ new Set(), symbols = /* @__PURE__ */ new Set();
|
|
114
|
+
for (const p of o.positions) {
|
|
115
|
+
if (!record(p)) throw new Error("Invalid portfolio position");
|
|
116
|
+
const key = `${p.symbol}:${p.direction}`;
|
|
117
|
+
if (typeof p.id !== "string" || !p.id || typeof p.symbol !== "string" || !p.symbol || !["LONG", "SHORT"].includes(String(p.direction)) || !positive(p.qty) || !positive(p.entryPrice) || ids.has(p.id) || symbols.has(key) || p.strategy !== null && (typeof p.strategy !== "string" || !p.strategy) || p.markPrice !== null && !positive(p.markPrice) || p.markedAt !== null && (!Number.isSafeInteger(p.markedAt) || Number(p.markedAt) < 0))
|
|
118
|
+
throw new Error("Invalid or duplicate portfolio position");
|
|
119
|
+
ids.add(p.id);
|
|
120
|
+
symbols.add(key);
|
|
121
|
+
}
|
|
122
|
+
const orders = /* @__PURE__ */ new Set();
|
|
123
|
+
for (const order of o.orders) {
|
|
124
|
+
if (!record(order) || typeof order.id !== "string" || !order.id || orders.has(order.id) || !["pending", "partial", "filled", "cancelled", "rejected"].includes(
|
|
125
|
+
String(order.status)
|
|
126
|
+
) || !Number.isFinite(order.filledQty) || Number(order.filledQty) < 0)
|
|
127
|
+
throw new Error("Invalid portfolio order observation");
|
|
128
|
+
orders.add(order.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
var fresh = (p, o, policy) => p.markPrice !== null && p.markedAt !== null && p.markedAt <= o.asOf && o.asOf - p.markedAt <= policy.maxPriceAgeMs;
|
|
132
|
+
var pnl = (p) => ((p.markPrice ?? p.entryPrice) - p.entryPrice) * p.qty * (p.direction === "LONG" ? 1 : -1);
|
|
133
|
+
function buildPortfolioContext(policy, strategy, state, o) {
|
|
134
|
+
const positions = o.positions.filter(
|
|
135
|
+
(p) => portfolioOwns(policy, strategy, p.strategy)
|
|
136
|
+
);
|
|
137
|
+
const complete = positions.every((p) => fresh(p, o, policy));
|
|
138
|
+
const value = positions.reduce((s, p) => s + p.qty * p.entryPrice, 0);
|
|
139
|
+
const risksKnown = positions.every((p) => positive(p.stopLossPrice));
|
|
140
|
+
return {
|
|
141
|
+
schema: "tradejs-portfolio-context/v1",
|
|
142
|
+
asOf: o.asOf,
|
|
143
|
+
policyId: policy.id,
|
|
144
|
+
positionCount: positions.length,
|
|
145
|
+
pendingEntryCount: state.reservations.filter(
|
|
146
|
+
(r) => !positions.some(
|
|
147
|
+
(p) => p.symbol === r.symbol && p.direction === r.direction
|
|
148
|
+
)
|
|
149
|
+
).length,
|
|
150
|
+
longCount: positions.filter((p) => p.direction === "LONG").length,
|
|
151
|
+
shortCount: positions.filter((p) => p.direction === "SHORT").length,
|
|
152
|
+
grossUnrealizedPnlR: complete ? positions.reduce((s, p) => s + pnl(p), 0) / policy.riskUnit : null,
|
|
153
|
+
openStopRiskR: risksKnown ? positions.reduce(
|
|
154
|
+
(s, p) => s + Math.max(
|
|
155
|
+
0,
|
|
156
|
+
(p.entryPrice - (p.stopLossPrice ?? p.entryPrice)) * (p.direction === "LONG" ? 1 : -1)
|
|
157
|
+
) * p.qty,
|
|
158
|
+
0
|
|
159
|
+
) / policy.riskUnit : null,
|
|
160
|
+
largestPositionShare: value > 0 ? Math.max(...positions.map((p) => p.qty * p.entryPrice / value)) : null,
|
|
161
|
+
pricesComplete: complete,
|
|
162
|
+
unknownOwnershipCount: o.positions.filter((p) => p.strategy === null).length
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function reconcilePortfolio(policy, strategy, previous, o) {
|
|
166
|
+
validatePortfolioObservation(o);
|
|
167
|
+
if (o.asOf < previous.observedAt)
|
|
168
|
+
throw new Error("Portfolio observation moved backwards");
|
|
169
|
+
const state = {
|
|
170
|
+
...previous,
|
|
171
|
+
reservations: previous.reservations.map((r) => ({ ...r })),
|
|
172
|
+
closing: previous.closing.map((c) => ({ ...c })),
|
|
173
|
+
completedSignals: { ...previous.completedSignals },
|
|
174
|
+
observedAt: o.asOf
|
|
175
|
+
};
|
|
176
|
+
const cycle = Math.floor(o.asOf / policy.observationIntervalMs) * policy.observationIntervalMs;
|
|
177
|
+
const positions = o.positions.filter(
|
|
178
|
+
(p) => portfolioOwns(policy, strategy, p.strategy)
|
|
179
|
+
);
|
|
180
|
+
const closingBefore = state.closing.length;
|
|
181
|
+
state.closing = state.closing.filter(
|
|
182
|
+
(c) => positions.some((p) => p.id === c.positionId)
|
|
183
|
+
);
|
|
184
|
+
if (state.closing.length < closingBefore)
|
|
185
|
+
state.blockedThrough = Math.max(
|
|
186
|
+
state.blockedThrough,
|
|
187
|
+
cycle + policy.observationIntervalMs - 1
|
|
188
|
+
);
|
|
189
|
+
const requestClose = (p, reason) => {
|
|
190
|
+
if (!state.closing.some((c) => c.positionId === p.id))
|
|
191
|
+
state.closing.push({
|
|
192
|
+
positionId: p.id,
|
|
193
|
+
position: { ...p },
|
|
194
|
+
reason,
|
|
195
|
+
createdAt: o.asOf,
|
|
196
|
+
lastAttemptAt: null
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
state.reservations = state.reservations.filter((r) => {
|
|
200
|
+
const order = o.orders.find((x) => x.id === r.id);
|
|
201
|
+
if (!order) return true;
|
|
202
|
+
if (order.filledQty > 0 && !r.oppositeHandled) {
|
|
203
|
+
const filled = positions.find(
|
|
204
|
+
(p) => p.id === order.positionId && p.symbol === r.symbol && p.direction === r.direction
|
|
205
|
+
);
|
|
206
|
+
if (filled) {
|
|
207
|
+
r.oppositeHandled = true;
|
|
208
|
+
if (policy.closeOppositeOnFill) {
|
|
209
|
+
for (const p of positions)
|
|
210
|
+
if (p.id !== filled.id && p.direction !== r.direction)
|
|
211
|
+
requestClose(p, "PORTFOLIO_OPPOSITE_FILL");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (["filled", "cancelled", "rejected"].includes(order.status)) {
|
|
216
|
+
if (order.filledQty > 0 && !r.oppositeHandled) return true;
|
|
217
|
+
state.completedSignals[r.signalId] = o.asOf;
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
222
|
+
if (cycle > state.evaluatedAt) {
|
|
223
|
+
state.evaluatedAt = cycle;
|
|
224
|
+
const profit = policy.profitExit;
|
|
225
|
+
if (profit?.scope === "position") {
|
|
226
|
+
for (const p of positions)
|
|
227
|
+
if (fresh(p, o, policy) && pnl(p) >= profit.multiple * policy.riskUnit)
|
|
228
|
+
requestClose(p, "PORTFOLIO_PROFIT_POSITION");
|
|
229
|
+
}
|
|
230
|
+
if (profit?.scope === "basket" && positions.length >= (profit.minPositions ?? 2) && positions.every((p) => fresh(p, o, policy)) && positions.reduce((sum, p) => sum + pnl(p), 0) >= profit.multiple * policy.riskUnit)
|
|
231
|
+
for (const p of positions) requestClose(p, "PORTFOLIO_PROFIT_BASKET");
|
|
232
|
+
}
|
|
233
|
+
for (const [id, t] of Object.entries(state.completedSignals))
|
|
234
|
+
if (t < o.asOf - 7 * DAY) delete state.completedSignals[id];
|
|
235
|
+
return state;
|
|
236
|
+
}
|
|
237
|
+
function reservePortfolioEntry(policy, strategy, state, o, entry) {
|
|
238
|
+
if (!portfolioOwns(policy, strategy, entry.strategy))
|
|
239
|
+
return { state, reason: "PORTFOLIO_OUTSIDE_SCOPE" };
|
|
240
|
+
if (!positive(entry.qty) || !entry.symbol || !["LONG", "SHORT"].includes(entry.direction) || !entry.id || !entry.signalId || !Number.isSafeInteger(entry.timestamp) || entry.timestamp < o.asOf - 7 * DAY || entry.timestamp > o.asOf)
|
|
241
|
+
return { state, reason: "PORTFOLIO_INVALID_ENTRY" };
|
|
242
|
+
if (state.reservations.some(
|
|
243
|
+
(r) => r.signalId === entry.signalId || r.id === entry.id
|
|
244
|
+
) || state.completedSignals[entry.signalId] !== void 0)
|
|
245
|
+
return { state, reason: "PORTFOLIO_DUPLICATE_ENTRY" };
|
|
246
|
+
if (state.closing.length || o.asOf <= state.blockedThrough)
|
|
247
|
+
return { state, reason: "PORTFOLIO_CLOSING" };
|
|
248
|
+
if (o.positions.some((p) => p.symbol === entry.symbol) || state.reservations.some((p) => p.symbol === entry.symbol))
|
|
249
|
+
return { state, reason: "PORTFOLIO_SYMBOL_OCCUPIED" };
|
|
250
|
+
const context = buildPortfolioContext(policy, strategy, state, o);
|
|
251
|
+
if (context.unknownOwnershipCount)
|
|
252
|
+
return { state, reason: "PORTFOLIO_UNKNOWN_OWNERSHIP" };
|
|
253
|
+
if (policy.maxPositions !== void 0 && context.positionCount + context.pendingEntryCount >= policy.maxPositions)
|
|
254
|
+
return { state, reason: "PORTFOLIO_POSITION_LIMIT" };
|
|
255
|
+
return {
|
|
256
|
+
state: { ...state, reservations: [...state.reservations, { ...entry }] }
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function portfolioCloseCommands(policy, state, o) {
|
|
260
|
+
const cycle = Math.floor(o.asOf / policy.observationIntervalMs) * policy.observationIntervalMs;
|
|
261
|
+
return state.closing.flatMap((intent) => {
|
|
262
|
+
const position = o.positions.find((p) => p.id === intent.positionId);
|
|
263
|
+
return position && fresh(position, o, policy) && (intent.lastAttemptAt === null || intent.lastAttemptAt < cycle) ? [{ intent, position }] : [];
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function assertPortfolioState(value) {
|
|
267
|
+
const fail = () => {
|
|
268
|
+
throw new Error("Invalid persisted portfolio state");
|
|
269
|
+
};
|
|
270
|
+
if (!record(value) || value.schema !== "tradejs-portfolio/v1" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0 || typeof value.fingerprint !== "string" || !value.fingerprint || !Array.isArray(value.reservations) || !Array.isArray(value.closing) || !record(value.completedSignals))
|
|
271
|
+
return fail();
|
|
272
|
+
for (const clock of [
|
|
273
|
+
value.observedAt,
|
|
274
|
+
value.evaluatedAt,
|
|
275
|
+
value.blockedThrough
|
|
276
|
+
])
|
|
277
|
+
if (!Number.isSafeInteger(clock) || Number(clock) < -1) return fail();
|
|
278
|
+
const ids = /* @__PURE__ */ new Set();
|
|
279
|
+
for (const r of value.reservations) {
|
|
280
|
+
if (!record(r) || typeof r.id !== "string" || !r.id || ids.has(r.id) || typeof r.signalId !== "string" || !r.signalId || typeof r.symbol !== "string" || !r.symbol || typeof r.strategy !== "string" || !r.strategy || !["LONG", "SHORT"].includes(String(r.direction)) || !positive(r.qty) || !Number.isSafeInteger(r.timestamp) || Number(r.timestamp) < 0 || typeof r.submitted !== "boolean" || typeof r.oppositeHandled !== "boolean")
|
|
281
|
+
return fail();
|
|
282
|
+
ids.add(r.id);
|
|
283
|
+
}
|
|
284
|
+
const closes = /* @__PURE__ */ new Set();
|
|
285
|
+
for (const c of value.closing) {
|
|
286
|
+
if (!record(c) || typeof c.positionId !== "string" || closes.has(c.positionId) || !record(c.position) || c.position.id !== c.positionId || ![
|
|
287
|
+
"PORTFOLIO_PROFIT_POSITION",
|
|
288
|
+
"PORTFOLIO_PROFIT_BASKET",
|
|
289
|
+
"PORTFOLIO_OPPOSITE_FILL"
|
|
290
|
+
].includes(String(c.reason)) || !Number.isSafeInteger(c.createdAt) || Number(c.createdAt) < 0 || c.lastAttemptAt !== null && (!Number.isSafeInteger(c.lastAttemptAt) || Number(c.lastAttemptAt) < 0))
|
|
291
|
+
return fail();
|
|
292
|
+
validatePortfolioObservation({
|
|
293
|
+
asOf: Number(c.createdAt),
|
|
294
|
+
positions: [c.position],
|
|
295
|
+
orders: []
|
|
296
|
+
});
|
|
297
|
+
closes.add(c.positionId);
|
|
298
|
+
}
|
|
299
|
+
for (const t of Object.values(value.completedSignals))
|
|
300
|
+
if (!Number.isSafeInteger(t) || Number(t) < 0) return fail();
|
|
301
|
+
}
|
|
302
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
303
|
+
0 && (module.exports = {
|
|
304
|
+
assertPortfolioState,
|
|
305
|
+
buildPortfolioContext,
|
|
306
|
+
createPortfolioState,
|
|
307
|
+
parsePortfolioPolicy,
|
|
308
|
+
portfolioCloseCommands,
|
|
309
|
+
portfolioFingerprint,
|
|
310
|
+
portfolioOwns,
|
|
311
|
+
reconcilePortfolio,
|
|
312
|
+
reservePortfolioEntry,
|
|
313
|
+
validatePortfolioObservation
|
|
314
|
+
});
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// src/portfolio.ts
|
|
2
|
+
var DAY = 864e5;
|
|
3
|
+
var positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
4
|
+
var record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5
|
+
function parsePortfolioPolicy(value) {
|
|
6
|
+
if (value === void 0 || value === null || value === false)
|
|
7
|
+
return void 0;
|
|
8
|
+
if (!record(value) || typeof value.id !== "string" || !/^[a-zA-Z0-9_-]+$/.test(value.id) || !positive(value.riskUnit) || !Number.isSafeInteger(value.observationIntervalMs) || !positive(value.observationIntervalMs) || !Number.isSafeInteger(value.maxPriceAgeMs) || !positive(value.maxPriceAgeMs))
|
|
9
|
+
throw new Error(
|
|
10
|
+
"Invalid portfolio policy identity, risk or observation clock"
|
|
11
|
+
);
|
|
12
|
+
const scope = value.scope;
|
|
13
|
+
if (scope !== "strategy" && scope !== "account" && !(record(scope) && Object.keys(scope).length === 1 && Array.isArray(scope.strategies) && scope.strategies.length && scope.strategies.every((s) => typeof s === "string" && s.trim()) && new Set(scope.strategies).size === scope.strategies.length))
|
|
14
|
+
throw new Error("Invalid portfolio scope");
|
|
15
|
+
if (value.maxPositions !== void 0 && (!positive(value.maxPositions) || !Number.isSafeInteger(value.maxPositions)))
|
|
16
|
+
throw new Error("Invalid portfolio position limit");
|
|
17
|
+
if (value.closeOppositeOnFill !== void 0 && typeof value.closeOppositeOnFill !== "boolean")
|
|
18
|
+
throw new Error("Invalid opposite-fill policy");
|
|
19
|
+
const profit = value.profitExit;
|
|
20
|
+
if (profit !== void 0 && (!record(profit) || profit.scope !== "position" && profit.scope !== "basket" || Object.keys(profit).some(
|
|
21
|
+
(k) => !["scope", "multiple", "minPositions"].includes(k)
|
|
22
|
+
) || !positive(profit.multiple) || profit.minPositions !== void 0 && (!positive(profit.minPositions) || !Number.isSafeInteger(profit.minPositions))))
|
|
23
|
+
throw new Error("Invalid portfolio profit exit");
|
|
24
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
25
|
+
"id",
|
|
26
|
+
"scope",
|
|
27
|
+
"riskUnit",
|
|
28
|
+
"observationIntervalMs",
|
|
29
|
+
"maxPriceAgeMs",
|
|
30
|
+
"maxPositions",
|
|
31
|
+
"profitExit",
|
|
32
|
+
"closeOppositeOnFill"
|
|
33
|
+
]);
|
|
34
|
+
if (value.maxPositions === void 0 && profit === void 0 && value.closeOppositeOnFill !== true)
|
|
35
|
+
throw new Error("Portfolio policy has no enabled rules");
|
|
36
|
+
if (Object.keys(value).some((k) => !allowed.has(k)))
|
|
37
|
+
throw new Error("Unknown portfolio policy option");
|
|
38
|
+
return {
|
|
39
|
+
id: value.id,
|
|
40
|
+
scope,
|
|
41
|
+
riskUnit: value.riskUnit,
|
|
42
|
+
observationIntervalMs: value.observationIntervalMs,
|
|
43
|
+
maxPriceAgeMs: value.maxPriceAgeMs,
|
|
44
|
+
...value.maxPositions !== void 0 ? { maxPositions: value.maxPositions } : {},
|
|
45
|
+
...record(profit) ? {
|
|
46
|
+
profitExit: {
|
|
47
|
+
scope: profit.scope,
|
|
48
|
+
multiple: profit.multiple,
|
|
49
|
+
...profit.minPositions !== void 0 ? { minPositions: profit.minPositions } : {}
|
|
50
|
+
}
|
|
51
|
+
} : {},
|
|
52
|
+
...value.closeOppositeOnFill !== void 0 ? { closeOppositeOnFill: value.closeOppositeOnFill } : {}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function portfolioFingerprint(policy, strategy) {
|
|
56
|
+
return JSON.stringify({
|
|
57
|
+
...policy,
|
|
58
|
+
scope: policy.scope === "strategy" ? { strategies: [strategy] } : policy.scope === "account" ? "account" : { strategies: [...policy.scope.strategies].sort() }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function createPortfolioState(fingerprint) {
|
|
62
|
+
return {
|
|
63
|
+
schema: "tradejs-portfolio/v1",
|
|
64
|
+
revision: 0,
|
|
65
|
+
fingerprint,
|
|
66
|
+
reservations: [],
|
|
67
|
+
closing: [],
|
|
68
|
+
completedSignals: {},
|
|
69
|
+
observedAt: -1,
|
|
70
|
+
evaluatedAt: -1,
|
|
71
|
+
blockedThrough: -1
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function portfolioOwns(policy, strategy, owner) {
|
|
75
|
+
return owner !== null && (policy.scope === "account" || (policy.scope === "strategy" ? owner === strategy : policy.scope.strategies.includes(owner)));
|
|
76
|
+
}
|
|
77
|
+
function validatePortfolioObservation(o) {
|
|
78
|
+
if (!record(o) || !Number.isSafeInteger(o.asOf) || Number(o.asOf) < 0 || !Array.isArray(o.positions) || !Array.isArray(o.orders))
|
|
79
|
+
throw new Error("Invalid portfolio observation");
|
|
80
|
+
const ids = /* @__PURE__ */ new Set(), symbols = /* @__PURE__ */ new Set();
|
|
81
|
+
for (const p of o.positions) {
|
|
82
|
+
if (!record(p)) throw new Error("Invalid portfolio position");
|
|
83
|
+
const key = `${p.symbol}:${p.direction}`;
|
|
84
|
+
if (typeof p.id !== "string" || !p.id || typeof p.symbol !== "string" || !p.symbol || !["LONG", "SHORT"].includes(String(p.direction)) || !positive(p.qty) || !positive(p.entryPrice) || ids.has(p.id) || symbols.has(key) || p.strategy !== null && (typeof p.strategy !== "string" || !p.strategy) || p.markPrice !== null && !positive(p.markPrice) || p.markedAt !== null && (!Number.isSafeInteger(p.markedAt) || Number(p.markedAt) < 0))
|
|
85
|
+
throw new Error("Invalid or duplicate portfolio position");
|
|
86
|
+
ids.add(p.id);
|
|
87
|
+
symbols.add(key);
|
|
88
|
+
}
|
|
89
|
+
const orders = /* @__PURE__ */ new Set();
|
|
90
|
+
for (const order of o.orders) {
|
|
91
|
+
if (!record(order) || typeof order.id !== "string" || !order.id || orders.has(order.id) || !["pending", "partial", "filled", "cancelled", "rejected"].includes(
|
|
92
|
+
String(order.status)
|
|
93
|
+
) || !Number.isFinite(order.filledQty) || Number(order.filledQty) < 0)
|
|
94
|
+
throw new Error("Invalid portfolio order observation");
|
|
95
|
+
orders.add(order.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var fresh = (p, o, policy) => p.markPrice !== null && p.markedAt !== null && p.markedAt <= o.asOf && o.asOf - p.markedAt <= policy.maxPriceAgeMs;
|
|
99
|
+
var pnl = (p) => ((p.markPrice ?? p.entryPrice) - p.entryPrice) * p.qty * (p.direction === "LONG" ? 1 : -1);
|
|
100
|
+
function buildPortfolioContext(policy, strategy, state, o) {
|
|
101
|
+
const positions = o.positions.filter(
|
|
102
|
+
(p) => portfolioOwns(policy, strategy, p.strategy)
|
|
103
|
+
);
|
|
104
|
+
const complete = positions.every((p) => fresh(p, o, policy));
|
|
105
|
+
const value = positions.reduce((s, p) => s + p.qty * p.entryPrice, 0);
|
|
106
|
+
const risksKnown = positions.every((p) => positive(p.stopLossPrice));
|
|
107
|
+
return {
|
|
108
|
+
schema: "tradejs-portfolio-context/v1",
|
|
109
|
+
asOf: o.asOf,
|
|
110
|
+
policyId: policy.id,
|
|
111
|
+
positionCount: positions.length,
|
|
112
|
+
pendingEntryCount: state.reservations.filter(
|
|
113
|
+
(r) => !positions.some(
|
|
114
|
+
(p) => p.symbol === r.symbol && p.direction === r.direction
|
|
115
|
+
)
|
|
116
|
+
).length,
|
|
117
|
+
longCount: positions.filter((p) => p.direction === "LONG").length,
|
|
118
|
+
shortCount: positions.filter((p) => p.direction === "SHORT").length,
|
|
119
|
+
grossUnrealizedPnlR: complete ? positions.reduce((s, p) => s + pnl(p), 0) / policy.riskUnit : null,
|
|
120
|
+
openStopRiskR: risksKnown ? positions.reduce(
|
|
121
|
+
(s, p) => s + Math.max(
|
|
122
|
+
0,
|
|
123
|
+
(p.entryPrice - (p.stopLossPrice ?? p.entryPrice)) * (p.direction === "LONG" ? 1 : -1)
|
|
124
|
+
) * p.qty,
|
|
125
|
+
0
|
|
126
|
+
) / policy.riskUnit : null,
|
|
127
|
+
largestPositionShare: value > 0 ? Math.max(...positions.map((p) => p.qty * p.entryPrice / value)) : null,
|
|
128
|
+
pricesComplete: complete,
|
|
129
|
+
unknownOwnershipCount: o.positions.filter((p) => p.strategy === null).length
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function reconcilePortfolio(policy, strategy, previous, o) {
|
|
133
|
+
validatePortfolioObservation(o);
|
|
134
|
+
if (o.asOf < previous.observedAt)
|
|
135
|
+
throw new Error("Portfolio observation moved backwards");
|
|
136
|
+
const state = {
|
|
137
|
+
...previous,
|
|
138
|
+
reservations: previous.reservations.map((r) => ({ ...r })),
|
|
139
|
+
closing: previous.closing.map((c) => ({ ...c })),
|
|
140
|
+
completedSignals: { ...previous.completedSignals },
|
|
141
|
+
observedAt: o.asOf
|
|
142
|
+
};
|
|
143
|
+
const cycle = Math.floor(o.asOf / policy.observationIntervalMs) * policy.observationIntervalMs;
|
|
144
|
+
const positions = o.positions.filter(
|
|
145
|
+
(p) => portfolioOwns(policy, strategy, p.strategy)
|
|
146
|
+
);
|
|
147
|
+
const closingBefore = state.closing.length;
|
|
148
|
+
state.closing = state.closing.filter(
|
|
149
|
+
(c) => positions.some((p) => p.id === c.positionId)
|
|
150
|
+
);
|
|
151
|
+
if (state.closing.length < closingBefore)
|
|
152
|
+
state.blockedThrough = Math.max(
|
|
153
|
+
state.blockedThrough,
|
|
154
|
+
cycle + policy.observationIntervalMs - 1
|
|
155
|
+
);
|
|
156
|
+
const requestClose = (p, reason) => {
|
|
157
|
+
if (!state.closing.some((c) => c.positionId === p.id))
|
|
158
|
+
state.closing.push({
|
|
159
|
+
positionId: p.id,
|
|
160
|
+
position: { ...p },
|
|
161
|
+
reason,
|
|
162
|
+
createdAt: o.asOf,
|
|
163
|
+
lastAttemptAt: null
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
state.reservations = state.reservations.filter((r) => {
|
|
167
|
+
const order = o.orders.find((x) => x.id === r.id);
|
|
168
|
+
if (!order) return true;
|
|
169
|
+
if (order.filledQty > 0 && !r.oppositeHandled) {
|
|
170
|
+
const filled = positions.find(
|
|
171
|
+
(p) => p.id === order.positionId && p.symbol === r.symbol && p.direction === r.direction
|
|
172
|
+
);
|
|
173
|
+
if (filled) {
|
|
174
|
+
r.oppositeHandled = true;
|
|
175
|
+
if (policy.closeOppositeOnFill) {
|
|
176
|
+
for (const p of positions)
|
|
177
|
+
if (p.id !== filled.id && p.direction !== r.direction)
|
|
178
|
+
requestClose(p, "PORTFOLIO_OPPOSITE_FILL");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (["filled", "cancelled", "rejected"].includes(order.status)) {
|
|
183
|
+
if (order.filledQty > 0 && !r.oppositeHandled) return true;
|
|
184
|
+
state.completedSignals[r.signalId] = o.asOf;
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
});
|
|
189
|
+
if (cycle > state.evaluatedAt) {
|
|
190
|
+
state.evaluatedAt = cycle;
|
|
191
|
+
const profit = policy.profitExit;
|
|
192
|
+
if (profit?.scope === "position") {
|
|
193
|
+
for (const p of positions)
|
|
194
|
+
if (fresh(p, o, policy) && pnl(p) >= profit.multiple * policy.riskUnit)
|
|
195
|
+
requestClose(p, "PORTFOLIO_PROFIT_POSITION");
|
|
196
|
+
}
|
|
197
|
+
if (profit?.scope === "basket" && positions.length >= (profit.minPositions ?? 2) && positions.every((p) => fresh(p, o, policy)) && positions.reduce((sum, p) => sum + pnl(p), 0) >= profit.multiple * policy.riskUnit)
|
|
198
|
+
for (const p of positions) requestClose(p, "PORTFOLIO_PROFIT_BASKET");
|
|
199
|
+
}
|
|
200
|
+
for (const [id, t] of Object.entries(state.completedSignals))
|
|
201
|
+
if (t < o.asOf - 7 * DAY) delete state.completedSignals[id];
|
|
202
|
+
return state;
|
|
203
|
+
}
|
|
204
|
+
function reservePortfolioEntry(policy, strategy, state, o, entry) {
|
|
205
|
+
if (!portfolioOwns(policy, strategy, entry.strategy))
|
|
206
|
+
return { state, reason: "PORTFOLIO_OUTSIDE_SCOPE" };
|
|
207
|
+
if (!positive(entry.qty) || !entry.symbol || !["LONG", "SHORT"].includes(entry.direction) || !entry.id || !entry.signalId || !Number.isSafeInteger(entry.timestamp) || entry.timestamp < o.asOf - 7 * DAY || entry.timestamp > o.asOf)
|
|
208
|
+
return { state, reason: "PORTFOLIO_INVALID_ENTRY" };
|
|
209
|
+
if (state.reservations.some(
|
|
210
|
+
(r) => r.signalId === entry.signalId || r.id === entry.id
|
|
211
|
+
) || state.completedSignals[entry.signalId] !== void 0)
|
|
212
|
+
return { state, reason: "PORTFOLIO_DUPLICATE_ENTRY" };
|
|
213
|
+
if (state.closing.length || o.asOf <= state.blockedThrough)
|
|
214
|
+
return { state, reason: "PORTFOLIO_CLOSING" };
|
|
215
|
+
if (o.positions.some((p) => p.symbol === entry.symbol) || state.reservations.some((p) => p.symbol === entry.symbol))
|
|
216
|
+
return { state, reason: "PORTFOLIO_SYMBOL_OCCUPIED" };
|
|
217
|
+
const context = buildPortfolioContext(policy, strategy, state, o);
|
|
218
|
+
if (context.unknownOwnershipCount)
|
|
219
|
+
return { state, reason: "PORTFOLIO_UNKNOWN_OWNERSHIP" };
|
|
220
|
+
if (policy.maxPositions !== void 0 && context.positionCount + context.pendingEntryCount >= policy.maxPositions)
|
|
221
|
+
return { state, reason: "PORTFOLIO_POSITION_LIMIT" };
|
|
222
|
+
return {
|
|
223
|
+
state: { ...state, reservations: [...state.reservations, { ...entry }] }
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function portfolioCloseCommands(policy, state, o) {
|
|
227
|
+
const cycle = Math.floor(o.asOf / policy.observationIntervalMs) * policy.observationIntervalMs;
|
|
228
|
+
return state.closing.flatMap((intent) => {
|
|
229
|
+
const position = o.positions.find((p) => p.id === intent.positionId);
|
|
230
|
+
return position && fresh(position, o, policy) && (intent.lastAttemptAt === null || intent.lastAttemptAt < cycle) ? [{ intent, position }] : [];
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function assertPortfolioState(value) {
|
|
234
|
+
const fail = () => {
|
|
235
|
+
throw new Error("Invalid persisted portfolio state");
|
|
236
|
+
};
|
|
237
|
+
if (!record(value) || value.schema !== "tradejs-portfolio/v1" || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0 || typeof value.fingerprint !== "string" || !value.fingerprint || !Array.isArray(value.reservations) || !Array.isArray(value.closing) || !record(value.completedSignals))
|
|
238
|
+
return fail();
|
|
239
|
+
for (const clock of [
|
|
240
|
+
value.observedAt,
|
|
241
|
+
value.evaluatedAt,
|
|
242
|
+
value.blockedThrough
|
|
243
|
+
])
|
|
244
|
+
if (!Number.isSafeInteger(clock) || Number(clock) < -1) return fail();
|
|
245
|
+
const ids = /* @__PURE__ */ new Set();
|
|
246
|
+
for (const r of value.reservations) {
|
|
247
|
+
if (!record(r) || typeof r.id !== "string" || !r.id || ids.has(r.id) || typeof r.signalId !== "string" || !r.signalId || typeof r.symbol !== "string" || !r.symbol || typeof r.strategy !== "string" || !r.strategy || !["LONG", "SHORT"].includes(String(r.direction)) || !positive(r.qty) || !Number.isSafeInteger(r.timestamp) || Number(r.timestamp) < 0 || typeof r.submitted !== "boolean" || typeof r.oppositeHandled !== "boolean")
|
|
248
|
+
return fail();
|
|
249
|
+
ids.add(r.id);
|
|
250
|
+
}
|
|
251
|
+
const closes = /* @__PURE__ */ new Set();
|
|
252
|
+
for (const c of value.closing) {
|
|
253
|
+
if (!record(c) || typeof c.positionId !== "string" || closes.has(c.positionId) || !record(c.position) || c.position.id !== c.positionId || ![
|
|
254
|
+
"PORTFOLIO_PROFIT_POSITION",
|
|
255
|
+
"PORTFOLIO_PROFIT_BASKET",
|
|
256
|
+
"PORTFOLIO_OPPOSITE_FILL"
|
|
257
|
+
].includes(String(c.reason)) || !Number.isSafeInteger(c.createdAt) || Number(c.createdAt) < 0 || c.lastAttemptAt !== null && (!Number.isSafeInteger(c.lastAttemptAt) || Number(c.lastAttemptAt) < 0))
|
|
258
|
+
return fail();
|
|
259
|
+
validatePortfolioObservation({
|
|
260
|
+
asOf: Number(c.createdAt),
|
|
261
|
+
positions: [c.position],
|
|
262
|
+
orders: []
|
|
263
|
+
});
|
|
264
|
+
closes.add(c.positionId);
|
|
265
|
+
}
|
|
266
|
+
for (const t of Object.values(value.completedSignals))
|
|
267
|
+
if (!Number.isSafeInteger(t) || Number(t) < 0) return fail();
|
|
268
|
+
}
|
|
269
|
+
export {
|
|
270
|
+
assertPortfolioState,
|
|
271
|
+
buildPortfolioContext,
|
|
272
|
+
createPortfolioState,
|
|
273
|
+
parsePortfolioPolicy,
|
|
274
|
+
portfolioCloseCommands,
|
|
275
|
+
portfolioFingerprint,
|
|
276
|
+
portfolioOwns,
|
|
277
|
+
reconcilePortfolio,
|
|
278
|
+
reservePortfolioEntry,
|
|
279
|
+
validatePortfolioObservation
|
|
280
|
+
};
|
package/dist/strategies.mjs
CHANGED
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
createIndicators,
|
|
6
6
|
getRequiredControllerSeedWindow
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-6XY6Z2DB.mjs";
|
|
8
8
|
import "./chunk-WJQRRGUY.mjs";
|
|
9
9
|
import {
|
|
10
10
|
getTimestamp
|
|
11
11
|
} from "./chunk-S4KHOAXM.mjs";
|
|
12
|
-
import "./chunk-LI6FPPKB.mjs";
|
|
13
12
|
import {
|
|
14
13
|
FEE_PERCENT
|
|
15
14
|
} from "./chunk-MKCQSB4H.mjs";
|
|
15
|
+
import "./chunk-LI6FPPKB.mjs";
|
|
16
16
|
|
|
17
17
|
// src/utils/strategyHelpers/indicators.ts
|
|
18
18
|
var buildDefaultIndicatorPeriods = (config) => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TickerProfileSnapshot, Candle } from '@tradejs/types';
|
|
2
|
+
|
|
3
|
+
declare const TICKER_PROFILE_DEFINITION: "hourly-30d-monthly-terciles/v1";
|
|
4
|
+
declare const tickerProfileMonth: (timestamp: number) => number;
|
|
5
|
+
declare const tickerProfileNextMonth: (timestamp: number) => number;
|
|
6
|
+
/** Input membership is explicit. Thresholds never depend on trades or outcomes. */
|
|
7
|
+
declare const buildTickerProfileSnapshot: ({ asOf, provider, universeId, membership, candlesBySymbol, }: {
|
|
8
|
+
asOf: number;
|
|
9
|
+
provider: string;
|
|
10
|
+
universeId: string;
|
|
11
|
+
membership: TickerProfileSnapshot["membership"];
|
|
12
|
+
candlesBySymbol: Readonly<Record<string, readonly Candle[]>>;
|
|
13
|
+
}) => TickerProfileSnapshot;
|
|
14
|
+
/** Validate persisted input before exposing it to strategy or gate code. */
|
|
15
|
+
declare const isTickerProfileSnapshot: (value: unknown) => value is TickerProfileSnapshot;
|
|
16
|
+
|
|
17
|
+
export { TICKER_PROFILE_DEFINITION, buildTickerProfileSnapshot, isTickerProfileSnapshot, tickerProfileMonth, tickerProfileNextMonth };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TickerProfileSnapshot, Candle } from '@tradejs/types';
|
|
2
|
+
|
|
3
|
+
declare const TICKER_PROFILE_DEFINITION: "hourly-30d-monthly-terciles/v1";
|
|
4
|
+
declare const tickerProfileMonth: (timestamp: number) => number;
|
|
5
|
+
declare const tickerProfileNextMonth: (timestamp: number) => number;
|
|
6
|
+
/** Input membership is explicit. Thresholds never depend on trades or outcomes. */
|
|
7
|
+
declare const buildTickerProfileSnapshot: ({ asOf, provider, universeId, membership, candlesBySymbol, }: {
|
|
8
|
+
asOf: number;
|
|
9
|
+
provider: string;
|
|
10
|
+
universeId: string;
|
|
11
|
+
membership: TickerProfileSnapshot["membership"];
|
|
12
|
+
candlesBySymbol: Readonly<Record<string, readonly Candle[]>>;
|
|
13
|
+
}) => TickerProfileSnapshot;
|
|
14
|
+
/** Validate persisted input before exposing it to strategy or gate code. */
|
|
15
|
+
declare const isTickerProfileSnapshot: (value: unknown) => value is TickerProfileSnapshot;
|
|
16
|
+
|
|
17
|
+
export { TICKER_PROFILE_DEFINITION, buildTickerProfileSnapshot, isTickerProfileSnapshot, tickerProfileMonth, tickerProfileNextMonth };
|