@dshtrading/strategies 0.5.0 → 0.6.1
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/lib/api/lib/index.d.ts +113 -20
- package/lib/api/lib/instrument-form.d.ts +1 -0
- package/lib/builtin-tombstones-fs.js +27 -18
- package/lib/custom-fs.js +32 -24
- package/lib/custom-screener-fs.js +32 -24
- package/lib/plugin.js +9 -9
- package/package.json +4 -4
package/lib/api/lib/index.d.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
|
+
import "./instrument-form.js";
|
|
2
|
+
import "@deepseek-ai/cordis";
|
|
1
3
|
//#region ../api/lib/index.d.ts
|
|
2
4
|
//#region src/index.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* @dshtrading/api — 纯类型契约包(crypto-slice-plan §关键接口草案)。
|
|
5
|
-
*
|
|
6
|
-
* 零运行时、零依赖:服务契约由连接器实现(ctx 键按市场命名空间,如 ctx.tradingCrypto),
|
|
7
|
-
* 消费方只依赖这里的类型。交易安全闸门(铁律 #3)在类型层面体现为
|
|
8
|
-
* OrderRequest.dryRun 默认 true、错误词汇含 LIVE_TRADING_DISABLED / APPROVAL_DENIED。
|
|
9
|
-
*
|
|
10
|
-
* @module @dshtrading/api
|
|
11
|
-
*/
|
|
12
5
|
/** K线周期(Binance 现货/合约 interval 词汇)。 */
|
|
13
6
|
type Interval = '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' | '6h' | '8h' | '12h' | '1d' | '3d' | '1w' | '1M';
|
|
14
7
|
/** 最新行情快照(公共数据,无需凭证)。 */
|
|
@@ -164,16 +157,32 @@ interface TradeTick {
|
|
|
164
157
|
readonly timestamp: number;
|
|
165
158
|
}
|
|
166
159
|
type PositionSide = 'long' | 'short';
|
|
160
|
+
/**
|
|
161
|
+
* 保证金模式(合约交易语义,P7):`cross` = 全仓、`isolated` = 逐仓。
|
|
162
|
+
* 现货(cash)不使用该字段——缺席即「非保证金/未设置」。
|
|
163
|
+
*/
|
|
164
|
+
type MarginMode = 'cross' | 'isolated';
|
|
167
165
|
/** 持仓快照。 */
|
|
168
166
|
interface Position {
|
|
169
167
|
readonly symbol: string;
|
|
170
168
|
readonly side: PositionSide;
|
|
171
|
-
/** 仓位数量(正数;方向由 side
|
|
169
|
+
/** 仓位数量(正数;方向由 side 表达)。合约仓位是 **base 币数**(连接器已按合约乘数 ctVal 由张换算)。 */
|
|
172
170
|
readonly size: number;
|
|
173
171
|
readonly entryPrice: number;
|
|
174
172
|
readonly markPrice?: number;
|
|
175
173
|
readonly unrealizedPnl?: number;
|
|
176
174
|
readonly leverage?: number;
|
|
175
|
+
/**
|
|
176
|
+
* 强平价(交易所回传原样透传;交易所未提供或不适用时缺席,绝不本地推算强平价——
|
|
177
|
+
* 强平口径与保证金模式、账户级保证金、维持保证金率有关,本地公式会给出错误的安全感)。
|
|
178
|
+
*/
|
|
179
|
+
readonly liquidationPrice?: number;
|
|
180
|
+
/** 维持保证金率(OKX `mgnRatio`;口径由交易所定义,原样透传,不在本地换算或反推)。 */
|
|
181
|
+
readonly marginRatio?: number;
|
|
182
|
+
/** 该仓位的保证金模式;现货持仓与交易所未提供时缺席。 */
|
|
183
|
+
readonly marginMode?: MarginMode;
|
|
184
|
+
/** 名义价值(USD,交易所口径;用于敞口核对,不参与本地风控公式)。 */
|
|
185
|
+
readonly notionalUsd?: number;
|
|
177
186
|
readonly timestamp: number;
|
|
178
187
|
}
|
|
179
188
|
type OrderSide = 'buy' | 'sell';
|
|
@@ -184,10 +193,18 @@ interface OrderRequest {
|
|
|
184
193
|
readonly symbol: string;
|
|
185
194
|
readonly side: OrderSide;
|
|
186
195
|
readonly type: OrderType;
|
|
196
|
+
/** **base 币数量**:合约(永续)由连接器按合约乘数换算成张,消费方恒用币数语义。 */
|
|
187
197
|
readonly quantity: number;
|
|
188
198
|
/** limit 单必填。 */
|
|
189
199
|
readonly price?: number;
|
|
190
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* 合约订单的保证金模式(缺省 `cross`)。仅对永续(`-SWAP`)有效:
|
|
202
|
+
* 现货订单只允许 cash 语义,传该字段由连接器结构化拒绝
|
|
203
|
+
* (现货逐仓/杠杆是另一个产品面,本仓未接线)。
|
|
204
|
+
* `isolated` 需先在交易所侧设好该标的的逐仓杠杆(OKX `setLeverage`)。
|
|
205
|
+
*/
|
|
206
|
+
readonly marginMode?: MarginMode;
|
|
207
|
+
/** 缺省/true 时仅模拟,不触碰交易所。实盘还受 @dshtrading/authority 的人工签署授权与 approval 约束 [S4]。 */
|
|
191
208
|
readonly dryRun?: boolean;
|
|
192
209
|
}
|
|
193
210
|
/** 订单回执/状态。 */
|
|
@@ -227,9 +244,49 @@ interface TradeFill {
|
|
|
227
244
|
interface Disposable {
|
|
228
245
|
dispose(): void;
|
|
229
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* 标的形态轴(2026-10-08 加密永续与 TradFi 永续落地):与 `market` 正交,**不新增市场键**。
|
|
249
|
+
* 缺省 = `spot`——已存自选、localStorage 与 iOS 冻结契约因此零迁移。
|
|
250
|
+
*
|
|
251
|
+
* 形态由规范符号后缀承载(`-SWAP` = 永续),见 docs/guides/symbol-vocabulary.md;
|
|
252
|
+
* `form` 只出现在名册/检索/wire 元数据里,行情方法不新增 form 形参。
|
|
253
|
+
*/
|
|
254
|
+
type InstrumentForm = 'spot' | 'perp';
|
|
255
|
+
/**
|
|
256
|
+
* 标的资产类别标签(仅展示与检索排序用,不承载交易语义)。
|
|
257
|
+
* **只信交易所元数据**(OKX `instCategory`、Binance `underlyingType`+`underlyingSubType`);
|
|
258
|
+
* 取不到时留空,禁止按符号字面猜(`SPX` 在 OKX 是迷因币 SPX6900,标普 500 是 `US500`)。
|
|
259
|
+
*/
|
|
260
|
+
type InstrumentAssetClass = 'crypto' | 'equity' | 'commodity' | 'index';
|
|
261
|
+
/** 合约专有元数据(仅 `form=perp` 有值):交易所元数据原样透传,不本地推断。 */
|
|
262
|
+
interface InstrumentContract {
|
|
263
|
+
/** 合约乘数(OKX ctVal / Binance contractSize)。 */
|
|
264
|
+
readonly multiplier?: number;
|
|
265
|
+
/** 最小价格变动。 */
|
|
266
|
+
readonly tickSize?: number;
|
|
267
|
+
/** 最小下单量/张数步长。 */
|
|
268
|
+
readonly lotSize?: number;
|
|
269
|
+
/** 交易所允许的最大杠杆。 */
|
|
270
|
+
readonly maxLeverage?: number;
|
|
271
|
+
/** 结算币种(如 USDT)。 */
|
|
272
|
+
readonly settleCcy?: string;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 标的检索结果的最小元数据(名册/搜索/wire 共用)。
|
|
276
|
+
* 元素形状对既有 `{ symbol, name? }` 实现向后兼容:`form`/`assetClass`/`contract` 全可选。
|
|
277
|
+
*/
|
|
278
|
+
interface InstrumentRef {
|
|
279
|
+
/** 市场规范词汇符号(docs/guides/symbol-vocabulary.md)。 */
|
|
280
|
+
readonly symbol: string;
|
|
281
|
+
readonly name?: string;
|
|
282
|
+
/** 缺省 = spot(老数据零迁移)。 */
|
|
283
|
+
readonly form?: InstrumentForm;
|
|
284
|
+
readonly assetClass?: InstrumentAssetClass;
|
|
285
|
+
readonly contract?: InstrumentContract;
|
|
286
|
+
}
|
|
230
287
|
/**
|
|
231
288
|
* 行情服务契约:由市场连接器实现,注册到按市场命名空间的 ctx 键(如 ctx.tradingCrypto)。
|
|
232
|
-
* 符号词汇(2026-08-31 规范,docs/symbol-vocabulary.md):入参接受市场规范形与连接器原生形,
|
|
289
|
+
* 符号词汇(2026-08-31 规范,docs/guides/symbol-vocabulary.md):入参接受市场规范形与连接器原生形,
|
|
233
290
|
* 输出 `symbol` 一律市场规范形——消费方(GUI/Agent/工作流)与数据源方言解耦。
|
|
234
291
|
*/
|
|
235
292
|
interface MarketDataService {
|
|
@@ -238,13 +295,12 @@ interface MarketDataService {
|
|
|
238
295
|
subscribeTicker(symbol: string, cb: (ticker: Ticker) => void): Disposable;
|
|
239
296
|
/**
|
|
240
297
|
* 查询本市场/交易所支持的全部标的名册(动态全集,Issue #15)。
|
|
241
|
-
* 输出 `symbol` 一律市场规范词汇(docs/symbol-vocabulary.md
|
|
298
|
+
* 输出 `symbol` 一律市场规范词汇(docs/guides/symbol-vocabulary.md),形态由符号后缀承载
|
|
299
|
+
* (`-SWAP` = 永续,`instrumentFormOf` 是唯一裁决实现);每行可带
|
|
300
|
+
* `form`/`assetClass`/`contract` 元数据(InstrumentRef),缺省 = 现货。
|
|
242
301
|
* 可选方法:无公开全集端点的数据源(如 tencent/yahoo/stooq)可缺省或由桥/前端回退。
|
|
243
302
|
*/
|
|
244
|
-
listInstruments?(): Promise<
|
|
245
|
-
symbol: string;
|
|
246
|
-
name?: string;
|
|
247
|
-
}>>;
|
|
303
|
+
listInstruments?(): Promise<InstrumentRef[]>;
|
|
248
304
|
/**
|
|
249
305
|
* 标的基本面与估值快照(GUI「基本面」页签用,2026-09-02)。
|
|
250
306
|
* 可选方法:仅当数据源在同一公共端点里携带基本面字段时实现
|
|
@@ -280,11 +336,41 @@ interface MarketDataService {
|
|
|
280
336
|
getRecentTrades?(symbol: string, limit?: number): Promise<TradeTick[]>;
|
|
281
337
|
}
|
|
282
338
|
/**
|
|
283
|
-
*
|
|
339
|
+
* 杠杆设置请求(合约交易语义,P7;仅 `form=perp` 的标的可传)。
|
|
340
|
+
* dryRun 缺省视为 true:只回本地模拟回执,不触碰交易所(与 placeOrder 同语义)。
|
|
341
|
+
*/
|
|
342
|
+
interface SetLeverageRequest {
|
|
343
|
+
readonly symbol: string;
|
|
344
|
+
/** 目标杠杆倍数(> 0;上限以交易所规格 `InstrumentContract.maxLeverage` 为准,超出即结构化拒绝)。 */
|
|
345
|
+
readonly leverage: number;
|
|
346
|
+
readonly marginMode: MarginMode;
|
|
347
|
+
/** 双向持仓模式下的方向(OKX `posSide`);净持仓模式下缺席。 */
|
|
348
|
+
readonly posSide?: 'long' | 'short';
|
|
349
|
+
/** 全仓模式下按币种设置杠杆时的保证金币(OKX `ccy`);逐仓与净持仓模式缺席。 */
|
|
350
|
+
readonly ccy?: string;
|
|
351
|
+
/** 缺省/true 时仅模拟。实盘还受 @dshtrading/authority 的人工签署授权与 approval 约束 [S4]。 */
|
|
352
|
+
readonly dryRun?: boolean;
|
|
353
|
+
}
|
|
354
|
+
/** 杠杆设置回执(dryRun=true 时是本地模拟回执,不触网)。 */
|
|
355
|
+
interface LeverageSetting {
|
|
356
|
+
readonly symbol: string;
|
|
357
|
+
readonly leverage: number;
|
|
358
|
+
readonly marginMode: MarginMode;
|
|
359
|
+
readonly posSide?: 'long' | 'short';
|
|
360
|
+
readonly ccy?: string;
|
|
361
|
+
/** 本次是否为模拟回执(回执必须显式回带,防 dry-run 语义丢失)。 */
|
|
362
|
+
readonly dryRun: boolean;
|
|
363
|
+
readonly timestamp: number;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* 交易服务契约:placeOrder 默认 dry-run;实盘前必须过 @dshtrading/authority 的人工签署授权与
|
|
284
367
|
* ctx.approval.request(交互形态;headless 下 ask=deny,fail-closed [S4])。
|
|
285
368
|
*
|
|
286
369
|
* R3(okx 切片 2026-08-29)修订:cancelOrder 增加可选 symbol、新增 getOrder——
|
|
287
370
|
* OKX 按 (instId, ordId) 双键定位订单,单参 id 形态不够;无其他实现方,扩展向后兼容。
|
|
371
|
+
*
|
|
372
|
+
* P7(合约交易 Tier 2,2026-10-08)修订:新增可选 `setLeverage`——杠杆/保证金模式变更
|
|
373
|
+
* 是实盘动作,必须与下单同门槛(服务缝三态闸门 + base 审批闸门 + 人工签署授权)。
|
|
288
374
|
*/
|
|
289
375
|
interface TradeService {
|
|
290
376
|
placeOrder(req: OrderRequest): Promise<Order>;
|
|
@@ -309,6 +395,13 @@ interface TradeService {
|
|
|
309
395
|
* 可选方法:时间升序(旧→新),最多 limit 条(缺省 ≤50)。
|
|
310
396
|
*/
|
|
311
397
|
listTradeFills?(symbol?: string, limit?: number): Promise<TradeFill[]>;
|
|
398
|
+
/**
|
|
399
|
+
* 设置合约杠杆与保证金模式(P7 合约交易 Tier 2;仅支持合约的交易所实现)。
|
|
400
|
+
* 可选方法:不实现的连接器缺席即可,消费方按 TRADING_NOT_IMPLEMENTED 语义处理。
|
|
401
|
+
* 这是会改变账户真实风险参数的实盘动作:dryRun 缺省 true(只回本地模拟回执),
|
|
402
|
+
* dryRun=false 必须同时具备人工签署的实盘授权,并经 base 审批闸门。
|
|
403
|
+
*/
|
|
404
|
+
setLeverage?(req: SetLeverageRequest): Promise<LeverageSetting>;
|
|
312
405
|
}
|
|
313
406
|
/**
|
|
314
407
|
* Cordis Context 服务键(能力三角色之「声明」):市场命名空间键由契约包统一声明,
|
|
@@ -339,7 +432,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
339
432
|
* 交易服务注册表(issue #40 GUI 交易台,@dshtrading/api 类型声明):
|
|
340
433
|
* 与 tradingMarketDataRegistry 同构的宿主平面注册面——交易连接器 host 面数据行
|
|
341
434
|
* 注册,GUI 桥按路由当前值惰性解析。**注册不改变安全语义**:placeOrder 的
|
|
342
|
-
* 服务缝闸门(dryRun 缺省 true +
|
|
435
|
+
* 服务缝闸门(dryRun 缺省 true + @dshtrading/authority 人工签署授权)随服务实例生效;
|
|
343
436
|
* GUI 桥只放行 dry-run 下单与只读查询,实盘路径仍走 Agent 工具的 base 审批闸门。
|
|
344
437
|
*/
|
|
345
438
|
tradingTradeRegistry: TradeRegistry;
|
|
@@ -540,4 +633,4 @@ interface TradingNewsRegistry {
|
|
|
540
633
|
markets(): string[];
|
|
541
634
|
}
|
|
542
635
|
//#endregion
|
|
543
|
-
export { AccountBalance, AggregateNewsOptions, AggregateNewsResult, DerivativesData, DerivativesHistory, DerivativesPoint, Disposable, FlashFeedService, Interval, Kline, MacroCalendarEntry, MacroFeedService, MacroRateEntry, MarketDataRegistration, MarketDataRegistry, MarketDataService, MarketRouterService, NewsAggregator, NewsItem, Order, OrderRequest, OrderSide, OrderStatus, OrderType, Orderbook, OrderbookLevel, Position, PositionSide, StockFundamentals, Ticker, TradeFill, TradeRegistration, TradeRegistry, TradeService, TradeTick, TradingNewsRegistry };
|
|
636
|
+
export { AccountBalance, AggregateNewsOptions, AggregateNewsResult, DerivativesData, DerivativesHistory, DerivativesPoint, Disposable, FlashFeedService, InstrumentAssetClass, InstrumentContract, InstrumentForm, InstrumentRef, Interval, Kline, LeverageSetting, MacroCalendarEntry, MacroFeedService, MacroRateEntry, MarginMode, MarketDataRegistration, MarketDataRegistry, MarketDataService, MarketRouterService, NewsAggregator, NewsItem, Order, OrderRequest, OrderSide, OrderStatus, OrderType, Orderbook, OrderbookLevel, Position, PositionSide, SetLeverageRequest, StockFundamentals, Ticker, TradeFill, TradeRegistration, TradeRegistry, TradeService, TradeTick, TradingNewsRegistry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./index.js";
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SKIP_WRITE, transactStore } from "@dshtrading/dsh-home";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
//#region src/builtin-tombstones-fs.ts
|
|
4
4
|
/**
|
|
5
5
|
* 文件持久化版内置策略/选股器墓碑存储(Node.js 宿主侧使用,
|
|
6
6
|
* 落 ~/.dsh/strategies/builtin-tombstones.json,形状 { deleted: string[] })。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 整表持久化经 `@dshtrading/dsh-home` 的 {@link transactStore}——跨进程排他锁 +
|
|
9
|
+
* 锁内新鲜读盘 + 原子写(2026-10-09 多进程互相覆盖事故修复):两进程各自删除的
|
|
10
|
+
* 内置策略都留下来,后到者不会用陈旧墓碑集合覆盖。旧宿主从未写过该文件,
|
|
9
11
|
* 无迁移问题(ENOENT 视为空表)。
|
|
10
12
|
*/
|
|
13
|
+
const LOG_PREFIX = "[dsh-trading/strategies] failed to atomic flush builtin tombstones to";
|
|
11
14
|
function createFileBuiltinTombstonesStore(filePath) {
|
|
12
15
|
let cache = null;
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
/** 新鲜读盘(坏 JSON 降级空表);供 load 与锁内重读共用。 */
|
|
17
|
+
async function readFromDisk() {
|
|
15
18
|
try {
|
|
16
19
|
const content = await readFile(filePath, "utf8");
|
|
17
20
|
const parsed = JSON.parse(content);
|
|
@@ -19,34 +22,40 @@ function createFileBuiltinTombstonesStore(filePath) {
|
|
|
19
22
|
if (Array.isArray(parsed?.deleted)) {
|
|
20
23
|
for (const id of parsed.deleted) if (typeof id === "string" && id) set.add(id);
|
|
21
24
|
}
|
|
22
|
-
cache = set;
|
|
23
25
|
return set;
|
|
24
26
|
} catch (err) {
|
|
25
27
|
if (err?.code !== "ENOENT") console.error(`[dsh-trading/strategies] failed to read builtin tombstones from ${filePath}:`, err);
|
|
26
|
-
|
|
27
|
-
return cache;
|
|
28
|
+
return /* @__PURE__ */ new Set();
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
async function
|
|
31
|
-
|
|
31
|
+
async function load() {
|
|
32
|
+
if (cache !== null) return cache;
|
|
33
|
+
cache = await readFromDisk();
|
|
34
|
+
return cache;
|
|
35
|
+
}
|
|
36
|
+
async function commit(mutate) {
|
|
37
|
+
cache = await transactStore(filePath, readFromDisk, mutate, (set) => ({ deleted: [...set] }), LOG_PREFIX);
|
|
38
|
+
return cache;
|
|
32
39
|
}
|
|
33
40
|
return {
|
|
34
41
|
async list() {
|
|
35
42
|
return [...await load()];
|
|
36
43
|
},
|
|
37
44
|
async add(id) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
45
|
+
let fresh = false;
|
|
46
|
+
await commit((onDisk) => {
|
|
47
|
+
fresh = !onDisk.has(id);
|
|
48
|
+
if (fresh) onDisk.add(id);
|
|
49
|
+
return fresh ? onDisk : SKIP_WRITE;
|
|
50
|
+
});
|
|
44
51
|
return fresh;
|
|
45
52
|
},
|
|
46
53
|
async remove(id) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
let existed = false;
|
|
55
|
+
await commit((onDisk) => {
|
|
56
|
+
existed = onDisk.delete(id);
|
|
57
|
+
return existed ? onDisk : SKIP_WRITE;
|
|
58
|
+
});
|
|
50
59
|
return existed;
|
|
51
60
|
}
|
|
52
61
|
};
|
package/lib/custom-fs.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { SKIP_WRITE, transactStore } from "@dshtrading/dsh-home";
|
|
3
3
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
//#region src/custom-fs.ts
|
|
5
5
|
/**
|
|
6
6
|
* 文件持久化版自定义策略存储(Node.js 宿主侧使用,落 ~/.dsh/strategies/custom.json)。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 整表持久化经 `@dshtrading/dsh-home` 的 {@link transactStore}——跨进程排他锁 +
|
|
9
|
+
* 锁内新鲜读盘 + 原子写(2026-10-09 多进程互相覆盖事故修复;issue #31 落盘形状不变)。
|
|
9
10
|
* remove(archive=true) 会先把被删记录归档到 sidecar 文件(<path>.archive.jsonl,
|
|
10
11
|
* JSONL 追加)——策略管理(覆盖 + 墓碑)丢弃用户 override 时可找回(2026-09-07)。
|
|
11
12
|
*/
|
|
13
|
+
const LOG_PREFIX = "[dsh-trading/strategies] failed to atomic flush custom strategies to";
|
|
12
14
|
function createFileCustomStrategyStore(filePath) {
|
|
13
15
|
let cache = null;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
/** 新鲜读盘(坏 JSON/坏形同既有 load 口径降级空表);供 load 与锁内重读共用。 */
|
|
17
|
+
async function readFromDisk() {
|
|
16
18
|
try {
|
|
17
19
|
const content = await readFile(filePath, "utf8");
|
|
18
20
|
const parsed = JSON.parse(content);
|
|
@@ -20,16 +22,20 @@ function createFileCustomStrategyStore(filePath) {
|
|
|
20
22
|
if (Array.isArray(parsed)) {
|
|
21
23
|
for (const item of parsed) if (item && typeof item.id === "string") map.set(item.id, item);
|
|
22
24
|
}
|
|
23
|
-
cache = map;
|
|
24
25
|
return map;
|
|
25
26
|
} catch (err) {
|
|
26
27
|
if (err?.code !== "ENOENT") console.error(`[dsh-trading/strategies] failed to read custom strategies from ${filePath}:`, err);
|
|
27
|
-
|
|
28
|
-
return cache;
|
|
28
|
+
return /* @__PURE__ */ new Map();
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
async function
|
|
32
|
-
|
|
31
|
+
async function load() {
|
|
32
|
+
if (cache !== null) return cache;
|
|
33
|
+
cache = await readFromDisk();
|
|
34
|
+
return cache;
|
|
35
|
+
}
|
|
36
|
+
async function commit(mutate) {
|
|
37
|
+
cache = await transactStore(filePath, readFromDisk, mutate, (table) => [...table.values()], LOG_PREFIX);
|
|
38
|
+
return cache;
|
|
33
39
|
}
|
|
34
40
|
return {
|
|
35
41
|
async list() {
|
|
@@ -39,23 +45,25 @@ function createFileCustomStrategyStore(filePath) {
|
|
|
39
45
|
return (await load()).get(id);
|
|
40
46
|
},
|
|
41
47
|
async save(record) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
await commit((onDisk) => {
|
|
49
|
+
onDisk.set(record.id, { ...record });
|
|
50
|
+
return onDisk;
|
|
51
|
+
});
|
|
45
52
|
},
|
|
46
53
|
async remove(id, archive = false) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
54
|
+
let existed = false;
|
|
55
|
+
let removedRecord;
|
|
56
|
+
await commit((onDisk) => {
|
|
57
|
+
removedRecord = onDisk.get(id);
|
|
58
|
+
existed = onDisk.delete(id);
|
|
59
|
+
return existed ? onDisk : SKIP_WRITE;
|
|
60
|
+
});
|
|
61
|
+
if (existed && archive && removedRecord !== void 0) try {
|
|
62
|
+
const archivePath = `${filePath}.archive.jsonl`;
|
|
63
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
64
|
+
await appendFile(archivePath, `${JSON.stringify(removedRecord)}\n`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("[dsh-trading/strategies] failed to archive removed strategy record:", error);
|
|
59
67
|
}
|
|
60
68
|
return existed;
|
|
61
69
|
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { SKIP_WRITE, transactStore } from "@dshtrading/dsh-home";
|
|
3
3
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
//#region src/custom-screener-fs.ts
|
|
5
5
|
/**
|
|
6
6
|
* 文件持久化版自定义选股器存储(Node.js 宿主侧使用,
|
|
7
7
|
* 落 ~/.dsh/strategies/custom-screeners.json)。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 整表持久化经 `@dshtrading/dsh-home` 的 {@link transactStore}——跨进程排他锁 +
|
|
10
|
+
* 锁内新鲜读盘 + 原子写(2026-10-09 多进程互相覆盖事故修复)。
|
|
10
11
|
* remove(archive=true) 先把被删记录归档到 sidecar(<path>.archive.jsonl,JSONL
|
|
11
12
|
* 追加)——选股器管理丢弃用户 override 时可找回(2026-09-07 审查补强)。
|
|
12
13
|
*/
|
|
14
|
+
const LOG_PREFIX = "[dsh-trading/strategies] failed to atomic flush custom screeners to";
|
|
13
15
|
function createFileCustomScreenerStore(filePath) {
|
|
14
16
|
let cache = null;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/** 新鲜读盘(坏 JSON/坏形同既有 load 口径降级空表);供 load 与锁内重读共用。 */
|
|
18
|
+
async function readFromDisk() {
|
|
17
19
|
try {
|
|
18
20
|
const content = await readFile(filePath, "utf8");
|
|
19
21
|
const parsed = JSON.parse(content);
|
|
@@ -21,16 +23,20 @@ function createFileCustomScreenerStore(filePath) {
|
|
|
21
23
|
if (Array.isArray(parsed)) {
|
|
22
24
|
for (const item of parsed) if (item && typeof item.id === "string") map.set(item.id, item);
|
|
23
25
|
}
|
|
24
|
-
cache = map;
|
|
25
26
|
return map;
|
|
26
27
|
} catch (err) {
|
|
27
28
|
if (err?.code !== "ENOENT") console.error(`[dsh-trading/strategies] failed to read custom screeners from ${filePath}:`, err);
|
|
28
|
-
|
|
29
|
-
return cache;
|
|
29
|
+
return /* @__PURE__ */ new Map();
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
async function
|
|
33
|
-
|
|
32
|
+
async function load() {
|
|
33
|
+
if (cache !== null) return cache;
|
|
34
|
+
cache = await readFromDisk();
|
|
35
|
+
return cache;
|
|
36
|
+
}
|
|
37
|
+
async function commit(mutate) {
|
|
38
|
+
cache = await transactStore(filePath, readFromDisk, mutate, (table) => [...table.values()], LOG_PREFIX);
|
|
39
|
+
return cache;
|
|
34
40
|
}
|
|
35
41
|
return {
|
|
36
42
|
async list() {
|
|
@@ -40,23 +46,25 @@ function createFileCustomScreenerStore(filePath) {
|
|
|
40
46
|
return (await load()).get(id);
|
|
41
47
|
},
|
|
42
48
|
async save(record) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
await commit((onDisk) => {
|
|
50
|
+
onDisk.set(record.id, { ...record });
|
|
51
|
+
return onDisk;
|
|
52
|
+
});
|
|
46
53
|
},
|
|
47
54
|
async remove(id, archive = false) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
55
|
+
let existed = false;
|
|
56
|
+
let removedRecord;
|
|
57
|
+
await commit((onDisk) => {
|
|
58
|
+
removedRecord = onDisk.get(id);
|
|
59
|
+
existed = onDisk.delete(id);
|
|
60
|
+
return existed ? onDisk : SKIP_WRITE;
|
|
61
|
+
});
|
|
62
|
+
if (existed && archive && removedRecord !== void 0) try {
|
|
63
|
+
const archivePath = `${filePath}.archive.jsonl`;
|
|
64
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
65
|
+
await appendFile(archivePath, `${JSON.stringify(removedRecord)}\n`);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("[dsh-trading/strategies] failed to archive removed screener record:", error);
|
|
60
68
|
}
|
|
61
69
|
return existed;
|
|
62
70
|
}
|
package/lib/plugin.js
CHANGED
|
@@ -54,7 +54,7 @@ function createStrategyAuthorTool(options) {
|
|
|
54
54
|
const { store, tombstones, onWritten } = options;
|
|
55
55
|
return defineTool({
|
|
56
56
|
name: "strategy_author",
|
|
57
|
-
description: "Author, validate
|
|
57
|
+
description: "Author, validate and persist a custom trading strategy from JavaScript compute source. compute(bars, params) must return StrategySignal[] (entry/exit at bar close, filled at the next bar open). The validator runs sandbox trials over kline scenarios and replays the sequence for engine replayability; a valid strategy is persisted and immediately backtestable. A built-in paradigm id overrides that built-in; strategy_reset restores the factory default.",
|
|
58
58
|
parameters: {
|
|
59
59
|
id: {
|
|
60
60
|
type: "string",
|
|
@@ -64,12 +64,12 @@ function createStrategyAuthorTool(options) {
|
|
|
64
64
|
title: {
|
|
65
65
|
type: "string",
|
|
66
66
|
required: true,
|
|
67
|
-
description: "Display name (1-32 chars), e.g. \"
|
|
67
|
+
description: "Display name (1-32 chars), e.g. \"double-MA stop loss and take profit\"."
|
|
68
68
|
},
|
|
69
69
|
horizon: {
|
|
70
70
|
type: "string",
|
|
71
71
|
required: true,
|
|
72
|
-
description: "Strategy horizon: \"short\" (
|
|
72
|
+
description: "Strategy horizon: \"short\" (intraday), \"swing\" (days to weeks) or \"long\" (months or more)."
|
|
73
73
|
},
|
|
74
74
|
summary: {
|
|
75
75
|
type: "string",
|
|
@@ -127,7 +127,7 @@ function createStrategyAuthorTool(options) {
|
|
|
127
127
|
function createStrategyListTool(options) {
|
|
128
128
|
return defineTool({
|
|
129
129
|
name: "strategy_list",
|
|
130
|
-
description: "List the
|
|
130
|
+
description: "List the strategy roster: built-in paradigms with their parameter keys/defaults, custom strategies authored via strategy_author, and tombstoned built-in ids. Read-only. Call it before strategy_backtest / strategy_delete / strategy_reset when you have no id from this session, and before passing paramsJson so you know the parameter keys. overridden=true means a built-in currently replaced by a user record; deleted=true means it is tombstoned until strategy_reset restores it.",
|
|
131
131
|
parameters: {},
|
|
132
132
|
output: {
|
|
133
133
|
schema: { type: "string" },
|
|
@@ -211,7 +211,7 @@ function screenerRecordEntry(record) {
|
|
|
211
211
|
function createScreenerListTool(options) {
|
|
212
212
|
return defineTool({
|
|
213
213
|
name: "screener_list",
|
|
214
|
-
description: "List the screener roster: built-in screeners
|
|
214
|
+
description: "List the screener roster: built-in screeners with their parameter and result-column keys, custom screeners authored via screener_author, and tombstoned built-in ids. Read-only. Call it before screener_run / screener_delete / screener_reset when you have no id from this session. overridden=true means a built-in currently replaced by a user record; deleted=true means the built-in is tombstoned until screener_reset restores it.",
|
|
215
215
|
parameters: {},
|
|
216
216
|
output: {
|
|
217
217
|
schema: { type: "string" },
|
|
@@ -295,7 +295,7 @@ async function resolveStrategyDefinition(store, strategyId, options) {
|
|
|
295
295
|
function createStrategyBacktestTool(deps) {
|
|
296
296
|
return defineTool({
|
|
297
297
|
name: "strategy_backtest",
|
|
298
|
-
description: "Backtest a strategy (custom
|
|
298
|
+
description: "Backtest a strategy (custom from strategy_author, or a built-in paradigm) on a symbol and interval with the pure-function engine. Returns 8 metrics (totalReturn, cagr, maxDrawdown, sharpe, winRate, profitFactor, tradeCount, exposure), the trade list and the equity curve. Signals confirm at bar close and fill at the next bar open with fee/slippage modeling; simulation only, never places orders. paramsJson overrides declared parameters and is echoed in params.effective.",
|
|
299
299
|
parameters: {
|
|
300
300
|
strategyId: {
|
|
301
301
|
type: "string",
|
|
@@ -495,7 +495,7 @@ function createScreenerAuthorTool(options) {
|
|
|
495
495
|
const { store, tombstones, onWritten } = options;
|
|
496
496
|
return defineTool({
|
|
497
497
|
name: "screener_author",
|
|
498
|
-
description: "Author, validate
|
|
498
|
+
description: "Author, validate and persist a custom stock screener from JavaScript evaluate source. evaluate(bars, params) is a single-point cross-section predicate returning a ScreenerMatch ({ metrics with keys declared in columns, reason }) or null when the symbol does not match or lacks data. A valid screener is persisted into the GUI roster. An id equal to a built-in screener id overrides that built-in; screener_reset restores the factory default.",
|
|
499
499
|
parameters: {
|
|
500
500
|
id: {
|
|
501
501
|
type: "string",
|
|
@@ -505,7 +505,7 @@ function createScreenerAuthorTool(options) {
|
|
|
505
505
|
title: {
|
|
506
506
|
type: "string",
|
|
507
507
|
required: true,
|
|
508
|
-
description: "Display name (1-32 chars), e.g. \"
|
|
508
|
+
description: "Display name (1-32 chars), e.g. \"price-volume double confirmation\"."
|
|
509
509
|
},
|
|
510
510
|
summary: {
|
|
511
511
|
type: "string",
|
|
@@ -834,7 +834,7 @@ function parseParamsOverride(paramsJson, specs, tool, ownerId) {
|
|
|
834
834
|
function createScreenerRunTool(deps) {
|
|
835
835
|
return defineTool({
|
|
836
836
|
name: "screener_run",
|
|
837
|
-
description: "Run one screener (
|
|
837
|
+
description: "Run one screener (id from screener_list) over a market and return matched instruments with their metric columns and a one-line reason. Read-only cross-sectional scan: daily candles come from the routed market-data provider, a pure function runs per instrument, and no order is ever placed. Instruments with too short a window are skipped silently; per-instrument failures are counted, never fatal. The result carries the cost guards (scanned, failed, truncated, deadlineExceeded).",
|
|
838
838
|
parameters: {
|
|
839
839
|
screenerId: {
|
|
840
840
|
type: "string",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/strategies",
|
|
3
3
|
"description": "Trading strategies kernel for dsh-trading: pure math backtesting engine, strategy contracts, and 6 reference paradigms (short, swing, long). Pure library (zero runtime dependencies, browser-packable).",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
],
|
|
22
22
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@dshtrading/dsh-home": "^0.
|
|
25
|
-
"@dshtrading/indicators": "0.
|
|
24
|
+
"@dshtrading/dsh-home": "^0.6.1",
|
|
25
|
+
"@dshtrading/indicators": "0.6.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"tsdown": "^0.22.0",
|
|
33
33
|
"vitest": "^3.0.0",
|
|
34
34
|
"typescript": "^5.5.0",
|
|
35
|
-
"@dshtrading/api": "0.
|
|
35
|
+
"@dshtrading/api": "0.6.1"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsdown",
|